Repository: quantumlib/Stim Branch: main Commit: 75d9cb12626a Files: 753 Total size: 13.2 MB Directory structure: gitextract_cojjamez/ ├── .bazelrc ├── .bazelversion ├── .clang-format ├── .editorconfig ├── .github/ │ ├── SECURITY.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .markdownlintrc ├── BUILD ├── CITATION.cff ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── MODULE.bazel ├── README.md ├── SUPPORT.md ├── WORKSPACE ├── dev/ │ ├── canvas_with_texture_for_3d_diagrams.html │ ├── clean_build_files.sh │ ├── compile_crumble_into_cpp_string_file.sh │ ├── compile_crumble_into_single_html_page.sh │ ├── doctest_proper.py │ ├── gen_known_gates_for_js.sh │ ├── gen_sinter_api_reference.py │ ├── gen_stim_api_reference.py │ ├── gen_stim_stub_file.py │ ├── make_logo.html │ ├── overwrite_dev_versions_with_date.py │ ├── regen_crumble_cpp_resource.sh │ ├── regen_docs.sh │ ├── regen_file_lists.sh │ ├── regen_texture_to_cpp_base64_string.sh │ └── util_gen_stub_file.py ├── doc/ │ ├── circuit_data_references.md │ ├── developer_documentation.md │ ├── file_format_dem_detector_error_model.md │ ├── file_format_stim_circuit.md │ ├── gates.md │ ├── getting_started.ipynb │ ├── python_api_reference_vDev.md │ ├── result_formats.md │ ├── sinter_api.md │ ├── sinter_command_line.md │ ├── stim.pyi │ └── usage_command_line.md ├── file_lists/ │ ├── perf_files │ ├── pybind_files │ ├── source_files_no_main │ └── test_files ├── glue/ │ ├── cirq/ │ │ ├── README.md │ │ ├── setup.py │ │ └── stimcirq/ │ │ ├── __init__.py │ │ ├── _cirq_to_stim.py │ │ ├── _cirq_to_stim_test.py │ │ ├── _cx_swap_gate.py │ │ ├── _cx_swap_test.py │ │ ├── _cz_swap_gate.py │ │ ├── _cz_swap_test.py │ │ ├── _det_annotation.py │ │ ├── _det_annotation_test.py │ │ ├── _i_error_gate.py │ │ ├── _i_error_gate_test.py │ │ ├── _ii_error_gate.py │ │ ├── _ii_error_gate_test.py │ │ ├── _ii_gate.py │ │ ├── _ii_gate_test.py │ │ ├── _measure_and_or_reset_gate.py │ │ ├── _measure_and_or_reset_gate_test.py │ │ ├── _obs_annotation.py │ │ ├── _obs_annotation_test.py │ │ ├── _shift_coords_annotation.py │ │ ├── _shift_coords_annotation_test.py │ │ ├── _stim_sampler.py │ │ ├── _stim_sampler_test.py │ │ ├── _stim_to_cirq.py │ │ ├── _stim_to_cirq_test.py │ │ ├── _sweep_pauli.py │ │ ├── _sweep_pauli_test.py │ │ ├── _two_qubit_asymmetric_depolarize.py │ │ └── _two_qubit_asymmetric_depolarize_test.py │ ├── crumble/ │ │ ├── README.md │ │ ├── base/ │ │ │ ├── cooldown_throttle.js │ │ │ ├── describe.js │ │ │ ├── describe.test.js │ │ │ ├── equate.js │ │ │ ├── equate.test.js │ │ │ ├── history_pusher.js │ │ │ ├── obs.js │ │ │ ├── obs.test.js │ │ │ ├── revision.js │ │ │ ├── revision.test.js │ │ │ ├── seq.js │ │ │ └── seq.test.js │ │ ├── circuit/ │ │ │ ├── circuit.js │ │ │ ├── circuit.test.js │ │ │ ├── layer.js │ │ │ ├── layer.test.js │ │ │ ├── operation.js │ │ │ ├── pauli_frame.js │ │ │ ├── pauli_frame.test.js │ │ │ ├── propagated_pauli_frames.js │ │ │ └── propagated_pauli_frames.test.js │ │ ├── crumble.html │ │ ├── draw/ │ │ │ ├── config.js │ │ │ ├── draw_util.js │ │ │ ├── main_draw.js │ │ │ ├── main_draw.test.js │ │ │ ├── state_snapshot.js │ │ │ └── timeline_viewer.js │ │ ├── editor/ │ │ │ ├── editor_state.js │ │ │ ├── editor_state.test.js │ │ │ └── sync_url_to_state.js │ │ ├── gates/ │ │ │ ├── gate.js │ │ │ ├── gate_draw_util.js │ │ │ ├── gateset.js │ │ │ ├── gateset.test.js │ │ │ ├── gateset_controlled_paulis.js │ │ │ ├── gateset_demolition_measurements.js │ │ │ ├── gateset_hadamard_likes.js │ │ │ ├── gateset_markers.js │ │ │ ├── gateset_mpp.js │ │ │ ├── gateset_pair_measurements.js │ │ │ ├── gateset_paulis.js │ │ │ ├── gateset_quarter_turns.js │ │ │ ├── gateset_resets.js │ │ │ ├── gateset_solo_measurements.js │ │ │ ├── gateset_sqrt_pauli_pairs.js │ │ │ ├── gateset_swaps.js │ │ │ └── gateset_third_turns.js │ │ ├── keyboard/ │ │ │ ├── chord.js │ │ │ ├── chord.test.js │ │ │ └── toolbox.js │ │ ├── main.js │ │ ├── package.json │ │ ├── run_tests_headless.js │ │ └── test/ │ │ ├── generated_gate_name_list.test.js │ │ ├── test.html │ │ ├── test_import_all.js │ │ ├── test_main.js │ │ ├── test_util.js │ │ └── test_util.test.js │ ├── javascript/ │ │ ├── README.md │ │ ├── build_wasm.sh │ │ ├── circuit.js.cc │ │ ├── circuit.js.h │ │ ├── circuit.test.js │ │ ├── common.js.cc │ │ ├── common.js.h │ │ ├── pauli_string.js.cc │ │ ├── pauli_string.js.h │ │ ├── pauli_string.test.js │ │ ├── stim.js.cc │ │ ├── stim.test_harness.js │ │ ├── tableau.js.cc │ │ ├── tableau.js.h │ │ ├── tableau.test.js │ │ ├── tableau_simulator.js.cc │ │ ├── tableau_simulator.js.h │ │ └── tableau_simulator.test.js │ ├── lattice_surgery/ │ │ ├── README.md │ │ ├── docs/ │ │ │ └── demo.ipynb │ │ ├── lassynth/ │ │ │ ├── __init__.py │ │ │ ├── lattice_surgery_synthesis.py │ │ │ ├── rewrite_passes/ │ │ │ │ ├── __init__.py │ │ │ │ ├── attach_fixups.py │ │ │ │ ├── color_z.py │ │ │ │ └── remove_unconnected.py │ │ │ ├── sat_synthesis/ │ │ │ │ ├── __init__.py │ │ │ │ └── lattice_surgery_sat.py │ │ │ ├── tools/ │ │ │ │ ├── __init__.py │ │ │ │ └── verify_stabilizers.py │ │ │ └── translators/ │ │ │ ├── __init__.py │ │ │ ├── gltf_generator.py │ │ │ ├── networkx_generator.py │ │ │ ├── textfig_generator.py │ │ │ └── zx_grid_graph.py │ │ ├── setup.py │ │ └── stimzx/ │ │ ├── __init__.py │ │ ├── _external_stabilizer.py │ │ ├── _external_stabilizer_test.py │ │ ├── _text_diagram_parsing.py │ │ ├── _text_diagram_parsing_test.py │ │ ├── _zx_graph_solver.py │ │ └── _zx_graph_solver_test.py │ ├── python/ │ │ ├── README.md │ │ └── src/ │ │ └── stim/ │ │ ├── __init__.py │ │ ├── __init__.pyi │ │ └── _main_argv.py │ ├── sample/ │ │ ├── MANIFEST.in │ │ ├── README.md │ │ ├── requirements.txt │ │ ├── setup.py │ │ └── src/ │ │ └── sinter/ │ │ ├── __init__.py │ │ ├── _collection/ │ │ │ ├── __init__.py │ │ │ ├── _collection.py │ │ │ ├── _collection_manager.py │ │ │ ├── _collection_manager_test.py │ │ │ ├── _collection_test.py │ │ │ ├── _collection_worker_loop.py │ │ │ ├── _collection_worker_state.py │ │ │ ├── _collection_worker_test.py │ │ │ ├── _mux_sampler.py │ │ │ ├── _printer.py │ │ │ ├── _sampler_ramp_throttled.py │ │ │ └── _sampler_ramp_throttled_test.py │ │ ├── _command/ │ │ │ ├── __init__.py │ │ │ ├── _main.py │ │ │ ├── _main_collect.py │ │ │ ├── _main_collect_test.py │ │ │ ├── _main_combine.py │ │ │ ├── _main_combine_test.py │ │ │ ├── _main_plot.py │ │ │ ├── _main_plot_test.py │ │ │ ├── _main_predict.py │ │ │ └── _main_predict_test.py │ │ ├── _data/ │ │ │ ├── __init__.py │ │ │ ├── _anon_task_stats.py │ │ │ ├── _anon_task_stats_test.py │ │ │ ├── _collection_options.py │ │ │ ├── _collection_options_test.py │ │ │ ├── _csv_out.py │ │ │ ├── _existing_data.py │ │ │ ├── _existing_data_test.py │ │ │ ├── _task.py │ │ │ ├── _task_stats.py │ │ │ ├── _task_stats_test.py │ │ │ └── _task_test.py │ │ ├── _decoding/ │ │ │ ├── __init__.py │ │ │ ├── _decoding.py │ │ │ ├── _decoding_all_built_in_decoders.py │ │ │ ├── _decoding_decoder_class.py │ │ │ ├── _decoding_fusion_blossom.py │ │ │ ├── _decoding_mwpf.py │ │ │ ├── _decoding_pymatching.py │ │ │ ├── _decoding_test.py │ │ │ ├── _decoding_vacuous.py │ │ │ ├── _perfectionist_sampler.py │ │ │ ├── _sampler.py │ │ │ ├── _stim_then_decode_sampler.py │ │ │ └── _stim_then_decode_sampler_test.py │ │ ├── _plotting.py │ │ ├── _plotting_test.py │ │ ├── _predict.py │ │ ├── _predict_test.py │ │ ├── _probability_util.py │ │ └── _probability_util_test.py │ └── zx/ │ ├── README.md │ ├── setup.py │ └── stimzx/ │ ├── __init__.py │ ├── _external_stabilizer.py │ ├── _external_stabilizer_test.py │ ├── _text_diagram_parsing.py │ ├── _text_diagram_parsing_test.py │ ├── _zx_graph_solver.py │ └── _zx_graph_solver_test.py ├── package.json ├── puppeteer_run_tests.js ├── pyproject.toml ├── setup.py ├── src/ │ ├── main.cc │ ├── stim/ │ │ ├── circuit/ │ │ │ ├── circuit.cc │ │ │ ├── circuit.h │ │ │ ├── circuit.perf.cc │ │ │ ├── circuit.pybind.cc │ │ │ ├── circuit.pybind.h │ │ │ ├── circuit.test.cc │ │ │ ├── circuit.test.h │ │ │ ├── circuit2.pybind.cc │ │ │ ├── circuit_instruction.cc │ │ │ ├── circuit_instruction.h │ │ │ ├── circuit_instruction.pybind.cc │ │ │ ├── circuit_instruction.pybind.h │ │ │ ├── circuit_instruction.test.cc │ │ │ ├── circuit_instruction_pybind_test.py │ │ │ ├── circuit_pybind_test.py │ │ │ ├── circuit_repeat_block.pybind.cc │ │ │ ├── circuit_repeat_block.pybind.h │ │ │ ├── circuit_repeat_block_test.py │ │ │ ├── gate_decomposition.cc │ │ │ ├── gate_decomposition.h │ │ │ ├── gate_decomposition.test.cc │ │ │ ├── gate_target.cc │ │ │ ├── gate_target.h │ │ │ ├── gate_target.pybind.cc │ │ │ ├── gate_target.pybind.h │ │ │ ├── gate_target.test.cc │ │ │ └── gate_target_pybind_test.py │ │ ├── cmd/ │ │ │ ├── command_analyze_errors.cc │ │ │ ├── command_analyze_errors.h │ │ │ ├── command_analyze_errors.test.cc │ │ │ ├── command_convert.cc │ │ │ ├── command_convert.h │ │ │ ├── command_convert.test.cc │ │ │ ├── command_detect.cc │ │ │ ├── command_detect.h │ │ │ ├── command_detect.test.cc │ │ │ ├── command_diagram.cc │ │ │ ├── command_diagram.h │ │ │ ├── command_diagram.pybind.cc │ │ │ ├── command_diagram.pybind.h │ │ │ ├── command_diagram.test.cc │ │ │ ├── command_explain_errors.cc │ │ │ ├── command_explain_errors.h │ │ │ ├── command_explain_errors.test.cc │ │ │ ├── command_gen.cc │ │ │ ├── command_gen.h │ │ │ ├── command_gen.test.cc │ │ │ ├── command_help.cc │ │ │ ├── command_help.h │ │ │ ├── command_m2d.cc │ │ │ ├── command_m2d.h │ │ │ ├── command_m2d.test.cc │ │ │ ├── command_repl.cc │ │ │ ├── command_repl.h │ │ │ ├── command_sample.cc │ │ │ ├── command_sample.h │ │ │ ├── command_sample.test.cc │ │ │ ├── command_sample_dem.cc │ │ │ ├── command_sample_dem.h │ │ │ └── command_sample_dem.test.cc │ │ ├── dem/ │ │ │ ├── dem_instruction.cc │ │ │ ├── dem_instruction.h │ │ │ ├── dem_instruction.pybind.cc │ │ │ ├── dem_instruction.pybind.h │ │ │ ├── dem_instruction.test.cc │ │ │ ├── dem_instruction_pybind_test.py │ │ │ ├── detector_error_model.cc │ │ │ ├── detector_error_model.h │ │ │ ├── detector_error_model.pybind.cc │ │ │ ├── detector_error_model.pybind.h │ │ │ ├── detector_error_model.test.cc │ │ │ ├── detector_error_model_pybind_test.py │ │ │ ├── detector_error_model_repeat_block.pybind.cc │ │ │ ├── detector_error_model_repeat_block.pybind.h │ │ │ ├── detector_error_model_repeat_block_pybind_test.py │ │ │ ├── detector_error_model_target.pybind.cc │ │ │ ├── detector_error_model_target.pybind.h │ │ │ └── detector_error_model_target_pybind_test.py │ │ ├── diagram/ │ │ │ ├── ascii_diagram.cc │ │ │ ├── ascii_diagram.h │ │ │ ├── ascii_diagram.test.cc │ │ │ ├── base64.cc │ │ │ ├── base64.h │ │ │ ├── base64.test.cc │ │ │ ├── basic_3d_diagram.cc │ │ │ ├── basic_3d_diagram.h │ │ │ ├── circuit_timeline_helper.cc │ │ │ ├── circuit_timeline_helper.h │ │ │ ├── coord.h │ │ │ ├── coord.test.cc │ │ │ ├── crumble.cc │ │ │ ├── crumble.h │ │ │ ├── crumble_data.cc │ │ │ ├── crumble_data.h │ │ │ ├── detector_slice/ │ │ │ │ ├── detector_slice_set.cc │ │ │ │ ├── detector_slice_set.h │ │ │ │ └── detector_slice_set.test.cc │ │ │ ├── diagram_util.cc │ │ │ ├── diagram_util.h │ │ │ ├── gate_data_3d.cc │ │ │ ├── gate_data_3d.h │ │ │ ├── gate_data_3d_texture_data.cc │ │ │ ├── gate_data_3d_texture_data.h │ │ │ ├── gate_data_svg.cc │ │ │ ├── gate_data_svg.h │ │ │ ├── gltf.cc │ │ │ ├── gltf.h │ │ │ ├── graph/ │ │ │ │ ├── match_graph_3d_drawer.cc │ │ │ │ ├── match_graph_3d_drawer.h │ │ │ │ ├── match_graph_3d_drawer.test.cc │ │ │ │ ├── match_graph_svg_drawer.cc │ │ │ │ ├── match_graph_svg_drawer.h │ │ │ │ └── match_graph_svg_drawer.test.cc │ │ │ ├── json_obj.cc │ │ │ ├── json_obj.h │ │ │ ├── json_obj.test.cc │ │ │ ├── lattice_map.cc │ │ │ ├── lattice_map.h │ │ │ └── timeline/ │ │ │ ├── timeline_3d_drawer.cc │ │ │ ├── timeline_3d_drawer.h │ │ │ ├── timeline_3d_drawer.test.cc │ │ │ ├── timeline_ascii_drawer.cc │ │ │ ├── timeline_ascii_drawer.h │ │ │ ├── timeline_ascii_drawer.test.cc │ │ │ ├── timeline_svg_drawer.cc │ │ │ ├── timeline_svg_drawer.h │ │ │ └── timeline_svg_drawer.test.cc │ │ ├── gates/ │ │ │ ├── gate_data_annotations.cc │ │ │ ├── gate_data_blocks.cc │ │ │ ├── gate_data_collapsing.cc │ │ │ ├── gate_data_controlled.cc │ │ │ ├── gate_data_hada.cc │ │ │ ├── gate_data_heralded.cc │ │ │ ├── gate_data_noisy.cc │ │ │ ├── gate_data_pair_measure.cc │ │ │ ├── gate_data_pauli.cc │ │ │ ├── gate_data_pauli_product.cc │ │ │ ├── gate_data_period_3.cc │ │ │ ├── gate_data_period_4.cc │ │ │ ├── gate_data_pp.cc │ │ │ ├── gate_data_swaps.cc │ │ │ ├── gates.cc │ │ │ ├── gates.h │ │ │ ├── gates.perf.cc │ │ │ ├── gates.pybind.cc │ │ │ ├── gates.pybind.h │ │ │ ├── gates.test.cc │ │ │ └── gates_test.py │ │ ├── gen/ │ │ │ ├── circuit_gen_params.cc │ │ │ ├── circuit_gen_params.h │ │ │ ├── circuit_gen_params.test.cc │ │ │ ├── gen_color_code.cc │ │ │ ├── gen_color_code.h │ │ │ ├── gen_color_code.test.cc │ │ │ ├── gen_rep_code.cc │ │ │ ├── gen_rep_code.h │ │ │ ├── gen_rep_code.test.cc │ │ │ ├── gen_surface_code.cc │ │ │ ├── gen_surface_code.h │ │ │ └── gen_surface_code.test.cc │ │ ├── io/ │ │ │ ├── README.md │ │ │ ├── measure_record.cc │ │ │ ├── measure_record.h │ │ │ ├── measure_record.test.cc │ │ │ ├── measure_record_batch.h │ │ │ ├── measure_record_batch.inl │ │ │ ├── measure_record_batch.test.cc │ │ │ ├── measure_record_batch_writer.cc │ │ │ ├── measure_record_batch_writer.h │ │ │ ├── measure_record_batch_writer.test.cc │ │ │ ├── measure_record_reader.h │ │ │ ├── measure_record_reader.inl │ │ │ ├── measure_record_reader.perf.cc │ │ │ ├── measure_record_reader.test.cc │ │ │ ├── measure_record_writer.cc │ │ │ ├── measure_record_writer.h │ │ │ ├── measure_record_writer.test.cc │ │ │ ├── raii_file.cc │ │ │ ├── raii_file.h │ │ │ ├── read_write.pybind.cc │ │ │ ├── read_write.pybind.h │ │ │ ├── read_write_pytest.py │ │ │ ├── sparse_shot.cc │ │ │ ├── sparse_shot.h │ │ │ ├── sparse_shot.test.cc │ │ │ ├── stim_data_formats.cc │ │ │ └── stim_data_formats.h │ │ ├── main.perf.cc │ │ ├── main_namespaced.cc │ │ ├── main_namespaced.h │ │ ├── main_namespaced.perf.cc │ │ ├── main_namespaced.test.cc │ │ ├── main_namespaced.test.h │ │ ├── mem/ │ │ │ ├── README.md │ │ │ ├── bit_ref.cc │ │ │ ├── bit_ref.h │ │ │ ├── bit_ref.test.cc │ │ │ ├── bitword.h │ │ │ ├── bitword_128_sse.h │ │ │ ├── bitword_256_avx.h │ │ │ ├── bitword_64.h │ │ │ ├── fixed_cap_vector.h │ │ │ ├── fixed_cap_vector.test.cc │ │ │ ├── monotonic_buffer.h │ │ │ ├── monotonic_buffer.test.cc │ │ │ ├── simd_bit_table.h │ │ │ ├── simd_bit_table.inl │ │ │ ├── simd_bit_table.perf.cc │ │ │ ├── simd_bit_table.test.cc │ │ │ ├── simd_bits.h │ │ │ ├── simd_bits.inl │ │ │ ├── simd_bits.perf.cc │ │ │ ├── simd_bits.test.cc │ │ │ ├── simd_bits_range_ref.h │ │ │ ├── simd_bits_range_ref.inl │ │ │ ├── simd_bits_range_ref.test.cc │ │ │ ├── simd_util.cc │ │ │ ├── simd_util.h │ │ │ ├── simd_util.test.cc │ │ │ ├── simd_word.cc │ │ │ ├── simd_word.h │ │ │ ├── simd_word.perf.cc │ │ │ ├── simd_word.test.cc │ │ │ ├── simd_word.test.h │ │ │ ├── span_ref.h │ │ │ ├── sparse_xor_vec.cc │ │ │ ├── sparse_xor_vec.h │ │ │ ├── sparse_xor_vec.perf.cc │ │ │ └── sparse_xor_vec.test.cc │ │ ├── perf.perf.h │ │ ├── py/ │ │ │ ├── README.md │ │ │ ├── base.pybind.cc │ │ │ ├── base.pybind.h │ │ │ ├── compiled_detector_sampler.pybind.cc │ │ │ ├── compiled_detector_sampler.pybind.h │ │ │ ├── compiled_detector_sampler_pybind_test.py │ │ │ ├── compiled_measurement_sampler.pybind.cc │ │ │ ├── compiled_measurement_sampler.pybind.h │ │ │ ├── compiled_measurement_sampler_pybind_test.py │ │ │ ├── march.pybind.cc │ │ │ ├── march.pybind.h │ │ │ ├── numpy.pybind.cc │ │ │ ├── numpy.pybind.h │ │ │ ├── stim.pybind.cc │ │ │ └── stim_pybind_test.py │ │ ├── search/ │ │ │ ├── graphlike/ │ │ │ │ ├── algo.cc │ │ │ │ ├── algo.h │ │ │ │ ├── algo.perf.cc │ │ │ │ ├── algo.test.cc │ │ │ │ ├── edge.cc │ │ │ │ ├── edge.h │ │ │ │ ├── edge.test.cc │ │ │ │ ├── graph.cc │ │ │ │ ├── graph.h │ │ │ │ ├── graph.test.cc │ │ │ │ ├── node.cc │ │ │ │ ├── node.h │ │ │ │ ├── node.test.cc │ │ │ │ ├── search_state.cc │ │ │ │ ├── search_state.h │ │ │ │ └── search_state.test.cc │ │ │ ├── hyper/ │ │ │ │ ├── algo.cc │ │ │ │ ├── algo.h │ │ │ │ ├── algo.test.cc │ │ │ │ ├── edge.cc │ │ │ │ ├── edge.h │ │ │ │ ├── edge.test.cc │ │ │ │ ├── graph.cc │ │ │ │ ├── graph.h │ │ │ │ ├── graph.test.cc │ │ │ │ ├── node.cc │ │ │ │ ├── node.h │ │ │ │ ├── node.test.cc │ │ │ │ ├── search_state.cc │ │ │ │ ├── search_state.h │ │ │ │ └── search_state.test.cc │ │ │ ├── sat/ │ │ │ │ ├── wcnf.cc │ │ │ │ ├── wcnf.h │ │ │ │ └── wcnf.test.cc │ │ │ └── search.h │ │ ├── simulators/ │ │ │ ├── README.md │ │ │ ├── dem_sampler.h │ │ │ ├── dem_sampler.inl │ │ │ ├── dem_sampler.perf.cc │ │ │ ├── dem_sampler.pybind.cc │ │ │ ├── dem_sampler.pybind.h │ │ │ ├── dem_sampler.test.cc │ │ │ ├── dem_sampler_pybind_test.py │ │ │ ├── error_analyzer.cc │ │ │ ├── error_analyzer.h │ │ │ ├── error_analyzer.perf.cc │ │ │ ├── error_analyzer.test.cc │ │ │ ├── error_matcher.cc │ │ │ ├── error_matcher.h │ │ │ ├── error_matcher.test.cc │ │ │ ├── force_streaming.cc │ │ │ ├── force_streaming.h │ │ │ ├── frame_simulator.h │ │ │ ├── frame_simulator.inl │ │ │ ├── frame_simulator.perf.cc │ │ │ ├── frame_simulator.pybind.cc │ │ │ ├── frame_simulator.pybind.h │ │ │ ├── frame_simulator.test.cc │ │ │ ├── frame_simulator_pybind_test.py │ │ │ ├── frame_simulator_util.h │ │ │ ├── frame_simulator_util.inl │ │ │ ├── frame_simulator_util.test.cc │ │ │ ├── graph_simulator.cc │ │ │ ├── graph_simulator.h │ │ │ ├── graph_simulator.test.cc │ │ │ ├── matched_error.cc │ │ │ ├── matched_error.h │ │ │ ├── matched_error.pybind.cc │ │ │ ├── matched_error.pybind.h │ │ │ ├── matched_error.test.cc │ │ │ ├── matched_error_pybind_test.py │ │ │ ├── measurements_to_detection_events.h │ │ │ ├── measurements_to_detection_events.inl │ │ │ ├── measurements_to_detection_events.pybind.cc │ │ │ ├── measurements_to_detection_events.pybind.h │ │ │ ├── measurements_to_detection_events.test.cc │ │ │ ├── measurements_to_detection_events_test.py │ │ │ ├── sparse_rev_frame_tracker.cc │ │ │ ├── sparse_rev_frame_tracker.h │ │ │ ├── sparse_rev_frame_tracker.test.cc │ │ │ ├── tableau_simulator.h │ │ │ ├── tableau_simulator.inl │ │ │ ├── tableau_simulator.perf.cc │ │ │ ├── tableau_simulator.pybind.cc │ │ │ ├── tableau_simulator.pybind.h │ │ │ ├── tableau_simulator.test.cc │ │ │ ├── tableau_simulator_pybind_test.py │ │ │ ├── vector_simulator.cc │ │ │ ├── vector_simulator.h │ │ │ └── vector_simulator.test.cc │ │ ├── stabilizers/ │ │ │ ├── README.md │ │ │ ├── clifford_string.h │ │ │ ├── clifford_string.perf.cc │ │ │ ├── clifford_string.pybind.cc │ │ │ ├── clifford_string.pybind.h │ │ │ ├── clifford_string.test.cc │ │ │ ├── clifford_string_pybind_test.py │ │ │ ├── flex_pauli_string.cc │ │ │ ├── flex_pauli_string.h │ │ │ ├── flex_pauli_string.test.cc │ │ │ ├── flow.h │ │ │ ├── flow.inl │ │ │ ├── flow.pybind.cc │ │ │ ├── flow.pybind.h │ │ │ ├── flow.test.cc │ │ │ ├── flow_pybind_test.py │ │ │ ├── pauli_string.h │ │ │ ├── pauli_string.inl │ │ │ ├── pauli_string.perf.cc │ │ │ ├── pauli_string.pybind.cc │ │ │ ├── pauli_string.pybind.h │ │ │ ├── pauli_string.test.cc │ │ │ ├── pauli_string_iter.h │ │ │ ├── pauli_string_iter.inl │ │ │ ├── pauli_string_iter.perf.cc │ │ │ ├── pauli_string_iter.pybind.cc │ │ │ ├── pauli_string_iter.pybind.h │ │ │ ├── pauli_string_iter.test.cc │ │ │ ├── pauli_string_pybind_test.py │ │ │ ├── pauli_string_ref.h │ │ │ ├── pauli_string_ref.inl │ │ │ ├── pauli_string_ref.test.cc │ │ │ ├── tableau.h │ │ │ ├── tableau.inl │ │ │ ├── tableau.perf.cc │ │ │ ├── tableau.pybind.cc │ │ │ ├── tableau.pybind.h │ │ │ ├── tableau.test.cc │ │ │ ├── tableau_iter.h │ │ │ ├── tableau_iter.inl │ │ │ ├── tableau_iter.perf.cc │ │ │ ├── tableau_iter.pybind.cc │ │ │ ├── tableau_iter.pybind.h │ │ │ ├── tableau_iter.test.cc │ │ │ ├── tableau_pybind_test.py │ │ │ ├── tableau_specialized_prepend.inl │ │ │ ├── tableau_transposed_raii.h │ │ │ └── tableau_transposed_raii.inl │ │ ├── util_bot/ │ │ │ ├── arg_parse.cc │ │ │ ├── arg_parse.h │ │ │ ├── arg_parse.test.cc │ │ │ ├── error_decomp.cc │ │ │ ├── error_decomp.h │ │ │ ├── error_decomp.perf.cc │ │ │ ├── error_decomp.test.cc │ │ │ ├── probability_util.cc │ │ │ ├── probability_util.h │ │ │ ├── probability_util.perf.cc │ │ │ ├── probability_util.test.cc │ │ │ ├── str_util.h │ │ │ ├── str_util.test.cc │ │ │ ├── test_util.test.cc │ │ │ ├── test_util.test.h │ │ │ ├── twiddle.h │ │ │ └── twiddle.test.cc │ │ └── util_top/ │ │ ├── circuit_flow_generators.h │ │ ├── circuit_flow_generators.inl │ │ ├── circuit_flow_generators.test.cc │ │ ├── circuit_flow_generators_test.py │ │ ├── circuit_inverse_qec.cc │ │ ├── circuit_inverse_qec.h │ │ ├── circuit_inverse_qec.inl │ │ ├── circuit_inverse_qec.test.cc │ │ ├── circuit_inverse_qec_test.py │ │ ├── circuit_inverse_unitary.cc │ │ ├── circuit_inverse_unitary.h │ │ ├── circuit_inverse_unitary.test.cc │ │ ├── circuit_to_dem.h │ │ ├── circuit_to_dem.test.cc │ │ ├── circuit_to_detecting_regions.cc │ │ ├── circuit_to_detecting_regions.h │ │ ├── circuit_to_detecting_regions.test.cc │ │ ├── circuit_to_detecting_regions_test.py │ │ ├── circuit_vs_amplitudes.cc │ │ ├── circuit_vs_amplitudes.h │ │ ├── circuit_vs_amplitudes.test.cc │ │ ├── circuit_vs_tableau.h │ │ ├── circuit_vs_tableau.inl │ │ ├── circuit_vs_tableau.test.cc │ │ ├── count_determined_measurements.h │ │ ├── count_determined_measurements.inl │ │ ├── count_determined_measurements.test.cc │ │ ├── export_crumble_url.cc │ │ ├── export_crumble_url.h │ │ ├── export_crumble_url.test.cc │ │ ├── export_crumble_url_pybind_test.py │ │ ├── export_qasm.cc │ │ ├── export_qasm.h │ │ ├── export_qasm.test.cc │ │ ├── export_qasm_pybind_test.py │ │ ├── export_quirk_url.cc │ │ ├── export_quirk_url.h │ │ ├── export_quirk_url.test.cc │ │ ├── export_quirk_url_pybind_test.py │ │ ├── has_flow.cc │ │ ├── has_flow.h │ │ ├── has_flow.inl │ │ ├── has_flow.test.cc │ │ ├── mbqc_decomposition.cc │ │ ├── mbqc_decomposition.h │ │ ├── mbqc_decomposition.test.cc │ │ ├── missing_detectors.cc │ │ ├── missing_detectors.h │ │ ├── missing_detectors.test.cc │ │ ├── reference_sample_tree.cc │ │ ├── reference_sample_tree.h │ │ ├── reference_sample_tree.inl │ │ ├── reference_sample_tree.perf.cc │ │ ├── reference_sample_tree.test.cc │ │ ├── simplified_circuit.cc │ │ ├── simplified_circuit.h │ │ ├── simplified_circuit.test.cc │ │ ├── stabilizers_to_tableau.h │ │ ├── stabilizers_to_tableau.inl │ │ ├── stabilizers_to_tableau.perf.cc │ │ ├── stabilizers_to_tableau.test.cc │ │ ├── stabilizers_vs_amplitudes.h │ │ ├── stabilizers_vs_amplitudes.inl │ │ ├── stabilizers_vs_amplitudes.test.cc │ │ ├── transform_without_feedback.cc │ │ ├── transform_without_feedback.h │ │ └── transform_without_feedback.test.cc │ ├── stim.cc │ ├── stim.h │ ├── stim.test.cc │ └── stim_included_twice.test.cc └── testdata/ ├── circuit_all_ops_3d.gltf ├── classical_feedback.gltf ├── collapsing.gltf ├── detector_pseudo_targets.gltf ├── empty_match_graph.gltf ├── lattice_surgery_cnot.gltf ├── match_graph_no_coords.gltf ├── match_graph_repetition_code.gltf ├── match_graph_surface_code.gltf ├── measurement_looping.gltf ├── noise_gates_1.gltf ├── noise_gates_2.gltf ├── noise_gates_3.gltf ├── repeat.gltf ├── repetition_code.gltf ├── single_qubits_gates.gltf ├── surface_code.gltf ├── test_circuit_all_ops.gltf ├── tick.gltf └── two_qubits_gates.gltf ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bazelrc ================================================ test --test_output=errors ================================================ FILE: .bazelversion ================================================ 7.2.0 ================================================ FILE: .clang-format ================================================ --- Language: Cpp BasedOnStyle: Google IndentWidth: 4 ColumnLimit: 120 AlignAfterOpenBracket: AlwaysBreak AllowShortBlocksOnASingleLine: Never AllowShortFunctionsOnASingleLine: None AllowShortIfStatementsOnASingleLine: Never AllowShortLoopsOnASingleLine: false AllowShortLambdasOnASingleLine: None BinPackArguments: false BinPackParameters: false PointerAlignment: Right IncludeCategories: - Regex: '^<.*>$' Priority: 1 - Regex: '^"gtest/.*"$' Priority: 2 - Regex: '^".*"$' Priority: 3 ... ================================================ FILE: .editorconfig ================================================ # Copyright 2025 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 # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Common editor configurations for this project. # # EditorConfig is a file format for specifying some common style parameters. # Many IDEs & editors read .editorconfig files, either natively or via plugins. # We mostly follow Google's style guides (https://google.github.io/styleguide/) # with only a few deviations for line length and indentation in some files. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ root = true [*] charset = utf-8 indent_style = space insert_final_newline = true spelling_language = en-US trim_trailing_whitespace = true max_line_length = 120 [{BUILD,*.BUILD,*.bzl,*.bazel,.bazelrc}] indent_size = 4 [{*.cc,*.h}] indent_size = 4 [{*.js,*.ts}] indent_size = 4 [*.json] indent_size = 2 [*.py] indent_size = 4 [*.sh] indent_size = 4 [{*.yaml,*.yml}] indent_size = 2 ================================================ FILE: .github/SECURITY.md ================================================ # Reporting security issues The Stim developers and community take security bugs in Stim seriously. We appreciate your efforts to disclose your findings responsibly, and will make every effort to acknowledge your contributions. Please **do not** use GitHub issues to report security vulnerabilities; GitHub issues are public, and doing so could allow someone to exploit the information before the problem can be addressed. Instead, please use the GitHub ["Report a Vulnerability"](https://github.com/quantumlib/Stim/security/advisories/new) interface from the _Security_ tab of the Stim repository. Please report security issues in third-party modules to the person or team maintaining the module rather than the Stim project stewards, unless you believe that some action needs to be taken with Stim in order to guard against the effects of a security vulnerability in a third-party module. ## Responses to security reports The project stewards at Google Quantum AI will send a response indicating the next steps in handling your report. After the initial reply to your report, the project stewards will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. ## Additional points of contact Please contact the project stewards at Google Quantum AI via email at quantum-oss-maintainers@google.com if you have questions or other concerns. If for any reason you are uncomfortable reaching out to the project stewards, please email opensource@google.com. ================================================ FILE: .github/workflows/ci.yml ================================================ # 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. name: ci on: push: branches: - main tags: - v* pull_request: branches: - main jobs: build_dist: runs-on: ${{ matrix.os_dist.os }} strategy: fail-fast: false matrix: os_dist: [ {os: ubuntu-24.04, dist: cp38-manylinux_x86_64}, {os: ubuntu-24.04, dist: cp39-manylinux_x86_64}, {os: ubuntu-24.04, dist: cp310-manylinux_x86_64}, {os: ubuntu-24.04, dist: cp311-manylinux_x86_64}, {os: ubuntu-24.04, dist: cp312-manylinux_x86_64}, {os: ubuntu-24.04, dist: cp313-manylinux_x86_64}, {os: ubuntu-24.04, dist: cp314-manylinux_x86_64}, # cp38-manylinux_i686 disabled because pandas isn't prebuilt and takes 20 minutes to build. # {os: ubuntu-latest, dist: cp38-manylinux_i686}, # cp39-manylinux_i686 disabled because pandas isn't prebuilt and takes 20 minutes to build. # {os: ubuntu-latest, dist: cp39-manylinux_i686}, # cp310-manylinux_i686 disabled because scipy isn't prebuilt and fails to build. # # The actual error seen in github actions: # # numpy.distutils.system_info.NotFoundError: No BLAS/LAPACK # libraries found. To build Scipy from sources, BLAS & LAPACK # libraries need to be installed. # #{os: ubuntu-latest, dist: cp310-manylinux_i686}, # pypy manylinux builds disabled because scipy isn't prebuilt and fails to build. # # The actual error seen in github actions: # # numpy.distutils.system_info.NotFoundError: No BLAS/LAPACK # libraries found. To build Scipy from sources, BLAS & LAPACK # libraries need to be installed. # # {os: ubuntu-latest, dist: pp37-manylinux_x86_64}, # {os: ubuntu-latest, dist: pp38-manylinux_x86_64}, # {os: ubuntu-latest, dist: pp39-manylinux_x86_64}, # {os: ubuntu-latest, dist: pp37-manylinux_i686}, # {os: ubuntu-latest, dist: pp38-manylinux_i686}, # {os: ubuntu-latest, dist: pp39-manylinux_i686}, # musllinux builds disabled because scipy isn't prebuilt and fails to build. # # The actual error seen in github actions: # # numpy.distutils.system_info.NotFoundError: No BLAS/LAPACK # libraries found. To build Scipy from sources, BLAS & LAPACK # libraries need to be installed. # # {os: ubuntu-latest, dist: cp36-musllinux_x86_64}, # {os: ubuntu-latest, dist: cp37-musllinux_x86_64}, # {os: ubuntu-latest, dist: cp38-musllinux_x86_64}, # {os: ubuntu-latest, dist: cp39-musllinux_x86_64}, # {os: ubuntu-latest, dist: cp310-musllinux_x86_64}, # {os: ubuntu-latest, dist: cp36-musllinux_i686}, # {os: ubuntu-latest, dist: cp37-musllinux_i686}, # {os: ubuntu-latest, dist: cp38-musllinux_i686}, # {os: ubuntu-latest, dist: cp39-musllinux_i686}, # {os: ubuntu-latest, dist: cp310-musllinux_i686}, {os: macos-14, dist: cp38-macosx_x86_64, macosarch: x86_64}, {os: macos-14, dist: cp39-macosx_x86_64, macosarch: x86_64}, {os: macos-14, dist: cp310-macosx_x86_64, macosarch: x86_64}, {os: macos-14, dist: cp311-macosx_x86_64, macosarch: x86_64}, {os: macos-14, dist: cp312-macosx_x86_64, macosarch: x86_64}, {os: macos-14, dist: cp313-macosx_x86_64, macosarch: x86_64}, {os: macos-14, dist: cp314-macosx_x86_64, macosarch: x86_64}, {os: macos-14, dist: cp38-macosx_arm64, macosarch: arm64}, {os: macos-14, dist: cp39-macosx_arm64, macosarch: arm64}, {os: macos-14, dist: cp310-macosx_arm64, macosarch: arm64}, {os: macos-14, dist: cp311-macosx_arm64, macosarch: arm64}, {os: macos-14, dist: cp312-macosx_arm64, macosarch: arm64}, {os: macos-14, dist: cp313-macosx_arm64, macosarch: arm64}, {os: macos-14, dist: cp314-macosx_arm64, macosarch: arm64}, # pypy OSX builds disabled because numpy isn't prebuilt and fails to build. # # The actual error seen in github actions: # # RuntimeError: Found /usr/lib/libcblas.dylib, but that file is a # symbolic link to the MacOS Accelerate framework, which is not # supported by NumPy. You must configure the build to use a # different optimized library, or disable the use of optimized # BLAS and LAPACK by setting the environment variables # NPY_BLAS_ORDER="" and NPY_LAPACK_ORDER="" before building NumPy. # # {os: macOS-10.15, dist: pp37-macosx_x86_64}, # {os: macOS-10.15, dist: pp38-macosx_x86_64}, # {os: macOS-10.15, dist: pp39-macosx_x86_64}, {os: windows-2025, dist: cp38-win_amd64}, {os: windows-2025, dist: cp39-win_amd64}, {os: windows-2025, dist: cp310-win_amd64}, {os: windows-2025, dist: cp311-win_amd64}, {os: windows-2025, dist: cp312-win_amd64}, {os: windows-2025, dist: cp313-win_amd64}, {os: windows-2025, dist: cp314-win_amd64}, # cp38-win32 and cp39-win32 disabled because scipy fails to build. # # The actual error seen in github actions: # # Need python for 64-bit, but found 32-bit # ..\..\meson.build:82:0: ERROR: Python dependency not found # #{os: windows-2025, dist: cp38-win32}, #{os: windows-2025, dist: cp39-win32}, # cp310-win32 disabled because numpy isn't prebuilt and fails to build. # # The actual error seen in github actions: # # CCompiler_spawn() got an unexpected keyword argument 'env' # # {os: windows-2025, dist: cp310-win32}, ] env: CIBW_BUILD: "${{ matrix.os_dist.dist }}" CIBW_ARCHS_MACOS: "${{ matrix.os_dist.macosarch }}" CIBW_TEST_REQUIRES: cirq-core pytest CIBW_TEST_COMMAND: pytest {project}/src {project}/glue/cirq && stim help steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3 - run: python dev/overwrite_dev_versions_with_date.py - run: python -m pip install pybind11~=2.11.1 cibuildwheel~=3.3.1 setuptools wheel - run: python -m cibuildwheel --print-build-identifiers - run: python -m cibuildwheel --output-dir dist - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: "dist-stim-${{ matrix.os_dist.os }}-${{ matrix.os_dist.dist }}-${{ matrix.os_dist.macosarch }}" path: dist/* build_sdist: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3 - run: python -m pip install setuptools pybind11~=2.11.1 - run: python dev/overwrite_dev_versions_with_date.py - run: mkdir output - run: python setup.py sdist - run: cd glue/cirq && python setup.py sdist - run: cd glue/cirq && python setup.py bdist_wheel - run: cd glue/sample && python setup.py sdist - run: cd glue/sample && python setup.py bdist_wheel - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: "dist-sinter" path: glue/sample/dist/*.tar.gz - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: "bdist-sinter" path: glue/sample/dist/*.whl - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: "dist-stimcirq" path: glue/cirq/dist/*.tar.gz - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: "bdist-stimcirq" path: glue/cirq/dist/*.whl - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: "dist-stim-sdist" path: dist/*.tar.gz check_sdist_installs_stim: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3 - run: python -m pip install pybind11~=2.11.1 cibuildwheel~=3.3.1 setuptools wheel - run: python setup.py sdist - run: pip install dist/*.tar.gz check_sdist_installs_sinter: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3 - run: python -m pip install setuptools - run: cd glue/sample && python setup.py sdist - run: pip install glue/sample/dist/* - run: python -c "import sinter" merge_upload_artifacts: needs: ["build_dist", "build_sdist"] runs-on: ubuntu-24.04 steps: - name: Merge Artifacts uses: actions/upload-artifact/merge@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 with: name: dist-stim pattern: dist-stim-* upload_dev_release_to_pypi: needs: ["merge_upload_artifacts"] if: github.ref == 'refs/heads/main' runs-on: ubuntu-24.04 steps: - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: dist-stim path: dist-stim - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: dist-stimcirq path: dist-stimcirq - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: dist-sinter path: dist-sinter - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 with: user: __token__ packages_dir: dist-stim password: ${{ secrets.pypi_token_stim }} - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 with: user: __token__ packages_dir: dist-stimcirq password: ${{ secrets.pypi_token_stimcirq }} - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 with: user: __token__ packages_dir: dist-sinter password: ${{ secrets.pypi_token_sinter }} run_main: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: cmake . - run: make stim -j 2 - run: echo -e "H 0 \n CNOT 0 1 \n M 0 1" | out/stim --sample build_bazel: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - 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 :all - run: bazel test :stim_test build_clang_stim_test: runs-on: 'ubuntu-24.04' steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: | cd .. git clone https://github.com/google/googletest.git -b release-1.12.1 mkdir googletest/build && cd googletest/build cmake .. -DBUILD_GMOCK=OFF make sudo make install - uses: egor-tensin/setup-clang@ef434b41eb33a70396fb336b1bae39c76d740c3d # v1 with: version: latest platform: x64 - run: cmake . -DCMAKE_C_COMPILER=cc -DCMAKE_CXX_COMPILER=c++ - run: cmake --build . --target stim_test build_clang_stim: runs-on: 'ubuntu-24.04' steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: | cd .. git clone https://github.com/google/googletest.git -b release-1.12.1 mkdir googletest/build && cd googletest/build cmake .. -DBUILD_GMOCK=OFF make sudo make install - uses: egor-tensin/setup-clang@ef434b41eb33a70396fb336b1bae39c76d740c3d # v1 with: version: latest platform: x64 - run: cmake . -DCMAKE_C_COMPILER=cc -DCMAKE_CXX_COMPILER=c++ - run: cmake --build . --target stim build_clang_stim_perf: runs-on: 'ubuntu-24.04' steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: | cd .. git clone https://github.com/google/googletest.git -b release-1.12.1 mkdir googletest/build && cd googletest/build cmake .. -DBUILD_GMOCK=OFF make sudo make install - uses: egor-tensin/setup-clang@ef434b41eb33a70396fb336b1bae39c76d740c3d # v1 with: version: latest platform: x64 - run: cmake . -DCMAKE_C_COMPILER=cc -DCMAKE_CXX_COMPILER=c++ - run: cmake --build . --target stim_perf build_lib: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: cmake . - run: make libstim -j 2 - run: echo -e '#include "stim.h"\nint main(int argc,const char **argv) {return !stim::find_bool_argument("test", argc, argv);}' > test.cc - run: g++ -std=c++20 test.cc out/libstim.a -I src - run: ./a.out test build_lib_install: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: mkdir install_dir - run: cmake . -DCMAKE_INSTALL_PREFIX=install_dir - run: make -j 2 - run: make install - run: echo -e '#include "stim.h"\nint main(int argc,const char **argv) {return !stim::find_bool_argument("test", argc, argv);}' > test.cc - run: g++ -std=c++20 test.cc install_dir/lib/libstim.a -I install_dir/include - run: ./a.out test - run: echo -e "H 0 \n CNOT 0 1 \n M 0 1" | install_dir/bin/stim --sample benchmark_windows: runs-on: windows-2025 steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d # v1.0.2 - run: cmake . - run: MSBuild.exe stim_perf.vcxproj /p:Configuration=Release /p:OutDir=msbuild_out /p:O=2 - run: msbuild_out/stim_perf.exe benchmark: runs-on: ubuntu-24.04 strategy: matrix: simd_width: [64, 128, 256] steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: cmake . -DSIMD_WIDTH=${{ matrix.simd_width }} - run: make stim_perf -j 2 - run: out/stim_perf test: runs-on: ubuntu-24.04 strategy: matrix: simd_width: [64, 128, 256] steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: | cd .. git clone https://github.com/google/googletest.git -b release-1.12.1 mkdir googletest/build && cd googletest/build cmake .. -DBUILD_GMOCK=OFF make sudo make install - run: cmake . -DSIMD_WIDTH=${{ matrix.simd_width }} - run: make stim_test -j 2 - run: out/stim_test test_o3: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 - run: | cd .. git clone https://github.com/google/googletest.git -b release-1.12.1 mkdir googletest/build && cd googletest/build cmake .. -DBUILD_GMOCK=OFF make sudo make install - run: cmake . -DSIMD_WIDTH=256 - run: make stim_test_o3 -j 2 - run: out/stim_test_o3 test_generated_docs_are_fresh: 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 - uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1 with: node-version: 16.x - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 with: python-version: '3.13' - run: bazel build :stim_dev_wheel - run: pip install bazel-bin/stim-0.0.dev0-py3-none-any.whl - run: diff <(python dev/gen_stim_api_reference.py -dev) doc/python_api_reference_vDev.md - run: diff <(python dev/gen_stim_stub_file.py -dev) glue/python/src/stim/__init__.pyi - run: diff <(python dev/gen_stim_stub_file.py -dev) doc/stim.pyi - run: diff <(python -c "import stim; stim.main(command_line_args=['help', 'gates_markdown'])") doc/gates.md - run: diff <(python -c "import stim; stim.main(command_line_args=['help', 'formats_markdown'])") doc/result_formats.md - run: diff <(python -c "import stim; stim.main(command_line_args=['help', 'commands_markdown'])") doc/usage_command_line.md - run: diff <(dev/gen_known_gates_for_js.sh) glue/crumble/test/generated_gate_name_list.test.js - run: python doc/stim.pyi - run: npm install -g rollup@3.21.2 uglify-js@3.17.4 - 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 test_generated_file_lists_are_fresh: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - run: dev/regen_file_lists.sh /tmp - run: diff /tmp/perf_files file_lists/perf_files - run: diff /tmp/pybind_files file_lists/pybind_files - run: diff /tmp/source_files_no_main file_lists/source_files_no_main - run: diff /tmp/test_files file_lists/test_files test_pybind: 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 pytest - run: pytest src - run: dev/doctest_proper.py --module stim test_stimcirq: 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/cirq - run: pip install pytest - run: pytest glue/cirq - run: dev/doctest_proper.py --module stimcirq --import cirq sympy test_sinter: 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/sample - run: pip install pytest pymatching fusion-blossom~=0.1.4 mwpf~=0.1.5 - run: pytest glue/sample - run: dev/doctest_proper.py --module sinter - run: sinter help test_stimzx: 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/zx - run: pip install pytest - run: pytest glue/zx - run: dev/doctest_proper.py --module stimzx test_stimjs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - uses: mymindstorm/setup-emsdk@6ab9eb1bda2574c4ddb79809fc9247783eaf9021 # v14 with: version: 4.0.1 actions-cache-folder: 'emsdk-cache' - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4 with: node-version: 18.x - run: npm install - run: bash glue/javascript/build_wasm.sh - run: node puppeteer_run_tests.js test_crumble: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3 with: node-version: 16 - run: node glue/crumble/run_tests_headless.js ================================================ FILE: .gitignore ================================================ .idea/* cmake-build-debug/* a.out perf.data perf.data.old CMakeFiles/* CMakeCache.txt googletest-build/* googletest-download/* googletest-src cmake_install.cmake Makefile lib out stim.cbp data/* _deps/* bazel-* python_build_stim/* *.egg-* stim.cpython* dist MANIFEST *.whl *.swp __pycache__ pip-wheel-metadata/* wheelhouse/* *.pyd venv .venv **/LastTest.log *.so .clwb/* .cmake/* coverage/* cmake-build-debug-coverage/* compile_commands.json .cache build.ninja .ninja_deps .ninja_log .pytest_cache node_modules MODULE.bazel.lock .ninja_lock ================================================ FILE: .markdownlintrc ================================================ { // -*- jsonc -*- // Copyright 2025 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 // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Markdownlint linter configuration for this project. // // Note: there are multiple programs programs named "markdownlint". We use // https://github.com/igorshubovych/markdownlint-cli/, which is the one you // get with "brew install markdownlint" on MacOS. // // These settings try to stay close to the Google Markdown Style as // described at https://google.github.io/styleguide/docguide/style.html // with very few differences. // // For a list of configuration options, see the following page: // https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md // (Beware that the above looks similar but is NOT the same as the page // https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md.) // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/main/schema/markdownlint-config-schema.json", // Require ATX-style headings. // https://google.github.io/styleguide/docguide/style.html#atx-style-headings "headings": { "style": "atx" }, // Google style does not require that the first line of a file is a heading // for the title; it only states that the first heading should be a level 1. // https://google.github.io/styleguide/docguide/style.html#document-layout "first-line-heading": false, // The Google style does not define what to do about trailing punctuation in // headings. The markdownlint default disallows exclamation points, which // seems likely to be more annoying than useful – I have definitely seen // people use exclamation points in headings in README files on GitHub. // This setting removes exclamation point from the banned characters. "no-trailing-punctuation": { "punctuation": ".,;:。,;:" }, // No trailing spaces. // https://google.github.io/styleguide/docguide/style.html#trailing-whitespace "whitespace": { "br_spaces": 0 }, // Google style is 80 characters. // Google style exempts some constructs from the line-length limit: // https://google.github.io/styleguide/docguide/style.html#exceptions "line-length": { "line_length": 120, "code_block_line_length": 120, "heading_line_length": 120, "code_blocks": false, "headings": false, "tables": false }, // Google Markdown style specifies 2 spaces after item numbers, 3 spaces // after bullets, so that the text itself is consistently indented 4 spaces. // https://google.github.io/styleguide/docguide/style.html#nested-list-spacing "list-marker-space": { "ol_multi": 2, "ol_single": 2, "ul_multi": 3, "ul_single": 3 }, "ul-indent": { "indent": 4 }, // Bare URLs are allowed in GitHub-flavored Markdown and in Google’s style. "no-bare-urls": false, // Basic Markdown allows raw HTML. Both GitHub & PyPI support subsets of // HTML, though it's unclear what subset PyPI supports. Google's style guide // recommends against using raw HTML, but does allow it. (C.f. the bottom of // https://google.github.io/styleguide/docguide/style.html) Google's in-house // documentation system allows many inline and block-level tags, but strips // others that can pose security risks (e.g., and standalone ). // The list below tries to capture the intersection of what GitHub allows // (c.f. https://github.com/github/markup/issues/245#issuecomment-682231577), // what PyPI seems to allow, what Google allows, and what seems likely to be // most useful in situations where someone needs to reach for HTML. "html": { "allowed_elements": [ "a", "abbr", "b", "blockquote", "br", "caption", "cite", "code", "dd", "del", "details", "dfn", "div", "dl", "dt", "em", "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd", "li", "mark", "ol", "p", "picture", "pre", "q", "s", "samp", "small", "span", "strong", "sub", "summary", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "time", "tr", "tt", "ul", "var", "wbr" ] } } ================================================ FILE: BUILD ================================================ package(default_visibility = ["//visibility:public"]) load("@rules_python//python:packaging.bzl", "py_wheel") SOURCE_FILES_NO_MAIN = glob( [ "src/**/*.cc", "src/**/*.h", "src/**/*.inl", ], exclude = glob([ "src/**/*.test.cc", "src/**/*.test.h", "src/**/*.perf.cc", "src/**/*.perf.h", "src/**/*.pybind.cc", "src/**/*.pybind.h", "src/**/main.cc", ]), ) TEST_FILES = glob( [ "src/**/*.test.cc", "src/**/*.test.h", ], ) PERF_FILES = glob( [ "src/**/*.perf.cc", "src/**/*.perf.h", ], ) PYBIND_FILES = glob( [ "src/**/*.pybind.cc", "src/**/*.pybind.h", ], ) cc_library( name = "stim_lib", srcs = SOURCE_FILES_NO_MAIN, copts = [ "-std=c++20", ], includes = ["src/"], ) cc_binary( name = "stim", srcs = SOURCE_FILES_NO_MAIN + glob(["src/**/main.cc"]), copts = [ "-std=c++20", "-march=native", "-O3", ], includes = ["src/"], ) cc_binary( name = "stim_benchmark", srcs = SOURCE_FILES_NO_MAIN + PERF_FILES, copts = [ "-std=c++20", "-march=native", "-O3", ], includes = ["src/"], ) cc_test( name = "stim_test", srcs = SOURCE_FILES_NO_MAIN + TEST_FILES, copts = [ "-std=c++20", "-march=native", ], data = glob(["testdata/**"]), includes = ["src/"], deps = [ "@googletest//:gtest", "@googletest//:gtest_main", ], ) cc_binary( name = "stim.so", srcs = SOURCE_FILES_NO_MAIN + PYBIND_FILES, copts = [ "-O3", "-std=c++20", "-fvisibility=hidden", "-march=native", "-DSTIM_PYBIND11_MODULE_NAME=stim", "-DVERSION_INFO=0.0.dev0", ], includes = ["src/"], linkshared = 1, deps = ["@pybind11//:pybind11"], ) genrule( name = "stim_wheel_files", srcs = ["doc/stim.pyi"], outs = ["stim.pyi"], cmd = "cp $(location doc/stim.pyi) $@", ) py_wheel( name = "stim_dev_wheel", distribution = "stim", requires = ["numpy"], version = "0.0.dev0", deps = [ ":stim.so", ":stim_wheel_files", ], ) ================================================ FILE: CITATION.cff ================================================ # Citation information for this repository. -*- yaml -*- # # CITATION.cff files provide human- & machine-readable citation information for # software and datasets. GitHub, Zenodo, and the Zotero browser plugin all use # CFF files automatically if provided. https://citation-file-format.github.io/. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cff-version: 1.2.0 message: If you use Stim, please cite it using this metadata. # CITATION.cff files describe how to cite software or datasets, with the goal of # making software and data be citable in their own right. However, sometimes # projects want citations to go to a paper instead. 'Preferred-citation' serves # to communicate that. The distinction matters in different situations. If this # field is present, GitHub uses the value for the "cite this repository" button # and ignores the rest of this file; conversely, the Zenodo-GitHub integration # ignores this field when creating an entry for a new software release because # the Zenodo entry is specifically about the software in this repository. preferred-citation: type: article authors: - family-names: Gidney given-names: Craig title: 'Stim: a fast stabilizer circuit simulator' journal: Quantum year: 2021 month: 7 volume: 5 start: 497 issn: 2521-327X url: https://doi.org/10.22331/q-2021-07-06-497 publisher: name: >- Verein zur Förderung des Open Access Publizierens in den Quantenwissenschaften # The remaining metadata in this file describes the current software release. title: Stim abstract: >- Stim is a tool for high performance simulation and analysis of quantum stabilizer circuits, especially quantum error correction (QEC) circuits. authors: - family-names: Gidney given-names: Craig version: 1.14.0 date-released: 2024-09-24 url: https://github.com/quantumlib/stim repository-code: https://github.com/quantumlib/stim license: Apache-2.0 type: software identifiers: - description: The GitHub repository for Stim value: https://github.com/quantumlib/Stim type: url - description: PyPI project for Stim value: https://pypi.org/project/Stim type: url - description: Publication about Stim value: 10.22331/q-2021-07-06-497 type: doi keywords: - algorithms - API - application programming interface - C++ - Clifford gates - Clifford operations - high performance - open-source software - physics - Python - QEC - quantum - quantum algorithms - quantum circuits - quantum computing - quantum error correction - quantum error decoder - quantum gates - quantum programming - quantum simulation - quantum state - qubit - science - SDK - simulation - simulator - software - software development toolkit - stabilizer circuits ================================================ FILE: CMakeLists.txt ================================================ # 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. cmake_minimum_required(VERSION 3.13) project(stim) include_directories(src) set(CMAKE_CXX_STANDARD 20) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY out) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY out) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY out) # Convert desired SIMD_WIDTH into machine architecture flags. if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|I386|ARM64)$") if(NOT(SIMD_WIDTH)) set(MACHINE_FLAG "-march=native") elseif(SIMD_WIDTH EQUAL 256) set(MACHINE_FLAG "-mavx2" "-msse2") elseif(SIMD_WIDTH EQUAL 128) set(MACHINE_FLAG "-mno-avx2" "-msse2") elseif(SIMD_WIDTH EQUAL 64) set(MACHINE_FLAG "-mno-avx2" "-mno-sse2") endif() else () set(MACHINE_FLAG "") endif() # make changes to file_lists trigger a reconfigure set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS file_lists/source_files_no_main) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS file_lists/test_files) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS file_lists/perf_files) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS file_lists/pybind_files) file(STRINGS file_lists/source_files_no_main SOURCE_FILES_NO_MAIN) file(STRINGS file_lists/test_files TEST_FILES) file(STRINGS file_lists/perf_files PERF_FILES) file(STRINGS file_lists/pybind_files PYBIND_FILES) add_executable(stim src/main.cc ${SOURCE_FILES_NO_MAIN}) if(NOT(MSVC)) target_compile_options(stim PRIVATE -O3 -Wall -Wpedantic -fno-strict-aliasing ${MACHINE_FLAG}) target_link_options(stim PRIVATE -O3) else() target_compile_options(stim PRIVATE ${MACHINE_FLAG} /O1) endif() install(TARGETS stim RUNTIME DESTINATION bin) add_library(libstim ${SOURCE_FILES_NO_MAIN}) set_target_properties(libstim PROPERTIES PREFIX "") target_include_directories(libstim PUBLIC src) if(NOT(MSVC)) target_compile_options(libstim PRIVATE -O3 -Wall -Wpedantic -fPIC -fno-strict-aliasing ${MACHINE_FLAG}) target_link_options(libstim PRIVATE -O3) else() target_compile_options(libstim PRIVATE ${MACHINE_FLAG} /O1) endif() install(TARGETS libstim LIBRARY DESTINATION) install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/" DESTINATION "include" FILES_MATCHING PATTERN "*.h" PATTERN "*.inl") add_executable(stim_perf ${SOURCE_FILES_NO_MAIN} ${PERF_FILES}) if(NOT(MSVC)) target_compile_options(stim_perf PRIVATE -Wall -Wpedantic -O3 -fno-strict-aliasing ${MACHINE_FLAG}) target_link_options(stim_perf PRIVATE) else() target_compile_options(stim_perf PRIVATE ${MACHINE_FLAG} /O1) endif() find_package(GTest QUIET) if(${GTest_FOUND}) add_executable(stim_test ${SOURCE_FILES_NO_MAIN} ${TEST_FILES}) target_link_libraries(stim_test GTest::gtest GTest::gtest_main) target_compile_options(stim_test PRIVATE -Wall -Wpedantic -g -fno-omit-frame-pointer -fno-strict-aliasing -fsanitize=undefined -fsanitize=address ${MACHINE_FLAG}) target_link_options(stim_test PRIVATE -g -fno-omit-frame-pointer -fsanitize=undefined -fsanitize=address) add_executable(stim_test_o3 ${SOURCE_FILES_NO_MAIN} ${TEST_FILES}) target_link_libraries(stim_test_o3 GTest::gtest GTest::gtest_main) target_compile_options(stim_test_o3 PRIVATE -O3 -Wall -Wpedantic -fno-strict-aliasing ${MACHINE_FLAG}) target_link_options(stim_test_o3 PRIVATE) else() message("WARNING: Skipped stim_test target. `GTest` not found. To fix, follow Standalone CMake Project install instructions at https://github.com/google/googletest/blob/master/googletest/README.md") endif() find_package(Python COMPONENTS Interpreter Development) find_package(pybind11 CONFIG) if (${pybind11_FOUND} AND ${Python_FOUND}) pybind11_add_module(stim_python_bindings ${PYBIND_FILES} ${SOURCE_FILES_NO_MAIN}) set_target_properties(stim_python_bindings PROPERTIES OUTPUT_NAME stim) add_compile_definitions(STIM_PYBIND11_MODULE_NAME=stim) if(NOT(MSVC)) target_compile_options(stim_python_bindings PRIVATE -O3 -Wall -Wpedantic -fno-strict-aliasing ${MACHINE_FLAG}) target_link_options(stim_python_bindings PRIVATE -O3) else() target_compile_options(stim_python_bindings PRIVATE ${MACHINE_FLAG} /O1) endif() else() message("WARNING: Skipped stim_python_bindings target. `pybind11` not found. To fix, install pybind11. On debian based distributions, the package name is `pybind11-dev`") endif() ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. This Code of Conduct also applies outside the project spaces when the Project Stewards have a reasonable belief that an individual's behavior may have a negative impact on the project or its community. ## Conflict Resolution We do not believe that all conflict is bad; healthy debate and disagreement often yield positive results. However, it is never okay to be disrespectful or to engage in behavior that violates the project’s Code of Conduct. If you see someone violating the Code of Conduct, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe. Reports should be directed to quantumai-oss-maintainers@googlegroups.com, the project stewards at Google Quantum AI. They will then work with a committee consisting of representatives from the Open Source Programs Office and the Google Open Source Strategy team. If for any reason you are uncomfortable reaching out to the Project Stewards, please email opensource@google.com. We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. We will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone's safety, we may take action without notice. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute We'd love to accept your patches and contributions to this project. We do have some guidelines to follow, covered in this document, but don't be concerned about getting everything right the first time! Create a pull request (discussed below) and we'll nudge you in the right direction. ## Before you begin ### Sign our Contributor License Agreement Contributions to this project must be accompanied by a [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). You (or your employer) retain the copyright to your contribution; the CLA simply gives us permission to use and redistribute your contributions as part of the project. Please visit https://cla.developers.google.com/ to see your current agreements on file or to sign a new one. You generally only need to submit a Google CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. > [!WARNING] > Please note carefully clauses [#5](https://cla.developers.google.com/about/google-corporate#:~:text=You%20represent%20that%20each%20of%20Your%20Contributions%20is%20Your%20original%20creation) > and [#7](https://cla.developers.google.com/about/google-corporate#:~:text=Should%20You%20wish%20to%20submit%20work%20that%20is%20not%20Your%20original%20creation%2C%20You%20may%20submit%20it%20to%20Google%20separately) > in the CLA. Any code that you contribute to this project must be **your** > original creation. Code generated by artificial intelligence tools **does > not** qualify as your original creation. ### Review our community guidelines We have a [code of conduct](CODE_OF_CONDUCT.md) to make the Stim project an open and welcoming community environment. Please make sure to read and abide by the code of conduct. ## Contribution process All submissions, including submissions by project members, require review. We use the tools provided by GitHub for [pull requests](https://help.github.com/articles/about-pull-requests/) for this purpose. The preferred manner for submitting pull requests is to fork the Stim [repository](https://github.com/quantumlib/Stim), create a [git branch](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell) in this fork to do your work, and when ready, create a pull request from this branch to the main Stim repository. ### Repository forks 1. Fork the Stim repository (you can use the _Fork_ button in upper right corner of the [repository page](https://github.com/quantumlib/Stim)). Forking creates a new GitHub repo at the location `https://github.com/USERNAME/Stim`, where `USERNAME` is your GitHub user name. 1. Clone (using `git clone`) or otherwise download your forked repository to your local computer, so that you have a local copy where you can do your development work using your preferred editor and development tools. 1. Check out the `main` branch and create a new git branch from `main`: ```shell git checkout main -b YOUR_BRANCH_NAME ``` where `YOUR_BRANCH_NAME` is the name of your new branch. ### Development and testing Please follow the detailed instructions in [`doc/developer_documentation.md`](doc/developer_documentation.md) for developing and testing Stim. ### Pull requests and code reviews 1. If your local copy has drifted out of sync with the `main` branch of the main Stim repository, you may need to merge the latest changes into your branch. To do this, first update your local `main` and then merge your local `main` into your branch: ```shell # Track the upstream repo (if your local repo hasn't): git remote add upstream https://github.com/quantumlib/Stim.git # Update your local main. git fetch upstream git checkout main git merge upstream/main # Merge local main into your branch. git checkout YOUR_BRANCH_NAME git merge main ``` If git reports conflicts during one or both of these merge processes, you may need to [resolve the merge conflicts]( https://docs.github.com/articles/about-merge-conflicts) before continuing. 1. Finally, push your changes to your fork of the Stim repo on GitHub: ```shell git push origin YOUR_BRANCH_NAME ``` 1. Now when you navigate to the Stim repository on GitHub (https://github.com/quantumlib/Stim), you should see the option to create a new pull request from your forked repository. Alternatively, you can create the pull request by navigating to the "Pull requests" tab near the top of the page, and selecting the appropriate branches. 1. A reviewer from the Stim team will comment on your code and may ask for changes. You can perform the necessary changes locally, commit them to your branch as usual, and then push changes to your fork on GitHub following the same process as above. When you do that, GitHub will update the code in the pull request automatically. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ================================================ FILE: MANIFEST.in ================================================ include glue/python/src/stim/__init__.pyi include glue/python/README.md recursive-include src *.cc *.h *.inl ================================================ FILE: MODULE.bazel ================================================ bazel_dep(name = "googletest", version = "1.14.0") bazel_dep(name = "pybind11_bazel", version = "2.11.1") ================================================ FILE: README.md ================================================ # Stim Stim logo High-performance simulation of quantum stabilizer circuits for quantum error correction. ◼︎︎  [What is Stim?](#what-is-stim)
◼︎︎  [How do I use Stim?](#how-use-stim)
◼︎  [How does Stim work?](#how-stim-work)
◼︎  [How do I cite Stim?](#how-cite-stim)
◼︎  [*Subproject*: Sinter decoding sampler](glue/sample)
◼︎  [*Subproject*: Crumble interactive editor](glue/crumble)
## What is Stim? Stim is a tool for high performance simulation and analysis of quantum stabilizer circuits, especially quantum error correction (QEC) circuits. Typically Stim is used as a Python package (`pip install stim`), though Stim can also be used as a command-line tool or a C++ library. * [Watch the 15 minute lightning talk presenting Stim at QPL2021](https://youtu.be/7m_JrJIskPM?t=895) * [Watch Stim being used to estimate the threshold of the honeycomb code over a weekend](https://www.youtube.com/watch?v=E9yj0o1LGII) Stim's key features: 1. **Really fast simulation of stabilizer circuits**. Have a circuit with thousands of qubits and millions of operations? [`stim.Circuit.compile_sampler()`](doc/python_api_reference_vDev.md#stim.Circuit.compile_sampler) will perform a few seconds of analysis and then produce an object that can sample shots at kilohertz rates. 2. **Semi-automatic decoder configuration**. [`stim.Circuit.detector_error_model()`](doc/python_api_reference_vDev.md#stim.Circuit.detector_error_model) converts a noisy circuit into a detector error model (a [Tanner graph](https://en.wikipedia.org/wiki/Tanner_graph)) which can be used to configure decoders. Adding the option `decompose_operations=True` will additionally suggest how hyper errors can be decomposed into graphlike errors, making it easier to configure matching-based decoders. 3. **Useful building blocks for working with stabilizers**, such as [`stim.PauliString`](doc/python_api_reference_vDev.md#stim.PauliString), [`stim.Tableau`](doc/python_api_reference_vDev.md#stim.Tableau), and [`stim.TableauSimulator`](doc/python_api_reference_vDev.md#stim.TableauSimulator). Stim's main limitations are: 1. There is no support for non-Clifford operations, such as T gates and Toffoli gates. Only stabilizer operations are supported. 2. `stim.Circuit` only supports Pauli noise channels (eg. no amplitude decay). For more complex noise you must manually drive a `stim.TableauSimulator`. 3. `stim.Circuit` only supports single-control Pauli feedback. For multi-control feedback, or non-Pauli feedback, you must manually drive a `stim.TableauSimulator`. Stim's design philosophy: * **Performance is king.** The goal is not to be fast *enough*, it is to be fast in an absolute sense. Think of it this way. The difference between doing one thing per second (human speeds) and doing ten billion things per second (computer speeds) is 100 decibels (100 factors of 1.26). Because software slowdowns tend to compound exponentially, the choices we make can be thought of multiplicatively; they can be thought of as spending or saving decibels. For example, under default usage, Python is 100 times slower than C++. That's 20dB of the 100dB budget! A *fifth* of the multiplicative performance budget allocated to *language choice*! Too expensive! Although Stim will never achieve the glory of [30 GiB per second of FizzBuzz](https://codegolf.stackexchange.com/a/236630/74349), it at least *wishes* it could. * **Bottom up.** Stim is intended to be like an assembly language: a mostly straightforward layer upon which more complex layers can be built. The user may define QEC constructions at some high level, perhaps as a set of stabilizers or as a parity check matrix, but these concepts are explained to Stim at a low level (e.g., as circuits). Stim is not necessarily the abstraction that the user wants, but Stim wants to implement low-level pieces simple enough and fast enough that the high-level pieces that the user wants can be built on top. * **Backwards compatibility.** Stim's Python package uses semantic versioning. Within a major version (1.X), Stim guarantees backwards compatibility of its Python API and of its command-line API. Note Stim DOESN'T guarantee backwards compatibility of the underlying C++ API. ## How do I use Stim? See the [Getting Started Notebook](doc/getting_started.ipynb). Stuck? [Get help on the quantum computing stack exchange](https://quantumcomputing.stackexchange.com) and use the [`stim`](https://quantumcomputing.stackexchange.com/questions/tagged/stim) tag. See the reference documentation: * [Stim Python API Reference](doc/python_api_reference_vDev.md) * [Stim Supported Gates Reference](doc/gates.md) * [Stim Command Line Reference](doc/usage_command_line.md) * [Stim Circuit File Format (.stim)](doc/file_format_stim_circuit.md) * [Stim Detector Error model Format (.dem)](doc/file_format_dem_detector_error_model.md) * [Stim Results Format Reference](doc/result_formats.md) * [Stim Internal Developer Reference](doc/developer_documentation.md) ## How does Stim work? See [the paper describing Stim](https://quantum-journal.org/papers/q-2021-07-06-497/). Stim makes three core improvements over previous stabilizer simulators: 1. **Vectorized code.** Stim's hot loops are heavily vectorized, using 256 bit wide AVX instructions. This makes them very fast. For example, Stim can multiply Pauli strings with 100 billion terms in one second. 2. **Reference Frame Sampling.** When bulk sampling, Stim only uses a general stabilizer simulator for an initial reference sample. After that, it cheaply derives as many samples as needed by propagating simulated errors diffed against the reference. This simple trick is *ridiculously* cheaper than the alternative: constant cost per gate, instead of linear cost or even quadratic cost. 3. **Inverted Stabilizer Tableau.** When doing general stabilizer simulation, Stim tracks the inverse of the stabilizer tableau that was historically used. This has the unexpected benefit of making measurements that commute with the current stabilizers take linear time instead of quadratic time. This is beneficial in error correcting codes, because the measurements they perform are usually redundant and so commute with the current stabilizers. ## How do I cite Stim? When using Stim for research, [please cite](https://quantum-journal.org/papers/q-2021-07-06-497/): ```latex @article{gidney2021stim, doi = {10.22331/q-2021-07-06-497}, url = {https://doi.org/10.22331/q-2021-07-06-497}, title = {Stim: a fast stabilizer circuit simulator}, author = {Gidney, Craig}, journal = {{Quantum}}, issn = {2521-327X}, publisher = {{Verein zur F{\"{o}}rderung des Open Access Publizierens in den Quantenwissenschaften}}, volume = 5, pages = 497, month = jul, year = 2021 } ``` ## Contact For any questions or concerns not addressed here, please email quantum-oss-maintainers@google.com. ## Disclaimer This is not an officially supported Google product. This project is not eligible for the [Google Open Source Software Vulnerability Rewards Program](https://bughunters.google.com/open-source-security). Copyright 2025 Google LLC.
Google Quantum AI
================================================ FILE: SUPPORT.md ================================================ # Support Thank you for your interest in this project! If you are experiencing problems or have questions, the following are some suggestions for how to get help. > [!NOTE] > Before participating in our community, please read our [code of > conduct](CODE_OF_CONDUCT.md). By interacting with this repository, > organization, or community, you agree to abide by its terms. ## Report an issue or request a feature To report an issue or request a feature in Stim, please first search the [issue tracker on GitHub](https://github.com/quantumlib/Stim/issues) to check if there is already an open issue identical or similar to your bug report/feature request. If there is none, go ahead and file a new issue in the issue tracker. ## Contact the maintainers For any questions or concerns not addressed here, please email [quantum-oss-maintainers@google.com](mailto:quantum-oss-maintainers@google.com). ================================================ FILE: WORKSPACE ================================================ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("@pybind11_bazel//:python_configure.bzl", "python_configure") http_archive( name = "pybind11", build_file = "@pybind11_bazel//:pybind11.BUILD", sha256 = "bf8f242abd1abcd375d516a7067490fb71abd79519a282d22b6e4d19282185a7", strip_prefix = "pybind11-2.12.0", urls = ["https://github.com/pybind/pybind11/archive/refs/tags/v2.12.0.tar.gz"], ) python_configure(name = "local_config_python") ================================================ FILE: dev/canvas_with_texture_for_3d_diagrams.html ================================================ ================================================ FILE: dev/clean_build_files.sh ================================================ #!/bin/bash set -e ######################################################################### # Deletes files created by cmake, python setup.py, and other build steps. ######################################################################### # Get to this script's git repo root. cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" rm CMakeFiles -rf rm CMakeCache.txt -f rm Makefile -f rm dist -rf rm cmake_install.cmake -f rm cmake-build-debug-coverage -rf rm coverage -rf rm stim.egg-info -rf rm bazel-bin -rf rm bazel-out -rf rm bazel-stim -rf rm bazel-testlogs -rf rm Testing -rf rm out -rf rm cmake-build-debug -rf rm .cmake -rf ================================================ FILE: dev/compile_crumble_into_cpp_string_file.sh ================================================ #!/bin/bash set -e # Get to this script's git repo root. cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" echo '#include "stim/diagram/crumble_data.h"' echo ''; echo 'std::string stim_draw_internal::make_crumble_html() {' echo ' std::string result;' dev/compile_crumble_into_single_html_page.sh | python -c ' import sys for line in sys.stdin: for k in range(0, len(line), 1024): part = line[k:k+1024] print(f""" result.append(R"CRUMBLE_PART({part})CRUMBLE_PART");""")'; echo ' return result;' echo '}' ================================================ FILE: dev/compile_crumble_into_single_html_page.sh ================================================ #!/bin/bash set -e cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" cat glue/crumble/crumble.html | grep -v "^"; # HACK: this temp file is to work around https://github.com/rollup/rollup/issues/5097 rollup glue/crumble/main.js --silent > tmp_crumble.tmp uglifyjs -c -m --mangle-props --toplevel < tmp_crumble.tmp; rm tmp_crumble.tmp echo ""; ================================================ FILE: dev/doctest_proper.py ================================================ #!/usr/bin/env python3 """Runs doctests on a module, including any objects imported into the module.""" import argparse import doctest import inspect import sys from typing import Dict SKIPPED_FIELDS = { '__base__', '__name__', '__path__', '__spec__', '__version__', '__package__', '__subclasshook__', '__abstractmethods__', '__bases__', '__basicsize__', '__class__', '__builtins__', '__cached__', '__doc__', '__loader__', '__file__', } def no_really_i_have_a_doc_why_is_this_needed_argh(v: object, fullname: str) -> object: def so_much_doc(): pass so_much_doc.__doc__ = v.__doc__ so_much_doc.__qualname__ = fullname return so_much_doc def gen(*, obj: object, fullname: str, out: Dict[str, object]) -> None: if obj is None: return if inspect.isfunction(obj) or inspect.ismethod(obj) or inspect.isbuiltin(obj) or inspect.isroutine(obj): if hasattr(obj, '__doc__'): out[fullname] = obj return if not inspect.ismodule(obj) and not inspect.isclass(obj): if hasattr(obj, '__doc__'): out[fullname] = no_really_i_have_a_doc_why_is_this_needed_argh(obj, fullname) return if hasattr(obj, '__doc__'): out[fullname] = obj for sub_name in dir(obj): if sub_name in SKIPPED_FIELDS: continue if sub_name.startswith('__pybind11_module'): continue sub_obj = getattr(obj, sub_name, None) if inspect.ismodule(sub_obj): continue sub_full_name = fullname + "." + sub_name gen(obj=sub_obj, fullname=sub_full_name, out=out) def main(): parser = argparse.ArgumentParser() parser.add_argument( "--module", type=str, required=True, nargs='+', help="The module to test. " "This module will be imported, " "its imported values will be recursively explored, " "and doctests will be run on them.") parser.add_argument( '--import', default=(), nargs='*', type=str, help="Modules to import for each doctest.") args = parser.parse_args() globs = { k: __import__(k) for k in getattr(args, 'import') } any_failed = False for module_name in args.module: module = __import__(module_name) out = {} gen(obj=module, fullname=module_name, out=out) for k, v in out.items(): if v.__doc__ is None: continue v = v.__doc__.lower() 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) module.__test__ = {k: v for k, v in out.items()} if doctest.testmod(module, globs=globs).failed: any_failed = True if any_failed: sys.exit(1) if __name__ == '__main__': main() ================================================ FILE: dev/gen_known_gates_for_js.sh ================================================ #!/bin/bash set -e ######################################################################### # Generates javascript exporting a string KNOWN_GATE_NAMES_FROM_STIM. ######################################################################### echo "const KNOWN_GATE_NAMES_FROM_STIM = \`" python -c "import stim; stim.main(command_line_args=['help', 'gates'])" | grep " " | sed 's/^ *//g' echo "\`" echo echo "export {KNOWN_GATE_NAMES_FROM_STIM};" ================================================ FILE: dev/gen_sinter_api_reference.py ================================================ """ Iterates over modules and classes, listing their attributes and methods in markdown. """ import sinter import sys from util_gen_stub_file import generate_documentation def main(): version = sinter.__version__ if "dev" in version or version == "VERSION_INFO" or "-dev" in sys.argv: version = "(Development Version)" is_dev = True else: version = "v" + version is_dev = False objects = [ obj for obj in generate_documentation(obj=sinter, full_name="sinter", level=0) if all('[DEPRECATED]' not in line for line in obj.lines) ] print(f"# Sinter {version} API Reference") print() if is_dev: print("*CAUTION*: this API reference is for the in-development version of sinter.") print("Methods and arguments mentioned here may not be accessible in stable versions, yet.") print("API references for stable versions are kept on the [stim github wiki](https://github.com/quantumlib/Stim/wiki)") print() print("## Index") for obj in objects: level = obj.level print((level - 1) * " " + f"- [`{obj.full_name}`](#{obj.full_name})") print(f''' ```python # Types used by the method definitions. from typing import overload, TYPE_CHECKING, Any, Counter, Dict, Iterable, List, Optional, Tuple, Union import abc import dataclasses import io import numpy as np import pathlib import stim ``` '''.strip()) replace_rules = [] for package in ['stim', 'sinter']: p = __import__(package) for name in dir(p): x = getattr(p, name) if isinstance(x, type) and 'class' in str(x): desired_name = f'{package}.{name}' if '._' in str(x): bad_name = str(x).split("'")[1] replace_rules.append((bad_name, desired_name)) lonely_name = desired_name.split(".")[-1] for q in ['"', "'"]: replace_rules.append(('ForwardRef(' + q + lonely_name + q + ')', desired_name)) replace_rules.append(('ForwardRef(' + q + desired_name + q + ')', desired_name)) replace_rules.append((q + desired_name + q, desired_name)) replace_rules.append((q + lonely_name + q, desired_name)) replace_rules.append(('ForwardRef(' + desired_name + ')', desired_name)) replace_rules.append(('ForwardRef(' + lonely_name + ')', desired_name)) 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(f'# (at top-level in the sinter module)') for line in obj.lines: for a, b in replace_rules: line = line.replace(a, b) print(line) print("```") if __name__ == '__main__': main() ================================================ FILE: dev/gen_stim_api_reference.py ================================================ """ Iterates over modules and classes, listing their attributes and methods in markdown. """ import stim import sys from util_gen_stub_file import generate_documentation def main(): version = stim.__version__ if "dev" in version or version == "VERSION_INFO" or "-dev" in sys.argv: version = "(Development Version)" is_dev = True else: version = "v" + version is_dev = False objects = [ obj for obj in generate_documentation(obj=stim, full_name="stim", level=0) if all('[DEPRECATED]' not in line for line in obj.lines) ] print(f"# Stim {version} API Reference") print() if is_dev: print("*CAUTION*: this API reference is for the in-development version of stim.") print("Methods and arguments mentioned here may not be accessible in stable versions, yet.") print("API references for stable versions are kept on the [stim github wiki](https://github.com/quantumlib/Stim/wiki)") print() print("## Index") for obj in objects: level = obj.level print((level - 1) * " " + f"- [`{obj.full_name}`](#{obj.full_name})") print(f''' ```python # Types used by the method definitions. from typing import overload, TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union 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(f'# (at top-level in the stim module)') print('\n'.join(obj.lines)) print("```") if __name__ == '__main__': main() ================================================ FILE: dev/gen_stim_stub_file.py ================================================ #!/usr/bin/env python3 """ Produces a .pyi file for stim, describing the contained classes and functions. """ import stim import sys from util_gen_stub_file import generate_documentation def main(): version = stim.__version__ if "dev" in version or version == "VERSION_INFO" or "-dev" in sys.argv: version = "(Development Version)" else: version = "v" + version print(f''' """Stim {version}: a fast quantum stabilizer circuit library.""" # (This is a stubs file describing the classes and methods in stim.) from __future__ import annotations from typing import overload, TYPE_CHECKING, List, Dict, Tuple, Any, Union, Iterable, Optional, Sequence, Literal if TYPE_CHECKING: import io import pathlib import numpy as np import stim '''.strip()) for obj in generate_documentation(obj=stim, full_name="stim", level=-1): text = '\n'.join((" " * obj.level + line).rstrip() for paragraph in obj.lines for line in paragraph.splitlines()) assert "stim::" not in text, "CONTAINS C++ STYLE TYPE SIGNATURE!!:\n" + text print(text) if __name__ == '__main__': main() ================================================ FILE: dev/make_logo.html ================================================ ================================================ FILE: dev/overwrite_dev_versions_with_date.py ================================================ #!/usr/bin/env python3 ######################################################### # Sets version numbers to a date-based dev version. # # Does nothing if not on a dev version. ######################################################### # Example usage (from repo root): # # ./dev/overwrite_dev_versions_with_date.sh ######################################################### import os import pathlib import re import subprocess def main(): os.chdir(pathlib.Path(__file__).parent) os.chdir(subprocess.check_output(["git", "rev-parse", "--show-toplevel"]).decode().strip()) # Generate dev version starting from major.minor version. # (Requires the existing version to have a 'dev' suffix.) # (Uses the timestamp of the HEAD commit, to ensure consistency when run multiple times.) with open('setup.py') as f: maj_min_version_line, = [line for line in f.read().splitlines() if re.match("^__version__ = '[^']+'", line)] maj_version, min_version, patch = maj_min_version_line.split()[-1].strip("'").split('.') if 'dev' not in patch: return # Do nothing for non-dev versions. timestamp = subprocess.check_output(['git', 'show', '-s', '--format=%ct', 'HEAD']).decode().strip() new_version = f"{maj_version}.{min_version}.dev{timestamp}" # Overwrite existing versions. package_setup_files = [ "setup.py", "glue/cirq/setup.py", "glue/cirq/stimcirq/__init__.py", "glue/zx/stimzx/__init__.py", "glue/zx/setup.py", "glue/sample/setup.py", "glue/sample/src/sinter/__init__.py", ] for path in package_setup_files: with open(path) as f: content = f.read() assert maj_min_version_line in content content = content.replace(maj_min_version_line, f"__version__ = '{new_version}'") with open(path, 'w') as f: print(content, file=f, end='') if __name__ == '__main__': main() ================================================ FILE: dev/regen_crumble_cpp_resource.sh ================================================ #!/bin/bash set -e # Get to this script's git repo root. cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" dev/compile_crumble_into_cpp_string_file.sh > src/stim/diagram/crumble_data.cc ================================================ FILE: dev/regen_docs.sh ================================================ #!/bin/bash set -e ######################################################################### # Regenerates doc files using the installed version of stim. ######################################################################### # Get to this script's git repo root. cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" python dev/gen_stim_api_reference.py -dev > doc/python_api_reference_vDev.md python dev/gen_stim_stub_file.py -dev > glue/python/src/stim/__init__.pyi python dev/gen_stim_stub_file.py -dev > doc/stim.pyi python dev/gen_sinter_api_reference.py -dev > doc/sinter_api.md python -c "import stim; stim.main(command_line_args=['help', 'gates_markdown'])" > doc/gates.md python -c "import stim; stim.main(command_line_args=['help', 'formats_markdown'])" > doc/result_formats.md python -c "import stim; stim.main(command_line_args=['help', 'commands_markdown'])" > doc/usage_command_line.md dev/gen_known_gates_for_js.sh > glue/crumble/test/generated_gate_name_list.test.js ================================================ FILE: dev/regen_file_lists.sh ================================================ #!/bin/bash set -e ######################################################################### # Regenerate file_lists ######################################################################### if [ "$#" -ne 1 ]; then FOLDER=file_lists else FOLDER=$1 fi # Get to this script's git repo root. cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" # LC_ALL=C forces sorting to happen by byte value find src | grep "\\.cc$" | grep -v "\\.\(test\|perf\|pybind\)\\.cc$" | grep -v "src/main\\.cc" | LC_ALL=C sort > "${FOLDER}/source_files_no_main" find src | grep "\\.test\\.cc$" | LC_ALL=C sort > "${FOLDER}/test_files" find src | grep "\\.perf\\.cc$" | LC_ALL=C sort > "${FOLDER}/perf_files" find src | grep "\\.pybind\\.cc$" | LC_ALL=C sort > "${FOLDER}/pybind_files" # Regenerate 'stim.h' to include all relevant headers. { echo "#ifndef _STIM_H"; echo "#define _STIM_H"; echo "/// WARNING: THE STIM C++ API MAKES NO COMPATIBILITY GUARANTEES."; echo "/// It may change arbitrarily and catastrophically from minor version to minor version."; echo "/// If you need a stable API, use stim's Python API."; find src | grep "\\.h$" | grep -v "\\.\(test\|perf\|pybind\)\\.h$" | grep -v "src/stim\\.h" | grep -v "src/stim/mem/simd_word_.*" | LC_ALL=C sort | sed 's/src\/\(.*\)/#include "\1"/g'; echo "#endif"; } > src/stim.h # Regenerate crumble's unit test imports. find glue/crumble | grep "\\.test.js$" | LC_ALL=C sort | sed 's/glue\/crumble\(.*\)/import "..\1"/g' > "glue/crumble/test/test_import_all.js" ================================================ FILE: dev/regen_texture_to_cpp_base64_string.sh ================================================ #!/bin/bash set -e ######################################################################### # Transforms binary data into an array of string literals containing the # base 64 data encoding the data. The strings are split into an array # to avoid exceeding C++'s maximum string literal length. ######################################################################### # Get to this script's git repo root. cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" { echo '#include "stim/diagram/gate_data_3d_texture_data.h"'; echo ''; echo 'std::string stim_draw_internal::make_gate_3d_texture_data_uri() {'; echo ' std::string result;'; echo ' result.append("data:image/png;base64,");'; base64 -w 1024 | sed 's/.*/ result.append("\0");/g'; echo ' return result;'; echo '}'; } > "src/stim/diagram/gate_data_3d_texture_data.cc" ================================================ FILE: dev/util_gen_stub_file.py ================================================ import dataclasses import types from typing import Any from typing import Optional, Iterator, List import inspect from typing import Tuple keep = { "__add__", "__ipow__", "__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 = { "__annotate_func__", "__annotations_cache__", "__firstlineno__", "__static_attributes__", "__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__", "__annotate_func__", "__annotations_cache__", } 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('list[', 'List[') sig = sig.replace('dict[', 'Dict[') sig = sig.replace('tuple[', 'Tuple[') sig = sig.replace('set[', 'Set[') 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 += ',' out.append(' ' + s) mark = k2 if level < 0: break assert level == -1 out.append(sig[mark:]) 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__", "")) 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(':')] if '@overload ' in line: _, sig = line.split('@overload ') out_obj.lines.append("@overload") is_static = '(self' not in sig and inspect.isclass(parent) if is_static: out_obj.lines.append("@staticmethod") out_obj.lines.extend(splay_signature(sig)) out_obj.lines.append(" pass") elif '@signature ' in line: _, sig = line.split('@signature ') is_static = '(self' not in sig and inspect.isclass(parent) if term_name not in sig: raise ValueError(f"Expected name {term_name!r} to appear in signature override for {full_name!r}:\n {line}") if is_static: out_obj.lines.append("@staticmethod") out_obj.lines.extend(splay_signature(sig)) sig_handled = True else: doc_lines_left.append(line) was_args = 'Args:' in line if is_property: if hasattr(obj, 'fget'): sig_name = term_name + obj.fget.__doc__.replace('arg0', 'self').strip() else: 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) -> Optional[DescribedObject]: out_obj = DescribedObject() out_obj.full_name = full_name out_obj.level = level doc = getattr(obj, "__doc__", None) 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 'sinter' 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(obj)}' text += "\n".join(splay_signature(f"def {sig_name}:")) # Replace default value lambdas with their source. if 'lambda' in str(text): for name, param in inspect.signature(obj).parameters.items(): if 'lambda' in str(param.default): _, lambda_src = inspect.getsource(param.default).split('lambda ') lambda_src = lambda_src.strip() assert lambda_src.endswith(',') lambda_src = 'lambda ' + lambda_src[:-1] text = text.replace(str(param.default), lambda_src) 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(f" pass") if dataclass_fields: for f in dataclasses.fields(obj): if str(f.type).startswith('typing'): t = str(f.type).replace('typing.', '') else: t = f.type.__name__ 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) 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) ================================================ FILE: doc/circuit_data_references.md ================================================ ## 2021 - [arXiv:2103.02202](https://arxiv.org/abs/2103.02202) → [Ancillary files of "Stim: a fast stabilizer circuit simulator"](https://arxiv.org/src/2103.02202v3/anc) - [arXiv:2108.10457](https://arxiv.org/abs/2108.10457) → [Ancillary files of "A Fault-Tolerant Honeycomb Memory"](https://arxiv.org/src/2108.10457v2/anc) ## 2022 - [arXiv:2202.11845](https://arxiv.org/abs/2202.11845) → [Data for "Benchmarking the Planar Honeycomb Code"](https://zenodo.org/records/7072889) - [arXiv:2204.13834](https://arxiv.org/abs/2204.13834) → [Data for "Stability Experiments: The Overlooked Dual of Memory Experiments"](https://zenodo.org/records/6859486) - [arXiv:2206.12780](https://arxiv.org/abs/2206.12780) → [Data for "A Pair Measurement Surface Code on Pentagons"](https://zenodo.org/records/6626417) - [arXiv:2207.06431](https://arxiv.org/abs/2207.06431) → [Data for "Suppressing quantum errors by scaling a surface code logical qubit"](https://zenodo.org/records/6804040) - [arXiv:2209.08552](https://arxiv.org/abs/2209.08552) → [Parallel window decoding enables scalable fault tolerant quantum computation](https://doi.org/10.5281/zenodo.8422904) ## 2023 - [arXiv:2302.02192](https://arxiv.org/abs/2302.02192) → [Data for "Relaxing Hardware Requirements for Surface Code Circuits using Time-dynamics"](https://zenodo.org/records/7587578) - [arXiv:2302.07395](https://arxiv.org/abs/2302.07395) → [Data for "Inplace Access to the Surface Code Y Basis"](https://zenodo.org/records/7487893) - [arXiv:2302.12292](https://arxiv.org/abs/2302.12292) → [Data for "Cleaner magic states with hook injection"](https://zenodo.org/records/7575030) - [arXiv:2305.12046](https://arxiv.org/abs/2305.12046) → [Data for "Less Bacon More Threshold"](https://zenodo.org/records/7901729) - [arXiv:2307.10147](https://arxiv.org/abs/2307.10147) → [Stim circuits for "Tangling schedules eases hardware connectivity requirements for quantum error correction" manuscript](https://zenodo.org/records/8391674) - [arXiv:2308.03750](https://arxiv.org/abs/2308.03750) → [Ancillary data for the paper "Constructions and performance of hyperbolic and semi-hyperbolic Floquet codes" ](https://github.com/oscarhiggott/hyperbolic-floquet-data) - [arXiv:2309.05558](https://arxiv.org/abs/2309.05558) → [Data supporting "A real-time, scalable, fast and resource-efficient decoder for a quantum computer"](https://zenodo.org/records/11621878) - [arXiv:2312.04522](https://arxiv.org/abs/2312.04522) → [Data for "Yoked Surface Codes"](https://zenodo.org/records/10277397) - [arXiv:2312.08813](https://arxiv.org/abs/2312.08813) → [Data for "New circuits and an open source decoder for the colorcode"](https://zenodo.org/records/10375289) - [arXiv:2312.11605](https://arxiv.org/abs/2312.11605) → [Stim circuits and collected data for "Error-corrected Hadamard gate simulated at the circuit level" manuscript](https://zenodo.org/records/10391116) - [arXiv:2312.14060](https://arxiv.org/abs/2312.14060) → [Data for "Fault-tolerant quantum architectures based on erasure qubits"](https://zenodo.org/records/13730128) ## 2024 - [arXiv:2405.15854](https://arxiv.org/abs/2405.15854) → [Stim circuits for 'Accommodating Fabrication Defects on Floquet Codes with Minimal Hardware Requirements' manuscript](https://zenodo.org/records/11241876) - [arXiv:2406.02700](https://arxiv.org/abs/2406.02700) → [Data for "Optimization of decoder priors for accurate quantum error correction"](https://zenodo.org/records/11403595) - [arXiv:2407.13826](https://arxiv.org/abs/2407.13826) → [Stim circuits for 'Designing fault-tolerant circuits using detector error models'](https://github.com/peter-janderks/short_measurement_schedules_simulations/tree/main/stim_circuits) - [arXiv:2408.00758](https://arxiv.org/abs/2408.00758) → [Stim circuits for ``To reset, or not to reset -- that is the question" manuscript](https://zenodo.org/records/13152440) - [arXiv:2408.11894](https://arxiv.org/abs/2408.11894) → [Stim circuits for 'Automated Synthesis of Fault-Tolerant State Preparation Circuits for Quantum Error Correction Codes'](https://github.com/cda-tum/mqt-qecc/tree/main/src/mqt/qecc/ft_stateprep/eval/circuits) - [arXiv:2408.13687](https://arxiv.org/abs/2408.13687) → [Data for "Quantum error correction below the surface code threshold"](https://zenodo.org/records/13273331) - [arXiv:2409.04628](https://arxiv.org/abs/2409.04628) → [Stim implementation of the [[16,4,4]] Tesseract Code](https://github.com/DeDuckProject/tesseract-code-stim) (circuits are in the stim_circuits/ directory) - [arXiv:2409.17595](https://arxiv.org/abs/2409.17595) → [Data for "Magic state cultivation: growing T states as cheap as CNOT gates"](https://zenodo.org/records/13777072) - [arXiv:2412.01391](https://arxiv.org/abs/2412.01391) → [GitHub repo for "Transversal Logical Clifford gates on rotated surface codes with reconfigurable neutral atom arrays"](https://github.com/Zihan-Chen-PhMA/Dynamical-S-gate-decoding/) (circuits are in the circuit_garage/ directory) - [arXiv:2412.14256](https://arxiv.org/abs/2412.14256) → [Data for "Scaling and logic in the color code on a superconducting quantum processor"](https://zenodo.org/records/14238944) - [arXiv:2412.14360](https://arxiv.org/abs/2412.14360) → [Data for "Demonstrating dynamic surface codes"](https://zenodo.org/records/14238907) - [arXiv:2412.15187](https://arxiv.org/abs/2412.15187) → [Stim circuits and results for "Universal quantum computation via scalable measurement-free error correction"](https://zenodo.org/records/15707012) ## 2025 - [arXiv:2503.04968](https://arxiv.org/abs/2503.04968) → [GitHub repo for "Optimized Noise-Resilient Surface Code Teleportation Interfaces"](https://github.com/QEC-pages/Modular-Surface-code-simulations/) (circuits are in the out/ directory) - [arXiv:2503.18657](https://arxiv.org/abs/2503.18657) → [GitHub repo for "Efficient Magic State Cultivation on RP^2"](https://github.com/Zihan-Chen-PhMA/Cultiv_T_RP2/) (circuits are in the circuit_garage/ directory) - [arXiv:2504.02935](https://arxiv.org/abs/2504.02935) → [Scripts, Stim circuits and simulation results for "Magic State Injection with Erasure Qubits"](https://zenodo.org/records/15874462) - [arXiv:2507.08069](https://arxiv.org/abs/2507.08069) → [Stim circuits and simulation results for "A dynamic circuit for the honeycomb Floquet code"](https://zenodo.org/records/15854678) - [arXiv:2507.19430](https://arxiv.org/abs/2507.19430) → [Stim circuits and parity check matrices for "Directional Codes: a new family of quantum LDPC codes on hexagonal- and square-grid connectivity hardware" manuscript](https://zenodo.org/records/16422162) - [arXiv:2512.17999](https://arxiv.org/abs/2512.17999) → [Stim circuits for "Logical gates on Floquet codes via folds and twists"](https://zenodo.org/records/17966122) ================================================ FILE: doc/developer_documentation.md ================================================ # Stim Developer Documentation This is documentation for programmers working with stim, e.g. how to build it. These notes generally assume you are on a Linux system. # Index - [compatibility guarantees across versions](#compatibility) - [releasing a new version](#release-checklist) - [building `stim` command line tool](#build) - [with cmake](#build.cmake) - [with bazel](#build.bazel) - [with gcc](#build.gcc) - [linking to `libstim` shared library](#link) - [with cmake](#link.cmake) - [with bazel](#link.bazel) - [running C++ tests](#test) - [with cmake](#test.cmake) - [with bazel](#test.bazel) - [running performance benchmarks](#perf) - [with cmake](#perf.cmake) - [with bazel](#perf.bazel) - [interpreting output from `stim_perf`](#perf.output) - [profiling with gcc and perf](#perf.profile) - [creating a python dev environment](#venv) - [running python unit tests](#test.pytest) - [running sinter tests against a custom decoder](#test.pytest.sinter.custom) - [python packaging `stim`](#pypackage.stim) - [with cibuildwheels](#pypackage.stim.cibuildwheels) - [with bazel](#pypackage.stim.bazel) - [with python setup.py](#pypackage.stim.python) - [with pip install -e](#pypackage.stim.pip) - [with cmake](#pypackage.stim.cmake) - [python packaging `stimcirq`](#pypackage.stimcirq) - [with python setup.py](#pypackage.stimcirq.python) - [with pip install -e](#pypackage.stimcirq.pip) - [python packaging `sinter`](#pypackage.sinter) - [with python setup.py](#pypackage.sinter.python) - [with pip install -e](#pypackage.sinter.pip) - [javascript packaging `stimjs`](#jspackage.stimjs) - [with emscripten](#jspackage.stimjs.emscripten) - [autoformating code](#autoformat) - [with clang-format](#autoformat.clang-format) - [adding new c++ files](#newfile) - [adding new CLI commands](#newcmd) # Compatibility guarantees across versions A *bug* is bad behavior that wasn't intended. For example, the program crashing instead of returning empty results when sampling from an empty circuit would be a bug. A *trap* is originally-intended behavior that leads to the user making mistakes. For example, allowing the user to take a number of shots that wasn't a multiple of 64 when using the data format `ptb64` was a trap because the shots were padded up to a multiple of 64 using zeroes (and these fake shots could then easily be treated as real by later analysis steps). A *spandrel* is an implementation detail that has observable effects, but which is not really required to behave in that specific way. For example, when stim derives a detector error model from a circuit, the exact probability of an error instruction depends on minor details such as floating point error (which is sensitive to compiler optimizations). The exact floating point error that occurs is a spandrel. - **Stim Python API** - The python API must maintain backwards compatibility from minor version to minor version (except for bug fixes, trap fixes, and spandrels). Violating this requires a new major version. - Trap fixes must be documented as breaking changes in the release notes. - The exact behavior of random seeding is a spandrel. The behavior must be consistent on a single machine for a single version, with the same seed always producing the same results, but it is **not** stable between minor versions. This is enforced by intentionally introducing changes for every single minor version. - **Stim Command Line API** - The command land API must maintain backwards compatibility from minor version to minor version (except for bug fixes, trap fixes, and spandrels). Violating this requires a new major version. - Trap fixes must be documented as breaking changes in the release notes. - It is explicitly **not** allowed for a command to stop working due to, for example, a cleanup effort to make the commands more consistent. Violating this requires a new major version. - The exact behavior of random seeding is a spandrel. The behavior must be consistent on a single machine for a single version, with the same seed always producing the same results, but it is **not** stable between minor versions. This is enforced by intentionally introducing changes for every single minor version. - **Stim C++ API** - The C++ API makes no compatibility guarantees. It may change arbitrarily and catastrophically from minor version to minor version. # Releasing a new version - Create an off-main-branch release commit - [ ] `git checkout main -b SOMEBRANCHNAME` - [ ] Global search replace `__version__ = 'X.Y.dev0'` with `__version__ = 'X.Y.0'` - [ ] `git commit -a -m "Bump to vX.Y.0"` - [ ] `git tag vX.Y.0` - [ ] Push tag to github - [ ] Check github `Actions` tab and confirm ci is running on the tag - [ ] Wait for ci to finish validating and producing artifacts for the tag - [ ] Get `stim`, `stimcirq`, and `sinter` wheels/sdists [from cibuildwheels](#pypackage.stim.cibuildwheels) of this tag - Bump to next dev version on main branch - [ ] `git checkout main -b SOMEBRANCHNAME` - [ ] Global search replace `__version__ = 'X.Y.dev0'` with `__version__ = 'X.(Y+1).dev0'` - [ ] Increment `INTENTIONAL_VERSION_SEED_INCOMPATIBILITY` in `src/stim/circuit/circuit.h` - [ ] `git commit -a -m "Start vX.(Y+1).dev"` - [ ] Push to github as a branch and merge into main using a pull request - Write release notes on github - [ ] In title, use two-word theming of most important changes - [ ] Flagship changes section - [ ] Notable changes section - [ ] Include wheels/sdists as attachments - Do these irreversible and public viewable steps last! - [ ] Upload wheels/sdists to pypi using `twine` - [ ] Publish the github release notes - [ ] Add gates reference page to wiki for the new version - [ ] Add python api reference page to wiki for the new version - [ ] Update main wiki page to point to latest reference pages - [ ] Tweet about the release # Building `stim` command line tool The stim command line tool is a binary program `stim` that accepts commands like `stim sample -shots 100 -in circuit.stim` (see [the command line reference](usage_command_line.md)). It can be built [with cmake](#build.cmake), [with bazel](#build.bazel), or manually [with gcc](#build.gcc). ## Building `stim` command line tool with cmake ```bash # from the repository root: cmake . make stim # output binary ends up at: # ./out/stim ``` Stim can also be installed: ```bash # from the repository root: cmake . make stim make install # output binary ends up at: # -CMAKE_INSTALL_PREFIX/bin # output library ends up at: # -CMAKE_INSTALL_PREFIX/lib # output headers end up at: # -CMAKE_INSTALL_PREFIX/include ``` by default `make install` will install to the system default directory (e.g. `/usr/local`). The install path can be controlled by passing the flag `-DCMAKE_INSTALL_PREFIX=/path/to/install` to `cmake`. Vectorization can be controlled by passing the flag `-DSIMD_WIDTH` to `cmake`: - `cmake . -DSIMD_WIDTH=256` means "use 256 bit avx operations" (forces `-mavx2`) - `cmake . -DSIMD_WIDTH=128` means "use 128 bit sse operations" (forces `-msse2`) - `cmake . -DSIMD_WIDTH=64` means "don't use simd operations" (no machine arch flags) - `cmake .` means "use the best thing possible on this machine" (forces `-march=native`) A `compile_commands.json` (used by clangd to provide IDE features over lsp) can be generated by passing the flag `-DCMAKE_EXPORT_COMPILE_COMMANDS=1` to `cmake`. ## Building `stim` command line tool with bazel ```bash bazel build stim ``` or, to build and run: ```bash bazel run stim ``` ## Building `stim` command line tool with gcc ```bash # from the repository root: find src \ | grep "\\.cc$" \ | grep -v "\\.\(test\|perf\|pybind\)\\.cc$" \ | xargs g++ -I src -pthread -std=c++20 -O3 -march=native # output binary ends up at: # ./a.out ``` # Linking to `libstim` shared library **!!!CAUTION!!! Stim's C++ API is not kept stable! Always pin to a specific version! I *WILL* break your downstream code when I update stim if you don't! The API is also not extensively documented; what you can find in the headers is what you get.** To use Stim functionality within your C++ program, you can build `libstim` and link to it to gain direct access to underlying Stim types and methods. If you want a stim API that promises backwards compatibility, use the python API. ## Linking to `libstim` shared library with cmake **!!!CAUTION!!! Stim's C++ API is not kept stable! Always pin to a specific version! I *WILL* break your downstream code when I update stim if you don't! The API is also not extensively documented; what you can find in the headers is what you get.** In your `CMakeLists.txt` file, use `FetchContent` to automatically fetch stim from github when running `cmake .`: ``` # in CMakeLists.txt file include(FetchContent) FetchContent_Declare(stim GIT_REPOSITORY https://github.com/quantumlib/stim.git GIT_TAG v1.4.0) # [[[<<<<<<< customize the version you want!!]]] FetchContent_MakeAvailable(stim) ``` (Replace `v1.4.0` with another version tag as appropriate.) For build targets that need to use stim functionality, add `libstim` to them using `target_link_libraries`: ``` # in CMakeLists.txt file target_link_libraries(some_cmake_target PRIVATE libstim) ``` In your source code, use `#include "stim.h"` to access stim types and functions: ``` // in a source code file #include "stim.h" stim::Circuit make_bell_pair_circuit() { return stim::Circuit(R"CIRCUIT( H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] rec[-2] )CIRCUIT"); } ``` ## Linking to `libstim` shared library with bazel **!!!CAUTION!!! Stim's C++ API is not kept stable! Always pin to a specific version! I *WILL* break your downstream code when I update stim if you don't! The API is also not extensively documented; what you can find in the headers is what you get.** In your `WORKSPACE` file, include stim's git repo using `git_repository`: ``` # in WORKSPACE file load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") git_repository( name = "stim", commit = "v1.4.0", remote = "https://github.com/quantumlib/stim.git", ) ``` (Replace `v1.4.0` with another version tag or commit SHA as appropriate.) In your `BUILD` file, add `@stim//:stim_lib` to the relevant target's `deps`: ``` # in BUILD file cc_binary( ... deps = [ ... "@stim//:stim_lib", ... ], ) ``` In your source code, use `#include "stim.h"` to access stim types and functions: ``` // in a source code file #include "stim.h" stim::Circuit make_bell_pair_circuit() { return stim::Circuit(R"CIRCUIT( H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] rec[-2] )CIRCUIT"); } ``` # Running unit tests Stim's code base includes a variety of types of tests, spanning over a few packages and languages. ## Running C++ unit tests with cmake Unit testing with cmake requires the GTest library to be installed on your system in a place that cmake can find it. Follow the ["Standalone CMake Project" instructions from the GTest README](https://github.com/google/googletest/tree/master/googletest) to get GTest installed correctly. Run tests with address and memory sanitization, without compile time optimization: ```bash # from the repository root: cmake . make stim_test ./out/stim_test ``` Run tests without sanitization, with compile time optimization: ```bash # from the repository root: cmake . make stim_test_o3 ./out/stim_test_o3 ``` Stim supports 256 bit (AVX), 128 bit (SSE), and 64 bit (native) vectorization. The type to use is chosen at compile time. To force this choice (so that each case can be tested on one machine), add `-DSIMD_WIDTH=256` or `-DSIMD_WIDTH=128` or `-DSIMD_WIDTH=64` to the `cmake .` command. ## Running C++ unit tests with bazel ```bash # from the repository root: bazel test stim_test ``` # Running performance benchmarks ## Running performance benchmarks with cmake ```bash cmake . make stim_perf ./out/stim_perf ``` ## Running performance benchmarks with bazel ```bash bazel run stim_perf ``` ## Interpreting output from `stim_perf` When you run `stim_perf` you will see output like: ``` [....................*....................] 460 ns (vs 450 ns) ( 21 GBits/s) simd_bits_randomize_10K [...................*|....................] 24 ns (vs 20 ns) (400 GBits/s) simd_bits_xor_10K [....................|>>>>*...............] 3.6 ns (vs 4.0 ns) (270 GBits/s) simd_bits_not_zero_100K [....................*....................] 5.8 ms (vs 6.0 ms) ( 17 GBits/s) simd_bit_table_inplace_square_transpose_diam10K [...............*<<<<|....................] 8.1 ms (vs 5.0 ms) ( 12 GOpQubits/s) FrameSimulator_depolarize1_100Kqubits_1Ksamples_per1000 [....................*....................] 5.3 ms (vs 5.0 ms) ( 18 GOpQubits/s) FrameSimulator_depolarize2_100Kqubits_1Ksamples_per1000 ``` The bars on the left show how fast each task is running compared to baseline expectations (on my dev machine). Each tick away from the center `|` is 1 decibel slower or faster (i.e. each `<` or `>` represents a factor of `1.26`). Basically, if you see `[......*<<<<<<<<<<<<<|....................]` then something is *seriously* wrong, because the code is running 25x slower than expected. The `stim_perf` binary supports a `--only=BENCHMARK_NAME` filter flag. Multiple filters can be specified by separating them with commas `--only=A,B`. Ending a filter with a `*` turns it into a prefix filter `--only=sim_*`. ## Profiling with gcc and perf ```bash find src \ | grep "\\.cc" \ | grep -v "\\.\(test\|perf\|pybind\)\\.cc" \ | xargs g++ -I src -pthread -std=c++20 -O3 -march=native -g -fno-omit-frame-pointer sudo perf record -g ./a.out # [ADD STIM FLAGS FOR THE CASE YOU WANT TO PROFILE] sudo perf report ``` ## Creating a python dev environment First, create a fresh python 3.6+ virtual environment using your favorite method: [`python -m venv`](https://docs.python.org/3/library/venv.html), [`virtualenvwrapper`](https://virtualenvwrapper.readthedocs.io/en/latest/), [`conda`](https://docs.conda.io/en/latest/), or etc. Second, build and install a stim wheel. Follow the [python packaging stim](#pypackage.stim) instructions to create a wheel. I recommend [packaging with bazel](#pypackage.stim) because it is **BY FAR** the fastest. Once you have the wheel, run `pip install [that_wheel]`. For example: ```base # from the repository root in a python virtual environment bazel build :stim_dev_wheel pip uninstall stim --yes pip install bazel-bin/stim-0.0.dev0-py3-none-any.whl ``` Note that you need to repeat the above steps each time you make a change to `stim`. Third, use `pip install -e` to install development references to the pure-python glue packages: ``` # install test dependencies pip install pytest pymatching # install stimcirq dev reference: pip install -e glue/cirq # install sinter dev reference: pip install -e glue/sample # install stimzx dev reference: pip install -e glue/zx ``` ## Running python unit tests See [creating a python dev environment](#venv) for instructions on creating a python virtual environment with your changes to stim installed. Unit tests are run using `pytest`. Examples in docstrings are tested using the `dev/doctest_proper` script, which uses python's [`doctest`](https://docs.python.org/3/library/doctest.html) module but ensures values added to a module at import time are also tested (instead of requiring them to be [manually listed in a `__test__` property](https://docs.python.org/3/library/doctest.html#which-docstrings-are-examined)). To test everything: ```bash # from the repository root in a virtualenv with development wheels installed: pytest src glue dev/doctest_proper.py --module stim dev/doctest_proper.py --module stimcirq --import cirq sympy dev/doctest_proper.py --module sinter dev/doctest_proper.py --module stimzx ``` Test only `stim`: ```bash # from the repository root in a virtualenv with development wheels installed: pytest src dev/doctest_proper.py --module stim ``` Test only `stimcirq`: ```bash # from the repository root in a virtualenv with development wheels installed: pytest glue/cirq dev/doctest_proper.py --module stimcirq --import cirq sympy ``` Test only `sinter`: ```bash # from the repository root in a virtualenv with development wheels installed: pytest glue/sample dev/doctest_proper.py --module sinter ``` Test only `stimzx`: ```bash # from the repository root in a virtualenv with development wheels installed: pytest glue/zx dev/doctest_proper.py --module stimzx ``` ## Running sinter's python unit tests against a custom decoder Some of sinter's python unit tests verify that a decoder is behaving correctly. It can be useful, when creating a custom decoder, to run these tests against the decoder. This can be done by setting the environment `SINTER_PYTEST_CUSTOM_DECODERS` variable to `custom_package:custom_method` where `custom_package` is a python package to import and `custom_method` is a method name implemented by that package that returns a `Dict[str, sinter.Decoder]`. This is the same form of argument that's given to `sinter collect --custom_decoders`. Example: ``` SINTER_PYTEST_CUSTOM_DECODERS="my_custom_package:my_custom_sinter_decoder_dict_method" pytest glue/sample ``` # python packaging `stim` Because stim is a C++ extension, it is non-trivial to create working python packages for it. To make cross-platform release wheels, we rely heavily on cibuildwheels. To make development wheels, various other options are possible. ## python packaging `stim` with cibuildwheels When a commit is merged into the `main` branch of stim's GitHub repository, there are GitHub actions that use [cibuildwheels](https://github.com/pypa/cibuildwheel) to build wheels for all supported platforms. cibuildwheels can also be invoked locally, assuming you have Docker installed, using a command like: ```bash CIBW_BUILD=cp39-manylinux_x86_64 cibuildwheel --platform linux # output goes into wheelhouse/ ```` When these wheels are finished building, they are automatically uploaded to pypi as a dev version of stim. For release versions, the artifacts created by the github action must be manually downloaded and uploaded using `twine`: ```bash twine upload --username="${PROD_TWINE_USERNAME}" --password="${PROD_TWINE_PASSWORD}" artifacts_directory/* ``` ## python packaging `stim` with bazel Bazel can be used to create dev versions of the stim python wheel: ```bash # from the repository root: bazel build stim_dev_wheel # output is at bazel-bin/stim-0.0.dev0-py3-none-any.whl ``` ## python packaging `stim` with python setup.py Python can be used to create dev versions of the stim python wheel (very slow): Binary distribution: ```bash # from the repository root in a python venv with pybind11 installed: python setup.py bdist # output is at dist/* ``` Source distribution: ```bash # from the repository root in a python venv with pybind11 installed: python setup.py sdist # output is at dist/* ``` ## python packaging `stim` with pip install -e You can directly install stim as a development python wheel by using pip (very slow): ```bash # from the repository root pip install -e . # stim is now installed in current virtualenv as dev reference ``` ## python packaging `stim` with cmake A python module can be built using cmake. The output can be imported as `import stim`, but is not packaged, so if you want to pip install, prefer building the bindings with Bazel. ```bash # from the repository root cmake stim_python_bindings # output is at out/stim.cpython-${PYTHON_VERSION}-${ARCH}.so PYTHONPATH=out python -c "import stim; print(stim.__version__)" ``` # Python packaging `stimcirq` ## Python packaging `stimcirq` with python setup.py ```bash # from repo root cd glue/cirq python setup.py sdist cd - # output in glue/cirq/dist/* ``` ## Python packaging `stimcirq` with pip install -e ```bash # from repo root pip install -e glue/cirq # stimcirq is now installed in current virtualenv as dev reference ``` # Python packaging `sinter` ## Python packaging `sinter` with python setup.py ```bash # from repo root cd glue/sample python setup.py sdist cd - # output in glue/sample/dist/* ``` ## Python packaging `sinter` with pip install -e ```bash # from repo root pip install -e glue/sample # sinter is now installed in current virtualenv as dev reference ``` # Python packaging `stimzx` ## Python packaging `stimzx` with python setup.py ```bash # from repo root cd glue/zx python setup.py sdist cd - # output in glue/zx/dist/* ``` ## Python packaging `stimzx` with pip install -e ```bash # from repo root pip install -e glue/zx # stimzx is now installed in current virtualenv as dev reference ``` # Javascript packaging `stimjs` ## Javascript packaging `stimjs` with emscripten Install and activate enscriptem (`emcc` must be in your PATH). Example: ```bash # [outside of repo] git clone git@github.com:emscripten-core/emsdk.git cd emsdk ./emsdk install latest ./emsdk activate latest source emsdk_env.sh ``` Run the bash build script: ```bash # [from repo root] glue/javascript/build_wasm.sh ``` Outputs are the binary `out/stim.js` and the test runner `out/all_stim_tests.html`. Run tests by opening in a browser and checking for an `All tests passed.` message in the browser console: ```bash firefox out/all_stim_tests.html ``` # Autoformating code ## Autoformating code with clang-format Run the following command from the repo root to auto-format all C++ code: ```bash find src | grep "\.\(cc\|h\)$" | grep -Pv "crumble_data.cc|gate_data_3d_texture_data.cc" | xargs clang-format -i ``` # Adding new C++ files For the cmake build system, we maintain C++ file lists in the folder `file_lists`. When you add a new C++ file, they can be updated using `dev/regen_file_lists.sh` # Adding new CLI commands A [reference](usage_command_line.md) is maintained to document the CLI usage. This is generated using `dev/regen_docs.sh` which assumes the script is run from a virtualenv with a [dev wheel of stim](#python-packaging-stim) installed. ================================================ FILE: doc/file_format_dem_detector_error_model.md ================================================ # The Detector Error Model File Format (.dem) A detector error model file (.dem) is a human-readable specification of error mechanisms. The intent of the file format is to act as a reasonably flexible configuration language that can be easily consumed by *decoders*, which attempt to predict the logical observable frame from symptoms within the context of an error model. ## Index - [Encoding](#Encoding) - [Syntax](#Syntax) - [Semantics](#Semantics) - [Instruction Types](#Instruction-Types) - [`detector` instruction](#detector-instruction) - [`logical_observable` instruction](#logical_observable-instruction) - [`shift_detectors` instruction](#shift_detectors-instruction) - [`error` instruction](#error-instruction) - [`repeat` block](#repeat-block) - [Target Types](#State-Space) - [`D#`: relative detector target](#relative-detector-target) - [`L#`: logical observable target](#logical-observable-target) - [`#`: numeric target](#numeric-target) - [`^`: separator target](#separator-target) - [State Space](#State-Space) - [Examples](#Examples) - [Circular Error Model](#circular-error-model) - [Repetition Code Error Model](#repetition-code-error-model) - [Surface Code Error Model](#surface-code-error-model) ## Encoding Detector error model files are always encoded using UTF-8. Furthermore, the only place in the file where non-ASCII characters are permitted is inside of comments. ## Syntax A detector error model file is made up of a series of lines. Each line is either blank, an instruction, a block initiator, or a block terminator. Also, each line may be indented with spacing characters and may end with a comment indicated by a hash (`#`). ``` ::= * ::= ( | | )? ? '\n' ::= /[ \t]*/ ::= '#' /[^\n]*/ ``` An *instruction* is composed of a name, then (introduced in stim v1.15) an optional tag inside square brackets, then an optional comma-separated list of arguments inside of parentheses, then a list of space-separated targets. For example, the line `error[test](0.1) D5 D6 L0` is an instruction with a name (`error`), a tag (`test`), one argument (`0.1`), and three targets (`D5`, `D6`, and `L0`). ``` ::= ? ? ::= '[' /[^\r\]\n]/* ']' ::= '(' ')' ::= /[ \t]*/ /[ \t]*/ (',' )? ::= /[ \t]+/ ? ``` An instruction *name* starts with a letter and then contains a series of letters, digits, and underscores. Names are case-insensitive. An instruction *tag* is an arbitrary string enclosed by square brackets. Certain characters cannot appear directly in the tag, and must instead be included using escape sequences. The closing square bracket character `]` cannot appear directly, and is instead encoded using the escape sequence `\C`. The carriage return character cannot appear directly, and is instead encoded using the escape sequence `\r`. The line feed character cannot appear directly, and is instead encoded using the escape sequence `\n`. The backslash character `\` cannot appear directly, and is instead encoded using the escape sequence `\B`. (This backslash escape sequence differs from the common escape sequence `\\` because that sequence causes exponential explosions when escaping multiple times.) An *argument* is a double precision floating point number. A *target* can either be a relative detector target (a non-negative integer prefixed by `D`), a logical observable target (a non-negative integer prefixed by `L`), a component separator (`^`), or an unsigned integer target (a non-negative integer). ``` ::= /[a-zA-Z][a-zA-Z0-9_]*/ ::= ::= | | | ::= 'D' ::= 'L' = = '^' ``` A *block initiator* is an instruction suffixed with `{`. Every block initiator must be followed, eventually, by a matching *block terminator* which is just a `}`. The `{` always goes in the same line as its instruction, and the `}` always goes on a line by itself. ``` ::= /[ \t]*/ '{' ::= '}' ``` ## Semantics A *detector error model* is a list of independent error mechanisms. Each error mechanism has a *probability*, *symptoms* (the detectors that the error flips), and *frame changes* (the logical observables that the error flips). Error mechanisms may also suggest a *decomposition* into simpler error mechanisms. A detector error model can be sampled by independently keeping or discarding each error mechanism, with a keep probability equal to each error mechanism's probability. The resulting sample contains the symptoms and frame changes that appeared an odd number of times total in the kept error mechanisms. A detector error model file (.dem) specifies a detector error model by a series of instructions which are interpreted one by one, from start to finish. The instructions iteratively build up a detector error model by introducing error mechanisms. ### Instruction Types A detector error model file can contain several different types of instructions and blocks. #### detector instruction A `detector` instruction declares a particular symptom that is in the error model. It is not necessary to explicitly declare detectors using detector instructions. Detectors can also be implicitly declared simply by being mentioned in an error mechanism, or by a detector with a larger absolute index being declared. However, an explicit declaration is the only way to annotate the detector with coordinates, suggesting a spacetime location for the detector. The detector instruction should have a detector target (the detector being declared, relative to the current detector index offset) and can include any number of arguments specifying the detector's coordinates (relative to the current coordinate offset). Note that when the current coordinate offset has more coordinates than the detector, the additional coordinates are skipped. For example, if the offset is `(1,2,3)` and the detector relative position is `(10,10)` then the detector's absolute position would be `11,12`; not `11,12,3`. Example: `detector D4` declares a detector with index 4 (relative to the current detector index offset). Example: `detector D5 D6` declares a detector with index 5 (relative to the current detector index offset) and also a detector with index 6 (relative to the current detector index offset). Example: `detector(2.5,3.5,6) D7` declares a detector with index 7 (relative to the current detector index offset) and coordinates `2.5`, `3.5`, `6` (relative to the current coordinate offset). #### logical_observable instruction A `logical_observable` instruction ensures a particular frame change's existence is noted by the error model, even if no error mechanism affects it. Frame changes can also be implicitly declared simply by being mentioned in an error mechanism or other instruction, or by a frame change with a larger absolute index being declared. Example: `logical_observable L1` declares a logical observable with index 1. Example: `logical_observable L1 L2` declares a logical observable with index 1 and also a logical observable with index 2. #### shift_detectors instruction A `shift_detectors` instruction adds an offset into the current detector offset and the current coordinate offset. Takes 1 numeric target indicating the detector offset. Takes a variable number of arguments indicating the coordinate offset. Shifting is useful when writing loops, because otherwise the detectors declared by each iteration of the loop would all lie on top of each other. The detector offset can only be increased, not decreased. Example: `shift_detectors(0, 0.5) 2` leaves the first coordinate's offset alone, increases the second coordinate's offset by `0.5`, leaves all other coordinate offsets alone, and increases the detector offset by 2. Example: declaring a diagonal line of detectors. ``` detector(0, 0) D0 repeat 1000 { detector(0.5, 0.5) D1 error(0.01) D0 D1 shift_detectors(0.5, 0.5) 1 } ``` #### error instruction An `error` instruction adds an error mechanism to the error model. The error instruction takes 1 argument (the probability of the error) and multiple targets. The targets can include detectors, observables, and separators. Separators are used to suggest a way to decompose complicated error mechanisms into simpler ones. For example: `error(0.1) D2 D3 L0` adds an error mechanism with probability equal to 10%, two symptoms (`D2`, `D3`), one frame change (`L0`), and no suggested decomposition. Another example: `error(0.02) D2 L0 ^ D5 D6` adds an error mechanism with probability equal to 2%, three symptoms (`D2`, `D5`, `D6`), one frame change (`L0`), and a suggested decomposition into `D2 L0` and `D5 D6`. Yet another example: `error(0.03) D2 L0 ^ D3 L0` adds an error mechanism with probability equal to 3%, two symptoms (`D2`, `D3`), no frame changes (because the two `L0` cancel out), and a suggested decomposition into `D2 L0` and `D3 L0`. An example of a situation where the decomposition is relevant is a surface code with X and Z basis stabilizers. In such a surface code, Y errors can be factored into correlated X and Z errors. So, error mechanisms in the detector error model corresponding to Y errors in the circuit can suggest decomposing into the X and Z parts. It is valid for multiple error mechanisms to have the exact same targets. Typically they would be fused as part of building the error model (via the equation `p_{combined} = p_1 (1 - p_2) + p_2 (1 - p_1)`). It is also valid for error mechanisms to have the same symptoms but different frame changes (though this guarantees the error correcting code has distance at most 2). Similarly, an error mechanism may have frame changes with no symptoms (guaranteeing a code distance equal to 1). #### repeat block A detector error model file can also contain `REPEAT K { ... }` blocks, which indicate that the block's instructions should be iterated over `K` times instead of just once. Example: declaring a diagonal line of detectors. ``` detector(0, 0) D0 repeat 1000 { detector(0.5, 0.5) D1 error(0.01) D0 D1 shift_detectors(0.5, 0.5) 1 } ``` ### Target Types There are four types of targets that can be given to instructions: relative detector targets, logical observable targets, numeric targets, and separator targets. #### relative detector target A relative detector target is a non-negative integer prefixed by `D`, such as `D5`. It refers to a symptom in the error model. To get the actual detector target specified by the relative detector target, the integer after the `D` has to be added into the current relative detector offset. #### logical observable target A logical observable target is a non-negative integer prefixed by `L`, such as `L5`. It refers to a possible frame change that an error can cause. #### numeric target A numeric target is a non-negative integer. For example, the `REPEAT` block instruction takes a single numeric target indicating the number of repetitions and the `shift_detectors` instruction takes a single numeric target indicating the detector index shift. #### separator target A separator target (`^`) is not an actual thing to target, but rather a marker used to split up the targets of an error mechanism into a suggested decomposition. ### State Space Interpreting a detector error model file, to produce a detector error model, involves tracking several pieces of state. 1. **The Offsets** As the error model file is interpreted, the *relative detector index* and *relative coordinate offset* are shifted by `shift_detectors` instructions. Interpreting relative detector targets and coordinate annotations requires tracking these two values, since they shift the targets and coordinates. 2. **The Nodes (possible symptoms and frame changes)**. The error model must include every explicitly and implicitly declared detector (symptom) and logical observable (frame change). In practice this means computing the absolute index of the largest detector, and including a number of detectors equal to that index plus one. The same is done for frame changes: find the largest mentioned frame change index, and include a number of frame changes equal to that index plus one. Getting the number of nodes correct is important when parsing densely packed data that does not include explicit detector indices or frame change indices. 3. **The Edges (error mechanisms)**. The error model must include the mentioned error mechanisms. ## Examples ### Circular Error Model This error model defines 10 symptoms, and 10 error mechanisms with two symptoms. One of the error mechanisms, the "bad error", also has a frame change (`L0`). If the symptoms are nodes, and error mechanisms connect two nodes, then the model forms the 10 node circular graph. ``` error(0.1) D9 D0 L0 error(0.1) D0 D1 error(0.1) D1 D2 error(0.1) D2 D3 error(0.1) D3 D4 error(0.1) D4 D5 error(0.1) D5 D6 error(0.1) D6 D7 error(0.1) D7 D8 error(0.1) D8 D9 ``` This model can be defined more succinctly by using a `repeat` block: ``` error(0.1) D9 D0 L0 repeat 9 { error(0.1) D0 D1 shift_detectors 1 } ``` ### Repetition Code Error Model This is the output from `stim --gen repetition_code --task memory --rounds 1000 --distance 4 --after_clifford_depolarization 0.001 | stim --analyze_errors --fold_loops`. It includes coordinate annotations for the spacetime layout of the detectors. ``` error(0.0002667378157289137966) D0 error(0.0002667378157289137966) D0 D1 error(0.0005333333333331479603) D0 D3 error(0.0005333333333331479603) D0 D4 error(0.0002667378157289137966) D1 D2 error(0.0005333333333331479603) D1 D4 error(0.0005333333333331479603) D1 D5 error(0.0005333333333331479603) D2 D5 error(0.0002667378157289137966) D2 L0 error(0.0002667378157289137966) D3 error(0.0002667378157289137966) D3 D4 error(0.0002667378157289137966) D4 D5 error(0.0002667378157289137966) D5 L0 detector(1, 0) D0 detector(3, 0) D1 detector(5, 0) D2 repeat 998 { error(0.0002667378157289137966) D3 error(0.0002667378157289137966) D3 D4 error(0.0005333333333331479603) D3 D6 error(0.0005333333333331479603) D3 D7 error(0.0002667378157289137966) D4 D5 error(0.0005333333333331479603) D4 D7 error(0.0005333333333331479603) D4 D8 error(0.0005333333333331479603) D5 D8 error(0.0002667378157289137966) D5 L0 error(0.0002667378157289137966) D6 error(0.0002667378157289137966) D6 D7 error(0.0002667378157289137966) D7 D8 error(0.0002667378157289137966) D8 L0 shift_detectors(0, 1) 0 detector(1, 0) D3 detector(3, 0) D4 detector(5, 0) D5 shift_detectors 3 } error(0.0002667378157289137966) D3 error(0.0002667378157289137966) D3 D4 error(0.0005333333333331479603) D3 D6 error(0.0005333333333331479603) D3 D7 error(0.0002667378157289137966) D4 D5 error(0.0005333333333331479603) D4 D7 error(0.0005333333333331479603) D4 D8 error(0.0005333333333331479603) D5 D8 error(0.0002667378157289137966) D5 L0 error(0.0002667378157289137966) D6 error(0.0002667378157289137966) D6 D7 error(0.0002667378157289137966) D7 D8 error(0.0002667378157289137966) D8 L0 shift_detectors(0, 1) 0 detector(1, 0) D3 detector(3, 0) D4 detector(5, 0) D5 detector(1, 1) D6 detector(3, 1) D7 detector(5, 1) D8 ``` ### Surface Code Error Model This is the output from `stim --gen surface_code --task rotated_memory_x --rounds 1000 --distance 2 --after_clifford_depolarization 0.001 | stim --analyze_errors --fold_loops --decompose_errors`. It includes coordinate annotations for the spacetime layout of the detectors. ``` error(0.0002667378157289137966) D0 error(0.001332444444444449679) D0 D2 error(0.0002667378157289137966) D0 L0 error(0.0005333333333331479603) D1 error(0.001332444444444449679) D1 D4 error(0.0002667378157289137966) D1 L0 error(0.0001333866998761607556) D1 L0 ^ D2 L0 error(0.0002667378157289137966) D1 L0 ^ D3 error(0.0001333866998761607556) D1 L0 ^ D3 ^ D2 L0 error(0.0002667378157289137966) D1 ^ D3 error(0.0005333333333331479603) D2 error(0.001332444444444449679) D2 D5 error(0.0007997866287252842132) D2 L0 error(0.0002667378157289137966) D2 L0 ^ D0 L0 error(0.0004000533570511300221) D2 L0 ^ D3 error(0.0002667378157289137966) D2 ^ D0 error(0.0001333866998761607556) D2 ^ D1 error(0.0001333866998761607556) D2 ^ D3 error(0.0001333866998761607556) D2 ^ D3 ^ D1 error(0.001731254257715537058) D3 error(0.0002667378157289137966) D3 ^ D1 error(0.0001333866998761607556) D3 ^ D2 error(0.0001333866998761607556) D3 ^ D2 L0 error(0.0001333866998761607556) D3 ^ D4 error(0.0001333866998761607556) D3 ^ D4 ^ D1 error(0.0001333866998761607556) D3 ^ D5 error(0.0001333866998761607556) D3 ^ D5 ^ D2 error(0.0004666977902291165391) D4 error(0.001332444444444449679) D4 D7 error(0.0003334000326131335203) D4 L0 error(0.0001333866998761607556) D4 L0 ^ D1 L0 error(0.0001333866998761607556) D4 L0 ^ D1 L0 ^ D3 error(0.0002000667052120994559) D4 L0 ^ D3 error(6.669779853440971351e-05) D4 L0 ^ D5 L0 error(6.669779853440971351e-05) D4 L0 ^ D5 L0 ^ D3 error(0.0002000667052120994559) D4 L0 ^ D6 error(6.669779853440971351e-05) D4 L0 ^ D6 ^ D3 error(6.669779853440971351e-05) D4 L0 ^ D6 ^ D5 L0 error(6.669779853440971351e-05) D4 L0 ^ D6 ^ D5 L0 ^ D3 error(0.0001333866998761607556) D4 ^ D1 error(0.0002000667052120994559) D4 ^ D3 error(0.0001333866998761607556) D4 ^ D6 error(0.0001333866998761607556) D4 ^ D6 ^ D3 error(0.0002000667052120994559) D5 error(0.0003334000326131335203) D5 L0 error(0.0001333866998761607556) D5 L0 ^ D2 L0 error(0.0001333866998761607556) D5 L0 ^ D2 L0 ^ D3 error(0.0003334000326131335203) D5 L0 ^ D3 error(0.0001333866998761607556) D5 L0 ^ D6 error(0.0001333866998761607556) D5 L0 ^ D6 ^ D3 error(0.0001333866998761607556) D5 ^ D2 error(6.669779853440971351e-05) D5 ^ D3 error(6.669779853440971351e-05) D5 ^ D4 error(6.669779853440971351e-05) D5 ^ D4 ^ D3 error(6.669779853440971351e-05) D5 ^ D6 error(6.669779853440971351e-05) D5 ^ D6 ^ D3 error(6.669779853440971351e-05) D5 ^ D6 ^ D4 error(6.669779853440971351e-05) D5 ^ D6 ^ D4 ^ D3 error(0.0006665777540627741476) D6 error(0.0004000533570511300221) D6 ^ D3 error(0.0002000667052120994559) D6 ^ D4 error(6.669779853440971351e-05) D6 ^ D4 ^ D3 error(6.669779853440971351e-05) D6 ^ D5 L0 error(6.669779853440971351e-05) D6 ^ D5 L0 ^ D3 error(0.0001333866998761607556) D6 ^ D7 error(0.0001333866998761607556) D6 ^ D7 ^ D4 error(0.0001333866998761607556) D7 error(0.0001333866998761607556) D7 L0 error(0.0001333866998761607556) D7 L0 ^ D4 L0 error(0.0001333866998761607556) D7 L0 ^ D4 L0 ^ D6 error(0.0001333866998761607556) D7 L0 ^ D6 error(0.0001333866998761607556) D7 ^ D4 detector(2, 0, 0) D0 detector(2, 4, 0) D1 shift_detectors(0, 0, 1) 0 detector(2, 0, 0) D2 detector(2, 2, 0) D3 detector(2, 4, 0) D4 repeat 498 { error(0.0001333866998761607556) D5 error(0.001332444444444449679) D5 D8 error(0.0001333866998761607556) D5 L0 error(0.0001333866998761607556) D5 L0 ^ D6 error(0.0006665777540627741476) D6 error(0.0001333866998761607556) D6 ^ D5 error(0.0001333866998761607556) D6 ^ D8 error(0.0001333866998761607556) D6 ^ D8 ^ D5 error(0.0003334000326131335203) D7 error(0.001332444444444449679) D7 D10 error(0.0002000667052120994559) D7 L0 error(6.669779853440971351e-05) D7 L0 ^ D6 error(6.669779853440971351e-05) D7 L0 ^ D8 L0 error(6.669779853440971351e-05) D7 L0 ^ D8 L0 ^ D6 error(0.0002000667052120994559) D7 L0 ^ D9 error(6.669779853440971351e-05) D7 L0 ^ D9 ^ D6 error(6.669779853440971351e-05) D7 L0 ^ D9 ^ D8 L0 error(6.669779853440971351e-05) D7 L0 ^ D9 ^ D8 L0 ^ D6 error(0.0002000667052120994559) D7 ^ D6 error(0.0001333866998761607556) D7 ^ D9 error(0.0001333866998761607556) D7 ^ D9 ^ D6 error(0.0003334000326131335203) D8 error(0.001332444444444449679) D8 D11 error(0.0004666977902291165391) D8 L0 error(0.0001333866998761607556) D8 L0 ^ D5 L0 error(0.0001333866998761607556) D8 L0 ^ D5 L0 ^ D6 error(0.0003334000326131335203) D8 L0 ^ D6 error(0.0002667378157289137966) D8 L0 ^ D9 error(0.0001333866998761607556) D8 L0 ^ D9 ^ D6 error(0.0001333866998761607556) D8 ^ D5 error(6.669779853440971351e-05) D8 ^ D6 error(6.669779853440971351e-05) D8 ^ D7 error(6.669779853440971351e-05) D8 ^ D7 ^ D6 error(6.669779853440971351e-05) D8 ^ D9 error(6.669779853440971351e-05) D8 ^ D9 ^ D6 error(6.669779853440971351e-05) D8 ^ D9 ^ D7 error(6.669779853440971351e-05) D8 ^ D9 ^ D7 ^ D6 error(0.001332266856321125473) D9 error(0.0004000533570511300221) D9 ^ D6 error(0.0002000667052120994559) D9 ^ D7 error(6.669779853440971351e-05) D9 ^ D7 ^ D6 error(0.0001333866998761607556) D9 ^ D8 error(6.669779853440971351e-05) D9 ^ D8 L0 error(6.669779853440971351e-05) D9 ^ D8 L0 ^ D6 error(0.0001333866998761607556) D9 ^ D10 error(0.0001333866998761607556) D9 ^ D10 ^ D7 error(0.0001333866998761607556) D9 ^ D11 error(0.0001333866998761607556) D9 ^ D11 ^ D8 error(0.0004666977902291165391) D10 error(0.001332444444444449679) D10 D13 error(0.0003334000326131335203) D10 L0 error(0.0001333866998761607556) D10 L0 ^ D7 L0 error(0.0001333866998761607556) D10 L0 ^ D7 L0 ^ D9 error(0.0002000667052120994559) D10 L0 ^ D9 error(6.669779853440971351e-05) D10 L0 ^ D11 L0 error(6.669779853440971351e-05) D10 L0 ^ D11 L0 ^ D9 error(0.0002000667052120994559) D10 L0 ^ D12 error(6.669779853440971351e-05) D10 L0 ^ D12 ^ D9 error(6.669779853440971351e-05) D10 L0 ^ D12 ^ D11 L0 error(6.669779853440971351e-05) D10 L0 ^ D12 ^ D11 L0 ^ D9 error(0.0001333866998761607556) D10 ^ D7 error(0.0002000667052120994559) D10 ^ D9 error(0.0001333866998761607556) D10 ^ D12 error(0.0001333866998761607556) D10 ^ D12 ^ D9 error(0.0002000667052120994559) D11 error(0.0003334000326131335203) D11 L0 error(0.0001333866998761607556) D11 L0 ^ D8 L0 error(0.0001333866998761607556) D11 L0 ^ D8 L0 ^ D9 error(0.0003334000326131335203) D11 L0 ^ D9 error(0.0001333866998761607556) D11 L0 ^ D12 error(0.0001333866998761607556) D11 L0 ^ D12 ^ D9 error(0.0001333866998761607556) D11 ^ D8 error(6.669779853440971351e-05) D11 ^ D9 error(6.669779853440971351e-05) D11 ^ D10 error(6.669779853440971351e-05) D11 ^ D10 ^ D9 error(6.669779853440971351e-05) D11 ^ D12 error(6.669779853440971351e-05) D11 ^ D12 ^ D9 error(6.669779853440971351e-05) D11 ^ D12 ^ D10 error(6.669779853440971351e-05) D11 ^ D12 ^ D10 ^ D9 error(0.0006665777540627741476) D12 error(0.0004000533570511300221) D12 ^ D9 error(0.0002000667052120994559) D12 ^ D10 error(6.669779853440971351e-05) D12 ^ D10 ^ D9 error(6.669779853440971351e-05) D12 ^ D11 L0 error(6.669779853440971351e-05) D12 ^ D11 L0 ^ D9 error(0.0001333866998761607556) D12 ^ D13 error(0.0001333866998761607556) D12 ^ D13 ^ D10 error(0.0001333866998761607556) D13 error(0.0001333866998761607556) D13 L0 error(0.0001333866998761607556) D13 L0 ^ D10 L0 error(0.0001333866998761607556) D13 L0 ^ D10 L0 ^ D12 error(0.0001333866998761607556) D13 L0 ^ D12 error(0.0001333866998761607556) D13 ^ D10 shift_detectors(0, 0, 1) 0 detector(2, 0, 0) D5 detector(2, 2, 0) D6 detector(2, 4, 0) D7 shift_detectors(0, 0, 1) 0 detector(2, 0, 0) D8 detector(2, 2, 0) D9 detector(2, 4, 0) D10 shift_detectors 6 } error(0.0001333866998761607556) D5 error(0.001332444444444449679) D5 D8 error(0.0001333866998761607556) D5 L0 error(0.0001333866998761607556) D5 L0 ^ D6 error(0.0006665777540627741476) D6 error(0.0001333866998761607556) D6 ^ D5 error(0.0001333866998761607556) D6 ^ D8 error(0.0001333866998761607556) D6 ^ D8 ^ D5 error(0.0003334000326131335203) D7 error(0.001332444444444449679) D7 D10 error(0.0002000667052120994559) D7 L0 error(6.669779853440971351e-05) D7 L0 ^ D6 error(6.669779853440971351e-05) D7 L0 ^ D8 L0 error(6.669779853440971351e-05) D7 L0 ^ D8 L0 ^ D6 error(0.0002000667052120994559) D7 L0 ^ D9 error(6.669779853440971351e-05) D7 L0 ^ D9 ^ D6 error(6.669779853440971351e-05) D7 L0 ^ D9 ^ D8 L0 error(6.669779853440971351e-05) D7 L0 ^ D9 ^ D8 L0 ^ D6 error(0.0002000667052120994559) D7 ^ D6 error(0.0001333866998761607556) D7 ^ D9 error(0.0001333866998761607556) D7 ^ D9 ^ D6 error(0.0003334000326131335203) D8 error(0.001332444444444449679) D8 D11 error(0.0004666977902291165391) D8 L0 error(0.0001333866998761607556) D8 L0 ^ D5 L0 error(0.0001333866998761607556) D8 L0 ^ D5 L0 ^ D6 error(0.0003334000326131335203) D8 L0 ^ D6 error(0.0002667378157289137966) D8 L0 ^ D9 error(0.0001333866998761607556) D8 L0 ^ D9 ^ D6 error(0.0001333866998761607556) D8 ^ D5 error(6.669779853440971351e-05) D8 ^ D6 error(6.669779853440971351e-05) D8 ^ D7 error(6.669779853440971351e-05) D8 ^ D7 ^ D6 error(6.669779853440971351e-05) D8 ^ D9 error(6.669779853440971351e-05) D8 ^ D9 ^ D6 error(6.669779853440971351e-05) D8 ^ D9 ^ D7 error(6.669779853440971351e-05) D8 ^ D9 ^ D7 ^ D6 error(0.001731254257715537058) D9 error(0.0004000533570511300221) D9 ^ D6 error(0.0002000667052120994559) D9 ^ D7 error(6.669779853440971351e-05) D9 ^ D7 ^ D6 error(0.0001333866998761607556) D9 ^ D8 error(6.669779853440971351e-05) D9 ^ D8 L0 error(6.669779853440971351e-05) D9 ^ D8 L0 ^ D6 error(0.0001333866998761607556) D9 ^ D10 error(0.0001333866998761607556) D9 ^ D10 ^ D7 error(0.0001333866998761607556) D9 ^ D11 error(0.0001333866998761607556) D9 ^ D11 ^ D8 error(0.0007997866287252842132) D10 error(0.001332444444444449679) D10 D12 error(0.0005333333333331479603) D10 L0 error(0.0001333866998761607556) D10 L0 ^ D7 L0 error(0.0001333866998761607556) D10 L0 ^ D7 L0 ^ D9 error(0.0002667378157289137966) D10 L0 ^ D9 error(0.0001333866998761607556) D10 L0 ^ D11 L0 error(0.0001333866998761607556) D10 L0 ^ D11 L0 ^ D9 error(0.0001333866998761607556) D10 ^ D7 error(0.0004000533570511300221) D10 ^ D9 error(0.0002667378157289137966) D11 error(0.0005333333333331479603) D11 L0 error(0.0001333866998761607556) D11 L0 ^ D8 L0 error(0.0001333866998761607556) D11 L0 ^ D8 L0 ^ D9 error(0.0005333333333331479603) D11 L0 ^ D9 error(0.0001333866998761607556) D11 ^ D8 error(0.0001333866998761607556) D11 ^ D9 error(0.0001333866998761607556) D11 ^ D10 error(0.0001333866998761607556) D11 ^ D10 ^ D9 error(0.0002667378157289137966) D12 error(0.0002667378157289137966) D12 L0 error(0.0002667378157289137966) D12 L0 ^ D10 L0 error(0.0002667378157289137966) D12 ^ D10 shift_detectors(0, 0, 1) 0 detector(2, 0, 0) D5 detector(2, 2, 0) D6 detector(2, 4, 0) D7 shift_detectors(0, 0, 1) 0 detector(2, 0, 0) D8 detector(2, 2, 0) D9 detector(2, 4, 0) D10 detector(2, 0, 1) D11 detector(2, 4, 1) D12 ``` ================================================ FILE: doc/file_format_stim_circuit.md ================================================ # The Stim Circuit File Format (.stim) A stim circuit file (.stim) is a human-readable specification of an annotated stabilizer circuit. The circuit file includes gates to apply to qubits, noise processes to apply during simulations, and annotations for tasks such as detection event sampling and drawing the circuit. ## Index - [Encoding](#Encoding) - [Syntax](#Syntax) - [Semantics](#Semantics) - [Instruction Types](#Instruction-Types) - [Supported Gates](gates.md) - [Broadcasting](#Broadcasting) - [State Space](#State-Space) - [Vacuous repeat blocks are not allowed](#Vacuous-repeat-blocks-are-not-allowed) - [Examples](#Examples) - [Teleportation Circuit](#Teleportation-Circuit) - [Repetition Code Circuit](#Repetition-Code-Circuit) - [Fully Annotated Noisy Repetition Code Circuit](#Fully-Annotated-Noisy-Repetition-Code-Circuit) - [Fully Annotated Noisy Surface Code Circuit](#Fully-Annotated-Noisy-Surface-Code-Circuit) ## Encoding Stim circuit files are always encoded using UTF-8. (Also, the only place in the file where non-ASCII characters can validly appear is inside of comments and tags.) ## Syntax A stim circuit file is made up of a series of lines. Each line is either blank, an instruction, a block initiator, or a block terminator. Also, each line may be indented with spacing characters and may end with a comment indicated by a hash (`#`). Comments and indentation are purely decorative; they carry no semantic significance. Here is a formal definition of the above paragraph. Entries like `/this/` are regular expressions. Entries like `` are named expressions. Entries like `'this'` are literal string expressions. The `::=` operator means "defined as". The `|` binary operator means "or". The `?` suffix operator means "zero-or-one". The `*` suffix operator means "zero-or-many". Parens are used to group expressions. Adjacent expressions are combined by concatenation. ``` ::= * ::= ( | | )? ? '\n' ::= /[ \t]*/ ::= '#' /[^\n]*/ ``` An *instruction* is composed of a name, then (introduced in stim v1.15) an optional tag inside square brackets, then an optional comma-separated list of arguments inside of parentheses, then a list of space-separated targets. For example, the line `X_ERROR[test](0.1) 5 6` is an instruction with a name (`X_ERROR`), a tag (`test`), one argument (`0.1`), and two targets (`5` and `6`). ``` ::= ? ? ::= '[' /[^\r\]\n]/* ']' ::= '(' ')' ::= /[ \t]*/ /[ \t]*/ (',' )? ::= /[ \t]+/ ? ``` An instruction *name* starts with a letter and then contains a series of letters, digits, and underscores. Names are case-insensitive. An instruction *tag* is an arbitrary string enclosed by square brackets. Certain characters cannot appear directly in the tag, and must instead be included using escape sequences. The closing square bracket character `]` cannot appear directly, and is instead encoded using the escape sequence `\C`. The carriage return character cannot appear directly, and is instead encoded using the escape sequence `\r`. The line feed character cannot appear directly, and is instead encoded using the escape sequence `\n`. The backslash character `\` cannot appear directly, and is instead encoded using the escape sequence `\B`. (This backslash escape sequence differs from the common escape sequence `\\` because that sequence causes exponential explosions when escaping multiple times.) An *argument* is a double precision floating point number. A *target* can either be a qubit target (a non-negative integer), a measurement record target (a negative integer prefixed by `rec[` and suffixed by `]`), a sweep bit target (a non-negative integer prefixed by `sweep[` and suffixed by `]`), a Pauli target (an integer prefixed by `X`, `Y`, or `Z`), or a combiner (`*`). Additionally, qubit targets and Pauli targets may be prefixed by a `!` to indicate that measurement results should be negated. ``` ::= /[a-zA-Z][a-zA-Z0-9_]*/ ::= ::= | | | | ::= '!'? ::= "rec[-" "]" ::= "sweep[" "]" ::= '!'? /[XYZ]/ ::= '*' ``` A *block initiator* is an instruction suffixed with `{`. Every block initiator must be followed, eventually, by a matching *block terminator* which is just a `}`. The `{` always goes in the same line as its instruction, and the `}` always goes on a line by itself. ``` ::= /[ \t]*/ '{' ::= '}' ``` Blocks can be nested. Block contents are indented by convention, but this is not necessary. ## Semantics A stim circuit file is executed by executing each of its instructions and blocks, one by one, from start to finish. ### Instruction Types For a complete list of instructions supported by stim and their individual meanings, see the [gates reference](gates.md). Generally speaking, the instructions that can appear in a stim circuit file can be divided up into three groups: 1. Operations 2. Annotations 3. Control Flow An *operation* is a quantum channel to apply to the quantum state of the system, possibly resulting in bits being appended to the measurement record. There are Clifford operations (e.g. the Hadamard gate `H` or the controlled-not gate `CNOT`), stabilizers operations (e.g. the measurement gate `M` or the reset gate `R`), and noise operations (e.g. the phase damping channel `Z_ERROR` or the two qubit depolarizing channel `DEPOLARIZE2`). An *annotation* is a piece of additional information that is not strictly necessary, but which enables other useful capabilities. The most functionally useful annotations are `DETECTOR` and `OBSERVABLE_INCLUDE`, which define the measurements that are compared when sampling a circuit's detection events. Other annotations include `QUBIT_COORDS` and `TICK`, which can be used to hint at the intended spacetime layout of a circuit. (Depending on your needs, you may also find yourself considering noisy operations to be annotations. They define a noise model for the circuit.) *Control flow* blocks make changes to the usual one-after-another execution order of instructions. Currently, control flow is limited to *repetition*. A circuit can contain `REPEAT K { ... }` blocks, which indicate that the block's instructions should be iterated over `K` times instead of just once. ### Tags Instruction tags have no effect on the function of a circuit. In general, tools should attempt to propagate tags through circuit transformations and otherwise ignore them. The intent is that users and tools can use tags to specify custom behavior that stim is not aware of. For example, consider the tagged instruction `TICK[100ns]`. In most situations, the `100ns` tag does nothing. But if you are using a tool that adds noise to circuits, and it's programmed to look at tags to get hints about what noise to add, then the `100ns` tag could be a message to that tool (specifying a duration, which the tool could use when computing the probability argument of an inserted `DEPOLARIZE1` instruction). ### Target Types There are four types of targets that can be given to instructions: qubit targets, measurement record targets, sweep bit targets, and Pauli targets. A qubit target refers to a qubit by index. There's a qubit `0`, a qubit `1`, a qubit `2`, and so forth. A qubit target may be prefixed by `!`, like `!2`, to mark it as inverted. Inverted qubit targets are only meaningful for operations that produce measurement results. They indicate that the recorded measurement result, for the given qubit target, should be inverted. For example `M 0 !1` measures qubit `0` and qubit `1`, but also inverts the result recorded for qubit `1`. A measurement record target refers to a recorded measurement result, relative to the current end of the measurement record. For example, `rec[-1]` is the most recent measurement result, `rec[-2]` is the second most recent, and so forth. (The semantics of the negative indices into the measurement record match the semantics of negative indices into lists in Python. The reason negative indices are used is to make it possible to write loops.) It is an error to refer to a measurement result so far back that it would precede the start of the circuit. A sweep bit target refers to a column in a data table where each row refers to a separate shots of the circuit, and each column refers to configuration bits that vary from shot to shot. For example, when using randomized spin echo, the spin echo operations that actually occurred could be recording into a table. For example, `CNOT sweep[5] 1` says an X operation was applied (or should be applied) to qubit 1 for shots where the sweep bit in the column with index 5 is set. Sweep bits default to False when running in a context where no table is provided, and sweep bits past the end of the provided table also default to False. A Pauli target is a qubit target prefixed by a Pauli operation `X`, `Y`, or `Z`. They are used when specifying Pauli products. For example, `CORRELATED_ERROR(0.1) X1 Y3 Z2` uses Pauli targets to specify the error that is applied. Pauli targets may be grouped using combiners (`*`) and may be prefixed by `!` to mark them as inverted. Inverted Pauli targets are only meaningful for operations that produce measurement results. They indicate that the recorded measurement result, for the given group of Paulis, should be inverted. For example `MPP !X1*Z2 Y3` measures the Pauli product `X1*Z2` and inverts the result, then also measures the Pauli `Y3`. ### Broadcasting When quantum operations are applied to too many targets, the operation is *broadcast* over the targets. When a single qubit operation (e.g. `H` or `DEPOLARIZE1`) is given multiple targets, it is applied to each target in order. For example, `H 0 1 2` is equivalent to `H 0` then `H 1` then `H 2`. Similarly, `X_ERROR(0.1) 3 2` is equivalent to `X_ERROR(0.1) 3` then `X_ERROR(0.1) 2`. When a two qubit operation (e.g. `CNOT` or `DEPOLARIZE2`) is given multiple targets, it is applied to aligned target pairs in order. For example, `CNOT 0 1 1 2 2 0` is equivalent to `CNOT 0 1` then `CNOT 1 2` then `CNOT 2 0`. It is an error to give a two qubit operation an odd number of targets. ### State Space A simulator executing a stim circuit is expected to store three things: 1. **The Qubits**. By convention, all qubits start in the |0> state. The simulator then tracks the state of any qubits that have been operated on. Note that stim circuit files don't explicitly state the number of qubits needed. Instead, the number of qubits is implied by the qubit targets present in the file. For example, a simulator may look over the circuit and find the largest qubit target `n-1` that is operated on and then size itself for operating on `n` qubits. 2. **The Measurement Record**. When a measurement operation is performed, the measurement result is appended to a list of bits referred to as the measurement record. The measurement record is an immutable log of all measurement results so far. Controlled operations can use a measurement record target as a control, instead of a qubit target. For example, `CZ rec[-1] 5` says "if the most recent measurement result was TRUE then apply a Z gate to qubit `5`". The measurement record is also used when defining detectors and observables. 3. **The "Correlated Error Occurred" Flag**. The `ELSE_CORRELATED_ERROR` instruction applies an error mechanism conditioned on the preceding `CORRELATED_ERROR` instruction (and any intermediate `ELSE_CORRELATED_ERROR` instructions) having not occurred. This is tracked by a hidden boolean flag. (The interpreter of the circuit may also track coordinate offsets accumulated from `SHIFT_COORDS` annotations, which affect the meaning of `QUBIT_COORDS` annotations and the coordinate arguments given to `DETECTOR`. But these have no effect on simulations, and so are often not strictly necessary to track.) ### Vacuous repeat blocks are not allowed It's an error for a circuit to contain a repeat block that is repeated 0 times. The reason it's an error is because it's ambiguous whether observables and qubits mentioned in the block "exist". For example, consider this malformed circuit: ``` REPEAT 0 { M 0 OBSERVABLE_INCLUDE(0) rec[-1] } ``` This circuit mentions a logical observable with index 0, suggesting the circuit has a logical observable. So, a tool that samples logical observables should produce 1 bit of information when sampling this circuit. But the logical observable is only mentioned in a block that is never run, effectively commenting it out, leaving behind an empty circuit with 0 logical observables. So, a tool that samples logical observables should produce 0 bits of information when sampling this circuit. Is there an observable in the circuit or isn't there? Should the tool produce 0 bits or 1 bit? That's the ambiguity. Note that a tool that unrolls loops in the circuit will implicitly delete the ambiguous logical observables. Conversely, note that a tool that finds logical observables by iterating over the lines of the circuit file, looking for `OBSERVABLE_INCLUDE` instructions, will implicitly keep the ambiguous logical observables. Both of these methods seem "obviously correct" on their own, but they disagree about whether or not to keep the ambiguous observables. It's very easy to write code that accidentally disagrees with itself about the correct behavior, and introduce a bug. Which is why vacuous repeat blocks are not allowed. ## Examples ### Teleportation Circuit [View equivalent circuit in Quirk](https://algassert.com/quirk#circuit=%7B%22cols%22%3A%5B%5B%22H%22%5D%2C%5B%22%E2%80%A2%22%2C1%2C1%2C1%2C1%2C1%2C1%2C%22X%22%5D%2C%5B1%2C%22H%22%5D%2C%5B1%2C%22Z%5E%C2%BD%22%5D%2C%5B%22%E2%80%A2%22%2C%22X%22%5D%2C%5B%22H%22%5D%2C%5B%22Measure%22%2C%22Measure%22%5D%2C%5B%22%E2%80%A2%22%2C1%2C1%2C1%2C1%2C1%2C1%2C%22Z%22%5D%2C%5B1%2C%22%E2%80%A2%22%2C1%2C1%2C1%2C1%2C1%2C%22X%22%5D%5D%7D) ``` # Distribute a Bell Pair. H 0 CNOT 0 99 # Sender creates an arbitrary qubit state to send. H 1 S 1 # Sender performs a Bell Basis measurement. CNOT 0 1 H 0 M 0 1 # Measure both of the sender's qubits. # Receiver performs frame corrections based on measurement results. CZ rec[-2] 99 CNOT rec[-1] 99 ``` ### Repetition Code Circuit [View equivalent circuit in Quirk](https://algassert.com/quirk#circuit=%7B%22cols%22%3A%5B%5B%22~ch91%22%2C1%2C%22~ch91%22%2C1%2C%22~ch91%22%5D%2C%5B1%2C%22~tbv6%22%2C1%2C%22~tbv6%22%2C1%2C%22~tbv6%22%5D%2C%5B1%2C%22ZDetectControlReset%22%2C1%2C%22ZDetectControlReset%22%2C1%2C%22ZDetectControlReset%22%5D%2C%5B%22~ch91%22%2C1%2C%22~ch91%22%2C1%2C%22~ch91%22%5D%2C%5B1%2C%22~tbv6%22%2C1%2C%22~tbv6%22%2C1%2C%22~tbv6%22%5D%2C%5B1%2C%22ZDetectControlReset%22%2C1%2C%22ZDetectControlReset%22%2C1%2C%22ZDetectControlReset%22%5D%2C%5B%22%E2%80%A6%22%2C%22%E2%80%A6%22%2C%22%E2%80%A6%22%2C%22%E2%80%A6%22%2C%22%E2%80%A6%22%2C%22%E2%80%A6%22%2C%22%E2%80%A6%22%5D%2C%5B%22~ch91%22%2C1%2C%22~ch91%22%2C1%2C%22~ch91%22%5D%2C%5B1%2C%22~tbv6%22%2C1%2C%22~tbv6%22%2C1%2C%22~tbv6%22%5D%2C%5B1%2C%22ZDetectControlReset%22%2C1%2C%22ZDetectControlReset%22%2C1%2C%22ZDetectControlReset%22%5D%2C%5B%22~ch91%22%2C1%2C%22~ch91%22%2C1%2C%22~ch91%22%5D%2C%5B1%2C%22~tbv6%22%2C1%2C%22~tbv6%22%2C1%2C%22~tbv6%22%5D%2C%5B%22ZDetectControlReset%22%2C%22ZDetectControlReset%22%2C%22ZDetectControlReset%22%2C%22ZDetectControlReset%22%2C%22ZDetectControlReset%22%2C%22ZDetectControlReset%22%2C%22ZDetectControlReset%22%5D%5D%2C%22gates%22%3A%5B%7B%22id%22%3A%22~ch91%22%2C%22circuit%22%3A%7B%22cols%22%3A%5B%5B%22%E2%80%A2%22%2C%22X%22%5D%5D%7D%7D%2C%7B%22id%22%3A%22~tbv6%22%2C%22circuit%22%3A%7B%22cols%22%3A%5B%5B%22X%22%2C%22%E2%80%A2%22%5D%5D%7D%7D%5D%7D) (without detector annotations). ``` # Measure the parities of adjacent data qubits. # Data qubits are 0, 2, 4, 6. # Measurement qubits are 1, 3, 5. CNOT 0 1 2 3 4 5 CNOT 2 1 4 3 6 5 MR 1 3 5 # Annotate that the measurements should be deterministic. DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] # Perform 1000 more rounds of measurements. REPEAT 1000 { # Measure the parities of adjacent data qubits. CNOT 0 1 2 3 4 5 CNOT 2 1 4 3 6 5 MR 1 3 5 # Annotate that the measurements should agree with previous round. DETECTOR rec[-3] rec[-6] DETECTOR rec[-2] rec[-5] DETECTOR rec[-1] rec[-4] } # Measure data qubits. M 0 2 4 6 # Annotate that the data measurements should agree with the parity measurements. DETECTOR rec[-3] rec[-4] rec[-7] DETECTOR rec[-2] rec[-3] rec[-6] DETECTOR rec[-1] rec[-2] rec[-5] # Declare one of the data qubit measurements to a logical measurement result. OBSERVABLE_INCLUDE(0) rec[-1] ``` ### Fully Annotated Noisy Repetition Code Circuit This is the output from `stim --gen repetition_code --task memory --rounds 1000 --distance 4 --after_clifford_depolarization 0.001`. It includes noise operations and annotations for the spacetime layout of the circuit. ``` # Generated repetition_code circuit. # task: memory # rounds: 1000 # distance: 3 # before_round_data_depolarization: 0 # before_measure_flip_probability: 0 # after_reset_flip_probability: 0 # after_clifford_depolarization: 0.001 # layout: # L0 Z1 d2 Z3 d4 Z5 d6 # Legend: # d# = data qubit # L# = data qubit with logical observable crossing # Z# = measurement qubit R 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.001) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.001) 2 1 4 3 6 5 TICK MR 1 3 5 DETECTOR(1, 0) rec[-3] DETECTOR(3, 0) rec[-2] DETECTOR(5, 0) rec[-1] REPEAT 999 { TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.001) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.001) 2 1 4 3 6 5 TICK MR 1 3 5 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-3] rec[-6] DETECTOR(3, 0) rec[-2] rec[-5] DETECTOR(5, 0) rec[-1] rec[-4] } M 0 2 4 6 DETECTOR(1, 1) rec[-3] rec[-4] rec[-7] DETECTOR(3, 1) rec[-2] rec[-3] rec[-6] DETECTOR(5, 1) rec[-1] rec[-2] rec[-5] OBSERVABLE_INCLUDE(0) rec[-1] ``` ### Fully Annotated Noisy Surface Code Circuit This is the output from `stim --gen surface_code --task rotated_memory_x --rounds 1000 --distance 3 --after_clifford_depolarization 0.001`. It includes noise operations and annotations for the spacetime layout of the circuit. ``` # Generated surface_code circuit. # task: rotated_memory_x # rounds: 1000 # distance: 3 # before_round_data_depolarization: 0 # before_measure_flip_probability: 0 # after_reset_flip_probability: 0 # after_clifford_depolarization: 0.001 # layout: # X25 # L15 d17 d19 # Z14 X16 Z18 # L8 d10 d12 # Z9 X11 Z13 # L1 d3 d5 # X2 # Legend: # d# = data qubit # L# = data qubit with logical observable crossing # X# = measurement qubit (X stabilizer) # Z# = measurement qubit (Z stabilizer) QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 1) 3 QUBIT_COORDS(5, 1) 5 QUBIT_COORDS(1, 3) 8 QUBIT_COORDS(2, 2) 9 QUBIT_COORDS(3, 3) 10 QUBIT_COORDS(4, 2) 11 QUBIT_COORDS(5, 3) 12 QUBIT_COORDS(6, 2) 13 QUBIT_COORDS(0, 4) 14 QUBIT_COORDS(1, 5) 15 QUBIT_COORDS(2, 4) 16 QUBIT_COORDS(3, 5) 17 QUBIT_COORDS(4, 4) 18 QUBIT_COORDS(5, 5) 19 QUBIT_COORDS(4, 6) 25 RX 1 3 5 8 10 12 15 17 19 R 2 9 11 13 14 16 18 25 TICK H 2 11 16 25 DEPOLARIZE1(0.001) 2 11 16 25 TICK CX 2 3 16 17 11 12 15 14 10 9 19 18 DEPOLARIZE2(0.001) 2 3 16 17 11 12 15 14 10 9 19 18 TICK CX 2 1 16 15 11 10 8 14 3 9 12 18 DEPOLARIZE2(0.001) 2 1 16 15 11 10 8 14 3 9 12 18 TICK CX 16 10 11 5 25 19 8 9 17 18 12 13 DEPOLARIZE2(0.001) 16 10 11 5 25 19 8 9 17 18 12 13 TICK CX 16 8 11 3 25 17 1 9 10 18 5 13 DEPOLARIZE2(0.001) 16 8 11 3 25 17 1 9 10 18 5 13 TICK H 2 11 16 25 DEPOLARIZE1(0.001) 2 11 16 25 TICK MR 2 9 11 13 14 16 18 25 DETECTOR(2, 0, 0) rec[-8] DETECTOR(2, 4, 0) rec[-3] DETECTOR(4, 2, 0) rec[-6] DETECTOR(4, 6, 0) rec[-1] REPEAT 999 { TICK H 2 11 16 25 DEPOLARIZE1(0.001) 2 11 16 25 TICK CX 2 3 16 17 11 12 15 14 10 9 19 18 DEPOLARIZE2(0.001) 2 3 16 17 11 12 15 14 10 9 19 18 TICK CX 2 1 16 15 11 10 8 14 3 9 12 18 DEPOLARIZE2(0.001) 2 1 16 15 11 10 8 14 3 9 12 18 TICK CX 16 10 11 5 25 19 8 9 17 18 12 13 DEPOLARIZE2(0.001) 16 10 11 5 25 19 8 9 17 18 12 13 TICK CX 16 8 11 3 25 17 1 9 10 18 5 13 DEPOLARIZE2(0.001) 16 8 11 3 25 17 1 9 10 18 5 13 TICK H 2 11 16 25 DEPOLARIZE1(0.001) 2 11 16 25 TICK MR 2 9 11 13 14 16 18 25 SHIFT_COORDS(0, 0, 1) DETECTOR(2, 0, 0) rec[-8] rec[-16] DETECTOR(2, 2, 0) rec[-7] rec[-15] DETECTOR(4, 2, 0) rec[-6] rec[-14] DETECTOR(6, 2, 0) rec[-5] rec[-13] DETECTOR(0, 4, 0) rec[-4] rec[-12] DETECTOR(2, 4, 0) rec[-3] rec[-11] DETECTOR(4, 4, 0) rec[-2] rec[-10] DETECTOR(4, 6, 0) rec[-1] rec[-9] } MX 1 3 5 8 10 12 15 17 19 DETECTOR(2, 0, 1) rec[-8] rec[-9] rec[-17] DETECTOR(2, 4, 1) rec[-2] rec[-3] rec[-5] rec[-6] rec[-12] DETECTOR(4, 2, 1) rec[-4] rec[-5] rec[-7] rec[-8] rec[-15] DETECTOR(4, 6, 1) rec[-1] rec[-2] rec[-10] OBSERVABLE_INCLUDE(0) rec[-3] rec[-6] rec[-9] ``` ================================================ FILE: doc/gates.md ================================================ # Gates supported by Stim - Pauli Gates - [I](#I) - [X](#X) - [Y](#Y) - [Z](#Z) - Single Qubit Clifford Gates - [C_NXYZ](#C_NXYZ) - [C_NZYX](#C_NZYX) - [C_XNYZ](#C_XNYZ) - [C_XYNZ](#C_XYNZ) - [C_XYZ](#C_XYZ) - [C_ZNYX](#C_ZNYX) - [C_ZYNX](#C_ZYNX) - [C_ZYX](#C_ZYX) - [H](#H) - [H_NXY](#H_NXY) - [H_NXZ](#H_NXZ) - [H_NYZ](#H_NYZ) - [H_XY](#H_XY) - [H_XZ](#H_XZ) - [H_YZ](#H_YZ) - [S](#S) - [SQRT_X](#SQRT_X) - [SQRT_X_DAG](#SQRT_X_DAG) - [SQRT_Y](#SQRT_Y) - [SQRT_Y_DAG](#SQRT_Y_DAG) - [SQRT_Z](#SQRT_Z) - [SQRT_Z_DAG](#SQRT_Z_DAG) - [S_DAG](#S_DAG) - Two Qubit Clifford Gates - [CNOT](#CNOT) - [CX](#CX) - [CXSWAP](#CXSWAP) - [CY](#CY) - [CZ](#CZ) - [CZSWAP](#CZSWAP) - [II](#II) - [ISWAP](#ISWAP) - [ISWAP_DAG](#ISWAP_DAG) - [SQRT_XX](#SQRT_XX) - [SQRT_XX_DAG](#SQRT_XX_DAG) - [SQRT_YY](#SQRT_YY) - [SQRT_YY_DAG](#SQRT_YY_DAG) - [SQRT_ZZ](#SQRT_ZZ) - [SQRT_ZZ_DAG](#SQRT_ZZ_DAG) - [SWAP](#SWAP) - [SWAPCX](#SWAPCX) - [SWAPCZ](#SWAPCZ) - [XCX](#XCX) - [XCY](#XCY) - [XCZ](#XCZ) - [YCX](#YCX) - [YCY](#YCY) - [YCZ](#YCZ) - [ZCX](#ZCX) - [ZCY](#ZCY) - [ZCZ](#ZCZ) - Noise Channels - [CORRELATED_ERROR](#CORRELATED_ERROR) - [DEPOLARIZE1](#DEPOLARIZE1) - [DEPOLARIZE2](#DEPOLARIZE2) - [E](#E) - [ELSE_CORRELATED_ERROR](#ELSE_CORRELATED_ERROR) - [HERALDED_ERASE](#HERALDED_ERASE) - [HERALDED_PAULI_CHANNEL_1](#HERALDED_PAULI_CHANNEL_1) - [II_ERROR](#II_ERROR) - [I_ERROR](#I_ERROR) - [PAULI_CHANNEL_1](#PAULI_CHANNEL_1) - [PAULI_CHANNEL_2](#PAULI_CHANNEL_2) - [X_ERROR](#X_ERROR) - [Y_ERROR](#Y_ERROR) - [Z_ERROR](#Z_ERROR) - Collapsing Gates - [M](#M) - [MR](#MR) - [MRX](#MRX) - [MRY](#MRY) - [MRZ](#MRZ) - [MX](#MX) - [MY](#MY) - [MZ](#MZ) - [R](#R) - [RX](#RX) - [RY](#RY) - [RZ](#RZ) - Pair Measurement Gates - [MXX](#MXX) - [MYY](#MYY) - [MZZ](#MZZ) - Generalized Pauli Product Gates - [MPP](#MPP) - [SPP](#SPP) - [SPP_DAG](#SPP_DAG) - Control Flow - [REPEAT](#REPEAT) - Annotations - [DETECTOR](#DETECTOR) - [MPAD](#MPAD) - [OBSERVABLE_INCLUDE](#OBSERVABLE_INCLUDE) - [QUBIT_COORDS](#QUBIT_COORDS) - [SHIFT_COORDS](#SHIFT_COORDS) - [TICK](#TICK) ## Pauli Gates ### The 'I' Gate The identity gate. Does nothing to the target qubits. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to do nothing to. Example: I 5 I 42 I 5 42 Stabilizer Generators: X -> X Z -> Z Bloch Rotation (axis angle): Axis: +X Angle: 0° Bloch Rotation (Euler angles): theta = 0° phi = 0° lambda = 0° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(0°) * RotZ(0°) unitary = I * I * I Unitary Matrix: [+1 , ] [ , +1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `I 0` # (no operations) # (The decomposition is empty because this gate has no effect.) MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `I 0` (but affects the measurement record and an ancilla qubit) # (The decomposition is empty because this gate has no effect.) ### The 'X' Gate The Pauli X gate. The bit flip gate. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: X 5 X 42 X 5 42 Stabilizer Generators: X -> X Z -> -Z Bloch Rotation (axis angle): Axis: +X Angle: 180° Bloch Rotation (Euler angles): theta = 180° phi = 0° lambda = 180° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(180°) * RotZ(180°) unitary = I * Y * Z Unitary Matrix: [ , +1 ] [+1 , ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `X 0` H 0 S 0 S 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `X 0` (but affects the measurement record and an ancilla qubit) X 0 # (The decomposition is trivial because this gate is in the target gate set.) ### The 'Y' Gate The Pauli Y gate. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: Y 5 Y 42 Y 5 42 Stabilizer Generators: X -> -X Z -> -Z Bloch Rotation (axis angle): Axis: +Y Angle: 180° Bloch Rotation (Euler angles): theta = 180° phi = 0° lambda = 0° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(180°) * RotZ(0°) unitary = I * Y * I Unitary Matrix: [ , -i] [ +i, ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `Y 0` S 0 S 0 H 0 S 0 S 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `Y 0` (but affects the measurement record and an ancilla qubit) Y 0 # (The decomposition is trivial because this gate is in the target gate set.) ### The 'Z' Gate The Pauli Z gate. The phase flip gate. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: Z 5 Z 42 Z 5 42 Stabilizer Generators: X -> -X Z -> Z Bloch Rotation (axis angle): Axis: +Z Angle: 180° Bloch Rotation (Euler angles): theta = 0° phi = 0° lambda = 180° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(0°) * RotZ(180°) unitary = I * I * Z Unitary Matrix: [+1 , ] [ , -1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `Z 0` S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `Z 0` (but affects the measurement record and an ancilla qubit) Z 0 # (The decomposition is trivial because this gate is in the target gate set.) ## Single Qubit Clifford Gates ### The 'C_NXYZ' Gate Performs the period-3 cycle -X -> Y -> Z -> -X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: C_NXYZ 5 C_NXYZ 42 C_NXYZ 5 42 Stabilizer Generators: X -> -Y Z -> -X Bloch Rotation (axis angle): Axis: -X+Y+Z Angle: -120° Bloch Rotation (Euler angles): theta = 90° phi = 180° lambda = 90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(180°) * RotY(90°) * RotZ(90°) unitary = Z * SQRT_Y * S Unitary Matrix: [+1+i, +1-i] [-1-i, +1-i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `C_NXYZ 0` S 0 S 0 S 0 H 0 S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `C_NXYZ 0` (but affects the measurement record and an ancilla qubit) MZ 1 MXX 0 1 MY 1 MZZ 0 1 MX 1 Z 0 CX rec[-3] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-2] 0 rec[-1] 0 ### The 'C_NZYX' Gate Performs the period-3 cycle X -> -Z -> Y -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: C_NZYX 5 C_NZYX 42 C_NZYX 5 42 Stabilizer Generators: X -> -Z Z -> -Y Bloch Rotation (axis angle): Axis: +X+Y-Z Angle: 120° Bloch Rotation (Euler angles): theta = 90° phi = -90° lambda = 0° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(-90°) * RotY(90°) * RotZ(0°) unitary = S_DAG * SQRT_Y * I Unitary Matrix: [+1+i, -1-i] [+1-i, +1-i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `C_NZYX 0` S 0 S 0 H 0 S 0 S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `C_NZYX 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 X 0 CX rec[-2] 0 rec[-1] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-3] 0 ### The 'C_XNYZ' Gate Performs the period-3 cycle X -> -Y -> Z -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: C_XNYZ 5 C_XNYZ 42 C_XNYZ 5 42 Stabilizer Generators: X -> -Y Z -> X Bloch Rotation (axis angle): Axis: +X-Y+Z Angle: -120° Bloch Rotation (Euler angles): theta = 90° phi = 0° lambda = -90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(90°) * RotZ(-90°) unitary = I * SQRT_Y * S_DAG Unitary Matrix: [+1+i, -1+i] [+1+i, +1-i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `C_XNYZ 0` S 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `C_XNYZ 0` (but affects the measurement record and an ancilla qubit) MZ 1 MXX 0 1 MY 1 MZZ 0 1 MX 1 X 0 CX rec[-3] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-2] 0 rec[-1] 0 ### The 'C_XYNZ' Gate Performs the period-3 cycle X -> Y -> -Z -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: C_XYNZ 5 C_XYNZ 42 C_XYNZ 5 42 Stabilizer Generators: X -> Y Z -> -X Bloch Rotation (axis angle): Axis: +X+Y-Z Angle: -120° Bloch Rotation (Euler angles): theta = 90° phi = 180° lambda = -90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(180°) * RotY(90°) * RotZ(-90°) unitary = Z * SQRT_Y * S_DAG Unitary Matrix: [+1-i, +1+i] [-1+i, +1+i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `C_XYNZ 0` S 0 H 0 S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `C_XYNZ 0` (but affects the measurement record and an ancilla qubit) MZ 1 MXX 0 1 MY 1 MZZ 0 1 MX 1 Y 0 CX rec[-3] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-2] 0 rec[-1] 0 ### The 'C_XYZ' Gate Right handed period 3 axis cycling gate, sending X -> Y -> Z -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: C_XYZ 5 C_XYZ 42 C_XYZ 5 42 Stabilizer Generators: X -> Y Z -> X Bloch Rotation (axis angle): Axis: +X+Y+Z Angle: 120° Bloch Rotation (Euler angles): theta = 90° phi = 0° lambda = 90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(90°) * RotZ(90°) unitary = I * SQRT_Y * S Unitary Matrix: [+1-i, -1-i] [+1-i, +1+i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `C_XYZ 0` S 0 S 0 S 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `C_XYZ 0` (but affects the measurement record and an ancilla qubit) MZ 1 MXX 0 1 MY 1 MZZ 0 1 MX 1 CX rec[-3] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-2] 0 rec[-1] 0 ### The 'C_ZNYX' Gate Performs the period-3 cycle X -> Z -> -Y -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: C_ZNYX 5 C_ZNYX 42 C_ZNYX 5 42 Stabilizer Generators: X -> Z Z -> -Y Bloch Rotation (axis angle): Axis: +X-Y+Z Angle: 120° Bloch Rotation (Euler angles): theta = 90° phi = -90° lambda = 180° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(-90°) * RotY(90°) * RotZ(180°) unitary = S_DAG * SQRT_Y * Z Unitary Matrix: [+1-i, +1-i] [-1-i, +1+i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `C_ZNYX 0` H 0 S 0 S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `C_ZNYX 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 Z 0 CX rec[-2] 0 rec[-1] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-3] 0 ### The 'C_ZYNX' Gate Performs the period-3 cycle -X -> Z -> Y -> -X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: C_ZYNX 5 C_ZYNX 42 C_ZYNX 5 42 Stabilizer Generators: X -> -Z Z -> Y Bloch Rotation (axis angle): Axis: -X+Y+Z Angle: 120° Bloch Rotation (Euler angles): theta = 90° phi = 90° lambda = 0° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(90°) * RotY(90°) * RotZ(0°) unitary = S * SQRT_Y * I Unitary Matrix: [+1-i, -1+i] [+1+i, +1+i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `C_ZYNX 0` S 0 S 0 H 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `C_ZYNX 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 Y 0 CX rec[-2] 0 rec[-1] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-3] 0 ### The 'C_ZYX' Gate Left handed period 3 axis cycling gate, sending Z -> Y -> X -> Z. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: C_ZYX 5 C_ZYX 42 C_ZYX 5 42 Stabilizer Generators: X -> Z Z -> Y Bloch Rotation (axis angle): Axis: +X+Y+Z Angle: -120° Bloch Rotation (Euler angles): theta = 90° phi = 90° lambda = 180° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(90°) * RotY(90°) * RotZ(180°) unitary = S * SQRT_Y * Z Unitary Matrix: [+1+i, +1+i] [-1+i, +1-i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `C_ZYX 0` H 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `C_ZYX 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 CX rec[-2] 0 rec[-1] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-3] 0 ### The 'H' Gate Alternate name: `H_XZ` The Hadamard gate. Swaps the X and Z axes. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: H 5 H 42 H 5 42 Stabilizer Generators: X -> Z Z -> X Bloch Rotation (axis angle): Axis: +X+Z Angle: 180° Bloch Rotation (Euler angles): theta = 90° phi = 0° lambda = 180° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(90°) * RotZ(180°) unitary = I * SQRT_Y * Z Unitary Matrix: [+1 , +1 ] [+1 , -1 ] / sqrt(2) Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `H 0` H 0 # (The decomposition is trivial because this gate is in the target gate set.) MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `H 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 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 ### The 'H_NXY' Gate A variant of the Hadamard gate that swaps the -X and +Y axes. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: H_NXY 5 H_NXY 42 H_NXY 5 42 Stabilizer Generators: X -> -Y Z -> -Z Bloch Rotation (axis angle): Axis: +X-Y Angle: 180° Bloch Rotation (Euler angles): theta = 180° phi = 0° lambda = -90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(180°) * RotZ(-90°) unitary = I * Y * S_DAG Unitary Matrix: [ , +1+i] [+1-i, ] / sqrt(2) Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `H_NXY 0` S 0 H 0 S 0 S 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `H_NXY 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 Y 0 CZ rec[-3] 0 rec[-2] 0 rec[-1] 0 ### The 'H_NXZ' Gate A variant of the Hadamard gate that swaps the -X and +Z axes. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: H_NXZ 5 H_NXZ 42 H_NXZ 5 42 Stabilizer Generators: X -> -Z Z -> -X Bloch Rotation (axis angle): Axis: +X-Z Angle: 180° Bloch Rotation (Euler angles): theta = 90° phi = 180° lambda = 0° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(180°) * RotY(90°) * RotZ(0°) unitary = Z * SQRT_Y * I Unitary Matrix: [-1 , +1 ] [+1 , +1 ] / sqrt(2) Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `H_NXZ 0` S 0 S 0 H 0 S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `H_NXZ 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 MX 1 MZZ 0 1 MY 1 Y 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 ### The 'H_NYZ' Gate A variant of the Hadamard gate that swaps the -Y and +Z axes. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: H_NYZ 5 H_NYZ 42 H_NYZ 5 42 Stabilizer Generators: X -> -X Z -> -Y Bloch Rotation (axis angle): Axis: +Y-Z Angle: 180° Bloch Rotation (Euler angles): theta = 90° phi = -90° lambda = -90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(-90°) * RotY(90°) * RotZ(-90°) unitary = S_DAG * SQRT_Y * S_DAG Unitary Matrix: [-1 , -i] [ +i, +1 ] / sqrt(2) Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `H_NYZ 0` S 0 S 0 H 0 S 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `H_NYZ 0` (but affects the measurement record and an ancilla qubit) MZ 1 MXX 0 1 MY 1 Y 0 CX rec[-3] 0 rec[-2] 0 rec[-1] 0 ### The 'H_XY' Gate A variant of the Hadamard gate that swaps the X and Y axes (instead of X and Z). Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: H_XY 5 H_XY 42 H_XY 5 42 Stabilizer Generators: X -> Y Z -> -Z Bloch Rotation (axis angle): Axis: +X+Y Angle: 180° Bloch Rotation (Euler angles): theta = 180° phi = 0° lambda = 90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(180°) * RotZ(90°) unitary = I * Y * S Unitary Matrix: [ , +1-i] [+1+i, ] / sqrt(2) Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `H_XY 0` H 0 S 0 S 0 H 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `H_XY 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 X 0 CZ rec[-3] 0 rec[-2] 0 rec[-1] 0 ### The 'H_YZ' Gate A variant of the Hadamard gate that swaps the Y and Z axes (instead of X and Z). Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: H_YZ 5 H_YZ 42 H_YZ 5 42 Stabilizer Generators: X -> -X Z -> Y Bloch Rotation (axis angle): Axis: +Y+Z Angle: 180° Bloch Rotation (Euler angles): theta = 90° phi = 90° lambda = 90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(90°) * RotY(90°) * RotZ(90°) unitary = S * SQRT_Y * S Unitary Matrix: [+1 , -i] [ +i, -1 ] / sqrt(2) Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `H_YZ 0` H 0 S 0 H 0 S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `H_YZ 0` (but affects the measurement record and an ancilla qubit) MZ 1 MXX 0 1 MY 1 Z 0 CX rec[-3] 0 rec[-2] 0 rec[-1] 0 ### The 'S' Gate Alternate name: `SQRT_Z` Principal square root of Z gate. Phases the amplitude of |1> by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: S 5 S 42 S 5 42 Stabilizer Generators: X -> Y Z -> Z Bloch Rotation (axis angle): Axis: +Z Angle: 90° Bloch Rotation (Euler angles): theta = 0° phi = 0° lambda = 90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(0°) * RotZ(90°) unitary = I * I * S Unitary Matrix: [+1 , ] [ , +i] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `S 0` S 0 # (The decomposition is trivial because this gate is in the target gate set.) MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `S 0` (but affects the measurement record and an ancilla qubit) MY 1 MZZ 0 1 MX 1 CZ rec[-3] 0 rec[-2] 0 rec[-1] 0 ### The 'SQRT_X' Gate Principal square root of X gate. Phases the amplitude of |-> by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: SQRT_X 5 SQRT_X 42 SQRT_X 5 42 Stabilizer Generators: X -> X Z -> -Y Bloch Rotation (axis angle): Axis: +X Angle: 90° Bloch Rotation (Euler angles): theta = 90° phi = -90° lambda = 90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(-90°) * RotY(90°) * RotZ(90°) unitary = S_DAG * SQRT_Y * S Unitary Matrix: [+1+i, +1-i] [+1-i, +1+i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_X 0` H 0 S 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_X 0` (but affects the measurement record and an ancilla qubit) MZ 1 MXX 0 1 MY 1 CX rec[-3] 0 rec[-2] 0 rec[-1] 0 ### The 'SQRT_X_DAG' Gate Adjoint of the principal square root of X gate. Phases the amplitude of |-> by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: SQRT_X_DAG 5 SQRT_X_DAG 42 SQRT_X_DAG 5 42 Stabilizer Generators: X -> X Z -> Y Bloch Rotation (axis angle): Axis: +X Angle: -90° Bloch Rotation (Euler angles): theta = 90° phi = 90° lambda = -90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(90°) * RotY(90°) * RotZ(-90°) unitary = S * SQRT_Y * S_DAG Unitary Matrix: [+1-i, +1+i] [+1+i, +1-i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_X_DAG 0` S 0 H 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_X_DAG 0` (but affects the measurement record and an ancilla qubit) MY 1 MXX 0 1 MZ 1 CX rec[-3] 0 rec[-2] 0 rec[-1] 0 ### The 'SQRT_Y' Gate Principal square root of Y gate. Phases the amplitude of |-i> by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: SQRT_Y 5 SQRT_Y 42 SQRT_Y 5 42 Stabilizer Generators: X -> -Z Z -> X Bloch Rotation (axis angle): Axis: +Y Angle: 90° Bloch Rotation (Euler angles): theta = 90° phi = 0° lambda = 0° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(90°) * RotZ(0°) unitary = I * SQRT_Y * I Unitary Matrix: [+1+i, -1-i] [+1+i, +1+i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_Y 0` S 0 S 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_Y 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 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 ### The 'SQRT_Y_DAG' Gate Adjoint of the principal square root of Y gate. Phases the amplitude of |-i> by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: SQRT_Y_DAG 5 SQRT_Y_DAG 42 SQRT_Y_DAG 5 42 Stabilizer Generators: X -> Z Z -> -X Bloch Rotation (axis angle): Axis: +Y Angle: -90° Bloch Rotation (Euler angles): theta = 90° phi = 180° lambda = 180° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(180°) * RotY(90°) * RotZ(180°) unitary = Z * SQRT_Y * Z Unitary Matrix: [+1-i, +1-i] [-1+i, +1-i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_Y_DAG 0` H 0 S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_Y_DAG 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 MX 1 MZZ 0 1 MY 1 Z 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 ### The 'S_DAG' Gate Alternate name: `SQRT_Z_DAG` Adjoint of the principal square root of Z gate. Phases the amplitude of |1> by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. Example: S_DAG 5 S_DAG 42 S_DAG 5 42 Stabilizer Generators: X -> -Y Z -> Z Bloch Rotation (axis angle): Axis: +Z Angle: -90° Bloch Rotation (Euler angles): theta = 0° phi = 0° lambda = -90° unitary = RotZ(phi) * RotY(theta) * RotZ(lambda) unitary = RotZ(0°) * RotY(0°) * RotZ(-90°) unitary = I * I * S_DAG Unitary Matrix: [+1 , ] [ , -i] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `S_DAG 0` S 0 S 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `S_DAG 0` (but affects the measurement record and an ancilla qubit) MX 1 MZZ 0 1 MY 1 CZ rec[-3] 0 rec[-2] 0 rec[-1] 0 ## Two Qubit Clifford Gates ### The 'CX' Gate Alternate name: `CNOT` Alternate name: `ZCX` The Z-controlled X gate. Applies an X gate to the target if the control is in the |1> state. Equivalently: negates the amplitude of the |1>|-> state. The first qubit is called the control, and the second qubit is the target. To perform a classically controlled X, replace the control with a `rec` target like rec[-2]. To perform an I or X gate as configured by sweep data, replace the control with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Bit flip qubit 5 controlled by qubit 2. CX 2 5 # Perform CX 2 5 then CX 4 2. CX 2 5 4 2 # Bit flip qubit 6 if the most recent measurement result was TRUE. CX rec[-1] 6 # Bit flip qubits 7 and 8 conditioned on sweep configuration data. CX sweep[5] 7 sweep[5] 8 Stabilizer Generators: X_ -> XX Z_ -> Z_ _X -> _X _Z -> ZZ Unitary Matrix (little endian): [+1 , , , ] [ , , , +1 ] [ , , +1 , ] [ , +1 , , ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `CX 0 1` CNOT 0 1 # (The decomposition is trivial because this gate is in the target gate set.) MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `CX 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MXX 1 2 MZ 2 CX rec[-3] 1 rec[-1] 1 CZ rec[-4] 0 rec[-2] 0 ### The 'CXSWAP' Gate A combination CX-then-SWAP gate. This gate is kak-equivalent to the iswap gate, but preserves X/Z noise bias. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: CXSWAP 5 6 CXSWAP 42 43 CXSWAP 5 6 42 43 Stabilizer Generators: X_ -> XX Z_ -> _Z _X -> X_ _Z -> ZZ Unitary Matrix (little endian): [+1 , , , ] [ , , +1 , ] [ , , , +1 ] [ , +1 , , ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `CXSWAP 0 1` CNOT 1 0 CNOT 0 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `CXSWAP 0 1` (but affects the measurement record and an ancilla qubit) MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 CX rec[-7] 0 rec[-7] 1 rec[-5] 0 rec[-5] 1 rec[-3] 1 rec[-1] 1 CZ rec[-6] 0 rec[-6] 1 rec[-2] 0 rec[-4] 1 ### The 'CY' Gate Alternate name: `ZCY` The Z-controlled Y gate. Applies a Y gate to the target if the control is in the |1> state. Equivalently: negates the amplitude of the |1>|-i> state. The first qubit is the control, and the second qubit is the target. To perform a classically controlled Y, replace the control with a `rec` target like rec[-2]. To perform an I or Y gate as configured by sweep data, replace the control with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Apply Y to qubit 5 controlled by qubit 2. CY 2 5 # Perform CY 2 5 then CY 4 2. CY 2 5 4 2 # Apply Y to qubit 6 if the most recent measurement result was TRUE. CY rec[-1] 6 # Apply Y to qubits 7 and 8 conditioned on sweep configuration data. CY sweep[5] 7 sweep[5] 8 Stabilizer Generators: X_ -> XY Z_ -> Z_ _X -> ZX _Z -> ZZ Unitary Matrix (little endian): [+1 , , , ] [ , , , -i] [ , , +1 , ] [ , +i, , ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `CY 0 1` S 1 S 1 S 1 CNOT 0 1 S 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `CY 0 1` (but affects the measurement record and an ancilla qubit) MY 2 MZZ 1 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MX 2 MZZ 1 2 MY 2 Z 0 CY rec[-6] 1 rec[-4] 1 CZ rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 ### The 'CZ' Gate Alternate name: `ZCZ` The Z-controlled Z gate. Applies a Z gate to the target if the control is in the |1> state. Equivalently: negates the amplitude of the |1>|1> state. The first qubit is called the control, and the second qubit is the target. To perform a classically controlled Z, replace either qubit with a `rec` target like rec[-2]. To perform an I or Z gate as configured by sweep data, replace either qubit with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Apply Z to qubit 5 controlled by qubit 2. CZ 2 5 # Perform CZ 2 5 then CZ 4 2. CZ 2 5 4 2 # Apply Z to qubit 6 if the most recent measurement result was TRUE. CZ rec[-1] 6 # Apply Z to qubit 7 if the 3rd most recent measurement result was TRUE. CZ 7 rec[-3] # Apply Z to qubits 7 and 8 conditioned on sweep configuration data. CZ sweep[5] 7 8 sweep[5] Stabilizer Generators: X_ -> XZ Z_ -> Z_ _X -> ZX _Z -> _Z Unitary Matrix (little endian): [+1 , , , ] [ , +1 , , ] [ , , +1 , ] [ , , , -1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `CZ 0 1` H 1 CNOT 0 1 H 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `CZ 0 1` (but affects the measurement record and an ancilla qubit) MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 1 2 MXX 0 2 MZ 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 CX rec[-13] 0 rec[-12] 0 rec[-2] 0 rec[-1] 0 CY rec[-10] 0 rec[-9] 0 rec[-5] 0 rec[-4] 0 CZ rec[-11] 0 rec[-10] 1 rec[-8] 0 rec[-6] 0 rec[-3] 0 rec[-13] 1 rec[-12] 1 rec[-7] 1 ### The 'CZSWAP' Gate Alternate name: `SWAPCZ` A combination CZ-and-SWAP gate. This gate is kak-equivalent to the iswap gate. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: CZSWAP 5 6 CZSWAP 42 43 CZSWAP 5 6 42 43 Stabilizer Generators: X_ -> ZX Z_ -> _Z _X -> XZ _Z -> Z_ Unitary Matrix (little endian): [+1 , , , ] [ , , +1 , ] [ , +1 , , ] [ , , , -1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `CZSWAP 0 1` H 0 CX 0 1 CX 1 0 H 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `CZSWAP 0 1` (but affects the measurement record and an ancilla qubit) MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 1 2 MY 2 MXX 1 2 MZ 2 CX rec[-10] 0 rec[-6] 0 rec[-15] 1 rec[-14] 1 rec[-2] 1 rec[-1] 1 CY rec[-12] 1 rec[-9] 1 rec[-7] 1 rec[-4] 1 CZ rec[-15] 0 rec[-14] 0 rec[-12] 0 rec[-9] 0 rec[-13] 1 rec[-10] 1 rec[-8] 1 rec[-3] 1 ### The 'II' Gate A two-qubit identity gate. Twice as much doing-nothing as the I gate! This gate only exists because it can be useful as a communication mechanism for systems built on top of stim. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Examples: II 0 1 R 0 II[ACTUALLY_A_LEAKAGE_ISWAP] 0 1 R 0 CX 1 0 Stabilizer Generators: X_ -> X_ Z_ -> Z_ _X -> _X _Z -> _Z Unitary Matrix (little endian): [+1 , , , ] [ , +1 , , ] [ , , +1 , ] [ , , , +1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `II 0 1` # (The decomposition is empty because this gate has no effect.) MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `II 0 1` (but affects the measurement record and an ancilla qubit) # (The decomposition is empty because this gate has no effect.) ### The 'ISWAP' Gate Swaps two qubits and phases the -1 eigenspace of the ZZ observable by i. Equivalent to `SWAP` then `CZ` then `S` on both targets. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: ISWAP 5 6 ISWAP 42 43 ISWAP 5 6 42 43 Stabilizer Generators: X_ -> ZY Z_ -> _Z _X -> YZ _Z -> Z_ Unitary Matrix (little endian): [+1 , , , ] [ , , +i, ] [ , +i, , ] [ , , , +1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `ISWAP 0 1` H 0 CNOT 0 1 CNOT 1 0 H 1 S 1 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `ISWAP 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MY 2 MZZ 1 2 MX 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 1 2 MY 2 MXX 1 2 MZ 2 Z 1 CX rec[-10] 0 rec[-6] 0 rec[-15] 1 rec[-14] 1 rec[-2] 1 rec[-1] 1 CY rec[-12] 1 rec[-9] 1 rec[-7] 1 rec[-4] 1 CZ rec[-18] 0 rec[-18] 1 rec[-17] 0 rec[-16] 0 rec[-15] 0 rec[-14] 0 rec[-12] 0 rec[-9] 0 rec[-20] 1 rec[-19] 1 rec[-13] 1 rec[-10] 1 rec[-8] 1 rec[-3] 1 ### The 'ISWAP_DAG' Gate Swaps two qubits and phases the -1 eigenspace of the ZZ observable by -i. Equivalent to `SWAP` then `CZ` then `S_DAG` on both targets. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: ISWAP_DAG 5 6 ISWAP_DAG 42 43 ISWAP_DAG 5 6 42 43 Stabilizer Generators: X_ -> -ZY Z_ -> _Z _X -> -YZ _Z -> Z_ Unitary Matrix (little endian): [+1 , , , ] [ , , -i, ] [ , -i, , ] [ , , , +1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `ISWAP_DAG 0 1` S 0 S 0 S 0 S 1 S 1 S 1 H 1 CNOT 1 0 CNOT 0 1 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `ISWAP_DAG 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MY 2 MZZ 1 2 MX 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 1 2 MY 2 MXX 1 2 MZ 2 Z 0 CX rec[-10] 0 rec[-6] 0 rec[-15] 1 rec[-14] 1 rec[-2] 1 rec[-1] 1 CY rec[-12] 1 rec[-9] 1 rec[-7] 1 rec[-4] 1 CZ rec[-18] 0 rec[-18] 1 rec[-17] 0 rec[-16] 0 rec[-15] 0 rec[-14] 0 rec[-12] 0 rec[-9] 0 rec[-20] 1 rec[-19] 1 rec[-13] 1 rec[-10] 1 rec[-8] 1 rec[-3] 1 ### The 'SQRT_XX' Gate Phases the -1 eigenspace of the XX observable by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: SQRT_XX 5 6 SQRT_XX 42 43 SQRT_XX 5 6 42 43 Stabilizer Generators: X_ -> X_ Z_ -> -YX _X -> _X _Z -> -XY Unitary Matrix (little endian): [+1+i, , , +1-i] [ , +1+i, +1-i, ] [ , +1-i, +1+i, ] [+1-i, , , +1+i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_XX 0 1` H 0 CNOT 0 1 H 1 S 0 S 1 H 0 H 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_XX 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MY 2 MXX 1 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 X 1 CX rec[-18] 1 rec[-17] 1 rec[-16] 0 rec[-13] 0 rec[-11] 0 rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-15] 1 rec[-12] 1 rec[-10] 1 rec[-9] 1 rec[-8] 1 CY rec[-18] 0 rec[-17] 0 rec[-5] 0 rec[-4] 0 CZ rec[-15] 0 rec[-14] 0 rec[-8] 0 rec[-7] 0 ### The 'SQRT_XX_DAG' Gate Phases the -1 eigenspace of the XX observable by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: SQRT_XX_DAG 5 6 SQRT_XX_DAG 42 43 SQRT_XX_DAG 5 6 42 43 Stabilizer Generators: X_ -> X_ Z_ -> YX _X -> _X _Z -> XY Unitary Matrix (little endian): [+1-i, , , +1+i] [ , +1-i, +1+i, ] [ , +1+i, +1-i, ] [+1+i, , , +1-i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_XX_DAG 0 1` H 0 CNOT 0 1 H 1 S 0 S 0 S 0 S 1 S 1 S 1 H 0 H 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_XX_DAG 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MY 2 MXX 1 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 X 0 CX rec[-18] 1 rec[-17] 1 rec[-16] 0 rec[-13] 0 rec[-11] 0 rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-15] 1 rec[-12] 1 rec[-10] 1 rec[-9] 1 rec[-8] 1 CY rec[-18] 0 rec[-17] 0 rec[-5] 0 rec[-4] 0 CZ rec[-15] 0 rec[-14] 0 rec[-8] 0 rec[-7] 0 ### The 'SQRT_YY' Gate Phases the -1 eigenspace of the YY observable by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: SQRT_YY 5 6 SQRT_YY 42 43 SQRT_YY 5 6 42 43 Stabilizer Generators: X_ -> -ZY Z_ -> XY _X -> -YZ _Z -> YX Unitary Matrix (little endian): [+1+i, , , -1+i] [ , +1+i, +1-i, ] [ , +1-i, +1+i, ] [-1+i, , , +1+i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_YY 0 1` S 0 S 0 S 0 S 1 S 1 S 1 H 0 CNOT 0 1 H 1 S 0 S 1 H 0 H 1 S 0 S 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_YY 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 1 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MY 2 MZZ 1 2 MX 2 X 0 Y 1 CX rec[-16] 1 rec[-15] 1 rec[-14] 0 rec[-6] 0 rec[-5] 0 rec[-3] 1 CY rec[-16] 0 rec[-15] 0 rec[-11] 0 rec[-8] 0 rec[-5] 1 rec[-13] 1 rec[-10] 1 rec[-4] 1 CZ rec[-13] 0 rec[-12] 0 rec[-7] 0 rec[-14] 1 rec[-2] 1 rec[-1] 1 ### The 'SQRT_YY_DAG' Gate Phases the -1 eigenspace of the YY observable by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: SQRT_YY_DAG 5 6 SQRT_YY_DAG 42 43 SQRT_YY_DAG 5 6 42 43 Stabilizer Generators: X_ -> ZY Z_ -> -XY _X -> YZ _Z -> -YX Unitary Matrix (little endian): [+1-i, , , -1-i] [ , +1-i, +1+i, ] [ , +1+i, +1-i, ] [-1-i, , , +1-i] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_YY_DAG 0 1` S 0 S 0 S 0 S 1 H 0 CNOT 0 1 H 1 S 0 S 1 H 0 H 1 S 0 S 1 S 1 S 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_YY_DAG 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 1 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MY 2 MZZ 1 2 MX 2 Z 0 CX rec[-16] 1 rec[-15] 1 rec[-14] 0 rec[-6] 0 rec[-5] 0 rec[-3] 1 CY rec[-16] 0 rec[-15] 0 rec[-11] 0 rec[-8] 0 rec[-5] 1 rec[-13] 1 rec[-10] 1 rec[-4] 1 CZ rec[-13] 0 rec[-12] 0 rec[-7] 0 rec[-14] 1 rec[-2] 1 rec[-1] 1 ### The 'SQRT_ZZ' Gate Phases the -1 eigenspace of the ZZ observable by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: SQRT_ZZ 5 6 SQRT_ZZ 42 43 SQRT_ZZ 5 6 42 43 Stabilizer Generators: X_ -> YZ Z_ -> Z_ _X -> ZY _Z -> _Z Unitary Matrix (little endian): [+1 , , , ] [ , +i, , ] [ , , +i, ] [ , , , +1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_ZZ 0 1` H 1 CNOT 0 1 H 1 S 0 S 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_ZZ 0 1` (but affects the measurement record and an ancilla qubit) MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 1 2 MXX 0 2 MZ 2 MY 2 MZZ 1 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MX 2 MZZ 0 2 MY 2 Z 0 CX rec[-15] 0 rec[-14] 0 rec[-8] 0 rec[-7] 0 CY rec[-18] 0 rec[-17] 0 rec[-5] 0 rec[-4] 0 CZ rec[-18] 1 rec[-17] 1 rec[-16] 0 rec[-13] 0 rec[-11] 0 rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-15] 1 rec[-12] 1 rec[-10] 1 rec[-9] 1 rec[-8] 1 ### The 'SQRT_ZZ_DAG' Gate Phases the -1 eigenspace of the ZZ observable by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: SQRT_ZZ_DAG 5 6 SQRT_ZZ_DAG 42 43 SQRT_ZZ_DAG 5 6 42 43 Stabilizer Generators: X_ -> -YZ Z_ -> Z_ _X -> -ZY _Z -> _Z Unitary Matrix (little endian): [+1 , , , ] [ , -i, , ] [ , , -i, ] [ , , , +1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SQRT_ZZ_DAG 0 1` H 1 CNOT 0 1 H 1 S 0 S 0 S 0 S 1 S 1 S 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SQRT_ZZ_DAG 0 1` (but affects the measurement record and an ancilla qubit) MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 1 2 MXX 0 2 MZ 2 MY 2 MZZ 1 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MX 2 MZZ 0 2 MY 2 Z 1 CX rec[-15] 0 rec[-14] 0 rec[-8] 0 rec[-7] 0 CY rec[-18] 0 rec[-17] 0 rec[-5] 0 rec[-4] 0 CZ rec[-18] 1 rec[-17] 1 rec[-16] 0 rec[-13] 0 rec[-11] 0 rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-15] 1 rec[-12] 1 rec[-10] 1 rec[-9] 1 rec[-8] 1 ### The 'SWAP' Gate Swaps two qubits. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: SWAP 5 6 SWAP 42 43 SWAP 5 6 42 43 Stabilizer Generators: X_ -> _X Z_ -> _Z _X -> X_ _Z -> Z_ Unitary Matrix (little endian): [+1 , , , ] [ , , +1 , ] [ , +1 , , ] [ , , , +1 ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SWAP 0 1` CNOT 0 1 CNOT 1 0 CNOT 0 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SWAP 0 1` (but affects the measurement record and an ancilla qubit) MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 CX rec[-6] 0 rec[-6] 1 rec[-2] 0 rec[-10] 1 rec[-8] 1 rec[-4] 1 CZ rec[-9] 0 rec[-5] 0 rec[-5] 1 rec[-7] 1 rec[-3] 1 rec[-1] 1 ### The 'SWAPCX' Gate A combination SWAP-then-CX gate. This gate is kak-equivalent to the iswap gate, but preserves X/Z noise bias. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: SWAPCX 5 6 SWAPCX 42 43 SWAPCX 5 6 42 43 Stabilizer Generators: X_ -> _X Z_ -> ZZ _X -> XX _Z -> Z_ Unitary Matrix (little endian): [+1 , , , ] [ , , , +1 ] [ , +1 , , ] [ , , +1 , ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SWAPCX 0 1` CNOT 0 1 CNOT 1 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SWAPCX 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 CX rec[-6] 0 rec[-6] 1 rec[-2] 0 rec[-4] 1 CZ rec[-7] 0 rec[-7] 1 rec[-5] 0 rec[-5] 1 rec[-3] 1 rec[-1] 1 ### The 'XCX' Gate The X-controlled X gate. First qubit is the control, second qubit is the target. Applies an X gate to the target if the control is in the |-> state. Negates the amplitude of the |->|-> state. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: XCX 5 6 XCX 42 43 XCX 5 6 42 43 Stabilizer Generators: X_ -> X_ Z_ -> ZX _X -> _X _Z -> XZ Unitary Matrix (little endian): [+1 , +1 , +1 , -1 ] [+1 , +1 , -1 , +1 ] [+1 , -1 , +1 , +1 ] [-1 , +1 , +1 , +1 ] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `XCX 0 1` H 0 CNOT 0 1 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `XCX 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 CX rec[-11] 0 rec[-10] 1 rec[-8] 0 rec[-6] 0 rec[-3] 0 rec[-13] 1 rec[-12] 1 rec[-7] 1 CY rec[-10] 0 rec[-9] 0 rec[-5] 0 rec[-4] 0 CZ rec[-13] 0 rec[-12] 0 rec[-2] 0 rec[-1] 0 ### The 'XCY' Gate The X-controlled Y gate. First qubit is the control, second qubit is the target. Applies a Y gate to the target if the control is in the |-> state. Negates the amplitude of the |->|-i> state. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: XCY 5 6 XCY 42 43 XCY 5 6 42 43 Stabilizer Generators: X_ -> X_ Z_ -> ZY _X -> XX _Z -> XZ Unitary Matrix (little endian): [+1 , +1 , -i, +i] [+1 , +1 , +i, -i] [ +i, -i, +1 , +1 ] [ -i, +i, +1 , +1 ] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `XCY 0 1` H 0 S 1 S 1 S 1 CNOT 0 1 H 0 S 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `XCY 0 1` (but affects the measurement record and an ancilla qubit) MY 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZ 2 MXX 1 2 MY 2 X 0 CX rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 CY rec[-6] 1 rec[-4] 1 ### The 'XCZ' Gate The X-controlled Z gate. Applies a Z gate to the target if the control is in the |-> state. Equivalently: negates the amplitude of the |->|1> state. Same as a CX gate, but with reversed qubit order. The first qubit is the control, and the second qubit is the target. To perform a classically controlled X, replace the Z target with a `rec` target like rec[-2]. To perform an I or X gate as configured by sweep data, replace the Z target with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Bit flip qubit 5 controlled by qubit 2. XCZ 5 2 # Perform CX 2 5 then CX 4 2. XCZ 5 2 2 4 # Bit flip qubit 6 if the most recent measurement result was TRUE. XCZ 6 rec[-1] # Bit flip qubits 7 and 8 conditioned on sweep configuration data. XCZ 7 sweep[5] 8 sweep[5] Stabilizer Generators: X_ -> X_ Z_ -> ZZ _X -> XX _Z -> _Z Unitary Matrix (little endian): [+1 , , , ] [ , +1 , , ] [ , , , +1 ] [ , , +1 , ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `XCZ 0 1` CNOT 1 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `XCZ 0 1` (but affects the measurement record and an ancilla qubit) MZ 2 MXX 0 2 MZZ 1 2 MX 2 CX rec[-4] 0 rec[-2] 0 CZ rec[-3] 1 rec[-1] 1 ### The 'YCX' Gate The Y-controlled X gate. First qubit is the control, second qubit is the target. Applies an X gate to the target if the control is in the |-i> state. Negates the amplitude of the |-i>|-> state. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: YCX 5 6 YCX 42 43 YCX 5 6 42 43 Stabilizer Generators: X_ -> XX Z_ -> ZX _X -> _X _Z -> YZ Unitary Matrix (little endian): [+1 , -i, +1 , +i] [ +i, +1 , -i, +1 ] [+1 , +i, +1 , -i] [ -i, +1 , +i, +1 ] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `YCX 0 1` S 0 S 0 S 0 H 1 CNOT 1 0 S 0 H 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `YCX 0 1` (but affects the measurement record and an ancilla qubit) MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 X 1 CX rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-7] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-5] 1 CY rec[-6] 0 rec[-4] 0 ### The 'YCY' Gate The Y-controlled Y gate. First qubit is the control, second qubit is the target. Applies a Y gate to the target if the control is in the |-i> state. Negates the amplitude of the |-i>|-i> state. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: YCY 5 6 YCY 42 43 YCY 5 6 42 43 Stabilizer Generators: X_ -> XY Z_ -> ZY _X -> YX _Z -> YZ Unitary Matrix (little endian): [+1 , -i, -i, +1 ] [ +i, +1 , -1 , -i] [ +i, -1 , +1 , -i] [+1 , +i, +i, +1 ] / 2 Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `YCY 0 1` S 0 S 0 S 0 S 1 S 1 S 1 H 0 CNOT 0 1 H 0 S 0 S 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `YCY 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 1 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 MZZ 1 2 MX 2 Y 1 CX rec[-10] 0 rec[-9] 0 rec[-5] 0 rec[-4] 0 rec[-3] 0 rec[-11] 1 CY rec[-13] 0 rec[-12] 0 rec[-10] 1 rec[-8] 0 rec[-6] 0 rec[-7] 1 CZ rec[-13] 1 rec[-12] 1 rec[-11] 0 rec[-3] 1 rec[-2] 1 rec[-1] 1 ### The 'YCZ' Gate The Y-controlled Z gate. Applies a Z gate to the target if the control is in the |-i> state. Equivalently: negates the amplitude of the |-i>|1> state. Same as a CY gate, but with reversed qubit order. The first qubit is called the control, and the second qubit is the target. To perform a classically controlled Y, replace the Z target with a `rec` target like rec[-2]. To perform an I or Y gate as configured by sweep data, replace the Z target with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Apply Y to qubit 5 controlled by qubit 2. YCZ 5 2 # Perform CY 2 5 then CY 4 2. YCZ 5 2 2 4 # Apply Y to qubit 6 if the most recent measurement result was TRUE. YCZ 6 rec[-1] # Apply Y to qubits 7 and 8 conditioned on sweep configuration data. YCZ 7 sweep[5] 8 sweep[5] Stabilizer Generators: X_ -> XZ Z_ -> ZZ _X -> YX _Z -> _Z Unitary Matrix (little endian): [+1 , , , ] [ , +1 , , ] [ , , , -i] [ , , +i, ] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `YCZ 0 1` S 0 S 0 S 0 CNOT 1 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `YCZ 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MY 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 0 2 MY 2 Z 0 CY rec[-6] 0 rec[-4] 0 CZ rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-7] 0 rec[-7] 1 rec[-3] 0 rec[-3] 1 rec[-2] 0 rec[-1] 0 rec[-5] 1 ## Noise Channels ### The 'DEPOLARIZE1' Instruction The single qubit depolarizing channel. Applies a single-qubit depolarizing error with the given probability. When a single-qubit depolarizing error is applied, a random Pauli error (except for I) is chosen and applied. Note that this means maximal mixing occurs when the probability parameter is set to 75%, rather than at 100%. Applies a randomly chosen Pauli with a given probability. Parens Arguments: A single float (p) specifying the depolarization strength. Targets: Qubits to apply single-qubit depolarizing noise to. Pauli Mixture: 1-p: I p/3: X p/3: Y p/3: Z Examples: # Apply 1-qubit depolarization to qubit 0 using p=1% DEPOLARIZE1(0.01) 0 # Apply 1-qubit depolarization to qubit 2 # Separately apply 1-qubit depolarization to qubits 3 and 5 DEPOLARIZE1(0.01) 2 3 5 # Maximally mix qubits 0 through 2 DEPOLARIZE1(0.75) 0 1 2 ### The 'DEPOLARIZE2' Instruction The two qubit depolarizing channel. Applies a two-qubit depolarizing error with the given probability. When a two-qubit depolarizing error is applied, a random pair of Pauli errors (except for II) is chosen and applied. Note that this means maximal mixing occurs when the probability parameter is set to 93.75%, rather than at 100%. Parens Arguments: A single float (p) specifying the depolarization strength. Targets: Qubit pairs to apply two-qubit depolarizing noise to. Pauli Mixture: 1-p: II p/15: IX p/15: IY p/15: IZ p/15: XI p/15: XX p/15: XY p/15: XZ p/15: YI p/15: YX p/15: YY p/15: YZ p/15: ZI p/15: ZX p/15: ZY p/15: ZZ Examples: # Apply 2-qubit depolarization to qubit 0 and qubit 1 using p=1% DEPOLARIZE2(0.01) 0 1 # Apply 2-qubit depolarization to qubit 2 and qubit 3 # Separately apply 2-qubit depolarization to qubit 5 and qubit 7 DEPOLARIZE2(0.01) 2 3 5 7 # Maximally mix qubits 0 through 3 DEPOLARIZE2(0.9375) 0 1 2 3 ### The 'E' Instruction Alternate name: `CORRELATED_ERROR` Probabilistically applies a Pauli product error with a given probability. Sets the "correlated error occurred flag" to true if the error occurred. Otherwise sets the flag to false. See also: `ELSE_CORRELATED_ERROR`. Parens Arguments: A single float specifying the probability of applying the Paulis making up the error. Targets: Pauli targets specifying the Paulis to apply when the error occurs. Note that, for backwards compatibility reasons, the targets are not combined using combiners (`*`). They are implicitly all combined. Example: # With 60% probability, uniformly pick X1*Y2 or Z2*Z3 or X1*Y2*Z3. CORRELATED_ERROR(0.2) X1 Y2 ELSE_CORRELATED_ERROR(0.25) Z2 Z3 ELSE_CORRELATED_ERROR(0.33333333333) X1 Y2 Z3 ### The 'ELSE_CORRELATED_ERROR' Instruction Probabilistically applies a Pauli product error with a given probability, unless the "correlated error occurred flag" is set. If the error occurs, sets the "correlated error occurred flag" to true. Otherwise leaves the flag alone. Note: when converting a circuit into a detector error model, every `ELSE_CORRELATED_ERROR` instruction must be preceded by an ELSE_CORRELATED_ERROR instruction or an E instruction. In other words, ELSE_CORRELATED_ERROR instructions should appear in contiguous chunks started by a CORRELATED_ERROR. See also: `CORRELATED_ERROR`. Parens Arguments: A single float specifying the probability of applying the Paulis making up the error, conditioned on the "correlated error occurred flag" being False. Targets: Pauli targets specifying the Paulis to apply when the error occurs. Note that, for backwards compatibility reasons, the targets are not combined using combiners (`*`). They are implicitly all combined. Example: # With 60% probability, uniformly pick X1*Y2 or Z2*Z3 or X1*Y2*Z3. CORRELATED_ERROR(0.2) X1 Y2 ELSE_CORRELATED_ERROR(0.25) Z2 Z3 ELSE_CORRELATED_ERROR(0.33333333333) X1 Y2 Z3 ### The 'HERALDED_ERASE' Instruction The heralded erasure noise channel. Whether or not this noise channel fires is recorded into the measurement record. When it doesn't fire, nothing happens to the target qubit and a 0 is recorded. When it does fire, a 1 is recorded and the target qubit is erased to the maximally mixed state by applying X_ERROR(0.5) and Z_ERROR(0.5). CAUTION: when converting a circuit with this error into a detector error model, this channel is split into multiple potential effects. In the context of a DEM, these effects are considered independent. This is an approximation, because independent effects can be combined. The effect of this approximation, assuming a detector is declared on the herald, is that it appears this detector can be cancelled out by two of the (originally disjoint) heralded effects firing together. Sampling from the DEM instead of the circuit can thus produce unheralded errors, even if the circuit noise model only contains heralded errors. These issues occur with probability p^2, where p is the probability of a heralded error, since two effects that came from the same heralded error must occur together to cancel out the herald detector. This also means a decoder configured using the DEM will think there's a chance of unheralded errors even if the circuit the DEM came from only uses heralded errors. Parens Arguments: A single float (p) specifying the chance of the noise firing. Targets: Qubits to apply single-qubit depolarizing noise to. Each target is operated on independently. Pauli Mixture: 1-p: record 0, apply I p/4: record 1, apply I p/4: record 1, apply X p/4: record 1, apply Y p/4: record 1, apply Z Examples: # Erase qubit 0 with probability 1% HERALDED_ERASE(0.01) 0 # Declare a flag detector based on the erasure DETECTOR rec[-1] # Erase qubit 2 with 2% probability # Separately, erase qubit 3 with 2% probability HERALDED_ERASE(0.02) 2 3 # Do an XXXX measurement MPP X2*X3*X5*X7 # Apply partially-heralded noise to the two qubits HERALDED_ERASE(0.01) 2 3 5 7 DEPOLARIZE1(0.0001) 2 3 5 7 # Repeat the XXXX measurement MPP X2*X3*X5*X7 # Declare a detector comparing the two XXXX measurements DETECTOR rec[-1] rec[-6] # Declare flag detectors based on the erasures DETECTOR rec[-2] DETECTOR rec[-3] DETECTOR rec[-4] DETECTOR rec[-5] ### The 'HERALDED_PAULI_CHANNEL_1' Instruction A heralded error channel that applies biased noise. This error records a bit into the measurement record, indicating whether or not the herald fired. How likely it is that the herald fires, and the corresponding chance of each possible error effect (I, X, Y, or Z) are configured by the parens arguments of the instruction. CAUTION: when converting a circuit with this error into a detector error model, this channel is split into multiple potential effects. In the context of a DEM, these effects are considered independent. This is an approximation, because independent effects can be combined. The effect of this approximation, assuming a detector is declared on the herald, is that it appears this detector can be cancelled out by two of the (originally disjoint) heralded effects firing together. Sampling from the DEM instead of the circuit can thus produce unheralded errors, even if the circuit noise model only contains heralded errors. These issues occur with probability p^2, where p is the probability of a heralded error, since two effects that came from the same heralded error must occur together to cancel out the herald detector. This also means a decoder configured using the DEM will think there's a chance of unheralded errors even if the circuit the DEM came from only uses heralded errors. Parens Arguments: This instruction takes four arguments (pi, px, py, pz). The arguments are disjoint probabilities, specifying the chances of heralding with various effects. pi is the chance of heralding with no effect (a false positive). px is the chance of heralding with an X error. py is the chance of heralding with a Y error. pz is the chance of heralding with a Z error. Targets: Qubits to apply heralded biased noise to. Pauli Mixture: 1-pi-px-py-pz: record 0, apply I pi: record 1, apply I px: record 1, apply X py: record 1, apply Y pz: record 1, apply Z Examples: # With 10% probability perform a phase flip of qubit 0. HERALDED_PAULI_CHANNEL_1(0, 0, 0, 0.1) 0 DETECTOR rec[-1] # Include the herald in detectors available to the decoder # With 20% probability perform a heralded dephasing of qubit 0. HERALDED_PAULI_CHANNEL_1(0.1, 0, 0, 0.1) 0 DETECTOR rec[-1] # Subject a Bell Pair to heralded noise. MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.03, 0.04) 0 1 MXX 0 1 MZZ 0 1 DETECTOR rec[-1] rec[-5] # Did ZZ stabilizer change? DETECTOR rec[-2] rec[-6] # Did XX stabilizer change? DETECTOR rec[-3] # Did the herald on qubit 1 fire? DETECTOR rec[-4] # Did the herald on qubit 0 fire? ### The 'II_ERROR' Instruction Applies a two-qubit identity with a given probability. This gate has no effect. It only exists because it can be useful as a communication mechanism for systems built on top of stim. Parens Arguments: A list of disjoint probabilities summing to at most 1. The probabilities have no effect on stim simulations or error analysis, but may be interpreted in arbitrary ways by external tools. Targets: Qubits to apply identity noise to. Pauli Mixture: *: II Examples: # does nothing II_ERROR 0 1 # does nothing with probability 0.1, else does nothing II_ERROR(0.1) 0 1 # checks for you that the targets are two-qubit pairs II_ERROR[TWO_QUBIT_LEAKAGE_NOISE_FOR_AN_ADVANCED_SIMULATOR:0.1] 0 2 4 6 # checks for you that the disjoint probabilities in the arguments are legal II_ERROR[MULTIPLE_TWO_QUBIT_NOISE_MECHANISMS](0.1, 0.2) 0 2 4 6 ### The 'I_ERROR' Instruction Applies an identity with a given probability. This gate has no effect. It only exists because it can be useful as a communication mechanism for systems built on top of stim. Parens Arguments: A list of disjoint probabilities summing to at most 1. The probabilities have no effect on stim simulations or error analysis, but may be interpreted in arbitrary ways by external tools. Targets: Qubits to apply identity noise to. Pauli Mixture: *: I Examples: # does nothing I_ERROR 0 # does nothing with probability 0.1, else does nothing I_ERROR(0.1) 0 # doesn't require a probability argument I_ERROR[LEAKAGE_NOISE_FOR_AN_ADVANCED_SIMULATOR:0.1] 0 2 4 # checks for you that the disjoint probabilities in the arguments are legal I_ERROR[MULTIPLE_NOISE_MECHANISMS](0.1, 0.2) 0 2 4 ### The 'PAULI_CHANNEL_1' Instruction A single qubit Pauli error channel with explicitly specified probabilities for each case. Parens Arguments: Three floats specifying disjoint Pauli case probabilities. px: Disjoint probability of applying an X error. py: Disjoint probability of applying a Y error. pz: Disjoint probability of applying a Z error. Targets: Qubits to apply the custom noise channel to. Example: # Sample errors from the distribution 10% X, 15% Y, 20% Z, 55% I. # Apply independently to qubits 1, 2, 4. PAULI_CHANNEL_1(0.1, 0.15, 0.2) 1 2 4 Pauli Mixture: 1-px-py-pz: I px: X py: Y pz: Z ### The 'PAULI_CHANNEL_2' Instruction A two qubit Pauli error channel with explicitly specified probabilities for each case. Parens Arguments: Fifteen floats specifying the disjoint probabilities of each possible Pauli pair that can occur (except for the non-error double identity case). The disjoint probability arguments are (in order): 1. pix: Probability of applying an IX operation. 2. piy: Probability of applying an IY operation. 3. piz: Probability of applying an IZ operation. 4. pxi: Probability of applying an XI operation. 5. pxx: Probability of applying an XX operation. 6. pxy: Probability of applying an XY operation. 7. pxz: Probability of applying an XZ operation. 8. pyi: Probability of applying a YI operation. 9. pyx: Probability of applying a YX operation. 10. pyy: Probability of applying a YY operation. 11. pyz: Probability of applying a YZ operation. 12. pzi: Probability of applying a ZI operation. 13. pzx: Probability of applying a ZX operation. 14. pzy: Probability of applying a ZY operation. 15. pzz: Probability of applying a ZZ operation. Targets: Pairs of qubits to apply the custom noise channel to. There must be an even number of targets. Example: # Sample errors from the distribution 10% XX, 20% YZ, 70% II. # Apply independently to qubit pairs (1,2), (5,6), and (8,3) PAULI_CHANNEL_2(0,0,0, 0,0.1,0,0, 0,0,0,0.2, 0,0,0,0) 1 2 5 6 8 3 Pauli Mixture: 1-pix-piy-piz-pxi-pxx-pxy-pxz-pyi-pyx-pyy-pyz-pzi-pzx-pzy-pzz: II pix: IX piy: IY piz: IZ pxi: XI pxx: XX pxy: XY pxz: XZ pyi: YI pyx: YX pyy: YY pyz: YZ pzi: ZI pzx: ZX pzy: ZY pzz: ZZ ### The 'X_ERROR' Instruction Applies a Pauli X with a given probability. Parens Arguments: A single float specifying the probability of applying an X operation. Targets: Qubits to apply bit flip noise to. Pauli Mixture: 1-p: I p : X Example: X_ERROR(0.001) 5 X_ERROR(0.001) 42 X_ERROR(0.001) 5 42 ### The 'Y_ERROR' Instruction Applies a Pauli Y with a given probability. Parens Arguments: A single float specifying the probability of applying a Y operation. Targets: Qubits to apply Y flip noise to. Pauli Mixture: 1-p: I p : Y Example: Y_ERROR(0.001) 5 Y_ERROR(0.001) 42 Y_ERROR(0.001) 5 42 ### The 'Z_ERROR' Instruction Applies a Pauli Z with a given probability. Parens Arguments: A single float specifying the probability of applying a Z operation. Targets: Qubits to apply phase flip noise to. Pauli Mixture: 1-p: I p : Z Example: Z_ERROR(0.001) 5 Z_ERROR(0.001) 42 Z_ERROR(0.001) 5 42 ## Collapsing Gates ### The 'M' Instruction Alternate name: `MZ` Z-basis measurement. Projects each target qubit into `|0>` or `|1>` and reports its value (false=`|0>`, true=`|1>`). Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The qubits to measure in the Z basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the Z basis, and append the result into the measurement record. M 5 # 'MZ' is the same as 'M'. This also measures qubit 5 in the Z basis. MZ 5 # Measure qubit 5 in the Z basis, and append the INVERSE of its result into the measurement record. MZ !5 # Do a noisy measurement where the result put into the measurement record is wrong 1% of the time. MZ(0.01) 5 # Measure multiple qubits in the Z basis, putting 3 bits into the measurement record. MZ 2 3 5 # Perform multiple noisy measurements. Each measurement fails independently with 2% probability. MZ(0.02) 2 3 5 Stabilizer Generators: Z -> rec[-1] Z -> Z Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `M 0` M 0 # (The decomposition is trivial because this gate is in the target gate set.) MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `M 0` (but affects the measurement record and an ancilla qubit) MZ 0 # (The decomposition is trivial because this gate is in the target gate set.) ### The 'MR' Instruction Alternate name: `MRZ` Z-basis demolition measurement (optionally noisy). Projects each target qubit into `|0>` or `|1>`, reports its value (false=`|0>`, true=`|1>`), then resets to `|0>`. Parens Arguments: If no parens argument is given, the demolition measurement is perfect. If one parens argument is given, the demolition measurement's result is noisy. The argument is the probability of returning the wrong result. The argument does not affect the fidelity of the reset. Targets: The qubits to measure and reset in the Z basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the Z basis, reset it to the |0> state, append the measurement result into the measurement record. MRZ 5 # MR is also a Z-basis demolition measurement. MR 5 # Demolition measure qubit 5 in the Z basis, but append the INVERSE of its result into the measurement record. MRZ !5 # Do a noisy demolition measurement where the result put into the measurement record is wrong 1% of the time. MRZ(0.01) 5 # Demolition measure multiple qubits in the Z basis, putting 3 bits into the measurement record. MRZ 2 3 5 # Perform multiple noisy demolition measurements. Each measurement result is flipped independently with 2% probability. MRZ(0.02) 2 3 5 Stabilizer Generators: Z -> rec[-1] 1 -> Z Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MR 0` M 0 R 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MR 0` (but affects the measurement record and an ancilla qubit) MZ 0 CX rec[-1] 0 ### The 'MRX' Instruction X-basis demolition measurement (optionally noisy). Projects each target qubit into `|+>` or `|->`, reports its value (false=`|+>`, true=`|->`), then resets to `|+>`. Parens Arguments: If no parens argument is given, the demolition measurement is perfect. If one parens argument is given, the demolition measurement's result is noisy. The argument is the probability of returning the wrong result. The argument does not affect the fidelity of the reset. Targets: The qubits to measure and reset in the X basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the X basis, reset it to the |+> state, append the measurement result into the measurement record. MRX 5 # Demolition measure qubit 5 in the X basis, but append the INVERSE of its result into the measurement record. MRX !5 # Do a noisy demolition measurement where the result put into the measurement record is wrong 1% of the time. MRX(0.01) 5 # Demolition measure multiple qubits in the X basis, putting 3 bits into the measurement record. MRX 2 3 5 # Perform multiple noisy demolition measurements. Each measurement result is flipped independently with 2% probability. MRX(0.02) 2 3 5 Stabilizer Generators: X -> rec[-1] 1 -> X Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MRX 0` H 0 M 0 R 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MRX 0` (but affects the measurement record and an ancilla qubit) MX 0 CZ rec[-1] 0 ### The 'MRY' Instruction Y-basis demolition measurement (optionally noisy). Projects each target qubit into `|i>` or `|-i>`, reports its value (false=`|i>`, true=`|-i>`), then resets to `|i>`. Parens Arguments: If no parens argument is given, the demolition measurement is perfect. If one parens argument is given, the demolition measurement's result is noisy. The argument is the probability of returning the wrong result. The argument does not affect the fidelity of the reset. Targets: The qubits to measure and reset in the Y basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the Y basis, reset it to the |i> state, append the measurement result into the measurement record. MRY 5 # Demolition measure qubit 5 in the Y basis, but append the INVERSE of its result into the measurement record. MRY !5 # Do a noisy demolition measurement where the result put into the measurement record is wrong 1% of the time. MRY(0.01) 5 # Demolition measure multiple qubits in the Y basis, putting 3 bits into the measurement record. MRY 2 3 5 # Perform multiple noisy demolition measurements. Each measurement result is flipped independently with 2% probability. MRY(0.02) 2 3 5 Stabilizer Generators: Y -> rec[-1] 1 -> Y Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MRY 0` S 0 S 0 S 0 H 0 M 0 R 0 H 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MRY 0` (but affects the measurement record and an ancilla qubit) MY 0 CX rec[-1] 0 ### The 'MX' Instruction X-basis measurement. Projects each target qubit into `|+>` or `|->` and reports its value (false=`|+>`, true=`|->`). Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The qubits to measure in the X basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the X basis, and append the result into the measurement record. MX 5 # Measure qubit 5 in the X basis, and append the INVERSE of its result into the measurement record. MX !5 # Do a noisy measurement where the result put into the measurement record is wrong 1% of the time. MX(0.01) 5 # Measure multiple qubits in the X basis, putting 3 bits into the measurement record. MX 2 3 5 # Perform multiple noisy measurements. Each measurement fails independently with 2% probability. MX(0.02) 2 3 5 Stabilizer Generators: X -> rec[-1] X -> X Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MX 0` H 0 M 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MX 0` (but affects the measurement record and an ancilla qubit) MX 0 # (The decomposition is trivial because this gate is in the target gate set.) ### The 'MY' Instruction Y-basis measurement. Projects each target qubit into `|i>` or `|-i>` and reports its value (false=`|i>`, true=`|-i>`). Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The qubits to measure in the Y basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the Y basis, and append the result into the measurement record. MY 5 # Measure qubit 5 in the Y basis, and append the INVERSE of its result into the measurement record. MY !5 # Do a noisy measurement where the result put into the measurement record is wrong 1% of the time. MY(0.01) 5 # Measure multiple qubits in the X basis, putting 3 bits into the measurement record. MY 2 3 5 # Perform multiple noisy measurements. Each measurement fails independently with 2% probability. MY(0.02) 2 3 5 Stabilizer Generators: Y -> rec[-1] Y -> Y Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MY 0` S 0 S 0 S 0 H 0 M 0 H 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MY 0` (but affects the measurement record and an ancilla qubit) MY 0 # (The decomposition is trivial because this gate is in the target gate set.) ### The 'R' Instruction Alternate name: `RZ` Z-basis reset. Forces each target qubit into the `|0>` state by silently measuring it in the Z basis and applying an `X` gate if it ended up in the `|1>` state. Parens Arguments: This instruction takes no parens arguments. Targets: The qubits to reset in the Z basis. Examples: # Reset qubit 5 into the |0> state. RZ 5 # R means the same thing as RZ. R 5 # Reset multiple qubits into the |0> state. RZ 2 3 5 Stabilizer Generators: 1 -> Z Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `R 0` R 0 # (The decomposition is trivial because this gate is in the target gate set.) MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `R 0` (but affects the measurement record and an ancilla qubit) MZ 0 CX rec[-1] 0 ### The 'RX' Instruction X-basis reset. Forces each target qubit into the `|+>` state by silently measuring it in the X basis and applying a `Z` gate if it ended up in the `|->` state. Parens Arguments: This instruction takes no parens arguments. Targets: The qubits to reset in the X basis. Examples: # Reset qubit 5 into the |+> state. RX 5 # Reset multiple qubits into the |+> state. RX 2 3 5 Stabilizer Generators: 1 -> X Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `RX 0` R 0 H 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `RX 0` (but affects the measurement record and an ancilla qubit) MX 0 CZ rec[-1] 0 ### The 'RY' Instruction Y-basis reset. Forces each target qubit into the `|i>` state by silently measuring it in the Y basis and applying an `X` gate if it ended up in the `|-i>` state. Parens Arguments: This instruction takes no parens arguments. Targets: The qubits to reset in the Y basis. Examples: # Reset qubit 5 into the |i> state. RY 5 # Reset multiple qubits into the |i> state. RY 2 3 5 Stabilizer Generators: 1 -> Y Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `RY 0` R 0 H 0 S 0 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `RY 0` (but affects the measurement record and an ancilla qubit) MY 0 CX rec[-1] 0 ## Pair Measurement Gates ### The 'MXX' Instruction Two-qubit X basis parity measurement. This operation measures whether pairs of qubits are in the {|++>,|-->} subspace or in the {|+->,|-+>} subspace of the two qubit state space. |+> and |-> are the +1 and -1 eigenvectors of the X operator. If the qubits were in the {|++>,|-->} subspace, False is appended to the measurement record. If the qubits were in the {|+->,|-+>} subspace, True is appended to the measurement record. Inverting one of the qubit targets inverts the result. Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The pairs of qubits to measure in the X basis. This operation accepts inverted qubit targets (like `!5` instead of `5`). Inverted targets flip the measurement result. Examples: # Measure the +XX observable of qubit 1 vs qubit 2. MXX 1 2 # Measure the -XX observable of qubit 1 vs qubit 2. MXX !1 2 # Do a noisy measurement of the +XX observable of qubit 2 vs qubit 3. # The result recorded to the measurement record will be flipped 1% of the time. MXX(0.01) 2 3 # Measure the +XX observable qubit 1 vs qubit 2, and also qubit 8 vs qubit 9 MXX 1 2 8 9 # Perform multiple noisy measurements. # Each measurement has an independent 2% chance of being recorded wrong. MXX(0.02) 2 3 5 7 11 19 17 4 Stabilizer Generators: X_ -> X_ _X -> _X ZZ -> ZZ XX -> rec[-1] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MXX 0 1` CX 0 1 H 0 M 0 H 0 CX 0 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MXX 0 1` (but affects the measurement record and an ancilla qubit) MXX 0 1 # (The decomposition is trivial because this gate is in the target gate set.) ### The 'MYY' Instruction Two-qubit Y basis parity measurement. This operation measures whether pairs of qubits are in the {|ii>,|jj>} subspace or in the {|ij>,|ji>} subspace of the two qubit state space. |i> and |j> are the +1 and -1 eigenvectors of the Y operator. If the qubits were in the {|ii>,|jj>} subspace, False is appended to the measurement record. If the qubits were in the {|ij>,|ji>} subspace, True is appended to the measurement record. Inverting one of the qubit targets inverts the result. Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The pairs of qubits to measure in the Y basis. This operation accepts inverted qubit targets (like `!5` instead of `5`). Inverted targets flip the measurement result. Examples: # Measure the +YY observable of qubit 1 vs qubit 2. MYY 1 2 # Measure the -YY observable of qubit 1 vs qubit 2. MYY !1 2 # Do a noisy measurement of the +YY observable of qubit 2 vs qubit 3. # The result recorded to the measurement record will be flipped 1% of the time. MYY(0.01) 2 3 # Measure the +YY observable qubit 1 vs qubit 2, and also qubit 8 vs qubit 9 MYY 1 2 8 9 # Perform multiple noisy measurements. # Each measurement has an independent 2% chance of being recorded wrong. MYY(0.02) 2 3 5 7 11 19 17 4 Stabilizer Generators: XX -> XX Y_ -> Y_ _Y -> _Y YY -> rec[-1] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MYY 0 1` S 0 1 CX 0 1 H 0 M 0 S 1 1 H 0 CX 0 1 S 0 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MYY 0 1` (but affects the measurement record and an ancilla qubit) MX 2 MZZ 0 2 MY 2 MX 2 MZZ 1 2 MY 2 MXX 0 1 MX 2 MZZ 0 2 MY 2 MX 2 MZZ 1 2 MY 2 Z 0 1 CZ rec[-13] 0 rec[-12] 0 rec[-11] 0 rec[-6] 0 rec[-5] 0 rec[-4] 0 rec[-10] 1 rec[-9] 1 rec[-8] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 ### The 'MZZ' Instruction Two-qubit Z basis parity measurement. This operation measures whether pairs of qubits are in the {|00>,|11>} subspace or in the {|01>,|10>} subspace of the two qubit state space. |0> and |1> are the +1 and -1 eigenvectors of the Z operator. If the qubits were in the {|00>,|11>} subspace, False is appended to the measurement record. If the qubits were in the {|01>,|10>} subspace, True is appended to the measurement record. Inverting one of the qubit targets inverts the result. Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The pairs of qubits to measure in the Z basis. This operation accepts inverted qubit targets (like `!5` instead of `5`). Inverted targets flip the measurement result. Examples: # Measure the +ZZ observable of qubit 1 vs qubit 2. MZZ 1 2 # Measure the -ZZ observable of qubit 1 vs qubit 2. MZZ !1 2 # Do a noisy measurement of the +ZZ observable of qubit 2 vs qubit 3. # The result recorded to the measurement record will be flipped 1% of the time. MZZ(0.01) 2 3 # Measure the +ZZ observable qubit 1 vs qubit 2, and also qubit 8 vs qubit 9 MZZ 1 2 8 9 # Perform multiple noisy measurements. # Each measurement has an independent 2% chance of being recorded wrong. MZZ(0.02) 2 3 5 7 11 19 17 4 Stabilizer Generators: XX -> XX Z_ -> Z_ _Z -> _Z ZZ -> rec[-1] Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MZZ 0 1` CX 0 1 M 1 CX 0 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MZZ 0 1` (but affects the measurement record and an ancilla qubit) MZZ 0 1 # (The decomposition is trivial because this gate is in the target gate set.) ## Generalized Pauli Product Gates ### The 'MPP' Instruction Measures general pauli product operators, like X1*Y2*Z3. Parens Arguments: An optional failure probability. If no argument is given, all measurements are perfect. If one argument is given, it's the chance of reporting measurement results incorrectly. Targets: A series of Pauli products to measure. Each Pauli product is a series of Pauli targets (like `X1`, `Y2`, or `Z3`) separated by combiners (`*`). Each Pauli term can be inverted (like `!Y2` instead of `Y2`). A negated product will record the opposite measurement result. Note that, although you can write down instructions that measure anti-Hermitian products, like `MPP X1*Z1`, doing this will cause exceptions when you simulate or analyze the circuit since measuring an anti-Hermitian operator doesn't have well defined semantics. Using overly-complicated Hermitian products, like saying `MPP X1*Y1*Y2*Z2` instead of `MPP !Z1*X2`, is technically allowed. But probably not a great idea since tools consuming the circuit may have assumed that each qubit would appear at most once in each product. Examples: # Measure the two-body +X1*Y2 observable. MPP X1*Y2 # Measure the one-body -Z5 observable. MPP !Z5 # Measure the two-body +X1*Y2 observable and also the three-body -Z3*Z4*Z5 observable. MPP X1*Y2 !Z3*Z4*Z5 # Noisily measure +Z1+Z2 and +X1*X2 (independently flip each reported result 0.1% of the time). MPP(0.001) Z1*Z2 X1*X2 Stabilizer Generators (for `MPP X0*Y1*Z2 X3*X4`): XYZ__ -> rec[-2] ___XX -> rec[-1] X____ -> X____ _Y___ -> _Y___ __Z__ -> __Z__ ___X_ -> ___X_ ____X -> ____X ZZ___ -> ZZ___ _XX__ -> _XX__ ___ZZ -> ___ZZ Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `MPP X0*Y1*Z2 X3*X4` S 1 1 1 H 0 1 3 4 CX 2 0 1 0 4 3 M 0 3 CX 2 0 1 0 4 3 H 0 1 3 4 S 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `MPP X0*Y1*Z2 X3*X4` (but affects the measurement record and an ancilla qubit) MY 5 MZZ 1 5 MX 5 MZZ 0 5 MXX 1 5 MZ 5 MX 5 MZZ 1 5 MY 5 MZ 5 MXX 2 5 MY 5 MZZ 2 5 MX 5 MXX 0 2 MXX 3 4 MX 5 MZZ 2 5 MY 5 MXX 2 5 MZ 5 MY 5 MZZ 1 5 MX 5 MZZ 0 5 MXX 1 5 MZ 5 MX 5 MZZ 1 5 MY 5 CX rec[-21] 2 rec[-20] 2 rec[-11] 2 rec[-10] 2 CY rec[-27] 1 rec[-25] 1 rec[-6] 1 rec[-4] 1 rec[-18] 2 rec[-13] 2 CZ rec[-28] 0 rec[-28] 1 rec[-26] 0 rec[-24] 0 rec[-24] 1 rec[-23] 0 rec[-23] 1 rec[-22] 0 rec[-22] 1 rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-30] 1 rec[-29] 1 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 rec[-19] 2 rec[-12] 2 ### The 'SPP' Gate The generalized S gate. Phases the -1 eigenspace of Pauli product observables by i. Parens Arguments: This instruction takes no parens arguments. Targets: A series of Pauli products to phase. Each Pauli product is a series of Pauli targets (like `X1`, `Y2`, or `Z3`) separated by combiners (`*`). Each Pauli term can be inverted (like `!Y2` instead of `Y2`), to negate the product. Note that, although you can write down instructions that phase anti-Hermitian products, like `SPP X1*Z1`, doing this will cause exceptions when you simulate or analyze the circuit since phasing an anti-Hermitian operator doesn't have well defined semantics. Using overly-complicated Hermitian products, like saying `SPP X1*Y1*Y2*Z2` instead of `SPP !Z1*X2`, is technically allowed. But probably not a great idea since tools consuming the circuit may have assumed that each qubit would appear at most once in each product. Examples: # Perform an S gate on qubit 1. SPP Z1 # Perform a SQRT_X gate on qubit 1. SPP X1 # Perform a SQRT_X_DAG gate on qubit 1. SPP !X1 # Perform a SQRT_XX gate between qubit 1 and qubit 2. SPP X1*X2 # Perform a SQRT_YY gate between qubit 1 and 2, and a SQRT_ZZ_DAG between qubit 3 and 4. SPP Y1*Y2 !Z1*Z2 # Phase the -1 eigenspace of -X1*Y2*Z3 by i. SPP !X1*Y2*Z3 Stabilizer Generators (for `SPP X0*Y1*Z2`): X__ -> X__ Z__ -> -YYZ _X_ -> -XZZ _Z_ -> XXZ __X -> XYY __Z -> __Z Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SPP X0*Y1*Z2` CX 2 1 CX 1 0 S 1 S 1 H 1 CX 1 0 CX 2 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SPP X0*Y1*Z2` (but affects the measurement record and an ancilla qubit) MY 3 MZZ 1 3 MX 3 MZZ 0 3 MXX 1 3 MZ 3 MX 3 MZZ 1 3 MY 3 MZ 3 MXX 0 3 MY 3 MZZ 0 3 MX 3 MZZ 2 3 MXX 0 3 MZ 3 MX 3 MZZ 0 3 MY 3 MXX 0 3 MZ 3 MXX 0 3 MY 3 MZ 3 MXX 0 3 MY 3 MZZ 0 3 MX 3 MZZ 2 3 MXX 0 3 MZ 3 MX 3 MZZ 0 3 MY 3 MXX 0 3 MZ 3 MY 3 MZZ 1 3 MX 3 MZZ 0 3 MXX 1 3 MZ 3 MX 3 MZZ 1 3 MY 3 X 0 Y 1 Z 2 CX rec[-46] 0 rec[-46] 1 rec[-45] 0 rec[-45] 1 rec[-37] 0 rec[-36] 0 rec[-26] 0 rec[-24] 0 rec[-23] 0 rec[-22] 0 rec[-21] 0 rec[-11] 0 rec[-10] 0 CY rec[-42] 0 rec[-42] 1 rec[-37] 1 rec[-36] 1 rec[-35] 0 rec[-35] 1 rec[-32] 0 rec[-32] 1 rec[-30] 0 rec[-30] 1 rec[-27] 0 rec[-27] 1 rec[-26] 1 rec[-24] 1 rec[-23] 1 rec[-22] 1 rec[-21] 1 rec[-19] 0 rec[-19] 1 rec[-18] 0 rec[-18] 1 rec[-14] 0 rec[-14] 1 rec[-13] 0 rec[-13] 1 rec[-11] 1 rec[-10] 1 rec[-43] 1 rec[-41] 1 rec[-6] 1 rec[-4] 1 CZ rec[-44] 0 rec[-44] 1 rec[-42] 2 rec[-40] 0 rec[-40] 1 rec[-39] 0 rec[-39] 1 rec[-38] 0 rec[-38] 1 rec[-35] 2 rec[-34] 0 rec[-34] 2 rec[-33] 0 rec[-32] 2 rec[-30] 2 rec[-29] 0 rec[-28] 0 rec[-27] 2 rec[-20] 0 rec[-19] 2 rec[-17] 0 rec[-15] 0 rec[-12] 0 rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-26] 2 rec[-24] 2 rec[-23] 2 rec[-22] 2 rec[-21] 2 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 rec[-46] 2 rec[-45] 2 rec[-31] 2 rec[-16] 2 ### The 'SPP_DAG' Gate The generalized S_DAG gate. Phases the -1 eigenspace of Pauli product observables by -i. Parens Arguments: This instruction takes no parens arguments. Targets: A series of Pauli products to phase. Each Pauli product is a series of Pauli targets (like `X1`, `Y2`, or `Z3`) separated by combiners (`*`). Each Pauli term can be inverted (like `!Y2` instead of `Y2`), to negate the product. Note that, although you can write down instructions that phase anti-Hermitian products, like `SPP X1*Z1`, doing this will cause exceptions when you simulate or analyze the circuit since phasing an anti-Hermitian operator doesn't have well defined semantics. Using overly-complicated Hermitian products, like saying `SPP X1*Y1*Y2*Z2` instead of `SPP !Z1*X2`, is technically allowed. But probably not a great idea since tools consuming the circuit may have assumed that each qubit would appear at most once in each product. Examples: # Perform an S_DAG gate on qubit 1. SPP_DAG Z1 # Perform a SQRT_X_DAG gate on qubit 1. SPP_DAG X1 # Perform a SQRT_X gate on qubit 1. SPP_DAG !X1 # Perform a SQRT_XX_DAG gate between qubit 1 and qubit 2. SPP_DAG X1*X2 # Perform a SQRT_YY_DAG gate between qubit 1 and 2, and a SQRT_ZZ between qubit 3 and 4. SPP_DAG Y1*Y2 !Z1*Z2 # Phase the -1 eigenspace of -X1*Y2*Z3 by -i. SPP_DAG !X1*Y2*Z3 Stabilizer Generators (for `SPP_DAG X0*Y1*Z2`): X__ -> X__ Z__ -> YYZ _X_ -> XZZ _Z_ -> -XXZ __X -> -XYY __Z -> __Z Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `SPP_DAG X0*Y1*Z2` CX 2 1 CX 1 0 H 1 S 1 S 1 CX 1 0 CX 2 1 MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback): # The following circuit performs `SPP_DAG X0*Y1*Z2` (but affects the measurement record and an ancilla qubit) MY 3 MZZ 1 3 MX 3 MZZ 0 3 MXX 1 3 MZ 3 MX 3 MZZ 1 3 MY 3 MZ 3 MXX 0 3 MY 3 MZZ 0 3 MX 3 MZZ 2 3 MXX 0 3 MZ 3 MX 3 MZZ 0 3 MY 3 MXX 0 3 MZ 3 MXX 0 3 MY 3 MZ 3 MXX 0 3 MY 3 MZZ 0 3 MX 3 MZZ 2 3 MXX 0 3 MZ 3 MX 3 MZZ 0 3 MY 3 MXX 0 3 MZ 3 MY 3 MZZ 1 3 MX 3 MZZ 0 3 MXX 1 3 MZ 3 MX 3 MZZ 1 3 MY 3 CX rec[-46] 0 rec[-46] 1 rec[-45] 0 rec[-45] 1 rec[-37] 0 rec[-36] 0 rec[-26] 0 rec[-24] 0 rec[-23] 0 rec[-22] 0 rec[-21] 0 rec[-11] 0 rec[-10] 0 CY rec[-42] 0 rec[-42] 1 rec[-37] 1 rec[-36] 1 rec[-35] 0 rec[-35] 1 rec[-32] 0 rec[-32] 1 rec[-30] 0 rec[-30] 1 rec[-27] 0 rec[-27] 1 rec[-26] 1 rec[-24] 1 rec[-23] 1 rec[-22] 1 rec[-21] 1 rec[-19] 0 rec[-19] 1 rec[-18] 0 rec[-18] 1 rec[-14] 0 rec[-14] 1 rec[-13] 0 rec[-13] 1 rec[-11] 1 rec[-10] 1 rec[-43] 1 rec[-41] 1 rec[-6] 1 rec[-4] 1 CZ rec[-44] 0 rec[-44] 1 rec[-42] 2 rec[-40] 0 rec[-40] 1 rec[-39] 0 rec[-39] 1 rec[-38] 0 rec[-38] 1 rec[-35] 2 rec[-34] 0 rec[-34] 2 rec[-33] 0 rec[-32] 2 rec[-30] 2 rec[-29] 0 rec[-28] 0 rec[-27] 2 rec[-20] 0 rec[-19] 2 rec[-17] 0 rec[-15] 0 rec[-12] 0 rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-26] 2 rec[-24] 2 rec[-23] 2 rec[-22] 2 rec[-21] 2 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 rec[-46] 2 rec[-45] 2 rec[-31] 2 rec[-16] 2 ## Control Flow ### The 'REPEAT' Instruction Repeats the instructions in its body N times. Currently, repetition counts of 0 are not allowed because they create corner cases with ambiguous resolutions. For example, if a logical observable is only given measurements inside a repeat block with a repetition count of 0, it's ambiguous whether the output of sampling the logical observables includes a bit for that logical observable. Parens Arguments: This instruction takes no parens arguments. Targets: A positive integer in [1, 10^18] specifying the number of repetitions. Example: REPEAT 2 { CNOT 0 1 CNOT 2 1 M 1 } REPEAT 10000000 { CNOT 0 1 CNOT 2 1 M 1 DETECTOR rec[-1] rec[-3] } ## Annotations ### The 'DETECTOR' Instruction Annotates that a set of measurements can be used to detect errors, because the set's parity should be deterministic. Note that it is not necessary to say whether the measurement set's parity is even or odd; all that matters is that the parity should be *consistent* when running the circuit and omitting all noisy operations. Note that, for example, this means that even though `X` and `X_ERROR(1)` have equivalent effects on the measurements making up a detector, they have differing effects on the detector (because `X` is intended, determining the expected value, and `X_ERROR` is noise, causing deviations from the expected value). Detectors are ignored when sampling measurements, but produce results when sampling detection events. In detector sampling mode, each detector produces a result bit (where 0 means "measurement set had expected parity" and 1 means "measurement set had incorrect parity"). When converting a circuit into a detector error model, errors are grouped based on the detectors they flip (the "symptoms" of the error) and the observables they flip (the "frame changes" of the error). It is permitted, though not recommended, for the measurement set given to a `DETECTOR` instruction to have inconsistent parity. When a detector's measurement set is inconsistent, the detector is called a "gauge detector" and the expected parity of the measurement set is chosen arbitrarily (in an implementation-defined way). Some circuit analysis tools (such as the circuit-to-detector-error-model conversion) will by default refuse to process circuits containing gauge detectors. Gauge detectors produce random results when sampling detection events, though these results will be appropriately correlated with other gauge detectors. For example, if `DETECTOR rec[-1]` and `DETECTOR rec[-2]` are gauge detectors but `DETECTOR rec[-1] rec[-2]` is not, then under noiseless execution the two gauge detectors would either always produce the same result or always produce opposite results. Detectors can specify coordinates using their parens arguments. Coordinates have no effect on simulations, but can be useful to tools consuming the circuit. For example, a tool drawing how the detectors in a circuit relate to each other can use the coordinates as hints for where to place the detectors in the drawing. Parens Arguments: Optional. Coordinate metadata, relative to the current coordinate offset accumulated from `SHIFT_COORDS` instructions. Can be any number of coordinates from 1 to 16. There is no required convention for which coordinate is which. Targets: The measurement records to XOR together to get the deterministic-under-noiseless-execution parity. Example: R 0 X_ERROR(0.1) 0 M 0 # This measurement is always False under noiseless execution. # Annotate that most recent measurement should be deterministic. DETECTOR rec[-1] R 0 X 0 X_ERROR(0.1) 0 M 0 # This measurement is always True under noiseless execution. # Annotate that most recent measurement should be deterministic. DETECTOR rec[-1] R 0 1 H 0 CNOT 0 1 DEPOLARIZE2(0.001) 0 1 M 0 1 # These two measurements are always equal under noiseless execution. # Annotate that the parity of the previous two measurements should be consistent. DETECTOR rec[-1] rec[-2] # A series of trivial detectors with hinted coordinates along the diagonal line Y = 2X + 3. REPEAT 100 { R 0 M 0 SHIFT_COORDS(1, 2) DETECTOR(0, 3) rec[-1] } ### The 'MPAD' Instruction Pads the measurement record with the listed measurement results. This can be useful for ensuring measurements are aligned to word boundaries, or that the number of measurement bits produced per circuit layer is always the same even if the number of measured qubits varies. Parens Arguments: If no parens argument is given, the padding bits are recorded perfectly. If one parens argument is given, the padding bits are recorded noisily. The argument is the probability of recording the wrong result. Targets: Each target is a measurement result to add. Targets should be the value 0 or the value 1. Examples: # Append a False result to the measurement record. MPAD 0 # Append a True result to the measurement record. MPAD 1 # Append a series of results to the measurement record. MPAD 0 0 1 0 1 ### The 'OBSERVABLE_INCLUDE' Instruction Adds measurement records to a specified logical observable. A potential point of confusion here is that Stim's notion of a logical observable is nothing more than a set of measurements, potentially spanning across the entire circuit, that together produce a deterministic result. It's more akin to the "boundary of a parity sheet" in a topological spacetime diagram than it is to the notion of a qubit observable. For example, consider a surface code memory experiment that initializes a logical |0>, preserves the state noise, and eventually performs a logical Z basis measurement. The circuit representing this experiment would use `OBSERVABLE_INCLUDE` instructions to specifying which physical measurements within the logical Z basis measurement should be XOR'd together to get the logical measurement result. This effectively identifies the logical Z observable. But the circuit would *not* declare an X observable, because the X observable is not deterministic in a Z basis memory experiment; it has no corresponding deterministic measurement set. Logical observables are ignored when sampling measurements, but can produce results (if requested) when sampling detection events. In detector sampling mode, each observable can produce a result bit (where 0 means "measurement set had expected parity" and 1 means "measurement set had incorrect parity"). When converting a circuit into a detector error model, errors are grouped based on the detectors they flip (the "symptoms" of the error) and the observables they flip (the "frame changes" of the error). Another potential point of confusion is that when sampling logical measurement results, as part of sampling detection events in the circuit, the reported results are not measurements of the logical observable but rather whether those measurement results *were flipped*. This has significant simulation speed benefits, and also makes it so that it is not necessary to say whether the logical measurement result is supposed to be False or True. Note that, for example, this means that even though `X` and `X_ERROR(1)` have equivalent effects on the measurements making up an observable, they have differing effects on the reported value of an observable when sampling detection events (because `X` is intended, determining the expected value, and `X_ERROR` is noise, causing deviations from the expected value). It is not recommended for the measurement set of an observable to have inconsistent parity. For example, the circuit-to-detector-error-model conversion will refuse to operate on circuits containing such observables. In addition to targeting measurements, observables can target Pauli operators. This has no effect when running the quantum computation, but is used when configuring the decoder. For example, when performing a logical Z initialization, it allows a logical X operator to be introduced (by marking its Pauli terms) despite the fact that it anticommutes with the initialization. In practice, when physically sampling a circuit or simulating sampling its measurements and then computing the observables from the measurements, these Pauli terms are effectively ignored. However, they affect detection event simulations and affect whether the observable is included in errors in the detector error model. This makes it easier to benchmark all observables of a code, without having to introduce noiseless qubits entangled with the logical qubit to avoid the testing of the X observable anticommuting with the testing of the Z observable. Unlike a `DETECTOR` instruction which provides a complete description of a detector by listing all its constituent measurement records, an individual `OBSERVABLE_INCLUDE` instruction is not required to (and generally does not) fully describe a logical observable. Instead, measurement records or Pauli targets are added to it incrementally. A logical observable can be given both types of description: as a collection of Pauli targets and as a collection of measurement record targets. Parens Arguments: A non-negative integer specifying the index of the logical observable to add the measurement records to. Targets: The measurement records or Pauli terms to add to the specified observable. Example: R 0 1 H 0 CNOT 0 1 M 0 1 # Observable 0 is the parity of the previous two measurements. OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] R 0 1 H 0 CNOT 0 1 M 0 1 # Observable 1 is the parity of the previous measurement... OBSERVABLE_INCLUDE(1) rec[-1] # ...and the one before that. OBSERVABLE_INCLUDE(1) rec[-2] # Unphysically tracking two anticommuting observables of a 2x2 surface code. QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(0, 1) 2 QUBIT_COORDS(1, 1) 3 OBSERVABLE_INCLUDE(0) X0 X1 OBSERVABLE_INCLUDE(1) Z0 Z2 MPP X0*X1*X2*X3 Z0*Z1 Z2*Z3 DEPOLARIZE1(0.001) 0 1 2 3 MPP X0*X1*X2*X3 Z0*Z1 Z2*Z3 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] rec[-6] OBSERVABLE_INCLUDE(0) X0 X1 OBSERVABLE_INCLUDE(1) Z0 Z2 # Stim circuit may include a description of an observable in terms of Pauli targets # alongside a description in terms of measurement records. OBSERVABLE_INCLUDE(0) Z0 Z1 M 0 1 OBSERVABLE_INCLUDE(0) rec[-2] rec[-1] ### The 'QUBIT_COORDS' Instruction Annotates the location of a qubit. Coordinates are not required and have no effect on simulations, but can be useful to tools consuming the circuit. For example, a tool drawing the circuit can use the coordinates as hints for where to place the qubits in the drawing. `stimcirq` uses `QUBIT_COORDS` instructions to preserve `cirq.LineQubit` and `cirq.GridQubit` coordinates when converting between stim circuits and cirq circuits A qubit's coordinates can be specified multiple times, with the intended interpretation being that the qubit is at the location of the most recent assignment. For example, this could be used to indicate a simulated qubit is iteratively playing the role of many physical qubits. Parens Arguments: Optional. The latest coordinates of the qubit, relative to accumulated offsets from `SHIFT_COORDS` instructions. Can be any number of coordinates from 1 to 16. There is no required convention for which coordinate is which. Targets: The qubit or qubits the coordinates apply to. Example: # Annotate that qubits 0 to 3 are at the corners of a square. QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 QUBIT_COORDS(1, 0) 2 QUBIT_COORDS(1, 1) 3 ### The 'SHIFT_COORDS' Instruction Accumulates offsets that affect qubit coordinates and detector coordinates. Note: when qubit/detector coordinates use fewer dimensions than SHIFT_COORDS, the offsets from the additional dimensions are ignored (i.e. not specifying a dimension is different from specifying it to be 0). See also: `QUBIT_COORDS`, `DETECTOR`. Parens Arguments: Offsets to add into the current coordinate offset. Can be any number of coordinate offsets from 1 to 16. There is no required convention for which coordinate is which. Targets: This instruction takes no targets. Example: SHIFT_COORDS(500.5) QUBIT_COORDS(1510) 0 # Actually at 2010.5 SHIFT_COORDS(1500) QUBIT_COORDS(11) 1 # Actually at 2011.5 QUBIT_COORDS(10.5) 2 # Actually at 2011.0 # Declare some detectors with coordinates along a diagonal line. REPEAT 1000 { CNOT 0 2 CNOT 1 2 MR 2 DETECTOR(10.5, 0) rec[-1] rec[-2] # Actually at (2011.0, iteration_count). SHIFT_COORDS(0, 1) # Advance 2nd coordinate to track loop iterations. } ### The 'TICK' Instruction Annotates the end of a layer of gates, or that time is advancing. This instruction is not necessary, it has no effect on simulations, but it can be used by tools that are transforming or visualizing the circuit. For example, a tool that adds noise to a circuit may include cross-talk terms that require knowing whether or not operations are happening in the same time step or not. TICK instructions are added, and checked for, by `stimcirq` in order to preserve the moment structure of cirq circuits converted between stim circuits and cirq circuits. Parens Arguments: This instruction takes no parens arguments. Targets: This instruction takes no targets. Example: # First time step. H 0 CZ 1 2 TICK # Second time step. H 1 TICK # Empty time step. TICK ================================================ FILE: doc/getting_started.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "7vg1S92NEAXs" }, "source": [ "\n", "

Welcome to the Stim tutorial!

\n", "\n", "This Jupyter notebook is a tutorial for [Stim](https://github.com/quantumlib/stim), a tool for high-performance simulation and analysis of quantum stabilizer circuits. You can run this notebook in your browser using [Google Colab](https://colab.google/), or download it to your computer and run it using [Jupyter](https://docs.jupyter.org/en/latest/running.html).\n" ] }, { "cell_type": "markdown", "metadata": { "id": "0V2LXIBKKqA9" }, "source": [ "# Table of contents\n", "\n", "0. [Prerequisites](#prerequisites)\n", "1. [What is Stim?](#what-is-stim)\n", "2. [Copy and use this Jupyter/Colab notebook](#using-this-notebook)\n", "3. [Install the `stim` Python package](#install-stim)\n", "4. [Create a simple circuit, and sample from it](#make-circuit)\n", "5. [Add detector annotations to a circuit, and sample them](#sample-detectors)\n", "6. [Generate example error correction circuits](#make-qec-circuits)\n", "7. [Use `pymatching` to correct errors in a circuit](#use-pymatching)\n", "8. [Estimate the threshold of a repetition code using Monte Carlo sampling](#rep-code)\n", "9. [Use `sinter` to streamline the Monte Carlo sampling process](#use-sinter)\n", "10. [Estimate the threshold and footprint of a surface code](#surface-code)\n", "11. [Conclusion](#end-of-tutorial)\n", "12. [Additional resources](additional-resources)" ] }, { "cell_type": "markdown", "metadata": { "id": "43rMSrBhKtyX" }, "source": [ "\n", "# 0. Prerequisites\n", "\n", "To get the most out of this tutorial, it's helpful to have the following background:\n", "\n", "- Familiarity with basic Python syntax, and having a working Python 3.7+ environment. (Perhaps you are reading this notebook in such an environment.)\n", "\n", "- Familiarity with the basic concepts of *quantum circuits*, such as qubits and common quantum gates like [Hadamard](https://en.wikipedia.org/wiki/Quantum_logic_gate#Hadamard_gate) and [CNOT](https://en.wikipedia.org/wiki/Quantum_logic_gate#Controlled_gates).\n", "\n", "- A basic understanding of stabilizer circuits. For example, this tutorial assumes you've heard that they can be simulated cheaply and that they can represent protocols such as quantum error correction.\n", "\n", "- *Some* familiarity with quantum error-correcting codes. For example, this tutorial assumes that you've heard the terms \"surface code\" and \"threshold\".\n", "\n", "If you don't have these prerequisites yet, take a look at the [additional resources](additional-resources) at the end of this tutorial for suggestions about how to learn more." ] }, { "cell_type": "markdown", "metadata": { "id": "ulLKqhVxmfxr", "jp-MarkdownHeadingCollapsed": true }, "source": [ "\n", "# 1. What is Stim?\n", "\n", "Stim is an [open-source tool](https://github.com/quantumlib/Stim) for high-performance analysis and simulation of quantum stabilizer circuits, with a focus on quantum error correction circuits.\n", "\n", "Here is a plot from the [paper introducing Stim](https://doi.org/10.22331/q-2021-07-06-497), showing that Stim is thousands of times faster than other tools at sampling stabilizer circuits:\n", "\n", "![image.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABEMAAAKMCAIAAACPUOHuAAAgAElEQVR4Aeydd1QUyRfv3V12BUUFyTnnDEN2BMkgSXJWQcRAUKKAoosiEhQRRURYEQVRkuQgougqKqIgioqikuMQB2aGSe8865x+8wZ1w29//mStPv7RDt3Vtz5V01PfqntvLaPCAxKABCABSAASgAQgAUgAEoAEIIGlRmDZUjMY2gsJQAKQACQACUACkAAkAAlAApAAFSoZ2AkgAUgAEoAEIAFIABKABCABSGDpEYBKZum1GbQYEoAEIAFIABKABCABSAASgASgkoF9ABKABCABSAASgAQgAUgAEoAElh4BqGSWXptBiyEBSAASgAQgAUgAEoAEIAFIACoZ2AcgAUgAEoAEIAFIABKABCABSGDpEYBKZum1GbQYEoAEIAFIABKABCABSAASgASgkoF9ABKABCABSAASgAQgAUgAEoAElh4BqGSWXptBiyEBSAASgAQgAUgAEoAEIAFIACoZ2AcgAUgAEoAEIAFIABKABCABSGDpEYBKZum1GbQYEoAEIAFIABKABCABSAASgASgkoF9ABKABCABSAASgAQgAUgAEoAElh4BqGSWXptBiyEBSAASgAQgAUgAEoAEIAFIACoZ2AcgAUgAEoAEIAFIABKABCABSGDpEYBKZum12Xdo8cjISFlZmdWfO+Li4urq6qytrePj41+8ePEt4CISiW/fvm1qaqJQKP8NeyYmJq5cubJ9+/ba2trF5ZNIpKmpqatXr0ZERHh4eLi4uOzZs6esrGxgYIDuYgKB8OHDh4yMjICAAFdXVy8vr5iYmKampunpaborSSTSu3fvLl68uGvXLmdnZ09Pz5iYmOrqajweD+o4Pz//5MkTOzs7T0/P+vp65Paenp7nz5/39vaCTwYGBpKTk/38/AoKChYWFpDL/t0nz58/j4iI2LRp06NHj0gk0j9S2ZGRkWvXru3evdvBwcHNzS03N/f9+/f/SMnfWiEUCoVAIDx48ODly5d/z7bp6ekXL160tbX9vdsX30UkEoeGhry9vUNCQhobGxdf8PU/mZ6evn//flhYmIuLi7Ozc3x8/JMnT76+Gf/bJ5LJ5Pb29ujo6IiIiLt37341Y8hkclRUlI+PT15e3ld7KHjQ/v37vb29L126hDx3dna2q6vr0aNHZDIZ+RCeQAL/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MjIOHDiwZs2aBw8e+Pn5LVu2zM7ObuHChT4+PllZWZ0BVlVVXb58ecOGDXZ2dvPnz/f39z9w4EDneTKiKJaXlwcGBq5du3bOnDmzZ892dHQ8evRoaWkpy7L6V5HjuM2bN3/++efbt2/XRwcnIAACIGAlBKBkrCTQcHMIEdi+ffvvf//78ePHUxQlimL3lhEEUVVV1draqhv43tjYmJCQsGjRos8+++zzzz8fN27cF1988cEHH8ydO/fChQv6b1o5OTlubm4TJkz4+OOPbWxsvvrqq9mzZ0+YMOHNN9/86quvdDP+NRpNcnLyqlWrvmg7vvrqq/Hjx3/66affffddREREbW1t98borpSXlwcHB0+dOnXEiBFjx4796quvpk+fPmvWrDfeeOPzzz/XzfgvKCj4xz/+8cknn4SFhUmSVFdXd+jQocmTJ48dO3bKlCmTJ08eNWrU6NGj9+3b9/Tp07q6Om9v79GjR7/22mu/+93vvvvuOx8fn/Ly8oKCgmPHjk2ePHnMmDGTJk2aOHHiZ5999t57740dOzYyMrKmpkar1RYVFX3adtja2trb2y9cuNDe3l7n0fbt2/Pz83XcGIZJTk5ev379p59++sUXX0ycOHHChAkjR46cN29eXFxcU1MTz/NlZWVubm6TJk0aNWqUDtrIkSMnT57s7e3dxxoJGo3m5MmTv//97zdv3qwbWpabm7tz585///d/P3LkiG7hKYIgoqOj33///TfeeGPDhg1S26HVak+dOjVmzJiJEyc2NDT4+/t/8sknX3/9tU5vNDU1hYWFzZ49e8SIEV9++eXEiROnTp06adKkjz766Be/+EVsbGx2dra3t/enn376s5/97N13350+ffrRo0f1Suaf//zn4sWLJ02aNGHChI8++mjEiBHz5s27ceOGTln8YZ1RAAAgAElEQVRFRUXNnj1bx637f6dOnern56dXMm+99ZaNjc3UqVN14fvb3/72ySefrFu3rqSkRPddvL6+Pjo6et68eV988cXYsWMnTpw4bty4Tz/9dNWqVbGxsTRN62b829jY/Otf/zp16pTuLUpJSXFycho9evQ///lPGxubL7/8cubMmVOnTn3jjTcWL16cmZmpUzI///nPP/vsM1tb2ylTpuhC9qc//WnKlCmhoaGtra3Nzc3r169/9913X3/99T/+8Y8LFiyIiop6+PDh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)\n", "\n", "In addition to fast simulation, Stim provides general utilities for editing, inspecting, and transforming stabilizer circuits. In particular, Stim can automatically derive a matching graph from a stabilizer circuit annotated with detectors and noise channels." ] }, { "cell_type": "markdown", "metadata": { "id": "fh8rot1PYyDh" }, "source": [ "\n", "# 2. Install the Stim python package\n", "\n", "The first thing to do is to install and import Stim.\n", "Thanks to the Python ecosystem, this is easy to do!\n", "Stim is available as a [PyPI](https://pypi.org/project/stim/) package, and can be installed using `pip install stim` and then imported with `import stim` (just like any other Python package)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "fMf_vlB7D6fR" }, "outputs": [], "source": [ "!pip install stim~=1.14\n", "!pip install numpy~=1.0 # 1.0 instead of 2.0 for pymatching compatibility later\n", "!pip install scipy" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "P0lD6ilJD8Pi", "outputId": "79a7cd42-9677-46f7-df1e-1cd4a518f601" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.14.0\n" ] } ], "source": [ "import stim\n", "print(stim.__version__)" ] }, { "cell_type": "markdown", "metadata": { "id": "vah6-5YpGdaG" }, "source": [ "\n", "# 3. Create a simple circuit, and sample from it\n", "\n", "In Stim, circuits are instances of the [`stim.Circuit`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit) class. You create a new empty circuit with `stim.Circuit()`, and add operations to it by calling `circuit.append(name_of_gate, list_of_targets)`.\n", "\n", "You can find the name of the gate you want from the [Stim gates reference](https://github.com/quantumlib/Stim/blob/main/doc/gates.md). Most of the names are straightforward, like \"H\" for the Hadamard gate. A *target* is just a number representing the identity of a qubit. There's a qubit `0`, a qubit `1`, etc.\n", "\n", "The first circuit you'll make is a circuit that prepares a [Bell pair](https://en.wikipedia.org/wiki/Bell_state) and then measures it:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "UP84FdeWGodO" }, "outputs": [], "source": [ "circuit = stim.Circuit()\n", "\n", "# First, the circuit will initialize a Bell pair.\n", "circuit.append(\"H\", [0])\n", "circuit.append(\"CNOT\", [0, 1])\n", "\n", "# Then, the circuit will measure both qubits of the Bell pair in the Z basis.\n", "circuit.append(\"M\", [0, 1])" ] }, { "cell_type": "markdown", "metadata": { "id": "ySG_dqfQ-qOQ" }, "source": [ "Stim has a few ways to let you look at the circuits you've made. The circuit's `repr` is an expression that recreates the circuit using [Stim's circuit file syntax](https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "L51yrzUW-qOQ", "outputId": "9b6d4b2f-1cb9-4941-d533-db148aaaf86f" }, "outputs": [ { "data": { "text/plain": [ "stim.Circuit('''\n", " H 0\n", " CX 0 1\n", " M 0 1\n", "''')" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "circuit" ] }, { "cell_type": "markdown", "metadata": { "id": "D-RpRUTw-qOQ" }, "source": [ "You can also use the [`circuit.diagram`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit.diagram) method to get an annotated text diagram of the circuit:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "_75gRftr-qOQ", "outputId": "9b6d4b2f-1cb9-4941-d533-db148aaaf86f" }, "outputs": [ { "data": { "text/html": [ "
q0: -H-@-M:rec[0]-\n",
              "       |\n",
              "q1: ---X-M:rec[1]-
" ], "text/plain": [ "q0: -H-@-M:rec[0]-\n", " |\n", "q1: ---X-M:rec[1]-" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "circuit.diagram()" ] }, { "cell_type": "markdown", "metadata": { "id": "XKnYuBEB-qOQ" }, "source": [ "There are also other types of diagrams. For example, specifying `timeline-svg` will return a Scalable Vector Graphics ([SVG](https://en.wikipedia.org/wiki/SVG)) picture of the circuit instead of a text diagram:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "NXlSD_sy-qOQ", "outputId": "9b6d4b2f-1cb9-4941-d533-db148aaaf86f" }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "q0\n", "\n", "q1\n", "\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "M\n", "rec[0]\n", "\n", "M\n", "rec[1]\n", "" ], "text/plain": [ "\n", "\n", "\n", "q0\n", "\n", "q1\n", "\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "M\n", "rec[0]\n", "\n", "M\n", "rec[1]\n", "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "circuit.diagram('timeline-svg')" ] }, { "cell_type": "markdown", "metadata": { "id": "5dRl_-WZHDP2" }, "source": [ "Anyways, let's stop looking at your circuit and start using it. You can sample from the circuit by using the [`circuit.compile_sampler()`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit.compile_sampler) method to get a sampler object, and then calling `sample` on that object.\n", "\n", "Try taking 10 shots from the circuit:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "sJVtzniUHL1X", "outputId": "2b54c59c-0d0a-492f-fae7-87f4751f8415" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[False False]\n", " [ True True]\n", " [False False]\n", " [False False]\n", " [ True True]\n", " [False False]\n", " [ True True]\n", " [False False]\n", " [False False]\n", " [ True True]]\n" ] } ], "source": [ "sampler = circuit.compile_sampler()\n", "print(sampler.sample(shots=10))" ] }, { "cell_type": "markdown", "metadata": { "id": "D5KUSysIJB5g" }, "source": [ "Notice how there are ten rows (because you took ten shots), with two results per row (because there were two measurements in the circuit).\n", "Also notice how the results are random from row to row, but always agree within each row.\n", "That makes sense; that's what's supposed to happen when you repeatedly prepare and measure the |00⟩ + |11⟩ state." ] }, { "cell_type": "markdown", "metadata": { "id": "zX7ZyHVAHfF_" }, "source": [ "\n", "# 4. Add detector annotations to a circuit, and sample them\n", "\n", "Stim circuits can include error-correction annotations.\n", "In particular, you can annotate that certain sets of measurements can be used to detect errors.\n", "For example, in the circuit you created above, the two measurement results should always be equal.\n", "You can tell Stim you care about that by adding a `DETECTOR` annotation to the circuit.\n", "\n", "The `DETECTOR` annotation will take two targets: the two measurements whose parity you are asserting should be consistent from run to run. You point at the measurements by using the [`stim.target_rec`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.target_rec) method (short for \"target measurement record\"). The most recent measurement is `stim.target_rec(-1)` (also known as `rec[-1]` in Stim's circuit language), and the second most recent measurement is `stim.target_rec(-2)`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "GxViFukZIVI4", "outputId": "218d0843-da8c-482a-d736-242b918aabc0" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "stim.Circuit('''\n", " H 0\n", " CX 0 1\n", " M 0 1\n", " DETECTOR rec[-1] rec[-2]\n", "''')\n" ] } ], "source": [ "# Indicate the two previous measurements are supposed to consistently agree.\n", "circuit.append(\"DETECTOR\", [stim.target_rec(-1), stim.target_rec(-2)])\n", "print(repr(circuit))" ] }, { "cell_type": "markdown", "metadata": { "id": "tEz4G3OjI164" }, "source": [ "A slightly subtle point about detectors is that they only assert that the parity of the measurements is *always the same under noiseless execution*.\n", "A detector doesn't say whether the parity should be even or should be odd, only that it should always be the same.\n", "You annotate that a pair of measurements is always different in the same way that you annotate that a pair of measurements is always the same; it's the *consistency* that's key.\n", "\n", "Moving on, now that you've annotated the circuit with a detector, you can sample from the circuit's detectors instead of sampling from its measurements.\n", "You do that by creating a detector sampler, using the [`compile_detector_sampler`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit.compile_detector_sampler) method, and then calling [`sample`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.CompiledDetectorSampler.sample) on it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "PkSoSb65JWsx", "outputId": "bf9101c0-db23-42bf-a978-72e29fa83dcb" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[False]\n", " [False]\n", " [False]\n", " [False]\n", " [False]]\n" ] } ], "source": [ "sampler = circuit.compile_detector_sampler()\n", "print(sampler.sample(shots=5))" ] }, { "cell_type": "markdown", "metadata": { "id": "kCby-NA4Jevl" }, "source": [ "There are 5 rows in the results, because you took 5 shots.\n", "There's one entry per row, because you put one detector in the circuit.\n", "\n", "Notice how the results are always `False`.\n", "This means the detector is never producing a detection event.\n", "That's because there's no noise in the circuit; nothing to disturb the peace and quiet of a perfectly working machine.\n", "Well... time to fix that!\n", "\n", "Stim has a variety of error channels to pick from, like single qubit depolarization (`DEPOLARIZE1`) and phase damping (`Z_ERROR`), but in this context a good error to try is `X_ERROR`.\n", "The `X_ERROR` noise channel probabilistically applies a bit flip (a Pauli X error) to each of its targets.\n", "Note that each target is operated on independently.\n", "They don't all flip together with the given probability, each one flips individually with the given probability.\n", "\n", "You can recreate the circuit, with the noise inserted, by using Stim's domain specific language for circuits. While you're at it, throw in some `TICK` instructions to indicate the progression of time:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "HniubUFXJ9jh" }, "outputs": [], "source": [ "circuit = stim.Circuit(\"\"\"\n", " H 0\n", " TICK\n", "\n", " CX 0 1\n", " X_ERROR(0.2) 0 1\n", " TICK\n", "\n", " M 0 1\n", " DETECTOR rec[-1] rec[-2]\n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": { "id": "U7_2QfyiKSBL" }, "source": [ "Thanks to adding the `TICK` instructions, you get access to a new type of diagram: `timeslice-svg`.\n", "This diagram shows the operations from each tick in a separate frame:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "1jOOCijd-qOR", "outputId": "44ff0bba-086b-4846-faa9-1dc4549c6b78" }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "ERRX\n", "0.2\n", "\n", "ERRX\n", "0.2\n", "\n", "M\n", "\n", "M\n", "\n", "Tick 0\n", "\n", "Tick 1\n", "\n", "Tick 2\n", "\n", "\n", "" ], "text/plain": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "ERRX\n", "0.2\n", "\n", "ERRX\n", "0.2\n", "\n", "M\n", "\n", "M\n", "\n", "Tick 0\n", "\n", "Tick 1\n", "\n", "Tick 2\n", "\n", "\n", "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "circuit.diagram('timeslice-svg')" ] }, { "cell_type": "markdown", "metadata": { "id": "7lBUrusk-qOR" }, "source": [ "Now that you've added some noise, try sampling some more detector shots and see what happens:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "nfbwr9d-KWL4", "outputId": "f5477d97-473c-4552-d73a-11190a296c05" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[False]\n", " [False]\n", " [ True]\n", " [ True]\n", " [False]\n", " [False]\n", " [ True]\n", " [ True]\n", " [ True]\n", " [False]]\n" ] } ], "source": [ "sampler = circuit.compile_detector_sampler()\n", "print(sampler.sample(shots=10))" ] }, { "cell_type": "markdown", "metadata": { "id": "AreMncCeKb1x" }, "source": [ "It's no longer all `False`s (unless you got very lucky).\n", "There are `True`s appearing amongst the `False`s.\n", "\n", "The *detection fraction* of the circuit is how often detectors fire on average.\n", "Given that an X error is being applied to each qubit with 20% probability, and the detector will fire when one of the qubits is hit (but not both), the detection fraction of the detectors in this circuit is $0.8 \\cdot 0.2 \\cdot 2 = 0.32$.\n", "\n", "You can estimate the detection fraction by just taking a lot of shots, and dividing by the number of shots and the number of detectors:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "6NqbK11eKlP1", "outputId": "28cb1ee1-551b-4039-c537-ec3495e957cf" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.320135\n" ] } ], "source": [ "import numpy as np\n", "print(np.sum(sampler.sample(shots=10**6)) / 10**6)" ] }, { "cell_type": "markdown", "metadata": { "id": "RKM6PLk8Tl13" }, "source": [ "As you can see, the sampled estimate ends up close to the expected value $0.32$." ] }, { "cell_type": "markdown", "metadata": { "id": "fG2jQsH2LZQP" }, "source": [ "\n", "# 5. Generate example error correction circuits\n", "\n", "Now it's time for you to work with a *real* error-correcting circuit.\n", "Well... a classical error-correcting circuit:\n", "the *repetition* code.\n", "\n", "You could generate a repetition code circuit for yourself, but for the purposes of this tutorial it's easiest to use the example included with Stim.\n", "You can do this by calling [`stim.Circuit.generated`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit.generated) with an argument of `\"repetition_code:memory\"`.\n", "(You can find out about other valid arguments in the method's doc string, or just by passing in a bad one and looking at the exception message that comes out.)\n", "\n", "Stim takes a few different parameters when generating circuits.\n", "You have to decide how many times the stabilizers of the code are measured by specifying `rounds`, you have to decide on the size of the code by specifying `distance`, and you can specify what kind of noise to include using a few optional parameters.\n", "\n", "To start with, just set `before_round_data_depolarization=0.04` and `before_measure_flip_probability=0.01`. This will insert a `DEPOLARIZE1(0.04)` operation at the start of each round targeting every data qubit, and an `X_ERROR(0.01)` just before each measurement operation.\n", "This is a \"phenomenological noise model\"." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ku1-_JnuLzVR", "outputId": "e9b813a7-f4bc-42f0-e4ac-e73f90204ee4" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "stim.Circuit('''\n", " R 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\n", " TICK\n", " DEPOLARIZE1(0.04) 0 2 4 6 8 10 12 14 16\n", " CX 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n", " TICK\n", " CX 2 1 4 3 6 5 8 7 10 9 12 11 14 13 16 15\n", " TICK\n", " X_ERROR(0.01) 1 3 5 7 9 11 13 15\n", " MR 1 3 5 7 9 11 13 15\n", " DETECTOR(1, 0) rec[-8]\n", " DETECTOR(3, 0) rec[-7]\n", " DETECTOR(5, 0) rec[-6]\n", " DETECTOR(7, 0) rec[-5]\n", " DETECTOR(9, 0) rec[-4]\n", " DETECTOR(11, 0) rec[-3]\n", " DETECTOR(13, 0) rec[-2]\n", " DETECTOR(15, 0) rec[-1]\n", " REPEAT 24 {\n", " TICK\n", " DEPOLARIZE1(0.04) 0 2 4 6 8 10 12 14 16\n", " CX 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n", " TICK\n", " CX 2 1 4 3 6 5 8 7 10 9 12 11 14 13 16 15\n", " TICK\n", " X_ERROR(0.01) 1 3 5 7 9 11 13 15\n", " MR 1 3 5 7 9 11 13 15\n", " SHIFT_COORDS(0, 1)\n", " DETECTOR(1, 0) rec[-8] rec[-16]\n", " DETECTOR(3, 0) rec[-7] rec[-15]\n", " DETECTOR(5, 0) rec[-6] rec[-14]\n", " DETECTOR(7, 0) rec[-5] rec[-13]\n", " DETECTOR(9, 0) rec[-4] rec[-12]\n", " DETECTOR(11, 0) rec[-3] rec[-11]\n", " DETECTOR(13, 0) rec[-2] rec[-10]\n", " DETECTOR(15, 0) rec[-1] rec[-9]\n", " }\n", " X_ERROR(0.01) 0 2 4 6 8 10 12 14 16\n", " M 0 2 4 6 8 10 12 14 16\n", " DETECTOR(1, 1) rec[-8] rec[-9] rec[-17]\n", " DETECTOR(3, 1) rec[-7] rec[-8] rec[-16]\n", " DETECTOR(5, 1) rec[-6] rec[-7] rec[-15]\n", " DETECTOR(7, 1) rec[-5] rec[-6] rec[-14]\n", " DETECTOR(9, 1) rec[-4] rec[-5] rec[-13]\n", " DETECTOR(11, 1) rec[-3] rec[-4] rec[-12]\n", " DETECTOR(13, 1) rec[-2] rec[-3] rec[-11]\n", " DETECTOR(15, 1) rec[-1] rec[-2] rec[-10]\n", " OBSERVABLE_INCLUDE(0) rec[-1]\n", "''')\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "q0\n", "\n", "q1\n", "\n", "q2\n", "\n", "q3\n", "\n", "q4\n", "\n", "q5\n", "\n", "q6\n", "\n", "q7\n", "\n", "q8\n", "\n", "q9\n", "\n", "q10\n", "\n", "q11\n", "\n", "q12\n", "\n", "q13\n", "\n", "q14\n", "\n", "q15\n", "\n", "q16\n", "\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "MR\n", "rec[0]\n", "\n", "MR\n", "rec[1]\n", "\n", "MR\n", "rec[2]\n", "\n", "MR\n", "rec[3]\n", "\n", "MR\n", "rec[4]\n", "\n", "MR\n", "rec[5]\n", "\n", "MR\n", "rec[6]\n", "\n", "MR\n", "rec[7]\n", "\n", "DETECTOR\n", "coords=(1,0)\n", "D0 = rec[0]\n", "\n", "DETECTOR\n", "coords=(3,0)\n", "D1 = rec[1]\n", "\n", "DETECTOR\n", "coords=(5,0)\n", "D2 = rec[2]\n", "\n", "DETECTOR\n", "coords=(7,0)\n", "D3 = rec[3]\n", "\n", "DETECTOR\n", "coords=(9,0)\n", "D4 = rec[4]\n", "\n", "DETECTOR\n", "coords=(11,0)\n", "D5 = rec[5]\n", "\n", "DETECTOR\n", "coords=(13,0)\n", "D6 = rec[6]\n", "\n", "DETECTOR\n", "coords=(15,0)\n", "D7 = rec[7]\n", "\n", "\n", "\n", "REP24\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "MR\n", "rec[8+iter*8]\n", "\n", "MR\n", "rec[9+iter*8]\n", "\n", "MR\n", "rec[10+iter*8]\n", "\n", "MR\n", "rec[11+iter*8]\n", "\n", "MR\n", "rec[12+iter*8]\n", "\n", "MR\n", "rec[13+iter*8]\n", "\n", "MR\n", "rec[14+iter*8]\n", "\n", "MR\n", "rec[15+iter*8]\n", "\n", "DETECTOR\n", "coords=(1,1+iter)\n", "D[8+iter*8] = rec[8+iter*8]*rec[0+iter*8]\n", "\n", "DETECTOR\n", "coords=(3,1+iter)\n", "D[9+iter*8] = rec[9+iter*8]*rec[1+iter*8]\n", "\n", "DETECTOR\n", "coords=(5,1+iter)\n", "D[10+iter*8] = rec[10+iter*8]*rec[2+iter*8]\n", "\n", "DETECTOR\n", "coords=(7,1+iter)\n", "D[11+iter*8] = rec[11+iter*8]*rec[3+iter*8]\n", "\n", "DETECTOR\n", "coords=(9,1+iter)\n", "D[12+iter*8] = rec[12+iter*8]*rec[4+iter*8]\n", "\n", "DETECTOR\n", "coords=(11,1+iter)\n", "D[13+iter*8] = rec[13+iter*8]*rec[5+iter*8]\n", "\n", "DETECTOR\n", "coords=(13,1+iter)\n", "D[14+iter*8] = rec[14+iter*8]*rec[6+iter*8]\n", "\n", "DETECTOR\n", "coords=(15,1+iter)\n", "D[15+iter*8] = rec[15+iter*8]*rec[7+iter*8]\n", "\n", "\n", "\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "M\n", "rec[200]\n", "\n", "M\n", "rec[201]\n", "\n", "M\n", "rec[202]\n", "\n", "M\n", "rec[203]\n", "\n", "M\n", "rec[204]\n", "\n", "M\n", "rec[205]\n", "\n", "M\n", "rec[206]\n", "\n", "M\n", "rec[207]\n", "\n", "M\n", "rec[208]\n", "\n", "DETECTOR\n", "coords=(1,25)\n", "D200 = rec[201]*rec[200]*rec[192]\n", "\n", "DETECTOR\n", "coords=(3,25)\n", "D201 = rec[202]*rec[201]*rec[193]\n", "\n", "DETECTOR\n", "coords=(5,25)\n", "D202 = rec[203]*rec[202]*rec[194]\n", "\n", "DETECTOR\n", "coords=(7,25)\n", "D203 = rec[204]*rec[203]*rec[195]\n", "\n", "DETECTOR\n", "coords=(9,25)\n", "D204 = rec[205]*rec[204]*rec[196]\n", "\n", "DETECTOR\n", "coords=(11,25)\n", "D205 = rec[206]*rec[205]*rec[197]\n", "\n", "DETECTOR\n", "coords=(13,25)\n", "D206 = rec[207]*rec[206]*rec[198]\n", "\n", "DETECTOR\n", "coords=(15,25)\n", "D207 = rec[208]*rec[207]*rec[199]\n", "\n", "OBS_INCLUDE(0)\n", "L0 *= rec[208]\n", "\n", "\n", "" ], "text/plain": [ "\n", "\n", "\n", "q0\n", "\n", "q1\n", "\n", "q2\n", "\n", "q3\n", "\n", "q4\n", "\n", "q5\n", "\n", "q6\n", "\n", "q7\n", "\n", "q8\n", "\n", "q9\n", "\n", "q10\n", "\n", "q11\n", "\n", "q12\n", "\n", "q13\n", "\n", "q14\n", "\n", "q15\n", "\n", "q16\n", "\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "MR\n", "rec[0]\n", "\n", "MR\n", "rec[1]\n", "\n", "MR\n", "rec[2]\n", "\n", "MR\n", "rec[3]\n", "\n", "MR\n", "rec[4]\n", "\n", "MR\n", "rec[5]\n", "\n", "MR\n", "rec[6]\n", "\n", "MR\n", "rec[7]\n", "\n", "DETECTOR\n", "coords=(1,0)\n", "D0 = rec[0]\n", "\n", "DETECTOR\n", "coords=(3,0)\n", "D1 = rec[1]\n", "\n", "DETECTOR\n", "coords=(5,0)\n", "D2 = rec[2]\n", "\n", "DETECTOR\n", "coords=(7,0)\n", "D3 = rec[3]\n", "\n", "DETECTOR\n", "coords=(9,0)\n", "D4 = rec[4]\n", "\n", "DETECTOR\n", "coords=(11,0)\n", "D5 = rec[5]\n", "\n", "DETECTOR\n", "coords=(13,0)\n", "D6 = rec[6]\n", "\n", "DETECTOR\n", "coords=(15,0)\n", "D7 = rec[7]\n", "\n", "\n", "\n", "REP24\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "DEP1\n", "0.04\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "MR\n", "rec[8+iter*8]\n", "\n", "MR\n", "rec[9+iter*8]\n", "\n", "MR\n", "rec[10+iter*8]\n", "\n", "MR\n", "rec[11+iter*8]\n", "\n", "MR\n", "rec[12+iter*8]\n", "\n", "MR\n", "rec[13+iter*8]\n", "\n", "MR\n", "rec[14+iter*8]\n", "\n", "MR\n", "rec[15+iter*8]\n", "\n", "DETECTOR\n", "coords=(1,1+iter)\n", "D[8+iter*8] = rec[8+iter*8]*rec[0+iter*8]\n", "\n", "DETECTOR\n", "coords=(3,1+iter)\n", "D[9+iter*8] = rec[9+iter*8]*rec[1+iter*8]\n", "\n", "DETECTOR\n", "coords=(5,1+iter)\n", "D[10+iter*8] = rec[10+iter*8]*rec[2+iter*8]\n", "\n", "DETECTOR\n", "coords=(7,1+iter)\n", "D[11+iter*8] = rec[11+iter*8]*rec[3+iter*8]\n", "\n", "DETECTOR\n", "coords=(9,1+iter)\n", "D[12+iter*8] = rec[12+iter*8]*rec[4+iter*8]\n", "\n", "DETECTOR\n", "coords=(11,1+iter)\n", "D[13+iter*8] = rec[13+iter*8]*rec[5+iter*8]\n", "\n", "DETECTOR\n", "coords=(13,1+iter)\n", "D[14+iter*8] = rec[14+iter*8]*rec[6+iter*8]\n", "\n", "DETECTOR\n", "coords=(15,1+iter)\n", "D[15+iter*8] = rec[15+iter*8]*rec[7+iter*8]\n", "\n", "\n", "\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "ERRX\n", "0.01\n", "\n", "M\n", "rec[200]\n", "\n", "M\n", "rec[201]\n", "\n", "M\n", "rec[202]\n", "\n", "M\n", "rec[203]\n", "\n", "M\n", "rec[204]\n", "\n", "M\n", "rec[205]\n", "\n", "M\n", "rec[206]\n", "\n", "M\n", "rec[207]\n", "\n", "M\n", "rec[208]\n", "\n", "DETECTOR\n", "coords=(1,25)\n", "D200 = rec[201]*rec[200]*rec[192]\n", "\n", "DETECTOR\n", "coords=(3,25)\n", "D201 = rec[202]*rec[201]*rec[193]\n", "\n", "DETECTOR\n", "coords=(5,25)\n", "D202 = rec[203]*rec[202]*rec[194]\n", "\n", "DETECTOR\n", "coords=(7,25)\n", "D203 = rec[204]*rec[203]*rec[195]\n", "\n", "DETECTOR\n", "coords=(9,25)\n", "D204 = rec[205]*rec[204]*rec[196]\n", "\n", "DETECTOR\n", "coords=(11,25)\n", "D205 = rec[206]*rec[205]*rec[197]\n", "\n", "DETECTOR\n", "coords=(13,25)\n", "D206 = rec[207]*rec[206]*rec[198]\n", "\n", "DETECTOR\n", "coords=(15,25)\n", "D207 = rec[208]*rec[207]*rec[199]\n", "\n", "OBS_INCLUDE(0)\n", "L0 *= rec[208]\n", "\n", "\n", "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "circuit = stim.Circuit.generated(\n", " \"repetition_code:memory\",\n", " rounds=25,\n", " distance=9,\n", " before_round_data_depolarization=0.04,\n", " before_measure_flip_probability=0.01)\n", "\n", "print(repr(circuit))\n", "circuit.diagram('timeline-svg')" ] }, { "cell_type": "markdown", "metadata": { "id": "DEE3Vqq_ZzXP" }, "source": [ "You can see that this circuit is more complicated than the example you started with. Notice the little \"REP24\" at the bottom of the diagram. This circuit is using a `REPEAT` block to repeatedly measure the stabilizers of the code." ] }, { "cell_type": "markdown", "metadata": { "id": "lTu556AOMTv6" }, "source": [ "With a circuit in hand, you can try sampling from it.\n", "Try sampling the measurements once, and printing out the results split up just right so that time advances from line to line:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "hQyBEti8Ng_S", "outputId": "1c988414-9080-4f0f-facf-70c27b935730" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "________\n", "________\n", "________\n", "________\n", "________\n", "_______1\n", "________\n", "________\n", "________\n", "_______1\n", "_______1\n", "________\n", "________\n", "11_____1\n", "11___1_1\n", "11_____1\n", "11_____1\n", "11_____1\n", "11_11__1\n", "11_11__1\n", "11_11__1\n", "11_11__1\n", "1__11__1\n", "11_11__1\n", "1_111__1\n", "_11_1___\n", "1\n" ] } ], "source": [ "sampler = circuit.compile_sampler()\n", "one_sample = sampler.sample(shots=1)[0]\n", "for k in range(0, len(one_sample), 8):\n", " timeslice = one_sample[k:k+8]\n", " print(\"\".join(\"1\" if e else \"_\" for e in timeslice))" ] }, { "cell_type": "markdown", "metadata": { "id": "I5J3W6bWOIhJ" }, "source": [ "See how the 1s seem to come in pairs of streaks?\n", "That's because once a data qubit is flipped it stays flipped, and the measurements to its left and right permanently change parity.\n", "\n", "If you sample the circuit's detectors, instead of its measurements, the streaks are replaced by spackle.\n", "You get much sparser data:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "jJCydGZnOeez", "outputId": "9149cd22-5b65-4b5b-ef39-549704f23f81" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "________\n", "________\n", "________\n", "________\n", "________\n", "________\n", "_____!!_\n", "____!!__\n", "_____!!_\n", "________\n", "________\n", "________\n", "________\n", "________\n", "________\n", "________\n", "___!_!__\n", "_____!!_\n", "!!______\n", "!_______\n", "!_______\n", "________\n", "________\n", "___!!___\n", "________\n", "________\n" ] } ], "source": [ "detector_sampler = circuit.compile_detector_sampler()\n", "one_sample = detector_sampler.sample(shots=1)[0]\n", "for k in range(0, len(one_sample), 8):\n", " timeslice = one_sample[k:k+8]\n", " print(\"\".join(\"!\" if e else \"_\" for e in timeslice))" ] }, { "cell_type": "markdown", "metadata": { "id": "fFNsm0_GOh4H" }, "source": [ "Notice how the `!`s tend to come in pairs, except near the sides.\n", "This \"comes in pairs\" property is extremely important, because it allows you to perform error correction.\n", "Every `!` must be paired with another `!`, with the left boundary, or with the right boundary.\n", "In the circuit generated by Stim, the logical observable is annotated to be a measurement of the leftmost data qubit.\n", "That data qubit was flipped once for each `!` that's paired with the left boundary.\n", "If the data qubit was flipped an even number of times, the observable that was measured is correct.\n", "If it was flipped an odd number of times, the observable that was measured needs to be flipped to be correct.\n", "If you just had a syndrome decoder, you could use it to solve the matching problem and figure out if the leftmost data qubit (and therefore the protected logical observable) ended up flipped or not...\n", "\n", "\n", "# 6. Use `pymatching` to correct errors in a circuit\n", "\n", "Stim has a key feature that makes it easier to use a decoder: converting a circuit into a detector error model.\n", "A detector error model is just a list of all the independent error mechanisms in a circuit, as well as their symptoms (which detectors they set off) and frame changes (which logical observables they flip).\n", "\n", "You can get the detector error mode for a circuit by calling [`circuit.detector_error_model()`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit.detector_error_model):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "qqUSe1BvO0V9", "outputId": "fabb01fe-1608-42fb-c07a-9be9f6eb13c8" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "stim.DetectorErrorModel('''\n", " error(0.0266667) D0\n", " error(0.0266667) D0 D1\n", " error(0.01) D0 D8\n", " error(0.0266667) D1 D2\n", " error(0.01) D1 D9\n", " error(0.0266667) D2 D3\n", " error(0.01) D2 D10\n", " error(0.0266667) D3 D4\n", " error(0.01) D3 D11\n", " error(0.0266667) D4 D5\n", " error(0.01) D4 D12\n", " error(0.0266667) D5 D6\n", " error(0.01) D5 D13\n", " error(0.0266667) D6 D7\n", " error(0.01) D6 D14\n", " error(0.01) D7 D15\n", " error(0.0266667) D7 L0\n", " detector(1, 0) D0\n", " detector(3, 0) D1\n", " detector(5, 0) D2\n", " detector(7, 0) D3\n", " detector(9, 0) D4\n", " detector(11, 0) D5\n", " detector(13, 0) D6\n", " detector(15, 0) D7\n", " repeat 23 {\n", " error(0.0266667) D8\n", " error(0.0266667) D8 D9\n", " error(0.01) D8 D16\n", " error(0.0266667) D9 D10\n", " error(0.01) D9 D17\n", " error(0.0266667) D10 D11\n", " error(0.01) D10 D18\n", " error(0.0266667) D11 D12\n", " error(0.01) D11 D19\n", " error(0.0266667) D12 D13\n", " error(0.01) D12 D20\n", " error(0.0266667) D13 D14\n", " error(0.01) D13 D21\n", " error(0.0266667) D14 D15\n", " error(0.01) D14 D22\n", " error(0.01) D15 D23\n", " error(0.0266667) D15 L0\n", " shift_detectors(0, 1) 0\n", " detector(1, 0) D8\n", " detector(3, 0) D9\n", " detector(5, 0) D10\n", " detector(7, 0) D11\n", " detector(9, 0) D12\n", " detector(11, 0) D13\n", " detector(13, 0) D14\n", " detector(15, 0) D15\n", " shift_detectors 8\n", " }\n", " error(0.0266667) D8\n", " error(0.0266667) D8 D9\n", " error(0.01) D8 D16\n", " error(0.0266667) D9 D10\n", " error(0.01) D9 D17\n", " error(0.0266667) D10 D11\n", " error(0.01) D10 D18\n", " error(0.0266667) D11 D12\n", " error(0.01) D11 D19\n", " error(0.0266667) D12 D13\n", " error(0.01) D12 D20\n", " error(0.0266667) D13 D14\n", " error(0.01) D13 D21\n", " error(0.0266667) D14 D15\n", " error(0.01) D14 D22\n", " error(0.01) D15 D23\n", " error(0.0266667) D15 L0\n", " error(0.01) D16\n", " error(0.01) D16 D17\n", " error(0.01) D17 D18\n", " error(0.01) D18 D19\n", " error(0.01) D19 D20\n", " error(0.01) D20 D21\n", " error(0.01) D21 D22\n", " error(0.01) D22 D23\n", " error(0.01) D23 L0\n", " shift_detectors(0, 1) 0\n", " detector(1, 0) D8\n", " detector(3, 0) D9\n", " detector(5, 0) D10\n", " detector(7, 0) D11\n", " detector(9, 0) D12\n", " detector(11, 0) D13\n", " detector(13, 0) D14\n", " detector(15, 0) D15\n", " detector(1, 1) D16\n", " detector(3, 1) D17\n", " detector(5, 1) D18\n", " detector(7, 1) D19\n", " detector(9, 1) D20\n", " detector(11, 1) D21\n", " detector(13, 1) D22\n", " detector(15, 1) D23\n", "''')\n" ] } ], "source": [ "dem = circuit.detector_error_model()\n", "print(repr(dem))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "QgqQ90GlBcB_" }, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": { "id": "2NpcBtBM-qOS" }, "source": [ "You can view the detector error model as a graph by using the `matchgraph-svg` diagram. Note that this diagram looking good relies heavily on the circuit specifying coordinate data for its detectors. Fortunately, the circuit you generated includes good coordinate data:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "DrUUoFMu-qOV", "outputId": "1ee89575-a83e-47ad-de32-f636edd8726d" }, "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", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\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", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\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": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dem.diagram(\"matchgraph-svg\")" ] }, { "cell_type": "markdown", "metadata": { "id": "fLY3a5w9PT1L" }, "source": [ "In the diagram above, each node is a detector and each edge is an error mechanism. The matcher is going to decode errors by trying to match each excited node to another nearby excited node, or to the side boundaries, which minimizes the number of edges that were used.\n", "\n", "The detector error model format is easier for decoders to consume than a raw circuit, because everything is explained in terms of observable symptoms and hidden symptoms, which is how decoders usually conceptualize the problem space.\n", "For example, some decoders can be configured using a weighted graph, and [`stim.DetectorErrorModel`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.DetectorErrorModel) is effectively just a weighted graph.\n", "It might be a pain to write the glue code that converts the [`stim.DetectorErrorModel`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.DetectorErrorModel) into exactly the right kind of graph expected by the decoder, but it's much easier than starting from the circuit or generating the graph from scratch – and you only have to write that code once instead of once per circuit.\n", "\n", "For this tutorial, you'll use existing packages instead of writing your own glue code.\n", "Specifically, you'll use the open-source package [PyMatching](https://github.com/oscarhiggott/PyMatching) as your decoder.\n", "PyMatching is a minimum-weight perfect matching decoder written by Oscar Higgott.\n", "You can install it using `pip install pymatching`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "xlH-hxBRPjNy" }, "outputs": [], "source": [ "!pip install pymatching~=2.0" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "cx7UkBiYQeOz" }, "outputs": [], "source": [ "import pymatching" ] }, { "cell_type": "markdown", "metadata": { "id": "ewrxiwXMQ-Yz" }, "source": [ "Now you're going to write a method that will sample a circuit using Stim, decode it using PyMatching, and count how often it gets the right answer.\n", "\n", "First, you sample detection events and observable flips from the circuit.\n", "You do this by creating a sampler with [`circuit.compile_detector_sampler()`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit.compile_detector_sampler) and then calling [`sampler.sample(shots, separate_observables=True)`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.CompiledDetectorSampler.sample).\n", "The `separate_observables=True` argument is saying that you want the result of the method to be a tuple where the first entry is detection event data to give to the decoder and the second entry is the observable flip data the decoder is supposed to predict.\n", "\n", "Second, you extract decoder information by using [`stim.Circuit.detector_error_model(...)`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit.detector_error_model) and create a decoder from this information using [`pymatching.Matching.from_detector_error_model`](https://pymatching.readthedocs.io/en/latest/api.html#pymatching.matching.Matching.from_detector_error_model).\n", "\n", "Third, you run `matching.predict` to get the predicted observable flips.\n", "\n", "Fourth, you compare the predictions made by PyMatching to the actual observable flip data that was sampled.\n", "Anytime the prediction differs, it indicates a logical error." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "32hBrUAQRbSc" }, "outputs": [], "source": [ "def count_logical_errors(circuit: stim.Circuit, num_shots: int) -> int:\n", " # Sample the circuit.\n", " sampler = circuit.compile_detector_sampler()\n", " detection_events, observable_flips = sampler.sample(num_shots, separate_observables=True)\n", "\n", " # Configure a decoder using the circuit.\n", " detector_error_model = circuit.detector_error_model(decompose_errors=True)\n", " matcher = pymatching.Matching.from_detector_error_model(detector_error_model)\n", "\n", " # Run the decoder.\n", " predictions = matcher.decode_batch(detection_events)\n", "\n", " # Count the mistakes.\n", " num_errors = 0\n", " for shot in range(num_shots):\n", " actual_for_shot = observable_flips[shot]\n", " predicted_for_shot = predictions[shot]\n", " if not np.array_equal(actual_for_shot, predicted_for_shot):\n", " num_errors += 1\n", " return num_errors" ] }, { "cell_type": "markdown", "metadata": { "id": "gVvL0sbLSi4R" }, "source": [ "You can try this method on the repetition code circuit:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "D93VJhBVS1Cn", "outputId": "abeb4610-b2ac-47fc-ae4d-eba90403e095" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "there were 5 wrong predictions (logical errors) out of 100000 shots\n" ] } ], "source": [ "circuit = stim.Circuit.generated(\"repetition_code:memory\", rounds=100, distance=9, before_round_data_depolarization=0.03)\n", "num_shots = 100_000\n", "num_logical_errors = count_logical_errors(circuit, num_shots)\n", "print(\"there were\", num_logical_errors, \"wrong predictions (logical errors) out of\", num_shots, \"shots\")" ] }, { "cell_type": "markdown", "metadata": { "id": "jRbSSRC2TcBA" }, "source": [ "You can check that increasing the physical noise strength increases the logical error rate.\n", "Try increasing the between-round depolarization strength to 13%:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "MLHl2S91Tnpi", "outputId": "801e1e00-4ca7-4783-b556-cef8f180a110" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "there were 1234 wrong predictions (logical errors) out of 10000 shots\n" ] } ], "source": [ "circuit = stim.Circuit.generated(\n", " \"repetition_code:memory\",\n", " rounds=100,\n", " distance=9,\n", " before_round_data_depolarization=0.13,\n", " before_measure_flip_probability=0.01)\n", "num_shots = 10_000\n", "num_logical_errors = count_logical_errors(circuit, num_shots)\n", "print(\"there were\", num_logical_errors, \"wrong predictions (logical errors) out of\", num_shots, \"shots\")" ] }, { "cell_type": "markdown", "metadata": { "id": "j2WD8EIIYSsO" }, "source": [ "As you can see, you get a lot more wrong predictions with this higher noise strength." ] }, { "cell_type": "markdown", "metadata": { "id": "pJt8euOoT1kQ" }, "source": [ "\n", "# 7. Estimate the threshold of a repetition code using Monte Carlo sampling\n", "\n", "Estimating the threshold of an error correcting code really just comes down to trying a bunch of physical error rates and code distances.\n", "You plot out the logical error rate vs physical error rate curve for each distance, and see where the curves cross.\n", "That's where the physical error rate gets bad enough that increasing the distance starts to make the logical error rate worse, instead of better.\n", "That's the threshold physical error rate.\n", "\n", "You can estimate the threshold of the repetition code, for the specific type of noise you're using, by plotting the logical error rate at various code distances and physical error rates:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 283 }, "id": "qkWlL6pwT5xc", "outputId": "cfd36225-8a66-4ce1-9c9c-79861041fb91" }, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "\n", "num_shots = 10_000\n", "for d in [3, 5, 7]:\n", " xs = []\n", " ys = []\n", " for noise in [0.1, 0.2, 0.3, 0.4, 0.5]:\n", " circuit = stim.Circuit.generated(\n", " \"repetition_code:memory\",\n", " rounds=d * 3,\n", " distance=d,\n", " before_round_data_depolarization=noise)\n", " num_errors_sampled = count_logical_errors(circuit, num_shots)\n", " xs.append(noise)\n", " ys.append(num_errors_sampled / num_shots)\n", " plt.plot(xs, ys, label=\"d=\" + str(d))\n", "plt.loglog()\n", "plt.xlabel(\"physical error rate\")\n", "plt.ylabel(\"logical error rate per shot\")\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "J5TZ-AlJVGmk" }, "source": [ "From the results here you can see that the repetition code has amazingly good performance! Well... it's not *quite* so amazing when you remember that you're using a phenomenological noise model (instead of a circuit-level noise model) and also that you're inserting depolarizing errors instead of bit-flip errors (the repetition code is immune to Z errors, and when a depolarizing error occurs it's a Z error one third of the time).\n", "\n", "Still, you can see that it's not so hard to run a few different cases and plot them out. A bit tedious, maybe." ] }, { "cell_type": "markdown", "metadata": { "id": "c88KJx_mVhZU" }, "source": [ "\n", "# 8. Use `sinter` to streamline the Monte Carlo sampling process\n", "\n", "Now that you understand the basic workflow of sampling from a circuit, making a decoder predict the observable flips, and plotting out the result, you probably never want to do that by hand ever again. And that's without even getting into dividing work into batches, or across multiple CPU cores!\n", "\n", "Fortunately, you can use [Sinter](https://pypi.org/project/sinter/) to do almost the entire thing for you. Install Sinter using `pip install sinter`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ogUrK7LhZyV-" }, "outputs": [], "source": [ "!pip install sinter~=1.14" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "VrayGWt0-qOW" }, "outputs": [], "source": [ "import sinter\n", "from typing import List" ] }, { "cell_type": "markdown", "metadata": { "id": "dxTADjNX-qOW" }, "source": [ "Wrap your circuits into [`sinter.Task`](https://github.com/quantumlib/Stim/blob/main/doc/sinter_api.md#sinter.Task) instances, and give those tasks to [`sinter.collect`](https://github.com/quantumlib/Stim/blob/main/doc/sinter_api.md#sinter.collect).\n", "Sinter will spin up multiple worker processes to sample from and decode these circuits.\n", "[`sinter.collect`](https://github.com/quantumlib/Stim/blob/main/doc/sinter_api.md#sinter.collect) takes a variety of useful options, such as the maximum number of shots or errors to take from each task, as well as the number of workers to use:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "4HmwlVCz-qOW" }, "outputs": [], "source": [ "tasks = [\n", " sinter.Task(\n", " circuit=stim.Circuit.generated(\n", " \"repetition_code:memory\",\n", " rounds=d * 3,\n", " distance=d,\n", " before_round_data_depolarization=noise,\n", " ),\n", " json_metadata={'d': d, 'p': noise},\n", " )\n", " for d in [3, 5, 7, 9]\n", " for noise in [0.05, 0.08, 0.1, 0.2, 0.3, 0.4, 0.5]\n", "]\n", "\n", "collected_stats: List[sinter.TaskStats] = sinter.collect(\n", " num_workers=4,\n", " tasks=tasks,\n", " decoders=['pymatching'],\n", " max_shots=100_000,\n", " max_errors=500,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "mVDInHcUbAc0" }, "source": [ "Sinter also has a [`sinter.plot_error_rate`](https://github.com/quantumlib/Stim/blob/main/doc/sinter_api.md#sinter.plot_error_rate) method which can be used to plot the logical error rates. This method automatically adds highlighted regions quantifying uncertainty in the estimates." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 484 }, "id": "ZOsb01E0bFhw", "outputId": "ceb5a5c3-bb97-4da1-c186-024ad0f5bc92" }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAArcAAAIkCAYAAAAEbwOaAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAABJ0AAASdAHeZh94AAEAAElEQVR4nOydd5xcZb3/36dML9t7NtkUEkIIoYXea4RQBASli1IuKhd/16siegmWe+WCosIFQaSIKCBSBEWkhF4SCAQIBEJ62c32Mv2U5/fHmZnd2ZndnU3ZbMLzzmte2TnnOc/znDJnvvM93+/nqwghBBKJRCKRSCQSyS6AuqMnIJFIJBKJRCKRbCukcSuRSCQSiUQi2WWQxq1EIpFIJBKJZJdBGrcSiUQikUgkkl0GadxKJBKJRCKRSHYZpHErkUgkEolEItllkMatRCKRSCQSiWSXQRq3EolEIpFIJJJdBmncSiQSiUQikUh2GaRxK5FIJBKJRCLZZZDGrUQikUgkEolkl0Eat5KdjqOOOgpFUUa1zb333ouiKNx7773bZ1I7MYqicNRRR+3oaUh2Ujo6OigvL+fKK6/cqn4uvvhiFEVhzZo122Ziku3GggULUBSFF198cczHHuv71Zo1a1AUhYsvvnjMxhwNY3k8hBDMmTOHww8/fEzG2xqkcbsLoShKzkvTNMrLyznqqKO49957EULs6CkWxZZ8yb344osoisKCBQu227y2F7Zt88gjj3DmmWfS2NiI1+slEAgwc+ZMLrvsMl577bUdPcWtInM+h3uN1y+Okch88Q186bpOdXU18+bN44knntgm4+xIY2IkrrvuOuLxOD/84Q9zlmfmPPDl8/mYPn063/jGN9iwYcMOmrFEsv0YeE848MADh2ynKAoTJkwYw5ltPYqi8OMf/5hXX32VRx55ZEdPZ1j0HT0BybbnuuuuA8AwDD777DMee+wxXnrpJd5++21uvfXWHTy7recPf/gDsVhsVNt88Ytf5KCDDqKurm47zWrLaGlp4ayzzuK1114jFApx/PHHM3XqVIQQrFixgj//+c/87ne/45ZbbuGb3/zmjp7uVnHaaaex9957F1w31PKdhZKSEq6++moAkskky5Yt46mnnuKZZ57hxhtv5Dvf+c6OneB2Yt26ddxxxx189atfpb6+vmCbI488MutZam9v51//+he33XYbDz/8MG+++SZTp04dwxlLdnY+/vhj/H7/jp5GUSxatIgHH3yQL3/5y9ttjLE+HqeddhozZ87k2muv5cwzzxz1U9SxQhq3uyCDvZevvfYaRxxxBLfddhv/8R//weTJk3fMxLYREydOHPU2JSUllJSUbIfZbDmxWIx58+axdOlSvvzlL3PbbbdRVlaW06a3t5ebbrqJnp6eHTTLbcfpp5++03poR6K0tDTvc/fggw/yla98heuuu44rr7xyp/lCHg133HEHpmkOe16POuqonGNjGAZf+MIXeP755/npT3/KPffcs/0nKtll2H333Xf0FIpi4sSJNDc384Mf/IAzzjgDt9u9XcbZEcfjoosu4vvf/z7PP/88xx133JiPXwwyLOFzwKGHHsruu++OEIJ33nknb/1bb73FWWedRW1tLW63m8bGRi6//HI2bdqU1zYT75pMJvnhD3/I5MmT8Xg8TJ06leuvv55UKlVwDsuXL+fiiy+msbERt9tNTU0N5557Lp988klOO0VRuO+++wCYPHly9vFOU1NT3hwyXHzxxRx99NEAXH/99TmPQTOPcYeLuX3nnXc488wzqa6uxuPxMGnSJK688kqam5vz2g4MmbjjjjuYPXs2Xq+XmpoaLrvsslEZoTfffDNLly7l0EMP5YEHHsgzbAHC4TA//vGP8zx/PT09XHPNNcyYMQOv10tZWRknnngizz33XMGxUqkUP/nJT5g6dSoej4fJkyfzwx/+kGQyOeT8TNPktttu46CDDiIcDuP3+9lnn3249dZbsW276P0cLQNj3D799FPOOeccqqurUVWVF198ccT14IR6/Pa3v2Xu3LkEg0ECgQBz587l9ttvLzj3TNxaS0sLX//612loaEDTtK2K0T7nnHMIBALEYjE++uijnHULFy7ksssuY4899iAcDuPz+dhzzz25/vrrSSQSOW2bmpq4/vrrATj66KNzru+BxGIx/ud//oe9996bQCBAMBjk4IMP5s9//nPe3IQQ3HfffRxyyCFUVVXh9XppbGzkxBNP5KGHHipq/4QQ3HPPPTQ2NnLIIYcUfVxcLheXXXYZ4Hi2CjGaz9aGDRv45je/yZQpU/B4PFRUVHDqqaeyePHivLYDwzseeeQRDjjgAPx+P+Xl5Xz5y19m48aNBcdYsWIFF154IQ0NDbjdburr67nwwgtZsWLFsGP8+c9/Zr/99sPv91NfX8//+3//L/uZe+GFFzjqqKMIh8OUlZVxwQUX0NHRsVPs43A8//zzzJs3j/LycjweD9OnT+f73//+kOdv8eLFnHDCCYRCIcLhMMcddxxvvPHGkKE4Q8WYWpbFb3/7Ww499FBKSkrw+XxMmzaNr3/96zn7sGnTJn784x9z6KGHZr/z6uvrOffcc/M+p1tDY2MjV155JatXr+aWW24perstvXcNpK+vj5/85CfsueeehMNhQqEQU6dO5ZxzztlqGwDIeqJ///vfF71fY4303H7OcLlcOe/vvvtuLrvsMjweD6eeeiqNjY2sWLGCu+66iyeffJI333yzoKf07LPPZvHixZx11lm4XC6eeOIJFixYwNtvv83f/va3nC/ef/7zn5xxxhkYhsEpp5zCtGnT2LBhA48++ih///vfWbhwIfvuuy/ghFQ8/vjjLF26lH//93+ntLQUIPt/IU4//XQA7rvvvpxHoECOUVyIp556ijPPPBMhBGeddRaTJk3inXfe4fbbb+eJJ57g1VdfLejp/u53v8szzzzDKaecwgknnMDChQv53e9+x2effcYLL7ww7JgZ7rzzTgB+9KMfoarD/870eDzZv7u7uzn00EP56KOPmDt3LldffTXt7e08/PDDnHDCCdx+++1cfvnl2fZCCM4++2yeeOIJpk6dyje/+U1SqRR33303H3zwQcHxMufqmWeeYcaMGZx77rl4vV4WLlzIt771Ld566y3uv//+ovZzS1m5ciUHHngg06dP57zzziMejxMOh4taf8EFF/CnP/2JxsZGvv71r6MoCo899hhXXnklr776Kg888EDeeJ2dnRx00EEEg0HOOOMMVFWlpqZmm+zL4M/dDTfcwPLlyznkkEM4+eSTSSQSvPbaayxYsIAXX3yR5557Dk3TALj66qt5/PHHeemll7jooosKXtPd3d0cc8wxvPvuu+y7775ccskl2LbNM888w7nnnsuyZcv46U9/mm1/7bXX8j//8z9MnjyZs88+m5KSEpqbm1m8eDF/+ctfOOecc0bcp2XLltHc3LxFj1wz8f+FHmmO5rO1ZMkSTjjhBDo7OznxxBM544wzaG9v5/HHH+ewww7jscce46STTsob47bbbuNvf/sbp556KkceeSRvvfUWDz30EEuXLuW9997L+bwtXryY4447jr6+Pk499VT22GMPli9fzh//+EeeeOIJnnvuOebOnZs3xi233MLTTz/N6aefzlFHHcW//vUvbr75Zjo7OznttNP48pe/zMknn8xll13G66+/zh//+Efa29t5+umnd5p9HMwdd9zBv/3bvxEIBPjSl75EdXU1L774IjfccANPPvkkr732Ws69/OWXX+aEE07AsizOOOMMpk6dygcffMDRRx/NMcccM+J4GVKpFPPnz+fZZ5+lsbGRc889l3A4zJo1a3jsscc47LDD2G233bJj/vznP+foo4/mzDPPJBgMsmLFCh555BH+9re/8dprrzFnzpyixx6O//qv/+K+++7jZz/7GV/96lcpLy8fcZstuXcNRAjBvHnzeP311zn44IP5+te/jq7rbNiwgYULF3L44Yez3377ZdtviQ0wadIkGhoaeO655xBCjM/QBCHZZQBEoVP60ksvCVVVhdvtFps2bcou/+STT4TL5RJTp04VGzZsyNnmueeeE6qqitNPPz1n+ZFHHikAsdtuu4nOzs7s8ng8Lg466CABiD/84Q/Z5Z2dnaK0tFRUVFSIZcuW5fT1wQcfiEAgIPbZZ5+c5RdddJEAxOrVqwvuZ2YOA1m4cKEAxHXXXVdwm3vuuUcA4p577sku6+vrE+Xl5UJVVfHyyy/ntP/5z38uAHH88ccXnFtjY6NYu3ZtdrlhGOLwww8XgHjrrbcKzmEg69atE4DQdV3E4/ER2w/ksssuE4C47LLLhG3b2eWffvqpCIfDwu125xy7Bx54QADioIMOyhmro6NDTJkyRQDiyCOPzBnjuuuuE4D45je/KUzTzC43TVNccsklAhCPP/54UfPNHLPTTjtNXHfddQVfH3/8cbb96tWrs9fyNddck9ffSOv/9Kc/CUDss88+oq+vL7s8EomI/fbbTwDigQceyNkm098FF1wgDMMoar8GzmXSpEl56+6//34BiKqqqrxzvHLlypxzl+GHP/yhAMSDDz6YszxzPhYuXFhwHpljfMMNN+Qsj8fj4sQTTxSKooh33303u7y8vFw0NDSIaDSa11dbW9sQe5vL7bffLgBx0003FVyfmfPgz6RhGOKYY44RgLjkkkvy9qHYz5ZhGGLq1KnC4/GIF198MWeMjRs3ivr6elFbWysSiUTenEKhkHj//fdztvnKV74iAPHQQw9ll9m2LXbffXcBiD/+8Y857R988EEBiBkz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0003AU7M4nnnnUdfXx8/+tGPctquXLmS3/zmN7hcLi644ILs8sz5uPbaa3MeIXd2dhaMqdV1nW9961s0Nzdz1VVXZR8/DaS5uXmbakJuSy655BIArrnmmpxKdrFYjO9///sAfO1rXxuTufzkJz8BnEfzmbi+jJTX4Ot+1apV2UeIgxnu+q6urua8887j7bff5ic/+UlBA3jlypWsXr0acH4oFSrpbBgGnZ2dAEUVnDj88MPRNC3vszNWnHbaaUydOpX/+7//4x//+EfBNm+88caoqxkO5NBDD2XGjBkFy40+8sgjvPLKK0yfPr1g3Oi2YGfax/PPPx+Xy8Utt9yS/QGX4Uc/+hG9vb2cf/752ScChx56KFOnTmXhwoV58md33nlnUfG24Pzwu/LKK4nH41xxxRV5IQWpVIq2tjbA+az4/X7eeeednEfthmHw7//+79s9fvSGG25A1/XsfWgw2+LetXr1alatWpW3vKuri2QymRNGsKU2QDKZ5L333mOfffYZVqZzRyLDEj4nNDQ0cMUVV/DrX/+a//3f/+V//ud/2H333bn77ru55JJLmDVrFvPmzWP69OkYhsG6det45ZVXqKqqKuiZmTlzJrNmzcrRuV25ciUnn3xyjmFVUVHBI488ki1/e+yxxzJr1iwURWH9+vW88cYb2bjTDMceeyw33ngjl156KWeeeSahUIjS0tJhy8/OmDGDhoYGHnzwQVwuF5MmTUJRFC644AImTZpUcJtgMMjdd9/Nl770JY488ki+9KUvMXHiRN555x3+9a9/UVtbyx133LEVR314/H4///znPznrrLN44IEHePLJJ3PK73722Wc8//zz9Pb25pRN/vnPf84rr7zCrbfeyuLFizn66KOzOrd9fX3ceuutOdq8X/nKV3jooYf429/+xp577slpp52GYRg88sgjzJ07l5UrV+bN7Uc/+hFLly7lt7/9LU8++STHHHMMDQ0NtLa2smLFCl577TV+9rOfjSoJ8PHHH8/GpQ6mqalpm1UvO/fcc3niiSd4+OGHmTVrFqeffno2nm716tWcc845nHfeedtkrJHYf//9Oe2003jiiSf4/e9/z+WXX57Vev7lL3/JBx98wD777MO6det46qmnOPnkkwsasEcffTSqqnLNNdfw4YcfZpO4fvjDHwJw6623smLFCv7rv/6L+++/n8MOO4yamho2bdrExx9/zOLFi/nzn//M5MmTicfjHHbYYUybNo399tuPSZMmkUgkePbZZ/n444859dRTs9614SgpKeHYY4/lxRdfpKura8TEsm2Ny+Xi0Ucf5cQTT+Tkk0/mkEMOYe+998bv97N+/XoWL17MqlWraG5u3uLqcJmiMscffzznnHMOp512GrvvvjuffPIJjz/+OKFQiD/84Q8j6lRvKTvTPjY1NfGrX/2Kb3zjG+y7776cffbZVFVV8dJLL/HGG2+w++67c8MNN2Tbq6rKXXfdxbx58zj11FM588wzmTp1Ku+//z7PPvssX/jCF3j66aeLOrbXXXcdb731Fk8++STTp09n/vz5hEIh1q9fz7/+9S9uvPFGLr74YlRV5aqrruLnP/85s2fP5rTTTiOVSrFw4UI6Ozs5+uijs57q7cGMGTO47LLLuO222wqu3xb3rqVLl3LGGWcwd+5cZs6cSX19PW1tbTzxxBMYhpHzA3pLbYAXX3yRVCrFmWeeufUHZXuxg9UaJNsQKKxzm6GlpUX4/X7h9/tFS0tLdvn7778vLrroIjFx4kThdrtFWVmZmDVrlrjsssvE888/n9NHRoYrkUiIa6+9NitxMnnyZLFgwYIhpYVWr14tvvGNb4hp06YJj8cjQqGQmDFjhjj//PNzZKAy/OIXvxC77767cLvdgkE6ooWkwIQQYtGiReKYY44R4XBYKIqSIyNTSAps4Hann366qKysFC6XSzQ2NoorrriioKzMcDJlI8mRDYVlWeLhhx8WX/ziF0VDQ4PweDzC5/OJGTNmiK997Wvitddey9umq6tLfPe73xXTpk0TbrdblJSUiOOOO66ghIwQjoTY9ddfLyZPnizcbreYNGmS+MEPfiASiUSelEwG27bFH/7wB3HMMceIsrIy4XK5RH19vTj00EPFz372M7Fu3bqi9q8YKbCB448kvVOMNI9lWeL//u//xH777Sd8Pp/w+Xxi3333FbfeemuOXmeGoY7BSAync5vhvffeE4qiiIaGhqze7bp168S5554r6uvrhdfrFXvssYe44YYbhGEYQ87l/vvvF3PmzBFer7fgZz2ZTIpbbrlFHHzwwVm948bGRnHMMceIm2++WbS3twshhEilUuKGG24Q8+bNE42NjcLj8YjKykpx4IEHittvv10kk8mi9z8jVXXbbbflrRtK53YotvSztXnzZvG9731PzJo1S/h8PhEIBMS0adPEmWeeKe6///4cObvhJNWGu66WL18uzj//fFFbWyt0XRe1tbXivPPOE8uXLx9yvwuNMdx9aFfZx2eeeUYcf/zxorS0VLjdbjF16lTxn//5n6KrqyuvrRBCvPnmm+K4444TwWBQBINBceyxx4rXX39dfOMb3xCQq/srxNCfVcMwxC233CLmzp0rAoGA8Pv9Ytq0aeLSSy8VK1asyGn3i1/8QsycOVN4vV5RU1Mjzj//fLFmzZqC1+DWSoENprW1VYTD4YJSYEJs/b1r/fr14pprrhGHHHKIqKmpEW63WzQ0NIh58+bl6QlnGI0NIISjl7wl0oljiSLEAP0ZiWQEjjrqKF566SXkZSORSCzLYvbs2bjdbt59993xWalIslNy6KGH8tZbb9HT00MgENjR05GkaW1tpampiXPPPZe77rprR09nSGTMbQHa2to4+eSTCQQCzJgxg+eff35HT0kikUjGHZqmcdNNN7F06dIdqqwh2TmJxWIFcw3uvfdeXn/9dU444QRp2I4z/vu//xtN07L5BOMVGXNbgG984xvU1tbS1tbGc889x9lnn82KFSuKqgstkUgknydOOukkfv3rX4+odyuRDGbdunXss88+HH/88UybNg3TNHn33Xd59dVXKS0t5Re/+MWOnqJkAEII6urquP/++7dZkYvthQxLGEQkEqG8vJxVq1YxYcIEwHkUf9FFF+VV+Po8IsMSJBKJRLIt6Orq4j//8z956aWXaGlpIZlMUltby3HHHce1117L1KlTd/QUJTspO31YQiQS4brrrmPevHmUl5ejKAr33ntvwbbJZJLvfe971NfX4/P5OPDAA3n22Wdz2qxYsYJgMJg1bAFmz57NsmXLtudu7DS8+OKL0rCVSCQSyVZTVlbGXXfdxYoVK+jr6yOVSrFu3TruvvtuadhKtoqd3rhtb2/nxz/+MR9//DFz5swZtu3FF1/ML3/5S8477zx+/etfo2kaJ510Uo6IdCQSyavUEQ6Hd0h5WIlEIpFIJBLJ6NjpY27r6upobm6mtraWt99+m7lz5xZst2jRIh588EFuvPFGvvOd7wBw4YUXsueee/Ld7343Ww0lGAzS29ubs21vb2+2FrNEIpFIJBKJZPyy03tuPR4PtbW1I7Z75JFH0DQtW/IUwOv18rWvfY033niD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BQvIzs5m8eLFbNu2jYceeogrr7ySP/7xjwD89Kc/5fe//z3vvPMOJ510Uvd2jY2NvP/++yxevLjPY3o8Hg4//HA2bdrExRdfzH777UdzczNvvvkmtbW15OXl4Xa7eeqpp5g7dy6XXnopnZ2drFy5kuOPP55169YxY8aMAT1fn8/XnUDvLCsrC7P5+38KNm/ezNy5c7nsssu49NJLeyTrd9xxB1arleuuu45gMIjVauWZZ57hoosu4sADD+See+7B6XSybNky/vnPf7J+/XqysrK6t1dVleOPP57DDjuM+++/f0DlINu2bQMgOzu7e1k05yw/P59HH32Uyy+/nNNOO43TTz8dgH333ReAjRs38sMf/pDS0lJuuukmHA4HL730Eqeeeiqvvvoqp512WsyxCiHiRBdCiCT29NNP64D+3nvv6S6XS6+pqdFffPFFPTc3V7fb7XptbW2P9Y477jg9HA53b3/NNdfoJpNJb29v13Vd1zVN08vKyvSzzz67x3EeeOAB3WAw6N999133ssrKSv3CCy/s/n3RokU6oL/22mu7xdl1TFVV9WAw2OOxtrY2vbCwUL/44ot7LAf0xYsX9/v8t27dqgN9/nz44Yc94gX0v/3tbz32sXr1ah3Qx48fr/t8vu7loVBILygo0KdNm6b7/f7u5W+//bYO6IsWLepeduGFF+qAftNNN/Ubb5fertsrr7yi5+fn6zabTa+pqeleN9pz5nK5+jxnxx57rL7PPvvogUCge1k4HNYPPfRQfa+99ooqZiFEYpA7t0KIMeG4447r8XtlZSV/+MMfKC0t7bF8/vz5GAyG7t8PP/xwHnzwQbZv386+++6L0WjkvPPOY/ny5XR2dpKeng7AH/7wBw499FDGjRvXZwyvvvoq06dP7/UuYNcxTSYTJpMJgHA4THt7O+FwmAMOOIBPP/10YE9+x/M688wzd1s+ZcqUHr+PGzeO448/vtd9XHjhhT3qb//973/T1NTEkiVLegzumj17NpMnT+bPf/5z9wCuLpdffnlMce963aqqqnjuuecoKyvrXjbYc9ba2sr777/P7bffTmdnJ52dnd2PHX/88SxevJi6urrdXitCiMQkya0QYkx4+OGHmTRpEmazmcLCQvbee+9eB0NVVFT0+L3r6++2trbuZRdccAH33nsvr7/+OhdccAGbN2/mP//5D4899li/MXz77becccYZe4x11apV/OY3v+Grr77qUdvbX+K8J3vttdduiWJv+jvGro9t374doEfpQpfJkyezdu3aHsvMZnOPpDQaXdeto6OD3/3ud3zwwQe9DtQbzDnbsmULuq6zcOFCFi5c2Os6TU1NktwKMUpIciuEGBNmzZrVPeq+P113AHel63r3/0+ZMoX999+f5557jgsuuIDnnnsOq9U6JKP3n3vuOebNm8epp57K9ddfT0FBASaTiXvuuafHIKrh0ltnhGgei4bNZou5u8LO1+3UU0/lsMMO49xzz2Xz5s2kpaUBgz9n4XAYgOuuu67Pu9YTJ06MKW4hRPxIciuEEANwwQUXcO2119LQ0MDzzz/P7Nmzewxy6s2ECRPYsGFDv+u88sorjB8/ntdee61HeUR/A9XipbKyEogMQuvqPtFl8+bN3Y8Pla6E9eijj2bFihXcdNNNQPTnbOfHdjZ+/HgALBZLVHe3hRCJTVqBCSHEAMydOxeDwcD//u//8t1330XVQ/aMM87g888/5/XXX9/tsa47w113jne+U/zxxx/z4YcfDlHkQ+eAAw6goKCAxx57jGAw2L38r3/9K5s2bWL27NlDfsyjjjqKWbNm8dBDDxEIBIDoz1lXd4b29vYeywsKCjjqqKN4/PHHaWho2O2YO7eCE0IkPrlzK4QQA5Cfn88JJ5zAyy+/TFZWVlSJ3PXXX88rr7zCmWeeycUXX8z+++9Pa2srb775Jo899hjTp0/npJNO4rXXXuO0005j9uzZbN26lccee4wpU6bg8XgGHO+nn37Kc889t9vyCRMmcMghhwxonxaLhXvvvZeLLrqII488krlz53a3AquqquKaa64ZcLz9uf766znzzDN55pln+PnPfx71ObPb7UyZMoU//vGPTJo0iZycHKZNm8a0adN4+OGHOeyww9hnn3249NJLGT9+PE6nkw8//JDa2lo+//zzYXkuQoihJ8mtEEIM0AUXXMDbb7/NWWedFdVsZGlpafzf//0fixcv5vXXX2fVqlUUFBRw7LHHdg+0mjdvHo2NjTz++OO88847TJkyheeee46XX36ZNWvWDDjWF154gRdeeGG35RdeeOGAk9uueFNTU/nVr37FjTfe2D3xxb333tujx+1QOv3005kwYQL3338/l156aUzn7KmnnmLBggVcc801hEIhFi9ezLRp05gyZQr//ve/Wbp0Kc888wwtLS0UFBQwc+ZMFi1aNCzPQwgxPAz6zt/jCCGEiNobb7zBqaeeygcffNA9c5YQQoj4kuRWCCEG6KSTTmLTpk1s2bKlz8FKQgghRpaUJQghRIxefPFFvvjiC/785z+zbNkySWyFECKByJ1bIYSIkcFgIC0tjbPPPpvHHnsMs1nuEwghRKKQd2QhhIiR3BMQQojEJX1uhRBCCCFE0pDkVgghhBBCJA1JboUQQgghRNKQ5FYIIYQQQiQNSW6FEEIIIUTSkORWCCGEEEIkDUluhRBCCCFE0pDkVgghhBBCJA1JboUQQgghRNKQ5FYIIYQQQiSN/w8D0+XutxTMJAAAAABJRU5ErkJggg==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, ax = plt.subplots(1, 1)\n", "sinter.plot_error_rate(\n", " ax=ax,\n", " stats=collected_stats,\n", " x_func=lambda stats: stats.json_metadata['p'],\n", " group_func=lambda stats: stats.json_metadata['d'],\n", ")\n", "ax.set_ylim(1e-4, 1e-0)\n", "ax.set_xlim(5e-2, 5e-1)\n", "ax.loglog()\n", "ax.set_title(\"Repetition Code Error Rates (Phenomenological Noise)\")\n", "ax.set_xlabel(\"Phyical Error Rate\")\n", "ax.set_ylabel(\"Logical Error Rate per Shot\")\n", "ax.grid(which='major')\n", "ax.grid(which='minor')\n", "ax.legend()\n", "fig.set_dpi(120) # Show it bigger" ] }, { "cell_type": "markdown", "metadata": { "id": "gU4GZ66sZllH" }, "source": [ "`sinter`'s goal is to make getting these kinds of results fast and easy." ] }, { "cell_type": "markdown", "metadata": { "id": "fe0PdLck-qOW" }, "source": [ "\n", "# 9. Estimate the threshold and footprint of a surface code\n", "\n", "Estimating the threshold of a repetition code under phenomenelogical noise is one thing.\n", "Estimating the threshold of a true quantum code, such as a surface code, under circuit noise, is...\n", "well, historically, it would be a whole other thing.\n", "But when using Stim, and PyMatching, and Sinter, the workflow is exactly identical.\n", "The only thing that changes are the circuits input into the process.\n", "\n", "The hard part is making the circuits in the first place.\n", "So, for this tutorial, you'll continue to lean on Stim's example circuits.\n", "You can make simple surface code circuits using [`stim.Circuit.generated`](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md#stim.Circuit.generated)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "gAtDhdGuV--e" }, "outputs": [], "source": [ "surface_code_circuit = stim.Circuit.generated(\n", " \"surface_code:rotated_memory_z\",\n", " rounds=9,\n", " distance=3,\n", " after_clifford_depolarization=0.001,\n", " after_reset_flip_probability=0.001,\n", " before_measure_flip_probability=0.001,\n", " before_round_data_depolarization=0.001)" ] }, { "cell_type": "markdown", "metadata": { "id": "huyjsuMBfSP9" }, "source": [ "Surface code circuits have a much more complex structure than repetition codes, because they are laid out in a 2-D grid instead of a 1-D line. A time slice diagram of the circuit without noise will be much clearer than a timeline diagram:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 404 }, "id": "-1O4qysufdXj", "outputId": "42a217ab-1a92-4850-b634-932f3a8f603e" }, "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", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\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", "Tick 18\n", "\n", "Tick 19\n", "\n", "Tick 20\n", "\n", "Tick 21\n", "\n", "Tick 22\n", "\n", "Tick 23\n", "\n", "Tick 24\n", "\n", "Tick 25\n", "\n", "Tick 26\n", "\n", "Tick 27\n", "\n", "Tick 28\n", "\n", "Tick 29\n", "\n", "Tick 30\n", "\n", "Tick 31\n", "\n", "Tick 32\n", "\n", "Tick 33\n", "\n", "Tick 34\n", "\n", "Tick 35\n", "\n", "Tick 36\n", "\n", "Tick 37\n", "\n", "Tick 38\n", "\n", "Tick 39\n", "\n", "Tick 40\n", "\n", "Tick 41\n", "\n", "Tick 42\n", "\n", "Tick 43\n", "\n", "Tick 44\n", "\n", "Tick 45\n", "\n", "Tick 46\n", "\n", "Tick 47\n", "\n", "Tick 48\n", "\n", "Tick 49\n", "\n", "Tick 50\n", "\n", "Tick 51\n", "\n", "Tick 52\n", "\n", "Tick 53\n", "\n", "Tick 54\n", "\n", "Tick 55\n", "\n", "Tick 56\n", "\n", "Tick 57\n", "\n", "Tick 58\n", "\n", "Tick 59\n", "\n", "Tick 60\n", "\n", "Tick 61\n", "\n", "Tick 62\n", "\n", "Tick 63\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", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\n", "\n", "M\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", "Tick 18\n", "\n", "Tick 19\n", "\n", "Tick 20\n", "\n", "Tick 21\n", "\n", "Tick 22\n", "\n", "Tick 23\n", "\n", "Tick 24\n", "\n", "Tick 25\n", "\n", "Tick 26\n", "\n", "Tick 27\n", "\n", "Tick 28\n", "\n", "Tick 29\n", "\n", "Tick 30\n", "\n", "Tick 31\n", "\n", "Tick 32\n", "\n", "Tick 33\n", "\n", "Tick 34\n", "\n", "Tick 35\n", "\n", "Tick 36\n", "\n", "Tick 37\n", "\n", "Tick 38\n", "\n", "Tick 39\n", "\n", "Tick 40\n", "\n", "Tick 41\n", "\n", "Tick 42\n", "\n", "Tick 43\n", "\n", "Tick 44\n", "\n", "Tick 45\n", "\n", "Tick 46\n", "\n", "Tick 47\n", "\n", "Tick 48\n", "\n", "Tick 49\n", "\n", "Tick 50\n", "\n", "Tick 51\n", "\n", "Tick 52\n", "\n", "Tick 53\n", "\n", "Tick 54\n", "\n", "Tick 55\n", "\n", "Tick 56\n", "\n", "Tick 57\n", "\n", "Tick 58\n", "\n", "Tick 59\n", "\n", "Tick 60\n", "\n", "Tick 61\n", "\n", "Tick 62\n", "\n", "Tick 63\n", "\n", "\n", "" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "surface_code_circuit.without_noise().diagram(\"timeslice-svg\")" ] }, { "cell_type": "markdown", "metadata": { "id": "fPOBAR4T-qOW" }, "source": [ "You can also make 3-D diagrams of the circuit, using `timeline-3d`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 404 }, "id": "XLqwFH29-qOW", "outputId": "42a217ab-1a92-4850-b634-932f3a8f603e" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "{\"accessors\":[{\"bufferView\":0,\"byteOffset\":0,\"componentType\":5126,\"count\":12,\"max\":[0,0.5,0.5],\"min\":[0,-0.5,-0.5],\"name\":\"cube\",\"type\":\"VEC3\"},{\"bufferView\":1,\"byteOffset\":0,\"componentType\":5126,\"count\":12,\"max\":[0.375,0.5625],\"min\":[0.3125,0.5],\"name\":\"tex_coords_gate_R\",\"type\":\"VEC2\"},{\"bufferView\":2,\"byteOffset\":0,\"componentType\":5126,\"count\":12,\"max\":[0.125,0.5],\"min\":[0.0625,0.4375],\"name\":\"tex_coords_gate_H\",\"type\":\"VEC2\"},{\"bufferView\":3,\"byteOffset\":0,\"componentType\":5126,\"count\":17,\"max\":[0,0.400000005960464,0.400000005960464],\"min\":[0,-0.400000005960464,-0.400000005960464],\"name\":\"circle_loop\",\"type\":\"VEC3\"},{\"bufferView\":4,\"byteOffset\":0,\"componentType\":5126,\"count\":17,\"max\":[0,0.400000005960464,0.400000005960464],\"min\":[0,-0.400000005960464,-0.400000005960464],\"name\":\"circle_loop\",\"type\":\"VEC3\"},{\"bufferView\":5,\"byteOffset\":0,\"componentType\":5126,\"count\":4,\"max\":[0,0.400000005960464,0.400000005960464],\"min\":[0,-0.400000005960464,-0.400000005960464],\"name\":\"control_x_line_cross\",\"type\":\"VEC3\"},{\"bufferView\":6,\"byteOffset\":0,\"componentType\":5126,\"count\":12,\"max\":[0.4375,0.5625],\"min\":[0.375,0.5],\"name\":\"tex_coords_gate_MR\",\"type\":\"VEC2\"},{\"bufferView\":7,\"byteOffset\":0,\"componentType\":5126,\"count\":12,\"max\":[0.3125,0.5625],\"min\":[0.25,0.5],\"name\":\"tex_coords_gate_M\",\"type\":\"VEC2\"},{\"bufferView\":8,\"byteOffset\":0,\"componentType\":5126,\"count\":130,\"max\":[1,-32,-32],\"min\":[-17,-40.4852828979492,-40.4852828979492],\"name\":\"buf_scattered_lines\",\"type\":\"VEC3\"},{\"bufferView\":9,\"byteOffset\":0,\"componentType\":5126,\"count\":30,\"max\":[0,-29.5,-31],\"min\":[-15.25,-41.4852828979492,-41.4852828979492],\"name\":\"buf_red_scattered_lines\",\"type\":\"VEC3\"}],\"asset\":{\"version\":\"2.0\"},\"bufferViews\":[{\"buffer\":0,\"byteLength\":144,\"byteOffset\":0,\"name\":\"cube\",\"target\":34962},{\"buffer\":1,\"byteLength\":96,\"byteOffset\":0,\"name\":\"tex_coords_gate_R\",\"target\":34962},{\"buffer\":2,\"byteLength\":96,\"byteOffset\":0,\"name\":\"tex_coords_gate_H\",\"target\":34962},{\"buffer\":3,\"byteLength\":204,\"byteOffset\":0,\"name\":\"circle_loop\",\"target\":34962},{\"buffer\":4,\"byteLength\":204,\"byteOffset\":0,\"name\":\"circle_loop\",\"target\":34962},{\"buffer\":5,\"byteLength\":48,\"byteOffset\":0,\"name\":\"control_x_line_cross\",\"target\":34962},{\"buffer\":6,\"byteLength\":96,\"byteOffset\":0,\"name\":\"tex_coords_gate_MR\",\"target\":34962},{\"buffer\":7,\"byteLength\":96,\"byteOffset\":0,\"name\":\"tex_coords_gate_M\",\"target\":34962},{\"buffer\":8,\"byteLength\":1560,\"byteOffset\":0,\"name\":\"buf_scattered_lines\",\"target\":34962},{\"buffer\":9,\"byteLength\":360,\"byteOffset\":0,\"name\":\"buf_red_scattered_lines\",\"target\":34962}],\"buffers\":[{\"byteLength\":144,\"name\":\"cube\",\"uri\":\"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/\"},{\"byteLength\":96,\"name\":\"tex_coords_gate_R\",\"uri\":\"data:application/octet-stream;base64,AADAPgAAAD8AAKA+AAAAPwAAwD4AABA/AACgPgAAAD8AAKA+AAAQPwAAwD4AABA/AADAPgAAED8AAMA+AAAAPwAAoD4AABA/AACgPgAAED8AAMA+AAAAPwAAoD4AAAA/\"},{\"byteLength\":96,\"name\":\"tex_coords_gate_H\",\"uri\":\"data:application/octet-stream;base64,AAAAPgAA4D4AAIA9AADgPgAAAD4AAAA/AACAPQAA4D4AAIA9AAAAPwAAAD4AAAA/AAAAPgAAAD8AAAA+AADgPgAAgD0AAAA/AACAPQAAAD8AAAA+AADgPgAAgD0AAOA+\"},{\"byteLength\":204,\"name\":\"circle_loop\",\"uri\":\"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA\"},{\"byteLength\":204,\"name\":\"circle_loop\",\"uri\":\"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA\"},{\"byteLength\":48,\"name\":\"control_x_line_cross\",\"uri\":\"data:application/octet-stream;base64,AAAAAM3MzL4AAAAAAAAAAM3MzD4AAAAAAAAAAAAAAADNzMy+AAAAAAAAAADNzMw+\"},{\"byteLength\":96,\"name\":\"tex_coords_gate_MR\",\"uri\":\"data:application/octet-stream;base64,AADgPgAAAD8AAMA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+AAAQPwAA4D4AABA/AADgPgAAED8AAOA+AAAAPwAAwD4AABA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/\"},{\"byteLength\":96,\"name\":\"tex_coords_gate_M\",\"uri\":\"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/\"},{\"byteLength\":1560,\"name\":\"buf_scattered_lines\",\"uri\":\"data:application/octet-stream;base64,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\"},{\"byteLength\":360,\"name\":\"buf_red_scattered_lines\",\"uri\":\"data:application/octet-stream;base64,AAAAAAAA8MF3+BDCAABAwAAA8MF3+BDCAAAgwAAA9MF3+BDCAABAwAAA8MF3+BDCAAAgwAAA7MF3+BDCAABAwAAA8MF3+BDCAAD4wAAA+MEAAPjBAAD4wAAA+MHu8CXCAAD4wAAA+MEAAPjBAAD4wO7wJcIAAPjBAAD4wAAA+MEAAPjBAAB0wQAA+MEAAPjBAAD4wAAA+MHu8CXCAAD4wO7wJcLu8CXCAAD4wAAA+MHu8CXCAAB0wQAA+MHu8CXCAAD4wO7wJcIAAPjBAAD4wO7wJcLu8CXCAAD4wO7wJcIAAPjBAAB0we7wJcIAAPjBAAD4wO7wJcLu8CXCAAB0we7wJcLu8CXCAAB0wQAA+MEAAPjBAAB0wQAA+MHu8CXCAAB0wQAA+MEAAPjBAAB0we7wJcIAAPjBAAB0wQAA+MHu8CXCAAB0we7wJcLu8CXCAAB0we7wJcIAAPjBAAB0we7wJcLu8CXC\"}],\"images\":[{\"uri\":\"data:image/png;base64,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\"}],\"materials\":[{\"doubleSided\":false,\"pbrMetallicRoughness\":{\"baseColorFactor\":[1,1,1,1],\"baseColorTexture\":{\"index\":0,\"texCoord\":0},\"metallicFactor\":0.4,\"roughnessFactor\":0.5}},{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[0,0,0,1],\"metallicFactor\":1,\"roughnessFactor\":1}},{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[1,1,1,1],\"metallicFactor\":0.4,\"roughnessFactor\":0.5}},{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[0,0,0,1],\"metallicFactor\":1,\"roughnessFactor\":1}},{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[0,0,0,1],\"metallicFactor\":1,\"roughnessFactor\":1}},{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[1,0,0,1],\"metallicFactor\":1,\"roughnessFactor\":1}}],\"meshes\":[{\"primitives\":[{\"attributes\":{\"POSITION\":0,\"TEXCOORD_0\":1},\"material\":0,\"mode\":4}]},{\"primitives\":[{\"attributes\":{\"POSITION\":0,\"TEXCOORD_0\":2},\"material\":0,\"mode\":4}]},{\"primitives\":[{\"attributes\":{\"POSITION\":3},\"material\":1,\"mode\":6}]},{\"primitives\":[{\"attributes\":{\"POSITION\":4},\"material\":2,\"mode\":6},{\"attributes\":{\"POSITION\":4},\"material\":3,\"mode\":3},{\"attributes\":{\"POSITION\":5},\"material\":3,\"mode\":1}]},{\"primitives\":[{\"attributes\":{\"POSITION\":0,\"TEXCOORD_0\":6},\"material\":0,\"mode\":4}]},{\"primitives\":[{\"attributes\":{\"POSITION\":0,\"TEXCOORD_0\":7},\"material\":0,\"mode\":4}]},{\"primitives\":[{\"attributes\":{\"POSITION\":8},\"material\":4,\"mode\":1}]},{\"primitives\":[{\"attributes\":{\"POSITION\":9},\"material\":5,\"mode\":1}]}],\"nodes\":[{\"mesh\":0,\"translation\":[-0,-33.4142,-33.4142]},{\"mesh\":0,\"translation\":[-0,-36.2426,-33.4142]},{\"mesh\":0,\"translation\":[-0,-39.0711,-33.4142]},{\"mesh\":0,\"translation\":[-0,-33.4142,-36.2426]},{\"mesh\":0,\"translation\":[-0,-36.2426,-36.2426]},{\"mesh\":0,\"translation\":[-0,-39.0711,-36.2426]},{\"mesh\":0,\"translation\":[-0,-33.4142,-39.0711]},{\"mesh\":0,\"translation\":[-0,-36.2426,-39.0711]},{\"mesh\":0,\"translation\":[-0,-39.0711,-39.0711]},{\"mesh\":0,\"translation\":[-0,-34.8284,-32]},{\"mesh\":0,\"translation\":[-0,-34.8284,-34.8284]},{\"mesh\":0,\"translation\":[-0,-37.6569,-34.8284]},{\"mesh\":0,\"translation\":[-0,-40.4853,-34.8284]},{\"mesh\":0,\"translation\":[-0,-32,-37.6569]},{\"mesh\":0,\"translation\":[-0,-34.8284,-37.6569]},{\"mesh\":0,\"translation\":[-0,-37.6569,-37.6569]},{\"mesh\":0,\"translation\":[-0,-37.6569,-40.4853]},{\"mesh\":1,\"translation\":[-1,-34.8284,-32]},{\"mesh\":1,\"translation\":[-1,-37.6569,-34.8284]},{\"mesh\":1,\"translation\":[-1,-34.8284,-37.6569]},{\"mesh\":1,\"translation\":[-1,-37.6569,-40.4853]},{\"mesh\":2,\"translation\":[-2,-34.8284,-32]},{\"mesh\":3,\"translation\":[-2,-36.2426,-33.4142]},{\"mesh\":2,\"translation\":[-2,-34.8284,-37.6569]},{\"mesh\":3,\"translation\":[-2,-36.2426,-39.0711]},{\"mesh\":2,\"translation\":[-2,-37.6569,-34.8284]},{\"mesh\":3,\"translation\":[-2,-39.0711,-36.2426]},{\"mesh\":2,\"translation\":[-2,-33.4142,-39.0711]},{\"mesh\":3,\"translation\":[-2,-32,-37.6569]},{\"mesh\":2,\"translation\":[-2,-36.2426,-36.2426]},{\"mesh\":3,\"translation\":[-2,-34.8284,-34.8284]},{\"mesh\":2,\"translation\":[-2,-39.0711,-39.0711]},{\"mesh\":3,\"translation\":[-2,-37.6569,-37.6569]},{\"mesh\":2,\"translation\":[-3,-34.8284,-32]},{\"mesh\":3,\"translation\":[-3,-33.4142,-33.4142]},{\"mesh\":2,\"translation\":[-3,-34.8284,-37.6569]},{\"mesh\":3,\"translation\":[-3,-33.4142,-39.0711]},{\"mesh\":2,\"translation\":[-3,-37.6569,-34.8284]},{\"mesh\":3,\"translation\":[-3,-36.2426,-36.2426]},{\"mesh\":2,\"translation\":[-3,-33.4142,-36.2426]},{\"mesh\":3,\"translation\":[-3,-32,-37.6569]},{\"mesh\":2,\"translation\":[-3,-36.2426,-33.4142]},{\"mesh\":3,\"translation\":[-3,-34.8284,-34.8284]},{\"mesh\":2,\"translation\":[-3,-39.0711,-36.2426]},{\"mesh\":3,\"translation\":[-3,-37.6569,-37.6569]},{\"mesh\":2,\"translation\":[-4,-34.8284,-37.6569]},{\"mesh\":3,\"translation\":[-4,-36.2426,-36.2426]},{\"mesh\":2,\"translation\":[-4,-37.6569,-34.8284]},{\"mesh\":3,\"translation\":[-4,-39.0711,-33.4142]},{\"mesh\":2,\"translation\":[-4,-37.6569,-40.4853]},{\"mesh\":3,\"translation\":[-4,-39.0711,-39.0711]},{\"mesh\":2,\"translation\":[-4,-33.4142,-36.2426]},{\"mesh\":3,\"translation\":[-4,-34.8284,-34.8284]},{\"mesh\":2,\"translation\":[-4,-36.2426,-39.0711]},{\"mesh\":3,\"translation\":[-4,-37.6569,-37.6569]},{\"mesh\":2,\"translation\":[-4,-39.0711,-36.2426]},{\"mesh\":3,\"translation\":[-4,-40.4853,-34.8284]},{\"mesh\":2,\"translation\":[-5,-34.8284,-37.6569]},{\"mesh\":3,\"translation\":[-5,-33.4142,-36.2426]},{\"mesh\":2,\"translation\":[-5,-37.6569,-34.8284]},{\"mesh\":3,\"translation\":[-5,-36.2426,-33.4142]},{\"mesh\":2,\"translation\":[-5,-37.6569,-40.4853]},{\"mesh\":3,\"translation\":[-5,-36.2426,-39.0711]},{\"mesh\":2,\"translation\":[-5,-33.4142,-33.4142]},{\"mesh\":3,\"translation\":[-5,-34.8284,-34.8284]},{\"mesh\":2,\"translation\":[-5,-36.2426,-36.2426]},{\"mesh\":3,\"translation\":[-5,-37.6569,-37.6569]},{\"mesh\":2,\"translation\":[-5,-39.0711,-33.4142]},{\"mesh\":3,\"translation\":[-5,-40.4853,-34.8284]},{\"mesh\":1,\"translation\":[-6,-34.8284,-32]},{\"mesh\":1,\"translation\":[-6,-37.6569,-34.8284]},{\"mesh\":1,\"translation\":[-6,-34.8284,-37.6569]},{\"mesh\":1,\"translation\":[-6,-37.6569,-40.4853]},{\"mesh\":4,\"translation\":[-7,-34.8284,-32]},{\"mesh\":4,\"translation\":[-7,-34.8284,-34.8284]},{\"mesh\":4,\"translation\":[-7,-37.6569,-34.8284]},{\"mesh\":4,\"translation\":[-7,-40.4853,-34.8284]},{\"mesh\":4,\"translation\":[-7,-32,-37.6569]},{\"mesh\":4,\"translation\":[-7,-34.8284,-37.6569]},{\"mesh\":4,\"translation\":[-7,-37.6569,-37.6569]},{\"mesh\":4,\"translation\":[-7,-37.6569,-40.4853]},{\"mesh\":1,\"translation\":[-9,-34.8284,-32]},{\"mesh\":1,\"translation\":[-9,-37.6569,-34.8284]},{\"mesh\":1,\"translation\":[-9,-34.8284,-37.6569]},{\"mesh\":1,\"translation\":[-9,-37.6569,-40.4853]},{\"mesh\":2,\"translation\":[-10,-34.8284,-32]},{\"mesh\":3,\"translation\":[-10,-36.2426,-33.4142]},{\"mesh\":2,\"translation\":[-10,-34.8284,-37.6569]},{\"mesh\":3,\"translation\":[-10,-36.2426,-39.0711]},{\"mesh\":2,\"translation\":[-10,-37.6569,-34.8284]},{\"mesh\":3,\"translation\":[-10,-39.0711,-36.2426]},{\"mesh\":2,\"translation\":[-10,-33.4142,-39.0711]},{\"mesh\":3,\"translation\":[-10,-32,-37.6569]},{\"mesh\":2,\"translation\":[-10,-36.2426,-36.2426]},{\"mesh\":3,\"translation\":[-10,-34.8284,-34.8284]},{\"mesh\":2,\"translation\":[-10,-39.0711,-39.0711]},{\"mesh\":3,\"translation\":[-10,-37.6569,-37.6569]},{\"mesh\":2,\"translation\":[-11,-34.8284,-32]},{\"mesh\":3,\"translation\":[-11,-33.4142,-33.4142]},{\"mesh\":2,\"translation\":[-11,-34.8284,-37.6569]},{\"mesh\":3,\"translation\":[-11,-33.4142,-39.0711]},{\"mesh\":2,\"translation\":[-11,-37.6569,-34.8284]},{\"mesh\":3,\"translation\":[-11,-36.2426,-36.2426]},{\"mesh\":2,\"translation\":[-11,-33.4142,-36.2426]},{\"mesh\":3,\"translation\":[-11,-32,-37.6569]},{\"mesh\":2,\"translation\":[-11,-36.2426,-33.4142]},{\"mesh\":3,\"translation\":[-11,-34.8284,-34.8284]},{\"mesh\":2,\"translation\":[-11,-39.0711,-36.2426]},{\"mesh\":3,\"translation\":[-11,-37.6569,-37.6569]},{\"mesh\":2,\"translation\":[-12,-34.8284,-37.6569]},{\"mesh\":3,\"translation\":[-12,-36.2426,-36.2426]},{\"mesh\":2,\"translation\":[-12,-37.6569,-34.8284]},{\"mesh\":3,\"translation\":[-12,-39.0711,-33.4142]},{\"mesh\":2,\"translation\":[-12,-37.6569,-40.4853]},{\"mesh\":3,\"translation\":[-12,-39.0711,-39.0711]},{\"mesh\":2,\"translation\":[-12,-33.4142,-36.2426]},{\"mesh\":3,\"translation\":[-12,-34.8284,-34.8284]},{\"mesh\":2,\"translation\":[-12,-36.2426,-39.0711]},{\"mesh\":3,\"translation\":[-12,-37.6569,-37.6569]},{\"mesh\":2,\"translation\":[-12,-39.0711,-36.2426]},{\"mesh\":3,\"translation\":[-12,-40.4853,-34.8284]},{\"mesh\":2,\"translation\":[-13,-34.8284,-37.6569]},{\"mesh\":3,\"translation\":[-13,-33.4142,-36.2426]},{\"mesh\":2,\"translation\":[-13,-37.6569,-34.8284]},{\"mesh\":3,\"translation\":[-13,-36.2426,-33.4142]},{\"mesh\":2,\"translation\":[-13,-37.6569,-40.4853]},{\"mesh\":3,\"translation\":[-13,-36.2426,-39.0711]},{\"mesh\":2,\"translation\":[-13,-33.4142,-33.4142]},{\"mesh\":3,\"translation\":[-13,-34.8284,-34.8284]},{\"mesh\":2,\"translation\":[-13,-36.2426,-36.2426]},{\"mesh\":3,\"translation\":[-13,-37.6569,-37.6569]},{\"mesh\":2,\"translation\":[-13,-39.0711,-33.4142]},{\"mesh\":3,\"translation\":[-13,-40.4853,-34.8284]},{\"mesh\":1,\"translation\":[-14,-34.8284,-32]},{\"mesh\":1,\"translation\":[-14,-37.6569,-34.8284]},{\"mesh\":1,\"translation\":[-14,-34.8284,-37.6569]},{\"mesh\":1,\"translation\":[-14,-37.6569,-40.4853]},{\"mesh\":4,\"translation\":[-15,-34.8284,-32]},{\"mesh\":4,\"translation\":[-15,-34.8284,-34.8284]},{\"mesh\":4,\"translation\":[-15,-37.6569,-34.8284]},{\"mesh\":4,\"translation\":[-15,-40.4853,-34.8284]},{\"mesh\":4,\"translation\":[-15,-32,-37.6569]},{\"mesh\":4,\"translation\":[-15,-34.8284,-37.6569]},{\"mesh\":4,\"translation\":[-15,-37.6569,-37.6569]},{\"mesh\":4,\"translation\":[-15,-37.6569,-40.4853]},{\"mesh\":5,\"translation\":[-16,-33.4142,-33.4142]},{\"mesh\":5,\"translation\":[-16,-36.2426,-33.4142]},{\"mesh\":5,\"translation\":[-16,-39.0711,-33.4142]},{\"mesh\":5,\"translation\":[-16,-33.4142,-36.2426]},{\"mesh\":5,\"translation\":[-16,-36.2426,-36.2426]},{\"mesh\":5,\"translation\":[-16,-39.0711,-36.2426]},{\"mesh\":5,\"translation\":[-16,-33.4142,-39.0711]},{\"mesh\":5,\"translation\":[-16,-36.2426,-39.0711]},{\"mesh\":5,\"translation\":[-16,-39.0711,-39.0711]},{\"mesh\":6,\"translation\":[0,0,0]},{\"mesh\":7,\"translation\":[0,0,0]}],\"samplers\":[{\"magFilter\":9728,\"minFilter\":9728,\"wrapS\":33071,\"wrapT\":33071}],\"scene\":0,\"scenes\":[{\"nodes\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155]}],\"textures\":[{\"sampler\":0,\"source\":0}]}" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "surface_code_circuit.without_noise().diagram(\"timeline-3d\")\n", "\n", "# Note: if you are viewing this notebook on GitHub, the 3d model viewer is likely blocked.\n", "# To view the 3d model, run this notebook locally or upload it to https://colab.google.com/\n", "# GLTF files can be viewed directly in online viewers such as https://gltf-viewer.donmccurdy.com/\n", "\n", "# The 3d viewer is interactive, try clicking and dragging!" ] }, { "cell_type": "markdown", "metadata": { "id": "Hgh1I4Fefztj" }, "source": [ "Yet another useful type of diagram, for understanding the structure of this circuit, is a \"detector slice diagram\".\n", "A detslice diagram shows how the stabilizers checked by the circuit's detectors change over time.\n", "If you look carefully, you can see that, halfway through the measurement cycle of the surface code, its state is actually temporarily an even larger surface code!\n", "You can also see the stabilizers establish themselves at the beginning of the circuit, and drain away at the end." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 373 }, "id": "BUFs-tWlfx_U", "outputId": "cd940223-754c-4841-f168-fff8be898166" }, "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", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\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", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\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": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "surface_code_circuit.diagram(\"detslice-svg\")" ] }, { "cell_type": "markdown", "metadata": { "id": "pWcO6j68-qOW" }, "source": [ "There is also the diagram type `detslice-with-ops-svg`, which overlays the time slice and detslice diagrams. For example, `detslice-with-ops-svg` shows how the first round gradually projects the system into the surface code state." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "aRPHBfk--qOW", "outputId": "759ce9ba-6f21-4618-ee24-f31e5607ba0c" }, "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", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\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", "\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", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "R\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "MR\n", "\n", "H\n", "\n", "H\n", "\n", "H\n", "\n", "H\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", "\n", "" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "surface_code_circuit.without_noise().diagram(\n", " \"detslice-with-ops-svg\",\n", " tick=range(0, 9),\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "K75kGz3IWYAW" }, "source": [ "Notice that when you created this surface code circuit, you specified a lot more error parameters.\n", "These parameters are adding full circuit noise, instead of just phenomenological noise.\n", "Because the noise is richer, and because this is a quantum code instead of a classical code, the decoding problem is much harder and threshold is going to be noticeably lower.\n", "Looking at the match graph, you can see `pymatching` has a much more complicated problem to solve than before!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 404 }, "id": "VWqbGm3ygkkp", "outputId": "d95d3bed-f7bf-4925-a0dc-2fecc2fb8e91" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "{\"accessors\":[{\"bufferView\":0,\"byteOffset\":0,\"componentType\":5126,\"count\":9,\"max\":[0,0.400000005960464,0.400000005960464],\"min\":[0,-0.400000005960464,-0.400000005960464],\"name\":\"circle_loop\",\"type\":\"VEC3\"},{\"bufferView\":1,\"byteOffset\":0,\"componentType\":5126,\"count\":9,\"max\":[0.400000005960464,0,0.400000005960464],\"min\":[-0.400000005960464,0,-0.400000005960464],\"name\":\"circle_loop\",\"type\":\"VEC3\"},{\"bufferView\":2,\"byteOffset\":0,\"componentType\":5126,\"count\":9,\"max\":[0.400000005960464,0.400000005960464,0],\"min\":[-0.400000005960464,-0.400000005960464,0],\"name\":\"circle_loop\",\"type\":\"VEC3\"},{\"bufferView\":3,\"byteOffset\":0,\"componentType\":5126,\"count\":9,\"max\":[0,0.400000005960464,0.400000005960464],\"min\":[0,-0.400000005960464,-0.400000005960464],\"name\":\"circle_loop\",\"type\":\"VEC3\"},{\"bufferView\":4,\"byteOffset\":0,\"componentType\":5126,\"count\":9,\"max\":[0.400000005960464,0,0.400000005960464],\"min\":[-0.400000005960464,0,-0.400000005960464],\"name\":\"circle_loop\",\"type\":\"VEC3\"},{\"bufferView\":5,\"byteOffset\":0,\"componentType\":5126,\"count\":9,\"max\":[0.400000005960464,0.400000005960464,0],\"min\":[-0.400000005960464,-0.400000005960464,0],\"name\":\"circle_loop\",\"type\":\"VEC3\"},{\"bufferView\":6,\"byteOffset\":0,\"componentType\":5126,\"count\":4102,\"max\":[18.6666679382324,27.3704261779785,36.539981842041],\"min\":[-9.3704252243042,-9.3704252243042,-9.5399808883667],\"name\":\"buf_scattered_lines\",\"type\":\"VEC3\"},{\"bufferView\":7,\"byteOffset\":0,\"componentType\":5126,\"count\":718,\"max\":[27.3704261779785,6,36.539981842041],\"min\":[-0.666666984558105,-0.666666984558105,-9.5399808883667],\"name\":\"buf_red_scattered_lines\",\"type\":\"VEC3\"}],\"asset\":{\"version\":\"2.0\"},\"bufferViews\":[{\"buffer\":0,\"byteLength\":108,\"byteOffset\":0,\"name\":\"circle_loop\",\"target\":34962},{\"buffer\":1,\"byteLength\":108,\"byteOffset\":0,\"name\":\"circle_loop\",\"target\":34962},{\"buffer\":2,\"byteLength\":108,\"byteOffset\":0,\"name\":\"circle_loop\",\"target\":34962},{\"buffer\":3,\"byteLength\":108,\"byteOffset\":0,\"name\":\"circle_loop\",\"target\":34962},{\"buffer\":4,\"byteLength\":108,\"byteOffset\":0,\"name\":\"circle_loop\",\"target\":34962},{\"buffer\":5,\"byteLength\":108,\"byteOffset\":0,\"name\":\"circle_loop\",\"target\":34962},{\"buffer\":6,\"byteLength\":49224,\"byteOffset\":0,\"name\":\"buf_scattered_lines\",\"target\":34962},{\"buffer\":7,\"byteLength\":8616,\"byteOffset\":0,\"name\":\"buf_red_scattered_lines\",\"target\":34962}],\"buffers\":[{\"byteLength\":108,\"name\":\"circle_loop\",\"uri\":\"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAMPQkD7D0JA+AAAAAPIwlrLNzMw+AAAAAMPQkL7D0JA+AAAAAM3MzL7yMBazAAAAAMHQkL7E0JC+AAAAAPLkozHNzMy+AAAAAMbQkD6/0JC+AAAAAM3MzD4AAAAA\"},{\"byteLength\":108,\"name\":\"circle_loop\",\"uri\":\"data:application/octet-stream;base64,AAAAAAAAAADNzMw+w9CQPgAAAADD0JA+zczMPgAAAADyMJayw9CQPgAAAADD0JC+8jAWswAAAADNzMy+xNCQvgAAAADB0JC+zczMvgAAAADy5KMxv9CQvgAAAADG0JA+AAAAAAAAAADNzMw+\"},{\"byteLength\":108,\"name\":\"circle_loop\",\"uri\":\"data:application/octet-stream;base64,zczMPgAAAAAAAAAAw9CQPsPQkD4AAAAA8jCWss3MzD4AAAAAw9CQvsPQkD4AAAAAzczMvvIwFrMAAAAAwdCQvsTQkL4AAAAA8uSjMc3MzL4AAAAAxtCQPr/QkL4AAAAAzczMPgAAAAAAAAAA\"},{\"byteLength\":108,\"name\":\"circle_loop\",\"uri\":\"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAMPQkD7D0JA+AAAAAPIwlrLNzMw+AAAAAMPQkL7D0JA+AAAAAM3MzL7yMBazAAAAAMHQkL7E0JC+AAAAAPLkozHNzMy+AAAAAMbQkD6/0JC+AAAAAM3MzD4AAAAA\"},{\"byteLength\":108,\"name\":\"circle_loop\",\"uri\":\"data:application/octet-stream;base64,AAAAAAAAAADNzMw+w9CQPgAAAADD0JA+zczMPgAAAADyMJayw9CQPgAAAADD0JC+8jAWswAAAADNzMy+xNCQvgAAAADB0JC+zczMvgAAAADy5KMxv9CQvgAAAADG0JA+AAAAAAAAAADNzMw+\"},{\"byteLength\":108,\"name\":\"circle_loop\",\"uri\":\"data:application/octet-stream;base64,zczMPgAAAAAAAAAAw9CQPsPQkD4AAAAA8jCWss3MzD4AAAAAw9CQvsPQkD4AAAAAzczMvvIwFrMAAAAAwdCQvsTQkL4AAAAA8uSjMc3MzL4AAAAAxtCQPr/QkL4AAAAAzczMPgAAAAAAAAAA\"},{\"byteLength\":49224,\"name\":\"buf_scattered_lines\",\"uri\":\"data:application/octet-stream;base64,AAAAAAAAQEEAAAAAo4uuwEYXXUG66ALBAAAAAAAAQEEAAAAAAADAQAAAwEAAAAAAAAAAAAAAQEEAAAAAAAAAAAAAQEEAAEBAAADAQAAAwEAAAAAAAABAQQAAQEEAAAAAAADAQAAAwEAAAAAAAADAQAAAwEAAAEBAAADAQAAAwEAAAAAAAADAQAAAwEAAAEBAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAADAQAAAwEAAAAAAAAAAAAAAQEEAAEBAAADAQAAAwEAAAAAAAAAAAAAAQEEAAEBAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAABAQQAAQEEAAAAAAACQQQAAwEAAAAAAAABAQQAAQEEAAAAAAACQQQAAwEAAAAAAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAAADAQAAAwEAAAEBAAABAQQAAQEEAAAAAAADAQAAAwEAAAEBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAAADAQAAAwEAAAEBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAABAQQAAQEEAAAAAAADAQAAAwEAAAEBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAQEEAAAAAAADAQAAAwEAAAEBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAAAAAAAAAQEEAAEBAAABAQQAAQEEAAAAAAAAAAAAAQEEAAEBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAAAAAAAAAQEEAAEBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAQEEAAAAAAAAAAAAAQEEAAEBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAAABAQQAAQEEAAEBAAABAQQAAQEEAAAAAAABAQQAAQEEAAEBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAAABAQQAAQEEAAEBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAACQQQAAwEAAAAAAAADAQAAAwEAAAEBAAACQQQAAwEAAAAAAAADAQAAAwEAAAEBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAACQQQAAwEAAAAAAAACQQQAAwEAAAEBAAACQQQAAwEAAAAAAAACQQQAAwEAAAEBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAACQQQAAwEAAAAAAAABAQQAAQEEAAEBAAACQQQAAwEAAAAAAAABAQQAAQEEAAEBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAADAQAAAAAAAAEBAAADAQAAAAAAAAMBAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAADAQAAAwEAAAEBAAAAAAAAAQEEAAEBAAADAQAAAwEAAAEBAAAAAAAAAQEEAAEBAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAADAQAAAwEAAAEBAAAAAAAAAQEEAAEBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAADAQAAAwEAAAEBAAAAAAAAAQEEAAEBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAADAQAAAwEAAAEBAAAAAAAAAQEEAAEBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAADAQAAAwEAAAEBAAABAQQAAQEEAAEBAAADAQAAAwEAAAEBAAABAQQAAQEEAAEBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAADAQAAAwEAAAEBAAADAQAAAwEAAAMBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAABAQQAAwEAAAEBAAABAQQAAwEAAAMBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAACQQQAAwEAAAEBAAABAQQAAQEEAAEBAAACQQQAAwEAAAEBAAABAQQAAQEEAAEBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAACQQQAAwEAAAEBAAACQQQAAwEAAAMBAAAAAAAAAQEEAAEBABIXLwIHrYUGGcI3AAAAAAAAAQEEAAEBAAAAAAAAAQEEAAMBAAAAAAAAAQEEAAEBABIXLwIHrYUGGcI3AAAAAAAAAQEEAAAAAo4uuwEYXXUG66ALBAAAAAAAAQEEAAEBABIXLwIHrYUGGcI3AAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAADAQAAAQEEAAEBAunVWQJJiakH8scjAAADAQAAAQEEAAEBAAADAQAAAQEEAAMBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAADAQAAAQEEAAEBAunVWQJJiakH8scjAAAAAAAAAQEEAAEBABIXLwIHrYUGGcI3AAADAQAAAQEEAAEBAunVWQJJiakH8scjAAAAAAAAAQEEAAEBABIXLwIHrYUGGcI3AAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAQEEAAEBAAABAQQAAQEEAAMBAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAQEEAAAAAgethQYHrYUHDoxjBAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAABAQQAAkEEAAEBAAABAQQAAkEEAAMBAAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAADAQAAAQEEAAEBAunVWQJJiakH8scjAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAADAQAAAAAAAAEBAAABAQQAAwEAAAEBAAADAQAAAAAAAAEBAAABAQQAAwEAAAEBAAADAQAAAAAAAAEBAAADAQAAAAAAAAMBAAADAQAAAAAAAAEBAAADAQAAAAAAAAMBAAADAQAAAAAAAAEBA/VF4QASFy8CGcI3AAADAQAAAwEAAAEBAAAAAAAAAQEEAAEBAAADAQAAAwEAAAEBAAAAAAAAAQEEAAEBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAADAQAAAwEAAAEBAAABAQQAAQEEAAEBAAADAQAAAwEAAAEBAAABAQQAAQEEAAEBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAADAQAAAwEAAAEBAAADAQAAAwEAAAMBAAADAQAAAwEAAAEBAAADAQAAAwEAAAMBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAADAQAAAwEAAAEBAAADAQAAAwEAAAMBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAADAQAAAwEAAAEBAAADAQAAAwEAAAMBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAADAQAAAAAAAAMBATDxhQI0l7sAglE6+AADAQAAAwEAAAEBAAADAQAAAwEAAAMBAAADAQAAAAAAAAMBATDxhQI0l7sAglE6+AADAQAAAwEAAAEBAAAAAAAAAQEEAAMBAAADAQAAAwEAAAEBAAAAAAAAAQEEAAMBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAADAQAAAwEAAAEBAAAAAAAAAQEEAAMBAAADAQAAAQEEAAEBAunVWQJJiakH8scjAAADAQAAAAAAAAMBATDxhQI0l7sAglE6+AADAQAAAwEAAAEBAAAAAAAAAQEEAAMBAAADAQAAAAAAAAMBATDxhQI0l7sAglE6+AABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAACQQQAAwEAAAEBAAABAQQAAQEEAAEBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAACQQQAAwEAAAEBAAADAQAAAwEAAAMBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAABAQQAAQEEAAEBAAADAQAAAwEAAAMBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAABAQQAAwEAAAEBAAADAQAAAQEEAAEBAAABAQQAAwEAAAEBAAADAQAAAAAAAAMBAAABAQQAAwEAAAEBAAADAQAAAAAAAAMBAAABAQQAAwEAAAEBAAADAQAAAAAAAAMBAAABAQQAAwEAAAEBAAADAQAAAAAAAAMBAAABAQQAAwEAAAEBAAABAQQAAwEAAAMBAAABAQQAAwEAAAEBAAABAQQAAwEAAAMBAAACQQQAAwEAAAEBAAABAQQAAQEEAAEBAAABAQQAAwEAAAEBAAABAQQAAwEAAAMBAAACQQQAAwEAAAEBAAADAQAAAwEAAAMBAAABAQQAAwEAAAEBAAABAQQAAwEAAAMBAAABAQQAAQEEAAEBAAADAQAAAwEAAAMBAAABAQQAAwEAAAEBAAABAQQAAwEAAAMBAAABAQQAAwEAAAEBAkmJqQbp1VkD8scjAAACQQQAAwEAAAEBA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AAAAAAAAQEEAAMBAAABAQQAAQEEAAEBAAAAAAAAAQEEAAMBAAABAQQAAwEAAAMBAAADAQAAAQEEAAMBAAABAQQAAQEEAAEBAAABAQQAAQEEAAMBAAABAQQAAQEEAAEBAAABAQQAAQEEAAMBAAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAABAQQAAQEEAAEBAAABAQQAAQEEAAMBAAABAQQAAwEAAAMBAcbR3QT0uIUBoDC3AAABAQQAAQEEAAEBAAABAQQAAQEEAAMBAAABAQQAAwEAAAMBAAADAQAAAQEEAAMBAAABAQQAAQEEAAEBAAABAQQAAQEEAAMBAAABAQQAAwEAAAMBAcbR3QT0uIUBoDC3AAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAABAQQAAkEEAAEBAAABAQQAAwEAAAMBAAABAQQAAkEEAAEBAAABAQQAAwEAAAMBAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAkEEAAEBAAABAQQAAwEAAAMBAAABAQQAAQEEAAMBAcbR3QXG0d0FoDC3AAABAQQAAkEEAAEBAAABAQQAAwEAAAMBAAABAQQAAQEEAAMBAcbR3QXG0d0FoDC3AAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAkEEAAEBAAADAQAAAQEEAAMBAAABAQQAAkEEAAEBAAADAQAAAQEEAAMBAAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAkEEAAEBAAADAQAAAQEEAAMBAAABAQQAAQEEAAMBAcbR3QXG0d0FoDC3AAABAQQAAkEEAAEBAAADAQAAAQEEAAMBAAABAQQAAQEEAAMBAcbR3QXG0d0FoDC3AAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAkEEAAEBAAABAQQAAkEEAAMBAAABAQQAAkEEAAEBAAABAQQAAkEEAAMBAAABAQQAAQEEAAMBAcbR3QXG0d0FoDC3AAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAABAQQAAQEEAAEBAkmJqQZJiakH8scjAAABAQQAAkEEAAEBAgethQUHhwkGGcI3AAABAQQAAQEEAAMBAcbR3QXG0d0FoDC3AAABAQQAAkEEAAEBAgethQUHhwkGGcI3A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AADAQAAAwEAAABBBAAAAAAAAQEEAAEBBAADAQAAAQEEAABBB3BuTP0LOhkHIqdw/AADAQAAAAAAAAEBB/Bg4QEPtFcEgAydBAADAQAAAwEAAABBBAAAAAAAAQEEAAEBBAADAQAAAAAAAAEBB/Bg4QEPtFcEgAydBAABAQQAAwEAAABBBQs6GQdwbkz/Iqdw/AABAQQAAwEAAABBBAADAQAAAQEEAABBBAABAQQAAwEAAABBBAADAQAAAQEEAABBBAABAQQAAwEAAABBBAADAQAAAQEEAABBBAACQQQAAwEAAABBBAABAQQAAQEEAABBBAABAQQAAwEAAABBBAADAQAAAQEEAABBBAACQQQAAwEAAABBBAADAQAAAwEAAAEBBAABAQQAAwEAAABBBAADAQAAAQEEAABBBAABAQQAAQEEAABBBAADAQAAAwEAAAEBBAABAQQAAwEAAABBBAADAQAAAQEEAABBBAABAQQAAwEAAABBBAADAQAAAQEEAABBBAABAQQAAwEAAABBBAADAQAAAAAAAAEBBAABAQQAAwEAAABBBAADAQAAAAAAAAEBBAABAQQAAwEAAABBBAADAQAAAAAAAAEBBAABAQQAAwEAAABBBAADAQAAAAAAAAEBBAABAQQAAwEAAABBBAABAQQAAwEAAAEBBAABAQQAAwEAAABBBAABAQQAAwEAAAEBBAACQQQAAwEAAABBBAABAQQAAQEEAABBBAABAQQAAwEAAABBBAABAQQAAwEAAAEBBAACQQQAAwEAAABBBAADAQAAAwEAAAEBBAABAQQAAwEAAABBBAABAQQAAwEAAAEBBAABAQQAAQEEAABBBAADAQAAAwEAAAEBBAABAQQAAwEAAABBBAABAQQAAwEAAAEBBAABAQQAAwEAAABBBQs6GQdwbkz/Iqdw/AACQQQAAwEAAABBBAABAQQAAQEEAABBBAACQQQAAwEAAABBBAABAQQAAQEEAABBBAABAQQAAwEAAABBBQs6GQdwbkz/Iqdw/AACQQQAAwEAAABBBAABAQQAAQEEAABBBAADAQAAAQEEAABBBAABAQQAAwEAAAEBBAACQQQAAwEAAABBBAABAQQAAQEEAABBBAABAQQAAkEEAABBB3LZtQUmS1EFu25ZAAACQQQAAwEAAABBBAABAQQAAQEEAABBBAABAQQAAwEAAAEBB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AABAQQAAQEEAAHBBAADAQAAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAADAQAAAwEAAAHBBAADAQAAAwEAAAJBBAABAQQAAwEAAAEBBVlWVQbCqKr+qqgpBAABAQQAAwEAAAEBBVlWVQbCqKr+qqgpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAEBBVlWVQbCqKr+qqgpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAEBBVlWVQbCqKr+qqgpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAQEEAAEBBVlWVQVZVlUGqqgpBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAQEEAAEBBVlWVQVZVlUGqqgpBAABAQQAAwEAAAHBBAABAQQAAwEAAAJBBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAEBBVlWVQbCqKr+qqgpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAEBBVlWVQbCqKr+qqgpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAkEEAAEBBwflxQaL22kEgAydBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAACQQQAAwEAAAHBBAACQQQAAwEAAAJBBAAAAAAAAQEEAAHBBQ+0VwcH5cUFwfoRBAAAAAAAAQEEAAHBBAAAAAAAAQEEAAJBBAAAAAAAAQEEAAHBBQ+0VwcH5cUFwfoRBAAAAAAAAQEEAAEBBQ+0VwcH5cUEgAydBAAAAAAAAQEEAAHBBQ+0VwcH5cUFwfoRBAABAQQAAQEEAAEBBVlWVQVZVlUGqqgpBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAQEEAAHBBAABAQQAAkEEAAHBBAADAQAAAQEEAAHBBAABAQQAAkEEAAHBBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAADAQAAAQEEAAHBBAADAQAAAQEEAAJBBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAQEEAAEBBsKoqv1ZVlUGqqgpBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAQEEAAEBBsKoqv1ZVlUGqqgpBAABAQQAAQEEAAEBBVlWVQVZVlUGqqgpBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAQEEAAEBBsKoqv1ZVlUGqqgpBAAAAAAAAQEEAAHBBQ+0VwcH5cUFwfoRBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAQEEAAEBBsKoqv1ZVlUGqqgpBAAAAAAAAQEEAAHBBQ+0VwcH5cUFwfoRBAABAQQAAQEEAAEBBVlWVQVZVlUGqqgpBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAABAQQAAQEEAAEBBVlWVQVZVlUGqqgpBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAAAAAAAAQEEAAHBBQ+0VwcH5cUFwfoRBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAAAAAAAAQEEAAHBBQ+0VwcH5cUFwfoRBAABAQQAAQEEAAEBBVlWVQVZVlUGqqgpBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAQEEAAHBBAABAQQAAQEEAAJBBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAQEEAAEBBVlWVQVZVlUGqqgpBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAkEEAAEBBwflxQaL22kEgAydBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAkEEAAHBBwflxQaL22kFwfoRBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAkEEAAHBBwflxQaL22kFwfoRBAABAQQAAkEEAAEBBwflxQaL22kEgAydBAABAQQAAkEEAAHBBwflxQaL22kFwfoRBAABAQQAAkEEAAHBBAABAQQAAkEEAAJBBAABAQQAAkEEAAHBBwflxQaL22kFwfoRBAABAQQAAkEEAAEBBwflxQaL22kEgAydBAADAQAAAAAAAAHBB/Bg4QEPtFcFwfoRBAADAQAAAAAAAAHBBAABAQQAAwEAAAHBBAADAQAAAAAAAAHBBAABAQQAAwEAAAHBBAADAQAAAAAAAAHBBAADAQAAAAAAAAJBBAADAQAAAAAAAAHBBAADAQAAAAAAAAJBBAADAQAAAAAAAAHBB/Bg4QEPtFcFwfoRBAADAQAAAwEAAAHBBAAAAAAAAQEEAAHBBAADAQAAAwEAAAHBBAAAAAAAAQEEAAHBBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAwEAAAHBBAABAQQAAQEEAAHBBAADAQAAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAADAQAAAwEAAAHBBAADAQAAAwEAAAJBBAADAQAAAwEAAAHBBAADAQAAAwEAAAJBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAADAQAAAwEAAAHBBAADAQAAAwEAAAJBBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAwEAAAHBBAADAQAAAwEAAAJBBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAAAAAAJBBkiRJQJIkCcEkSbJBAADAQAAAwEAAAHBBAADAQAAAwEAAAJBBAADAQAAAAAAAAJBBkiRJQJIkCcEkSbJBAADAQAAAwEAAAHBBAAAAAAAAQEEAAJBBAADAQAAAwEAAAHBBAAAAAAAAQEEAAJBBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAwEAAAHBBAAAAAAAAQEEAAJBBAADAQAAAQEEAAHBBsKoqv1ZVlUGrqpJBAADAQAAAAAAAAJBBkiRJQJIkCcEkSbJBAADAQAAAwEAAAHBBAAAAAAAAQEEAAJBBAADAQAAAAAAAAJBBkiRJQJIkCcEkSbJBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAACQQQAAwEAAAHBBAADAQAAAwEAAAJBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAQEEAAHBBAADAQAAAwEAAAJBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAwEAAAHBBAADAQAAAQEEAAHBBAABAQQAAwEAAAHBBAADAQAAAAAAAAJBBAABAQQAAwEAAAHBBAADAQAAAAAAAAJBBAABAQQAAwEAAAHBBAADAQAAAAAAAAJBBAABAQQAAwEAAAHBBAADAQAAAAAAAAJBBAABAQQAAwEAAAHBBAABAQQAAwEAAAJBBAABAQQAAwEAAAHBBAABAQQAAwEAAAJBBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAHBBAABAQQAAwEAAAJBBAACQQQAAwEAAAHBBAADAQAAAwEAAAJBBAABAQQAAwEAAAHBBAABAQQAAwEAAAJBBAABAQQAAQEEAAHBBAADAQAAAwEAAAJBBAABAQQAAwEAAAHBBAABAQQAAwEAAAJBBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAADAQAAAQEEAAHBBAABAQQAAwEAAAJBBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAkEEAAHBBwflxQaL22kFwfoRBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAJBBQs6GQdwbkz9kNcpBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAJBBQs6GQdwbkz9kNcpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAACQQQAAwEAAAHBBAABAQQAAQEEAAHBBAABAQQAAwEAAAJBBQs6GQdwbkz9kNcpBAABAQQAAkEEAAHBBwflxQaL22kFwfoRBAACQQQAAwEAAAHBBAADAQAAAwEAAAJBBAACQQQAAwEAAAHBB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Qs6GQdwbkz9kNcpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAJBBQs6GQdwbkz9kNcpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAJBBQs6GQdwbkz9kNcpBAABAQQAAwEAAAJBBQs6GQdwbkz9kNcpBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAACQQQAAwEAAAJBBAABAQQAAQEEAAJBBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAACQQQAAwEAAAJBBAADAQAAAwEAAAKhBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAABAQQAAQEEAAJBBQs6GQULOhkFkNcpBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAABAQQAAQEEAAJBBAADAQAAAwEAAAKhBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAABAQQAAQEEAAJBBQs6GQULOhkFkNcpBAABAQQAAQEEAAHBBVlWVQVZVlUGrqpJBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAABAQQAAwEAAAJBBAADAQAAAQEEAAJBBAABAQQAAwEAAAJBBAADAQAAAAAAAAKhBAABAQQAAwEAAAJBBAADAQAAAAAAAAKhBAABAQQAAwEAAAJBBAADAQAAAAAAAAKhBAABAQQAAwEAAAJBBAADAQAAAAAAAAKhBAABAQQAAwEAAAJBBAABAQQAAwEAAAKhBAABAQQAAwEAAAJBBAABAQQAAwEAAAKhBAACQQQAAwEAAAJBBAABAQQAAQEEAAJBBAABAQQAAwEAAAJBBAABAQQAAwEAAAKhBAACQQQAAwEAAAJBBAADAQAAAwEAAAKhBAABAQQAAwEAAAJBBAABAQQAAwEAAAKhBAABAQQAAQEEAAJBBAADAQAAAwEAAAKhBAABAQQAAwEAAAJBBAABAQQAAwEAAAKhBAABAQQAAwEAAAJBBQs6GQdwbkz9kNcpBAABAQQAAwEAAAHBBVlWVQbCqKr+rqpJBAABAQQAAwEAAAJBBQs6GQdwbkz9kNcpB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AABAQQAAwEAAAKhBAADAQAAAAAAAAMBBAABAQQAAwEAAAKhBAADAQAAAAAAAAMBBAABAQQAAwEAAAKhBAADAQAAAAAAAAMBBAABAQQAAwEAAAKhBAADAQAAAAAAAAMBBAABAQQAAwEAAAKhBAABAQQAAwEAAAMBBAABAQQAAwEAAAKhBAABAQQAAwEAAAMBBAACQQQAAwEAAAKhBAABAQQAAQEEAAKhBAABAQQAAwEAAAKhBAABAQQAAwEAAAMBBAACQQQAAwEAAAKhBAADAQAAAwEAAAMBBAABAQQAAwEAAAKhBAABAQQAAwEAAAMBBAABAQQAAQEEAAKhBAADAQAAAwEAAAMBBAABAQQAAwEAAAKhBAABAQQAAwEAAAMBBAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAACQQQAAwEAAAKhBAABAQQAAQEEAAKhBAACQQQAAwEAAAKhBAABAQQAAQEEAAKhBAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAACQQQAAwEAAAKhBAABAQQAAQEEAAKhBAADAQAAAQEEAAKhBAABAQQAAwEAAAMBBAACQQQAAwEAAAKhBAABAQQAAQEEAAKhBAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAACQQQAAwEAAAKhBAABAQQAAQEEAAKhBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAACQQQAAwEAAAKhBAABAQQAAQEEAAKhBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAACQQQAAwEAAAKhBAABAQQAAQEEAAKhBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAACQQQAAwEAAAKhBAADAQAAAwEAAAMBBAACQQQAAwEAAAKhBAADAQAAAwEAAAMBBAADAQAAAQEEAAKhBAABAQQAAwEAAAMBBAACQQQAAwEAAAKhBAACQQQAAwEAAAMBBAACQQQAAwEAAAKhBAACQQQAAwEAAAMBBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAACQQQAAwEAAAKhBAABAQQAAQEEAAMBBAACQQQAAwEAAAKhBAABAQQAAQEEAAMBBAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAACQQQAAwEAAAKhBAABAQQAAQEEAAMBBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAACQQQAAwEAAAKhBAABAQQAAQEEAAMBBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAAAAAAAAQEEAAKhBjSXuwO2wZ0EondlBAAAAAAAAQEEAAKhBAAAAAAAAQEEAAMBBAAAAAAAAQEEAAKhBAAAAAAAAQEEAAMBBAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAADAQAAAQEEAAKhBAABAQQAAkEEAAKhBAADAQAAAQEEAAKhBAABAQQAAkEEAAKhBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAQEEAAKhBAADAQAAAAAAAAMBBAADAQAAAQEEAAKhBAADAQAAAAAAAAMBBAADAQAAAQEEAAKhBAADAQAAAAAAAAMBBAADAQAAAQEEAAKhBAADAQAAAAAAAAMBBAADAQAAAQEEAAKhBAABAQQAAwEAAAMBBAADAQAAAQEEAAKhBAABAQQAAwEAAAMBBAABAQQAAQEEAAKhBAADAQAAAwEAAAMBBAADAQAAAQEEAAKhBAABAQQAAwEAAAMBBAABAQQAAQEEAAKhBAAAAAAAAQEEAAMBBAADAQAAAQEEAAKhBAABAQQAAwEAAAMBBAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAADAQAAAQEEAAKhBAADAQAAAQEEAAMBBAADAQAAAQEEAAKhBAADAQAAAQEEAAMBBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAQEEAAKhBAADAQAAAQEEAAMBBAABAQQAAQEEAAKhBAADAQAAAwEAAAMBBAADAQAAAQEEAAKhBAADAQAAAQEEAAMBBAABAQQAAQEEAAKhBAAAAAAAAQEEAAMBBAADAQAAAQEEAAKhBAADAQAAAQEEAAMBBAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAAAAAAAAQEEAAKhBjSXuwO2wZ0EondlBAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAAAAAAAAQEEAAKhBjSXuwO2wZ0EondlBAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAQEEAAKhBAADAQAAAwEAAAMBBAABAQQAAQEEAAKhBAADAQAAAwEAAAMBBAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAABAQQAAQEEAAKhBAAAAAAAAQEEAAMBBAABAQQAAQEEAAKhBAAAAAAAAQEEAAMBBAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAABAQQAAQEEAAKhBAABAQQAAQEEAAMBBAABAQQAAQEEAAKhBAABAQQAAQEEAAMBBAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAQEEAAKhBAABAQQAAQEEAAMBBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAQEEAAKhBAABAQQAAQEEAAMBBAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAABAQQAAQEEAAKhBAABAQQAAQEEAAMBBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAkEEAAKhBAABAQQAAwEAAAMBBAABAQQAAkEEAAKhBAABAQQAAwEAAAMBBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAkEEAAKhBAABAQQAAwEAAAMBBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAKhBAABAQQAAwEAAAMBBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAkEEAAKhBAADAQAAAQEEAAMBBAABAQQAAkEEAAKhBAADAQAAAQEEAAMBBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAkEEAAKhBAADAQAAAQEEAAMBBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAKhBAADAQAAAQEEAAMBBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAkEEAAKhBAABAQQAAkEEAAMBBAABAQQAAkEEAAKhBAABAQQAAkEEAAMBBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAAAAAAMBBAABAQQAAwEAAAMBBAADAQAAAAAAAAMBBAABAQQAAwEAAAMBBAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAAAAAAKhBTDxhQI0l7sAondlBAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAAAAAAKhBTDxhQI0l7sAondlBAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAADAQAAAAAAAAMBB/VF4QASFy8AiXPtBAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAwEAAAMBBAAAAAAAAQEEAAMBBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAADAQAAAwEAAAMBBAABAQQAAQEEAAMBBAADAQAAAwEAAAMBBAABAQQAAQEEAAMBBAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAADAQAAAwEAAAMBBAAAAAAAAQEEAANhBAADAQAAAwEAAAMBBAAAAAAAAQEEAANhBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAwEAAAMBBAADAQAAAwEAAANhBAADAQAAAwEAAAMBBAADAQAAAwEAAANhBAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAADAQAAAwEAAAMBBAADAQAAAwEAAANhBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAABAQQAAQEEAAMBBAADAQAAAwEAAANhBAABAQQAAwEAAAMBBAADAQAAAQEEAAMBBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAKhBcbR3QT0uIUCNoe1BAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAACQQQAAwEAAAMBBAABAQQAAQEEAAMBBAACQQQAAwEAAAMBBAABAQQAAQEEAAMBBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAACQQQAAwEAAAMBBAABAQQAAQEEAAMBBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAACQQQAAwEAAAMBBAABAQQAAQEEAAMBBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAACQQQAAwEAAAMBBAABAQQAAQEEAAMBBAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAACQQQAAwEAAAMBBAADAQAAAwEAAANhBAACQQQAAwEAAAMBBAADAQAAAwEAAANhBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAACQQQAAwEAAAMBBAADAQAAAwEAAANhBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAACQQQAAwEAAAMBBAADAQAAAwEAAANhBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAACQQQAAwEAAAMBBAABAQQAAQEEAANhBAACQQQAAwEAAAMBBAABAQQAAQEEAANhBAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAACQQQAAwEAAAMBBAACQQQAAwEAAANhBAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAAAAAAAAQEEAAMBBAAAAAAAAQEEAANhBAAAAAAAAQEEAAMBBAAAAAAAAQEEAANhBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAAAAAAAAQEEAAKhBjSXuwO2wZ0EondlBAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAQEEAAMBBAABAQQAAkEEAAMBBAADAQAAAQEEAAMBBAABAQQAAkEEAAMBBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAQEEAAKhBPS4hQHG0d0GNoe1BAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAAAAAAAAQEEAANhBo4uuwEYXXUEuugxCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAAAAAAAAQEEAANhBo4uuwEYXXUEuugxCAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAAAAAAAAQEEAANhBo4uuwEYXXUEuugxCAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAQEEAAMBBAAAAAAAAQEEAANhBAABAQQAAQEEAAMBBAAAAAAAAQEEAANhBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAQEEAAMBBAADAQAAAwEAAANhBAABAQQAAQEEAAMBBAADAQAAAwEAAANhBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAABAQQAAQEEAAMBBAADAQAAAwEAAANhBAABAQQAAwEAAAMBBkmJqQbp1VkBAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAQEEAAMBBAADAQAAAwEAAANhBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAQEEAAMBBAABAQQAAQEEAANhBAABAQQAAQEEAAMBBAABAQQAAQEEAANhBAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAQEEAAKhBcbR3QXG0d0GNoe1BAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAABAQQAAkEEAAKhB7bBnQWOJy0EondlBAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAABAQQAAQEEAANhBgethQYHrYUHxKBJCAABAQQAAkEEAAMBBgethQUHhwkEiXPtBAABAQQAAQEEAANhBgethQYHrYUHxKBJCAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAAAAAAAAQEEAANhBo4uuwEYXXUEuugxCAAAAAAAAQEEAANhBAADAQAAAwEAAANhBAAAAAAAAQEEAANhBAADAQAAAwEAAANhBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAAAAAAAAQEEAANhBo4uuwEYXXUEuugxCAAAAAAAAQEEAAMBBBIXLwIHrYUEiXPtBAAAAAAAAQEEAANhBo4uuwEYXXUEuugxCAABAQQAAQEEAAMBBkmJqQZJiakFAFgVCAADAQAAAwEAAANhBAABAQQAAQEEAANhBAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAADAQAAAQEEAAMBBunVWQJJiakFAFgVCAABAQQAAQEEAANhBgethQYHrYUHxKBJCAABAQQAAQEEAANhBAACQQQAAwEAAANhBAABAQQAAQEEAANhBgethQYHrYUHxKBJCAABAQQAAQEEAAMBBkmJqQZJiakFAFgVC\"},{\"byteLength\":8616,\"name\":\"buf_red_scattered_lines\",\"uri\":\"data:application/octet-stream;base64,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3BuTP9wbkz/Iqdw/AADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAABBB3BuTP9wbkz/Iqdw/AADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAACQQQAAwEAAABBBSZLUQZIkSUBu25ZAAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAEBBsKoqv7CqKr+qqgpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAACQQQAAwEAAAHBBovbaQfwYOEBwfoRBAACQQQAAwEAAAHBBovbaQfwYOEBwfoRBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAACQQQAAwEAAAHBBovbaQfwYOEBwfoRBAACQQQAAwEAAAHBBovbaQfwYOEBwfoRBAACQQQAAwEAAAEBBovbaQfwYOEAgAydBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAACQQQAAwEAAAHBBovbaQfwYOEBwfoRBAACQQQAAwEAAAHBBovbaQfwYOEBwfoRBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAHBBsKoqv7CqKr+rqpJBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpBAADAQAAAwEAAAJBB3BuTP9wbkz9kNcpB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\"}],\"materials\":[{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[0,0,0,1],\"metallicFactor\":1,\"roughnessFactor\":1}},{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[1,0.5,0.5,1],\"metallicFactor\":1,\"roughnessFactor\":1}},{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[0,0,0,1],\"metallicFactor\":1,\"roughnessFactor\":1}},{\"doubleSided\":true,\"pbrMetallicRoughness\":{\"baseColorFactor\":[1,0,0,1],\"metallicFactor\":1,\"roughnessFactor\":1}}],\"meshes\":[{\"primitives\":[{\"attributes\":{\"POSITION\":0},\"material\":0,\"mode\":6},{\"attributes\":{\"POSITION\":1},\"material\":0,\"mode\":6},{\"attributes\":{\"POSITION\":2},\"material\":0,\"mode\":6}]},{\"primitives\":[{\"attributes\":{\"POSITION\":3},\"material\":1,\"mode\":6},{\"attributes\":{\"POSITION\":4},\"material\":1,\"mode\":6},{\"attributes\":{\"POSITION\":5},\"material\":1,\"mode\":6}]},{\"primitives\":[{\"attributes\":{\"POSITION\":6},\"material\":2,\"mode\":1}]},{\"primitives\":[{\"attributes\":{\"POSITION\":7},\"material\":3,\"mode\":1}]}],\"nodes\":[{\"mesh\":0,\"translation\":[0,12,0]},{\"mesh\":1,\"translation\":[6,6,0]},{\"mesh\":0,\"translation\":[12,12,0]},{\"mesh\":1,\"translation\":[18,6,0]},{\"mesh\":0,\"translation\":[6,0,3]},{\"mesh\":1,\"translation\":[6,6,3]},{\"mesh\":0,\"translation\":[12,6,3]},{\"mesh\":1,\"translation\":[18,6,3]},{\"mesh\":0,\"translation\":[0,12,3]},{\"mesh\":0,\"translation\":[6,12,3]},{\"mesh\":0,\"translation\":[12,12,3]},{\"mesh\":0,\"translation\":[12,18,3]},{\"mesh\":0,\"translation\":[6,0,6]},{\"mesh\":1,\"translation\":[6,6,6]},{\"mesh\":0,\"translation\":[12,6,6]},{\"mesh\":1,\"translation\":[18,6,6]},{\"mesh\":0,\"translation\":[0,12,6]},{\"mesh\":0,\"translation\":[6,12,6]},{\"mesh\":0,\"translation\":[12,12,6]},{\"mesh\":0,\"translation\":[12,18,6]},{\"mesh\":0,\"translation\":[6,0,9]},{\"mesh\":1,\"translation\":[6,6,9]},{\"mesh\":0,\"translation\":[12,6,9]},{\"mesh\":1,\"translation\":[18,6,9]},{\"mesh\":0,\"translation\":[0,12,9]},{\"mesh\":0,\"translation\":[6,12,9]},{\"mesh\":0,\"translation\":[12,12,9]},{\"mesh\":0,\"translation\":[12,18,9]},{\"mesh\":0,\"translation\":[6,0,12]},{\"mesh\":1,\"translation\":[6,6,12]},{\"mesh\":0,\"translation\":[12,6,12]},{\"mesh\":1,\"translation\":[18,6,12]},{\"mesh\":0,\"translation\":[0,12,12]},{\"mesh\":0,\"translation\":[6,12,12]},{\"mesh\":0,\"translation\":[12,12,12]},{\"mesh\":0,\"translation\":[12,18,12]},{\"mesh\":0,\"translation\":[6,0,15]},{\"mesh\":1,\"translation\":[6,6,15]},{\"mesh\":0,\"translation\":[12,6,15]},{\"mesh\":1,\"translation\":[18,6,15]},{\"mesh\":0,\"translation\":[0,12,15]},{\"mesh\":0,\"translation\":[6,12,15]},{\"mesh\":0,\"translation\":[12,12,15]},{\"mesh\":0,\"translation\":[12,18,15]},{\"mesh\":0,\"translation\":[6,0,18]},{\"mesh\":1,\"translation\":[6,6,18]},{\"mesh\":0,\"translation\":[12,6,18]},{\"mesh\":1,\"translation\":[18,6,18]},{\"mesh\":0,\"translation\":[0,12,18]},{\"mesh\":0,\"translation\":[6,12,18]},{\"mesh\":0,\"translation\":[12,12,18]},{\"mesh\":0,\"translation\":[12,18,18]},{\"mesh\":0,\"translation\":[6,0,21]},{\"mesh\":1,\"translation\":[6,6,21]},{\"mesh\":0,\"translation\":[12,6,21]},{\"mesh\":1,\"translation\":[18,6,21]},{\"mesh\":0,\"translation\":[0,12,21]},{\"mesh\":0,\"translation\":[6,12,21]},{\"mesh\":0,\"translation\":[12,12,21]},{\"mesh\":0,\"translation\":[12,18,21]},{\"mesh\":0,\"translation\":[6,0,24]},{\"mesh\":1,\"translation\":[6,6,24]},{\"mesh\":0,\"translation\":[12,6,24]},{\"mesh\":1,\"translation\":[18,6,24]},{\"mesh\":0,\"translation\":[0,12,24]},{\"mesh\":0,\"translation\":[6,12,24]},{\"mesh\":0,\"translation\":[12,12,24]},{\"mesh\":0,\"translation\":[12,18,24]},{\"mesh\":0,\"translation\":[0,12,27]},{\"mesh\":1,\"translation\":[6,6,27]},{\"mesh\":0,\"translation\":[12,12,27]},{\"mesh\":1,\"translation\":[18,6,27]},{\"mesh\":2,\"translation\":[0,0,0]},{\"mesh\":3,\"translation\":[0,0,0]}],\"scene\":0,\"scenes\":[{\"nodes\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73]}]}" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "surface_code_circuit.diagram(\"matchgraph-3d\")\n", "\n", "# Note: if you are viewing this notebook on GitHub, the 3d model viewer is likely blocked.\n", "# To view the 3d model, run this notebook locally or upload it to https://colab.google.com/\n", "# GLTF files can be viewed directly in online viewers such as https://gltf-viewer.donmccurdy.com/" ] }, { "cell_type": "markdown", "metadata": { "id": "_aw3w686hJCa" }, "source": [ "Okay, enough looking at the circuits, time to collect.\n", "\n", "Collecting data using [`sinter.collect`](https://github.com/quantumlib/Stim/blob/main/doc/sinter_api.md#sinter.collect) will take a bit longer this time.\n", "You can specify `print_progress=True` to get progress updates while the collection runs.\n", "Another useful argument (not used here) is `save_resume_filepath`, which allows you to cancel and restart collection without losing the work that was done." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "p4hgivJmeG0G", "scrolled": true }, "outputs": [], "source": [ "import os\n", "\n", "surface_code_tasks = [\n", " sinter.Task(\n", " circuit = stim.Circuit.generated(\n", " \"surface_code:rotated_memory_z\",\n", " rounds=d * 3,\n", " distance=d,\n", " after_clifford_depolarization=noise,\n", " after_reset_flip_probability=noise,\n", " before_measure_flip_probability=noise,\n", " before_round_data_depolarization=noise,\n", " ),\n", " json_metadata={'d': d, 'r': d * 3, 'p': noise},\n", " )\n", " for d in [3, 5, 7]\n", " for noise in [0.008, 0.009, 0.01, 0.011, 0.012]\n", "]\n", "\n", "collected_surface_code_stats: List[sinter.TaskStats] = sinter.collect(\n", " num_workers=os.cpu_count(),\n", " tasks=surface_code_tasks,\n", " decoders=['pymatching'],\n", " max_shots=1_000_000,\n", " max_errors=5_000,\n", " print_progress=True,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "w3JgGXrSe5r-" }, "source": [ "You can now plot the collected data.\n", "Try using the `failure_units_per_shot_func` argument to plot per round error rates instead of per shot error rates, by retrieving the `'r'` entry (short for rounds) that you put in the metadata:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 484 }, "id": "kJSbrfBDe8tQ", "outputId": "abc40cb7-b334-4458-f69e-5016890c5635" }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAtMAAAIkCAYAAADcY6eQAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAABJ0AAASdAHeZh94AAEAAElEQVR4nOzdd5iU1dn48e/0ujPbgaUqVaRJFURAUTSoiIoVC2mWFGMSX33VGNGYqNFoLDGv+WkIGmtsscQCBlEQKSKISIelLmyfmZ3+PM/5/TG7yw47u2yjKPfnuvbSecp5zhRm7jlzn/uYlFIKIYQQQgghRKuZj3QHhBBCCCGE+LaSYFoIIYQQQog2kmBaCCGEEEKINpJgWgghhBBCiDaSYFoIIYQQQog2kmBaCCGEEEKINpJgWgghhBBCiDaSYFoIIYQQQog2kmBaCCGEEEKINpJgWgghhBBCiDaSYFoIIYQQQog2kmBafCskk0nuuusu+vbti8PhwGQy8eabbx7pbn3rzJ49G5PJxMcff3ykuyKEaKXi4mJMJhOzZs060l1psW/ze06vXr3o1avXke5GmkmTJmEymY50N9rk29z3g5FgWjRL13X+3//7f0ycOJHc3FxsNhuFhYUMGTKEH/3oR7z11luHpR9/+tOfuOeeeygqKuLmm2/mrrvuYsCAAYfl2h1t586d/O///i8jRowgJyen/jE944wzePTRRwkEAke6i21W92F/sL/i4uIj3dU2mTVrVqP74na7GThwIL/+9a8pKyvrkOuYTCYmTZrUIW0dKw58XiwWC7m5uUyaNIl//OMfKKWOdBfFEVZRUcHvfvc7xo0bR35+Pjabjby8PE499VT+8Ic/sG/fviPdxTb5+OOPMZlMzJ49u9Xn9urVC5PJRFZWVpP3vy4I3rx5czt7+t1lPdIdEEcvXdc599xzef/998nOzuacc86hW7duJBIJ1q5dywsvvMD69euZNm3aIe/LO++8g9frZd68edjt9kN+vUPl6aef5mc/+xnxeJyhQ4dy+eWXk5OTQ0VFBYsWLeKmm27id7/7HeXl5Ue6q+3i9/u56aabmtyfnZ192PpyKJx//vkMGzYMgH379vGf//yHhx9+mNdee40vvviCvLy8I9vBY9hdd90FpH7N2rx5M2+88QYLFy5kxYoVPPHEE0e4d+JIeeedd7jyyisJBAL06dOHCy64gMLCQgKBAEuXLuU3v/kNf/jDH9i8eTOdO3cG4KOPPjrCvW7s2WefJRKJdHi7NTU13HXXXfzf//1fh7dd51D1/WggwbRo0osvvsj777/P0KFDWbhwIX6/P21/JBJh6dKlh6Uve/bsIS8v71sdSD///PP8+Mc/Jicnh9dee41zzjmn0TGLFy/mpz/96RHoXcfKzs5u0yjJt8X06dPTfmqPxWKcfPLJrF69mieeeKI+oBOH34Gvu8WLFzNhwgSefPJJfv3rX3PccccdmY6JI2bhwoVccMEFWK1W5syZwzXXXNMo3WDNmjX84he/IBaL1W/r3bv34e7qQfXo0eOQtNunTx+efvppfvGLX3DCCScckmscqr4fDSTNQzTps88+A1I/bR8YSAO43W5OO+20tG3N5cc1le9X99P51q1befzxxxkyZAgul4tJkybV79u2bRvbt2+v/wm3YR7bP/7xDy666CKOP/54XC4XPp+PU045hX/+859N3rfKykruuOMOBg0ahNvtxu/3M3ToUP73f/+XcDjc6NjbbruNE044AZfLhd/vZ/LkyXz44YcHeQT3C4VC3HjjjQC89NJLGQNpgFNOOSXjF5SPPvqIs88+m9zcXBwOB/369eN///d/m0wJ+eKLLzj77LPJysrC5/NxxhlnsGTJkmb7uH79embNmkX37t2x2+106tSJK664gg0bNrT4frZFc89/S/YDbNq0iauvvpquXbtit9spKiri6quvZtOmTY2u1/A1+sILLzBmzBi8Xm+7ciOdTiczZ84EYPny5Wn7AoEADz74IKeffjrdunXDbrdTUFDAtGnTGj0n//jHP+o/5BcuXJiWtnBgkLh06VJmzJhB586dsdvtdO/eneuuu449e/Y06t/WrVu59tpr6dOnDy6Xi9zcXAYPHsz1119PRUVFi+5jXerJnj17uOqqqygsLMTlcjFixAheeOGFJs/74IMPmDp1Kvn5+TgcDnr37s3//M//UF1d3ejYuhzVYDDIr371K3r16oXNZmvXF7NTTjmFAQMGoJTiiy++aLT/lVdeYcKECfj9flwuF4MHD+a+++4jHo83+RhkUvc6bZjC1PA9r7i4mMsuu4z8/HycTicjR47knXfeydhWKBTiV7/6Fd26dcPpdDJgwAAefvhhDMNo1X2vez394x//yLg/0/1p+O/j1VdfZfTo0bjdbnJzc7nsssvYvXt3xrYO9XtOS94HMjEMg+uuuw5N03j00Ufr2znQ4MGDmT9/Pl27dq3flilnuuFj+v777zNp0iT8fn9am7qu83//93+ccsop9a+rPn368KMf/SjtPSnTa6ZOU6kbB+Ydz5o1q/5z+O677057z2hNnvp9992HruvccsstLT4HWvfZlClnWinF3LlzGTduHAUFBTidTrp3785ZZ53Fyy+/3KiNXbt28bOf/Yzjjz8eh8NBXl4e06ZNa/S+e7jJyLRoUt1P1Rs3bjws1/vFL37Bp59+yjnnnMPUqVOxWCyMGjWKXr168ec//xmgPnWgYZrADTfcwIknnsiECRPo0qULFRUV/Oc//+Gqq65iw4YN/O53v0u7zrZt2zjttNPYvn07I0aM4IYbbsAwDDZu3MgjjzzC9ddfj8fjAWD79u1MmjSJ4uJiTj31VM4++2zC4TDvvPMOZ599Nk899RQ//vGPD3rfXn31VSorKzn55JOZMmVKs8c6HI6020899RQ33HADHo+Hiy++mMLCQj7++GMeeOAB3n77bRYvXpz2eHz22WecccYZJBIJLrzwQvr06cOqVauYNGkSp59+esZrvv/++1x44YUkk0nOO+88+vTpw65du3j99dd59913WbBgAcOHDz/o/WyPTM9/S/YvX76cM844g1AoxLRp0xg4cCDr16/nn//8J//+97+ZP38+o0aNanS9P/3pT8ybN4/zzjuP0047rcNy1W02W9rtdevWcccddzBhwgTOOecccnJy2LFjB2+99Rbvvfceb7/9NmeffTYAw4YN46677uLuu++mZ8+eaV88GwYNf//737n22mtxOBxMmzaN7t27s2nTJp5++mnefvttPv/88/pRoJKSEkaNGkUwGGTq1KlcdNFFxGIxtm3bxnPPPcfPfvazFqelVFVVMW7cOLKzs/n+979PdXU1r7zyCjNnzmT37t38z//8T9rxd999N7NnzyY3N5dzzz2XwsJCvvrqKx566CH+85//sGTJEnw+X9o5iUSC008/ncrKSqZMmYLP5+uw0eQDn5vbb7+d++67j/z8fK644gq8Xi/vvfcet99+Ox988AEffvhhh/watn37dkaPHs3xxx/PVVddRWVlJS+//DLnn38+8+fPTxuUiMfjTJ48meXLlzN06FBmzpxJdXU1v/vd71i4cGG7+9JSTz75JG+99RbTpk1j4sSJLF26lJdffpnVq1ezatWqtPepw/mec7D3iQMtXLiQDRs20LVrV374wx82e6zZbMZsbtkY46uvvsr777/P9773Pa6//nq2b98OpF6/5557LvPmzaN79+5cccUV+Hw+iouLeeONNxg/fjx9+/Zt0TVaYvr06QDMnTuXiRMnpr1PtGaAYPr06UyYMIF33nmHBQsWNBooy6S1n02Z3HHHHdx3330cd9xxXHLJJfj9fkpKSli+fDn/+te/uPTSS+uPXblyJVOmTKGyspKzzjqLCy+8kPLyct58803Gjx/PG2+8wdSpU1t8nzuUEqIJK1euVDabTZlMJnXllVeq1157TRUXFzd7zl133aUAtWDBgkb7tm3bpgB1zTXXpG2/5pprFKCKiorU1q1bM7bbs2dP1bNnz4z7Nm/e3GhbPB5Xp59+urJarWrXrl1p+8aOHasA9Yc//KHReWVlZSoajdbfnjhxojKZTOrFF19MO66qqkoNHTpUOZ1OtXfv3oz9augHP/iBAtQdd9xx0GMbKi4uVna7XWVlZal169al7bvhhhsUoH784x/XbzMMQ/Xv318B6s0330w7/s9//rMCGj0/lZWVKjs7W+Xl5am1a9emnbNmzRrl8XjUSSed1KL+1j3Hfr9f3XXXXRn//vrXv6adc7Dnv7n9hmGoAQMGKED985//TNv30ksvKUD1799f6bpev73uNep2u9XKlStbdL8O7MucOXPStkciETV48GAFqIceeihtX3V1tSorK2vU1s6dO1WXLl3UgAEDGu0D1MSJEzP2YcOGDcpms6nevXs3em3Pnz9fmc1mNX369Pptjz32mALUn//850Zt1dTUqEgk0tTdbdQnQF188cVpj+fWrVtVTk6OstlsasuWLfXb//vf/ypAjR07VlVVVaW1NWfOHAWom266KW17z549FaAmT56sampqWtSvA/t3oIULFyqz2azsdrvas2dP/fbPPvtMAap79+6qpKSkfnsymVTnnnuuAtTvf//7Rtdo6nmpe21s27atflvdvwdAzZ49O+34999/XwHqe9/7Xtr23//+9wpQF154YcbHOdN7aFPqHucDX6/N3Z+6fx9ZWVnqq6++Stt3+eWXK0C9/PLL9dsO13tOSz4nMrnnnnsUoGbOnNnic+pk+type0xNJpN67733Gp1z2223KUCdd955KhaLpe2LxWKqtLS0/nam10ydBQsWKEDdddddadsnTpzY6HXe1LEtUfdvLplMqmXLlimTyaRGjBihDMNodM1NmzbVb2vtZ1NTfc/NzVVdu3ZV4XC4Ud8avm8mk0nVu3dv5XA41Mcff5x23O7du1VRUZHq3Llzo8f8cJFgWjTr5ZdfVp07d65/QwRUbm6umj59unrrrbcaHd+eYDrTh32d5oLpprz22msKUHPnzq3ftmLFCgWoYcOGpX1QZbJq1SoFqBkzZmTc/+abbypA/eUvfzloX773ve8poFEgeTD33nuvAtRtt93WaF9lZaXKyspSTqez/g1k0aJFClATJkxodLymaap3796Nnp+6D7wnnngiYx9uuukmBTT60MukYfDQ1N/QoUPTzjnY89/c/rr7O3bs2Iznjh8/XgFq4cKF9dvqXqMHBnItUdeX888/v/7LwQ033KC6d+9e/7hn+lBoys9//nMFqO3bt6dtby5oq3s+3nnnnYz7p0+friwWiwoGg0qp/cH0U0891eJ+ZQIoi8WSMZCpe0wbBozTp09XgPr6668ztjds2DBVUFCQtq3ug33VqlVt6l9dQHHXXXep22+/XV1yySX1AwKPPfZY2vE/+tGPmnxcNmzYoMxmszruuOMaXaMtwXTPnj2VpmmNzunRo4fKy8tL29anTx9lNpszDhLUPc6HI5jO9MW/7gvSr3/96/pth+s9pyWfE5nUBXa33nprq85TqvlguuEX1jqapim/369cLpfavXv3Qds/2oJppZS67LLLFKCee+65RtdsGEy39rOpqb7n5uaqXr16HTQIrvu8vfnmmzPur3tNvfvuuwe/04eApHmIZl1yySVccMEFLFiwgEWLFvHll1+yaNEi3nzzTd58802uvvrqtDzP9hg9enSbztuxYwcPPPAAH330ETt27CAajabtb5jj9/nnnwNw1llnHfTnvLp8v0AgkDFns64M2rp169rU75ZYuXIlQMafSnNycjjppJP45JNPWL9+PUOHDq0/fuLEiY2Ot1gsjB8/ni1btqRtr7ufq1evzng/69J81q1bx8CBA1vU7549e7a6/N3Bnv9M+5t7fOq2171uJ0yY0KrrNeff//43//73v9O2nXnmmbz77ruNUgkgNQnu0UcfZcmSJZSWlpJIJNL27969u8WTc+qer4ULF2bMEywtLUXXdTZu3MiIESOYNm0at99+Oz/96U/54IMPOOusszjllFMYOHBgq//d9ujRI2PKxaRJk7j77rv58ssv0/pps9n417/+xb/+9a9G5yQSCcrKyqioqEhLM3E6nQwZMqRV/Wro7rvvTrttMpl45pln+P73v5+2vbnXTr9+/ejWrRvbtm0jEAhknDPSGsOGDcuYjtC9e/e0vOJQKMTmzZvp3r17xslvdY/z4TBy5MhG27p37w6k0n3qHO73nPb8u+1Imfqxfv16AoEAY8aMoaio6Aj0qv3uu+8+3njjDe644w5mzJiB0+nMeFxrP5uaMnPmTB5//HEGDhzIJZdcwsSJExk7dmyjf3N1r5nt27dnfM3U5aKvW7fuiKR6SDAtDspmszFlypT6XF9d13nttdf4wQ9+wLPPPssFF1xQn7fVHnXliFpj69atjB49mqqqKk499VSmTJmC3+/HYrFQXFzM3Llz0yYS1U16ajjJpCl1E7PmzZvHvHnzmjyupqbmoG116dIFoMnJO02py+OtO7+pduvuV93xnTp1ynh8pse47n7+v//3/5rtS0vuZ3sc7PnPtL+1j09rrtecOXPmMGvWLHRdZ+vWrdx55528/PLL3HDDDTz99NNpx77xxhv1H0pnnnkmvXv3xuPxYDab+fjjj1m4cGHGyW5NqXu+HnzwwWaPq3u+evbsybJly5g9ezbvv/8+r7/+OpAKjG6++eb6ibEtcbDXVcO884qKCjRNO2jwV1NTkxZMFxYWtuvLuaqtJx0Oh1myZAk//OEPuf766+nZs2faB39LXjs7duygurq63cF0U3mjVqs1bVJhW/79HiqZ+my1pkIGXdfrtx3u95zWPgZtfe89mEz9aM3ny9GqV69e/PznP+ehhx7i0Ucf5dZbb814XHveext65JFHOP7445kzZw73338/999/P1arlalTp/KnP/2JPn36APtfM5m+mDd0qD+nmiLVPESrWSwWLrnkEn75y18C8N///rd+X91or6Zpjc472D+qtnyAPvzww1RUVPDMM8/w8ccf89hjj/G73/2O2bNnc9ZZZzU6vu4DoiVvrHUfoI8++igqlRKV8W/OnDkHbWv8+PFA6+uW1vVh7969GfeXlJSkHVf336aK72dqp+6c1atXN3s/r7nmmlb1vbUO9vxn2t/ax6c112sJi8VC375966uCPPPMM40WMrrzzjux2+2sWLGCN998s34BotmzZ9O/f/9WX7PuvgQCgWafr4YjhSeccAIvv/wyFRUVrFixgvvvvx/DMPjFL37BM8880+JrH+x11fBx9vv95OTkNNtHpRQ9e/ZMa6ujVkjzeDycccYZvP322+i6zjXXXJNW47Ytrx2TyZTxvQ0O/v7WEm3599uc9rwft9Thfs9p7euj7r33448/TvsS0F6Z+tGazxc4PM9PW9xxxx3k5uZy3333NbnmQXveexuyWCzcdNNNrF69mn379vHaa69xwQUX8NZbb3H22WfXDzTUtfPvf/+72dfMkSpLKsG0aLOsrCxg/0gQpH7egdQqfwdasWJFh/ehbkWmiy66qNG+TDPfTz75ZCBVrutgZabqjv3000/b201mzJhBbm4uS5YsYf78+c0e23CU8qSTTgLIWOKourqaVatW4XQ66+uC1s1+z3TfdV1n0aJFjbZ35P083Jp7fAAWLFgAcMgrkZjNZh599FEAbr311rQP7c2bNzNw4MBGtVsNw8j4fNS119QHf3ueL6vVyogRI7j11lt58cUXAXjzzTdbfP6OHTuaLOMF+5+Pun5WVVWxdu3aVvezIw0ZMoQf//jH7Nq1i0ceeaR+e3Ovnc2bN7Nr1y6OO+64tBHanJycjO9tuq6zatWqdvc1KyuLPn36sHv37kapEU31tTmH4/34aH/PmThxIv3792fXrl0HHfgwDINkMtnmaw0YMIDs7Gy++uqrjCUqD9RRz09dClFHfVnIzs7mzjvvJBAINPnLUms/m1qisLCQCy+8kFdeeYXTTz+dLVu28PXXXwNH/+eUBNOiSS+++CLz5s3LGHTu3bu3/ie6hrmodXlkc+bMSfu2vXPnTu65554O72Nd6Z8D/0F/8MEHjX5uBxgxYgTjxo1j1apVPPDAA432V1RU1BftHzlyJKeeeiqvv/46f//73zNef82aNZSWlh60n1lZWTz22GMAXHrppXzwwQcZj/v8888ZO3Zs/e0rr7wSm83G448/3mgp1zvvvJNgMMiVV15ZX6Zq3Lhx9O/fn08++aRRTu8TTzyR8QP6+9//PtnZ2dx9990sW7as0X7DMFr9IX64nHLKKfTv359Fixbx6quvpu179dVX+fTTT+nXr1/96NShNGbMGM4991zWr1/Ps88+W7+9V69ebNq0Ke3DVSnF7Nmz+eabbzK2lZeXl/EDFuBnP/sZNpuNX/7ylxnLViYSibQPnC+++CJj2b+6kUS3292yO0jqw/rWW29Ne0/Ytm0bjz32GFarlSuvvLJ+e90vVz/+8Y8zBhbhcLh+DsOh9pvf/AaHw8FDDz1Un+/7gx/8AIB77703bRl4Xde5+eabMQyjUSm10aNHs2PHjkY15u+999760mjt9f3vfx/DMJp8nFtj5MiRmM1mXnjhhbRR+crKylbXE27K0f6eYzabeeqpp7Bardx4443885//zLi0/DfffMOUKVPalQ5isVj4yU9+QjQa5frrr2+UvlU3T6BO3eflgekua9asqf9y3hJ1aVI7duxoa9cb+clPfkLv3r156qmnMn6Bbu1nUybxeJzFixc32p5MJqmsrAT2vz+df/759O7dm7/85S/85z//ydjekiVLjtgKi5IzLZq0dOlSHn30UTp37sz48ePrJx5t27aNd999l2g0yvnnn8+MGTPqzxkzZgwTJkzgk08+YfTo0Zx++uns27ePt99+m7POOqvJAKGtfvKTnzBnzhwuvvhiZsyYQVFREV9//TXvv/8+l1xyScai7//85z+ZNGkSt99+O6+99hqTJk1CKcWmTZv48MMPWb9+fX2Q/sILL3D66afzwx/+kMcee4wxY8aQnZ3Nrl27+Oqrr/j6669ZsmQJhYWFB+3rzJkziUaj/OxnP+Pss89m2LBhjBs3rn458SVLlrB69Wry8/Prz6mrsf3Tn/6U4cOHc8kll1BQUMDChQtZsmQJAwYMSPtSUDfZ6swzz+Siiy5Kq/laV1z//fffT+tXXl4er776KhdccAEnn3wykydP5sQTT8RkMrFz506WLFmS9iWjJaqrq5tdaGPWrFntWiSljslkYu7cuZx55plceumlnH/++QwYMIANGzbw5ptvkpWVxbPPPtvi2rHtdc899/Duu+9y9913M3PmTOx2O7/85S+5/vrrOemkk7jooouw2WwsXryYb775hvPOO4+33367UTuTJ0/mpZde4rzzzmP48OHYbDYmTJjAhAkTGDBgAH//+9/5wQ9+wIknnsjZZ59Nv379SCaT7Nixg08//ZSCggLWr18PwHPPPcdTTz3F+PHj6d27Nzk5OWzZsoW3334bh8PR7LLvBxoyZAhLly5lxIgRTJkypb7OdHV1NX/84x/TJs1NnjyZ+++/n9tuu42+ffsydepUjjvuOGpqati+fTsLFy5k/PjxjV6Ph0LXrl25/vrrefTRR/njH//Ifffdx7hx47jlllv44x//yKBBg5gxYwYej4f33nuPr7/+mvHjxzeqm33zzTfzwQcfcP7553PppZeSm5vLZ599xrZt25g0aVKHBIC//vWvefPNN3nttdcYPnw4Z511Vv3jPGHChEZpRM3p0qULM2fO5LnnnmPYsGGcc845BINB/vOf/zBhwoS0CaNtdbS85zRn4sSJvP7661x11VVcddVV/O53v2PSpEkUFBQQCARYsWIFS5cuxePx4HK52nWtu+66i6VLl/L222/Tr18/zj33XLKysti5cycffvghDz74YH39+PPPP5++ffvy4osvsmvXLsaMGcOOHTv497//zfnnn88rr7zSomv279+frl278tJLL2Gz2ejZsycmk4mrrrqqURpVS9ntdu677z4uueSSjF8UW/vZlEk0GmX8+PH06dOHESNG0LNnT2KxGPPmzWPdunVMmzatfmTbZrPx+uuvc9ZZZ3HOOecwbtw4hg0bhtvtZufOnSxfvpytW7dSUlLSqgGCDtPxBULEd8WOHTvUE088oaZPn6769eunsrKylM1mU507d1bf+9731HPPPZexvFxVVZX60Y9+pAoKCpTdblcnnniieuqppw5aGi9TeaA6zZXGW7x4sTrttNNUdna28nq96pRTTlFvvPFGs+WCysvL1S233KL69eunHA6H8vv9aujQoer2229vVNosGAyq3//+92r48OHK4/Eop9OpevXqpaZOnaqeeuqpVtfD3bFjh7rlllvUSSedpPx+v7JarSo/P19NmjRJPfLIIyoQCDQ654MPPlBnnnmmys7OVna7XfXu3Vv9z//8T6P6vXVWrFihzjrrLOX1epXX61WTJ09Wn3322UFLF/70pz9Vffr0UQ6HQ2VlZan+/furK6+8Ur3xxhstum8tKY134PUP9vy35PWxfv16deWVV6rOnTsrq9WqOnfurGbOnKnWr1/f6NjmHoODaarOdEMXXnihAtJKsc2ZM0cNHTpUud1ulZeXp6ZPn66++uqrJvuyb98+dfnll6vCwkJlNpszvo6/+uordc0116gePXoou92ucnJy1IknnqiuvfZa9dFHH9Uf9/nnn6vrr79eDRkyROXk5Cin06l69+6tZs2apdasWdPi+05tGbXdu3ermTNnqoKC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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, ax = plt.subplots(1, 1)\n", "sinter.plot_error_rate(\n", " ax=ax,\n", " stats=collected_surface_code_stats,\n", " x_func=lambda stat: stat.json_metadata['p'],\n", " group_func=lambda stat: stat.json_metadata['d'],\n", " failure_units_per_shot_func=lambda stat: stat.json_metadata['r'],\n", ")\n", "ax.set_ylim(5e-3, 5e-2)\n", "ax.set_xlim(0.008, 0.012)\n", "ax.loglog()\n", "ax.set_title(\"Surface Code Error Rates per Round under Circuit Noise\")\n", "ax.set_xlabel(\"Phyical Error Rate\")\n", "ax.set_ylabel(\"Logical Error Rate per Round\")\n", "ax.grid(which='major')\n", "ax.grid(which='minor')\n", "ax.legend()\n", "fig.set_dpi(120) # Show it bigger" ] }, { "cell_type": "markdown", "metadata": { "id": "6bPcsxWhZebc" }, "source": [ "You can see from the plot that the threshold of the surface code is roughly 1%.\n", "\n", "There is a problem here, though.\n", "The problem is that the threshold isn't the only metric you care about.\n", "The threshold tells you the absolute worse qubit quality that could possibly work, but it doesn't tell you how many of those qubits you would need to hit a target logical error rate.\n", "What you **really** want to estimate is the quality *and corresponding quantity* of qubits needed to do fault tolerant computation.\n", "\n", "Suppose, for the sake of example, that you have qubits with a physical error rate of 0.1% according to the noise model you are using.\n", "Collect logical error rates from a variety of code distances so you can predict the code distance needed to achieve a target logical error rate." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "OVjDeVXzkEO2", "scrolled": true }, "outputs": [], "source": [ "noise = 1e-3\n", "\n", "surface_code_tasks = [\n", " sinter.Task(\n", " circuit = stim.Circuit.generated(\n", " \"surface_code:rotated_memory_z\",\n", " rounds=d * 3,\n", " distance=d,\n", " after_clifford_depolarization=noise,\n", " after_reset_flip_probability=noise,\n", " before_measure_flip_probability=noise,\n", " before_round_data_depolarization=noise,\n", " ),\n", " json_metadata={'d': d, 'r': d * 3, 'p': noise},\n", " )\n", " for d in [3, 5, 7, 9]\n", "]\n", "\n", "collected_surface_code_stats: List[sinter.TaskStats] = sinter.collect(\n", " num_workers=os.cpu_count(),\n", " tasks=surface_code_tasks,\n", " decoders=['pymatching'],\n", " max_shots=5_000_000,\n", " max_errors=100,\n", " print_progress=True,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "41iMwGAQqYz2" }, "source": [ "To a good first approximation, logical error rates decrease exponentially with code distance.\n", "Use [SciPy](https://scipy.org/)'s [linear regression facilities](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.linregress.html) to get a line fit of code distance versus log error rate." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "w7QmBLpTkzxr", "outputId": "8db9ac92-e3ec-4043-cdc9-64cde485af8e" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "LinregressResult(slope=-1.1895861614770902, intercept=-4.615053480324744, rvalue=-0.998930299836957, pvalue=0.0010697001630429748, stderr=0.0389381734731575, intercept_stderr=0.24932596232733958)\n" ] } ], "source": [ "import scipy.stats\n", "\n", "# Compute the line fit.\n", "xs = []\n", "ys = []\n", "log_ys = []\n", "for stats in collected_surface_code_stats:\n", " d = stats.json_metadata['d']\n", " if not stats.errors:\n", " print(f\"Didn't see any errors for d={d}\")\n", " continue\n", " per_shot = stats.errors / stats.shots\n", " per_round = sinter.shot_error_rate_to_piece_error_rate(per_shot, pieces=stats.json_metadata['r'])\n", " xs.append(d)\n", " ys.append(per_round)\n", " log_ys.append(np.log(per_round))\n", "fit = scipy.stats.linregress(xs, log_ys)\n", "print(fit)" ] }, { "cell_type": "markdown", "metadata": { "id": "bt6fVetwq0os" }, "source": [ "Plot the collected points and the line fit, to get a projection of the distance needed to achieve a per-round error rate below one in a trillion." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 482 }, "id": "fWTvZ_Xmqv9M", "outputId": "3ef3584c-1ad8-49ee-d68f-0ee15e4ea7a0" }, "outputs": [ { "data": { "image/png": "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27FjRyxZsgSffvopBgwYAHNzc1hZWWHy5Mlo1qwZWrdujQMHDqBVq1bw8PDA69evceLECdSsWbPQbqD08fGBn58f1q1bhwsXLqBLly4wMDBAcHAwTp48iSNHjihv/PLx8cHTp08xd+5c7NixAx4eHqhQoQJevXqFgIAA3Lx5E7t37y6UeqNZ1apVC5s3b8bYsWNRt25ddO3aFTVq1EB6ejqeP3+Oy5cvw9bWFo8ePVK+Z9euXejQoQM++eQTrFy5Eu7u7rCyskJYWBju378PPz8/XL16VVlL9T//+Q8OHTqEP/74A02aNEGXLl0QGxurPJ6OHDmicfvlZ5sU1nEGAGZmZnB3d8fly5cxfPhw1KhRAzKZDL1790aDBg0wbNgwHD58GL///jvq1q2Lvn37QiKR4NChQwgODsbHH3+cpyTz9OnTmDlzJlq2bIkaNWrAzs4OYWFhOHz4MKRSKWbOnKns6+DggOHDh2PHjh1o1KgRevTogfj4eBw/fhxt27bN042C2WndujVcXV2xb98+pKeno1evXsp9lx+jRo3CnDlz8N1332WbaBfkHJkdb29vbNmyBYMGDcLAgQPh6OgIPz8//PXXXxg8eDD27t2b59hbtmwJExMTLF++HFFRUcoxuFOmTNHpk1VbtWqFL774Aj/99BPq1auHgQMHwtTUFCdOnICfnx88PDxUjpMPyczMDJs3b8agQYPQrl07DBo0CJUrV8bt27dx6tQp2NvbK+8dyTRmzBgsWbIEixYtwj///IM6dergyZMnOHHiBPr164c//vhD67hat26NKVOmYNWqVcptllknuVy5cmpjqF++fInGjRujfv36aNCgAZycnBAfH49jx47h33//xdSpU3P97i8xdFxdgz4wZFMnWZPcyqcJIURYWJj47LPPROXKlYW+vr4oX7686NOnj7hx44Za35xKBr1+/Vp8+eWXom7dusLY2FiYmpoKV1dXMWDAALFjxw6VskpCvCuXs2rVKtGsWTNhamoqTExMhKurq/j000/Vyt7s2LFDWYs26/rnp9xQpuxK8sTExIgpU6YIBwcHYWBgIGrWrCl+/vlncf36dQFAfP7559lux6xSU1PFggULRNWqVYWBgYFwdnYWs2fPFikpKXkuAbd3714xZMgQ4erqKkxNTYW5ubmoW7eumD17tkot6UxLly4VtWrVEgYGBmol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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, ax = plt.subplots(1, 1)\n", "ax.scatter(xs, ys, label=f\"sampled logical error rate at p={noise}\")\n", "ax.plot([0, 25],\n", " [np.exp(fit.intercept), np.exp(fit.intercept + fit.slope * 25)],\n", " linestyle='--',\n", " label='least squares line fit')\n", "ax.set_ylim(1e-12, 1e-0)\n", "ax.set_xlim(0, 25)\n", "ax.semilogy()\n", "ax.set_title(\"Projecting distance needed to survive a trillion rounds\")\n", "ax.set_xlabel(\"Code Distance\")\n", "ax.set_ylabel(\"Logical Error Rate per Round\")\n", "ax.grid(which='major')\n", "ax.grid(which='minor')\n", "ax.legend()\n", "fig.set_dpi(120) # Show it bigger" ] }, { "cell_type": "markdown", "metadata": { "id": "F7fFfO-akgqF" }, "source": [ "Based on this data, it looks like a distance 20 patch would be sufficient to survive a trillion rounds.\n", "That's a surface code with around 800 physical qubits.\n", "\n", "Beware that this line fit is being extrapolated quite far. It would be wise to sample a few more code distances. Also, keep in mind that the footprint you just estimated is for a **specific realization of the surface code circuit**, using a **specific choice of circuit-level noise**, and using **a specific kind of decoder**. Also, you only estimated the error of memory in one basis (Z); to be thorough you have to also check the X basis." ] }, { "cell_type": "markdown", "metadata": { "id": "lYPJMxqyEQvy" }, "source": [ "\n", "# 11. Conclusion\n", "\n", "Congratulations for making it this far! Historically, estimating the threshold of a quantum error-correcting code under circuit noise would have taken weeks or months of work.\n", "By leveraging open-source tools, you just did it in a single sitting.\n", "Nicely done!\n", "\n", "\n", "# 12. Additional resources\n", "\n", "## Getting help\n", "\n", "- You can ask questions about quantum circuits, Stim, error correction, and related topics on the [Quantum Computing Stack Exchange](https://quantumcomputing.stackexchange.com/).\n", "Use the tag `stim`.\n", "\n", "## Stim reference material\n", "\n", "- [Stim Python API Reference](https://github.com/quantumlib/Stim/blob/main/doc/python_api_reference_vDev.md)\n", "- [Stim Supported Gates Reference](https://github.com/quantumlib/Stim/blob/main/doc/gates.md)\n", "- [Stim Command Line Reference](https://github.com/quantumlib/Stim/blob/main/doc/usage_command_line.md)\n", "- [Stim Circuit File Format (.stim)](https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md)\n", "- [Stim Detector Error model Format (.dem)](https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md)\n", "- [Stim Results Format Reference](https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md)\n", "\n", "## Talks and videos\n", "\n", "- [\"Software and the Honeycomb Code\"](https://www.youtube.com/watch?v=O3NaTGmY0Rw) by Craig Gidney at the weekly Duke/Pratt quantum computing seminar\n", "- [\"Estimating overheads for quantum fault-tolerance in the honeycomb code\"](https://www.youtube.com/watch?v=ND9OoqJ0NMw) by Mike Newman at the [APS March Meeting 2022](https://web.archive.org/web/20240716155117/https://meetings.aps.org/Meeting/MAR22/Content/4178)\n", "- [\"(Demo/Tutorial) Estimating the threshold of a new quantum code using Stim and PyMatching\"](https://www.youtube.com/watch?v=E9yj0o1LGII) by Craig Gidney\n", "- [\"Relaxing Hardware Requirements for Surface Code Circuits using Time Dynamics\"](https://www.youtube.com/watch?v=FuP1exdZJkg) by Matt McEwen at [QEC 2023](https://web.archive.org/web/20241013042627/https://quantum.sydney.edu.au/qec23)\n", "\n", "## Papers with Stim circuits\n", "\n", "- A list of papers known to have included downloadable Stim circuits is available from the [doc/circuit_data_references.md](circuit_data_references.md) file in the [Stim repository on GitHub](https://github.com/quantumlib/stim).\n", "\n", "## Learning Python\n", "\n", "- [Google's Python class](https://developers.google.com/edu/python)\n", "- [Google's Crash Course on Python](https://www.coursera.org/learn/python-crash-course) at Coursera\n", "- University of Michigan's [Python for Everybody](https://www.coursera.org/specializations/python) at Coursera\n", "- Python.org's [Python Tutorial](https://docs.python.org/3/tutorial/)\n", "\n", "## Learning quantum computing\n", "\n", "\n", "- [Quantum computing for the very curious](https://quantum.country/qcvc) by Andy Matuschak and Michael Nielsen\n", "- [Building Google's quantum computer](https://www.youtube.com/watch?v=dUdqfqS9Dvg) by Marissa Giustina (2018)\n", "- MIT's [Quantum Information Science I](https://openlearninglibrary.mit.edu/courses/course-v1:MITx+8.370.1x+1T2018/about)\n", "- John Preskill's [Ph/CS 219A Quantum Computation](https://www.youtube.com/playlist?list=PL0ojjrEqIyPy-1RRD8cTD_lF1hflo89Iu) on YouTube\n", "- Quantum AI's YouTube content:\n", " - [Quantum Programming with Cirq](https://www.youtube.com/playlist?list=PLpO2pyKisOjLVt_tDJ2K6ZTapZtHXPLB4)\n", " - [QuantumCasts](https://www.youtube.com/playlist?list=PLQY2H8rRoyvwcpm6Nf-fL4sIYQUXtq3HR)\n", "- [Quantum computing for the determined](https://michaelnielsen.org/blog/quantum-computing-for-the-determined/) by Michael Nielsen\n", "- Michael Nielsen and Isaac Chuang's [Quantum Computation And Quantum Information](https://archive.org/embed/QuantumComputationAndQuantumInformation10thAnniversaryEdition)\n", "\n", "\n", "## Learning quantum error correction\n", "\n", "- Quantum AI's [quantum computing journey](https://quantumai.google/learn/map)\n", "- Coursera course [Hands-on quantum error correction with Google Quantum AI](https://www.coursera.org/learn/quantum-error-correction) by Austin Fowler" ] } ], "metadata": { "colab": { "provenance": [] }, "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.11.0" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: doc/python_api_reference_vDev.md ================================================ # Stim (Development Version) API Reference *CAUTION*: this API reference is for the in-development version of stim. Methods and arguments mentioned here may not be accessible in stable versions, yet. API references for stable versions are kept on the [stim github wiki](https://github.com/quantumlib/Stim/wiki) ## Index - [`stim.Circuit`](#stim.Circuit) - [`stim.Circuit.__add__`](#stim.Circuit.__add__) - [`stim.Circuit.__eq__`](#stim.Circuit.__eq__) - [`stim.Circuit.__getitem__`](#stim.Circuit.__getitem__) - [`stim.Circuit.__iadd__`](#stim.Circuit.__iadd__) - [`stim.Circuit.__imul__`](#stim.Circuit.__imul__) - [`stim.Circuit.__init__`](#stim.Circuit.__init__) - [`stim.Circuit.__len__`](#stim.Circuit.__len__) - [`stim.Circuit.__mul__`](#stim.Circuit.__mul__) - [`stim.Circuit.__ne__`](#stim.Circuit.__ne__) - [`stim.Circuit.__repr__`](#stim.Circuit.__repr__) - [`stim.Circuit.__rmul__`](#stim.Circuit.__rmul__) - [`stim.Circuit.__str__`](#stim.Circuit.__str__) - [`stim.Circuit.append`](#stim.Circuit.append) - [`stim.Circuit.append_from_stim_program_text`](#stim.Circuit.append_from_stim_program_text) - [`stim.Circuit.approx_equals`](#stim.Circuit.approx_equals) - [`stim.Circuit.clear`](#stim.Circuit.clear) - [`stim.Circuit.compile_detector_sampler`](#stim.Circuit.compile_detector_sampler) - [`stim.Circuit.compile_m2d_converter`](#stim.Circuit.compile_m2d_converter) - [`stim.Circuit.compile_sampler`](#stim.Circuit.compile_sampler) - [`stim.Circuit.copy`](#stim.Circuit.copy) - [`stim.Circuit.count_determined_measurements`](#stim.Circuit.count_determined_measurements) - [`stim.Circuit.decomposed`](#stim.Circuit.decomposed) - [`stim.Circuit.detecting_regions`](#stim.Circuit.detecting_regions) - [`stim.Circuit.detector_error_model`](#stim.Circuit.detector_error_model) - [`stim.Circuit.diagram`](#stim.Circuit.diagram) - [`stim.Circuit.explain_detector_error_model_errors`](#stim.Circuit.explain_detector_error_model_errors) - [`stim.Circuit.flattened`](#stim.Circuit.flattened) - [`stim.Circuit.flow_generators`](#stim.Circuit.flow_generators) - [`stim.Circuit.from_file`](#stim.Circuit.from_file) - [`stim.Circuit.generated`](#stim.Circuit.generated) - [`stim.Circuit.get_detector_coordinates`](#stim.Circuit.get_detector_coordinates) - [`stim.Circuit.get_final_qubit_coordinates`](#stim.Circuit.get_final_qubit_coordinates) - [`stim.Circuit.has_all_flows`](#stim.Circuit.has_all_flows) - [`stim.Circuit.has_flow`](#stim.Circuit.has_flow) - [`stim.Circuit.insert`](#stim.Circuit.insert) - [`stim.Circuit.inverse`](#stim.Circuit.inverse) - [`stim.Circuit.likeliest_error_sat_problem`](#stim.Circuit.likeliest_error_sat_problem) - [`stim.Circuit.missing_detectors`](#stim.Circuit.missing_detectors) - [`stim.Circuit.num_detectors`](#stim.Circuit.num_detectors) - [`stim.Circuit.num_measurements`](#stim.Circuit.num_measurements) - [`stim.Circuit.num_observables`](#stim.Circuit.num_observables) - [`stim.Circuit.num_qubits`](#stim.Circuit.num_qubits) - [`stim.Circuit.num_sweep_bits`](#stim.Circuit.num_sweep_bits) - [`stim.Circuit.num_ticks`](#stim.Circuit.num_ticks) - [`stim.Circuit.pop`](#stim.Circuit.pop) - [`stim.Circuit.reference_detector_and_observable_signs`](#stim.Circuit.reference_detector_and_observable_signs) - [`stim.Circuit.reference_sample`](#stim.Circuit.reference_sample) - [`stim.Circuit.search_for_undetectable_logical_errors`](#stim.Circuit.search_for_undetectable_logical_errors) - [`stim.Circuit.shortest_error_sat_problem`](#stim.Circuit.shortest_error_sat_problem) - [`stim.Circuit.shortest_graphlike_error`](#stim.Circuit.shortest_graphlike_error) - [`stim.Circuit.solve_flow_measurements`](#stim.Circuit.solve_flow_measurements) - [`stim.Circuit.time_reversed_for_flows`](#stim.Circuit.time_reversed_for_flows) - [`stim.Circuit.to_crumble_url`](#stim.Circuit.to_crumble_url) - [`stim.Circuit.to_file`](#stim.Circuit.to_file) - [`stim.Circuit.to_qasm`](#stim.Circuit.to_qasm) - [`stim.Circuit.to_quirk_url`](#stim.Circuit.to_quirk_url) - [`stim.Circuit.to_tableau`](#stim.Circuit.to_tableau) - [`stim.Circuit.with_inlined_feedback`](#stim.Circuit.with_inlined_feedback) - [`stim.Circuit.without_noise`](#stim.Circuit.without_noise) - [`stim.Circuit.without_tags`](#stim.Circuit.without_tags) - [`stim.CircuitErrorLocation`](#stim.CircuitErrorLocation) - [`stim.CircuitErrorLocation.__init__`](#stim.CircuitErrorLocation.__init__) - [`stim.CircuitErrorLocation.flipped_measurement`](#stim.CircuitErrorLocation.flipped_measurement) - [`stim.CircuitErrorLocation.flipped_pauli_product`](#stim.CircuitErrorLocation.flipped_pauli_product) - [`stim.CircuitErrorLocation.instruction_targets`](#stim.CircuitErrorLocation.instruction_targets) - [`stim.CircuitErrorLocation.noise_tag`](#stim.CircuitErrorLocation.noise_tag) - [`stim.CircuitErrorLocation.stack_frames`](#stim.CircuitErrorLocation.stack_frames) - [`stim.CircuitErrorLocation.tick_offset`](#stim.CircuitErrorLocation.tick_offset) - [`stim.CircuitErrorLocationStackFrame`](#stim.CircuitErrorLocationStackFrame) - [`stim.CircuitErrorLocationStackFrame.__init__`](#stim.CircuitErrorLocationStackFrame.__init__) - [`stim.CircuitErrorLocationStackFrame.instruction_offset`](#stim.CircuitErrorLocationStackFrame.instruction_offset) - [`stim.CircuitErrorLocationStackFrame.instruction_repetitions_arg`](#stim.CircuitErrorLocationStackFrame.instruction_repetitions_arg) - [`stim.CircuitErrorLocationStackFrame.iteration_index`](#stim.CircuitErrorLocationStackFrame.iteration_index) - [`stim.CircuitInstruction`](#stim.CircuitInstruction) - [`stim.CircuitInstruction.__eq__`](#stim.CircuitInstruction.__eq__) - [`stim.CircuitInstruction.__init__`](#stim.CircuitInstruction.__init__) - [`stim.CircuitInstruction.__ne__`](#stim.CircuitInstruction.__ne__) - [`stim.CircuitInstruction.__repr__`](#stim.CircuitInstruction.__repr__) - [`stim.CircuitInstruction.__str__`](#stim.CircuitInstruction.__str__) - [`stim.CircuitInstruction.gate_args_copy`](#stim.CircuitInstruction.gate_args_copy) - [`stim.CircuitInstruction.name`](#stim.CircuitInstruction.name) - [`stim.CircuitInstruction.num_measurements`](#stim.CircuitInstruction.num_measurements) - [`stim.CircuitInstruction.tag`](#stim.CircuitInstruction.tag) - [`stim.CircuitInstruction.target_groups`](#stim.CircuitInstruction.target_groups) - [`stim.CircuitInstruction.targets_copy`](#stim.CircuitInstruction.targets_copy) - [`stim.CircuitRepeatBlock`](#stim.CircuitRepeatBlock) - [`stim.CircuitRepeatBlock.__eq__`](#stim.CircuitRepeatBlock.__eq__) - [`stim.CircuitRepeatBlock.__init__`](#stim.CircuitRepeatBlock.__init__) - [`stim.CircuitRepeatBlock.__ne__`](#stim.CircuitRepeatBlock.__ne__) - [`stim.CircuitRepeatBlock.__repr__`](#stim.CircuitRepeatBlock.__repr__) - [`stim.CircuitRepeatBlock.body_copy`](#stim.CircuitRepeatBlock.body_copy) - [`stim.CircuitRepeatBlock.name`](#stim.CircuitRepeatBlock.name) - [`stim.CircuitRepeatBlock.num_measurements`](#stim.CircuitRepeatBlock.num_measurements) - [`stim.CircuitRepeatBlock.repeat_count`](#stim.CircuitRepeatBlock.repeat_count) - [`stim.CircuitRepeatBlock.tag`](#stim.CircuitRepeatBlock.tag) - [`stim.CircuitTargetsInsideInstruction`](#stim.CircuitTargetsInsideInstruction) - [`stim.CircuitTargetsInsideInstruction.__init__`](#stim.CircuitTargetsInsideInstruction.__init__) - [`stim.CircuitTargetsInsideInstruction.args`](#stim.CircuitTargetsInsideInstruction.args) - [`stim.CircuitTargetsInsideInstruction.gate`](#stim.CircuitTargetsInsideInstruction.gate) - [`stim.CircuitTargetsInsideInstruction.tag`](#stim.CircuitTargetsInsideInstruction.tag) - [`stim.CircuitTargetsInsideInstruction.target_range_end`](#stim.CircuitTargetsInsideInstruction.target_range_end) - [`stim.CircuitTargetsInsideInstruction.target_range_start`](#stim.CircuitTargetsInsideInstruction.target_range_start) - [`stim.CircuitTargetsInsideInstruction.targets_in_range`](#stim.CircuitTargetsInsideInstruction.targets_in_range) - [`stim.CliffordString`](#stim.CliffordString) - [`stim.CliffordString.__add__`](#stim.CliffordString.__add__) - [`stim.CliffordString.__eq__`](#stim.CliffordString.__eq__) - [`stim.CliffordString.__getitem__`](#stim.CliffordString.__getitem__) - [`stim.CliffordString.__iadd__`](#stim.CliffordString.__iadd__) - [`stim.CliffordString.__imul__`](#stim.CliffordString.__imul__) - [`stim.CliffordString.__init__`](#stim.CliffordString.__init__) - [`stim.CliffordString.__ipow__`](#stim.CliffordString.__ipow__) - [`stim.CliffordString.__len__`](#stim.CliffordString.__len__) - [`stim.CliffordString.__mul__`](#stim.CliffordString.__mul__) - [`stim.CliffordString.__ne__`](#stim.CliffordString.__ne__) - [`stim.CliffordString.__pow__`](#stim.CliffordString.__pow__) - [`stim.CliffordString.__repr__`](#stim.CliffordString.__repr__) - [`stim.CliffordString.__rmul__`](#stim.CliffordString.__rmul__) - [`stim.CliffordString.__setitem__`](#stim.CliffordString.__setitem__) - [`stim.CliffordString.__str__`](#stim.CliffordString.__str__) - [`stim.CliffordString.all_cliffords_string`](#stim.CliffordString.all_cliffords_string) - [`stim.CliffordString.copy`](#stim.CliffordString.copy) - [`stim.CliffordString.random`](#stim.CliffordString.random) - [`stim.CliffordString.x_outputs`](#stim.CliffordString.x_outputs) - [`stim.CliffordString.y_outputs`](#stim.CliffordString.y_outputs) - [`stim.CliffordString.z_outputs`](#stim.CliffordString.z_outputs) - [`stim.CompiledDemSampler`](#stim.CompiledDemSampler) - [`stim.CompiledDemSampler.sample`](#stim.CompiledDemSampler.sample) - [`stim.CompiledDemSampler.sample_write`](#stim.CompiledDemSampler.sample_write) - [`stim.CompiledDetectorSampler`](#stim.CompiledDetectorSampler) - [`stim.CompiledDetectorSampler.__init__`](#stim.CompiledDetectorSampler.__init__) - [`stim.CompiledDetectorSampler.__repr__`](#stim.CompiledDetectorSampler.__repr__) - [`stim.CompiledDetectorSampler.sample`](#stim.CompiledDetectorSampler.sample) - [`stim.CompiledDetectorSampler.sample_write`](#stim.CompiledDetectorSampler.sample_write) - [`stim.CompiledMeasurementSampler`](#stim.CompiledMeasurementSampler) - [`stim.CompiledMeasurementSampler.__init__`](#stim.CompiledMeasurementSampler.__init__) - [`stim.CompiledMeasurementSampler.__repr__`](#stim.CompiledMeasurementSampler.__repr__) - [`stim.CompiledMeasurementSampler.sample`](#stim.CompiledMeasurementSampler.sample) - [`stim.CompiledMeasurementSampler.sample_write`](#stim.CompiledMeasurementSampler.sample_write) - [`stim.CompiledMeasurementsToDetectionEventsConverter`](#stim.CompiledMeasurementsToDetectionEventsConverter) - [`stim.CompiledMeasurementsToDetectionEventsConverter.__init__`](#stim.CompiledMeasurementsToDetectionEventsConverter.__init__) - [`stim.CompiledMeasurementsToDetectionEventsConverter.__repr__`](#stim.CompiledMeasurementsToDetectionEventsConverter.__repr__) - [`stim.CompiledMeasurementsToDetectionEventsConverter.convert`](#stim.CompiledMeasurementsToDetectionEventsConverter.convert) - [`stim.CompiledMeasurementsToDetectionEventsConverter.convert_file`](#stim.CompiledMeasurementsToDetectionEventsConverter.convert_file) - [`stim.DemInstruction`](#stim.DemInstruction) - [`stim.DemInstruction.__eq__`](#stim.DemInstruction.__eq__) - [`stim.DemInstruction.__init__`](#stim.DemInstruction.__init__) - [`stim.DemInstruction.__ne__`](#stim.DemInstruction.__ne__) - [`stim.DemInstruction.__repr__`](#stim.DemInstruction.__repr__) - [`stim.DemInstruction.__str__`](#stim.DemInstruction.__str__) - [`stim.DemInstruction.args_copy`](#stim.DemInstruction.args_copy) - [`stim.DemInstruction.tag`](#stim.DemInstruction.tag) - [`stim.DemInstruction.target_groups`](#stim.DemInstruction.target_groups) - [`stim.DemInstruction.targets_copy`](#stim.DemInstruction.targets_copy) - [`stim.DemInstruction.type`](#stim.DemInstruction.type) - [`stim.DemRepeatBlock`](#stim.DemRepeatBlock) - [`stim.DemRepeatBlock.__eq__`](#stim.DemRepeatBlock.__eq__) - [`stim.DemRepeatBlock.__init__`](#stim.DemRepeatBlock.__init__) - [`stim.DemRepeatBlock.__ne__`](#stim.DemRepeatBlock.__ne__) - [`stim.DemRepeatBlock.__repr__`](#stim.DemRepeatBlock.__repr__) - [`stim.DemRepeatBlock.body_copy`](#stim.DemRepeatBlock.body_copy) - [`stim.DemRepeatBlock.repeat_count`](#stim.DemRepeatBlock.repeat_count) - [`stim.DemRepeatBlock.type`](#stim.DemRepeatBlock.type) - [`stim.DemTarget`](#stim.DemTarget) - [`stim.DemTarget.__eq__`](#stim.DemTarget.__eq__) - [`stim.DemTarget.__init__`](#stim.DemTarget.__init__) - [`stim.DemTarget.__ne__`](#stim.DemTarget.__ne__) - [`stim.DemTarget.__repr__`](#stim.DemTarget.__repr__) - [`stim.DemTarget.__str__`](#stim.DemTarget.__str__) - [`stim.DemTarget.is_logical_observable_id`](#stim.DemTarget.is_logical_observable_id) - [`stim.DemTarget.is_relative_detector_id`](#stim.DemTarget.is_relative_detector_id) - [`stim.DemTarget.is_separator`](#stim.DemTarget.is_separator) - [`stim.DemTarget.logical_observable_id`](#stim.DemTarget.logical_observable_id) - [`stim.DemTarget.relative_detector_id`](#stim.DemTarget.relative_detector_id) - [`stim.DemTarget.separator`](#stim.DemTarget.separator) - [`stim.DemTarget.val`](#stim.DemTarget.val) - [`stim.DemTargetWithCoords`](#stim.DemTargetWithCoords) - [`stim.DemTargetWithCoords.__init__`](#stim.DemTargetWithCoords.__init__) - [`stim.DemTargetWithCoords.coords`](#stim.DemTargetWithCoords.coords) - [`stim.DemTargetWithCoords.dem_target`](#stim.DemTargetWithCoords.dem_target) - [`stim.DetectorErrorModel`](#stim.DetectorErrorModel) - [`stim.DetectorErrorModel.__add__`](#stim.DetectorErrorModel.__add__) - [`stim.DetectorErrorModel.__eq__`](#stim.DetectorErrorModel.__eq__) - [`stim.DetectorErrorModel.__getitem__`](#stim.DetectorErrorModel.__getitem__) - [`stim.DetectorErrorModel.__iadd__`](#stim.DetectorErrorModel.__iadd__) - [`stim.DetectorErrorModel.__imul__`](#stim.DetectorErrorModel.__imul__) - [`stim.DetectorErrorModel.__init__`](#stim.DetectorErrorModel.__init__) - [`stim.DetectorErrorModel.__len__`](#stim.DetectorErrorModel.__len__) - [`stim.DetectorErrorModel.__mul__`](#stim.DetectorErrorModel.__mul__) - [`stim.DetectorErrorModel.__ne__`](#stim.DetectorErrorModel.__ne__) - [`stim.DetectorErrorModel.__repr__`](#stim.DetectorErrorModel.__repr__) - [`stim.DetectorErrorModel.__rmul__`](#stim.DetectorErrorModel.__rmul__) - [`stim.DetectorErrorModel.__str__`](#stim.DetectorErrorModel.__str__) - [`stim.DetectorErrorModel.append`](#stim.DetectorErrorModel.append) - [`stim.DetectorErrorModel.approx_equals`](#stim.DetectorErrorModel.approx_equals) - [`stim.DetectorErrorModel.clear`](#stim.DetectorErrorModel.clear) - [`stim.DetectorErrorModel.compile_sampler`](#stim.DetectorErrorModel.compile_sampler) - [`stim.DetectorErrorModel.copy`](#stim.DetectorErrorModel.copy) - [`stim.DetectorErrorModel.diagram`](#stim.DetectorErrorModel.diagram) - [`stim.DetectorErrorModel.flattened`](#stim.DetectorErrorModel.flattened) - [`stim.DetectorErrorModel.from_file`](#stim.DetectorErrorModel.from_file) - [`stim.DetectorErrorModel.get_detector_coordinates`](#stim.DetectorErrorModel.get_detector_coordinates) - [`stim.DetectorErrorModel.num_detectors`](#stim.DetectorErrorModel.num_detectors) - [`stim.DetectorErrorModel.num_errors`](#stim.DetectorErrorModel.num_errors) - [`stim.DetectorErrorModel.num_observables`](#stim.DetectorErrorModel.num_observables) - [`stim.DetectorErrorModel.rounded`](#stim.DetectorErrorModel.rounded) - [`stim.DetectorErrorModel.shortest_graphlike_error`](#stim.DetectorErrorModel.shortest_graphlike_error) - [`stim.DetectorErrorModel.to_file`](#stim.DetectorErrorModel.to_file) - [`stim.DetectorErrorModel.without_tags`](#stim.DetectorErrorModel.without_tags) - [`stim.ExplainedError`](#stim.ExplainedError) - [`stim.ExplainedError.__init__`](#stim.ExplainedError.__init__) - [`stim.ExplainedError.circuit_error_locations`](#stim.ExplainedError.circuit_error_locations) - [`stim.ExplainedError.dem_error_terms`](#stim.ExplainedError.dem_error_terms) - [`stim.FlipSimulator`](#stim.FlipSimulator) - [`stim.FlipSimulator.__init__`](#stim.FlipSimulator.__init__) - [`stim.FlipSimulator.append_measurement_flips`](#stim.FlipSimulator.append_measurement_flips) - [`stim.FlipSimulator.batch_size`](#stim.FlipSimulator.batch_size) - [`stim.FlipSimulator.broadcast_pauli_errors`](#stim.FlipSimulator.broadcast_pauli_errors) - [`stim.FlipSimulator.clear`](#stim.FlipSimulator.clear) - [`stim.FlipSimulator.copy`](#stim.FlipSimulator.copy) - [`stim.FlipSimulator.do`](#stim.FlipSimulator.do) - [`stim.FlipSimulator.generate_bernoulli_samples`](#stim.FlipSimulator.generate_bernoulli_samples) - [`stim.FlipSimulator.get_detector_flips`](#stim.FlipSimulator.get_detector_flips) - [`stim.FlipSimulator.get_measurement_flips`](#stim.FlipSimulator.get_measurement_flips) - [`stim.FlipSimulator.get_observable_flips`](#stim.FlipSimulator.get_observable_flips) - [`stim.FlipSimulator.num_detectors`](#stim.FlipSimulator.num_detectors) - [`stim.FlipSimulator.num_measurements`](#stim.FlipSimulator.num_measurements) - [`stim.FlipSimulator.num_observables`](#stim.FlipSimulator.num_observables) - [`stim.FlipSimulator.num_qubits`](#stim.FlipSimulator.num_qubits) - [`stim.FlipSimulator.peek_pauli_flips`](#stim.FlipSimulator.peek_pauli_flips) - [`stim.FlipSimulator.set_pauli_flip`](#stim.FlipSimulator.set_pauli_flip) - [`stim.FlipSimulator.to_numpy`](#stim.FlipSimulator.to_numpy) - [`stim.FlippedMeasurement`](#stim.FlippedMeasurement) - [`stim.FlippedMeasurement.__init__`](#stim.FlippedMeasurement.__init__) - [`stim.FlippedMeasurement.observable`](#stim.FlippedMeasurement.observable) - [`stim.FlippedMeasurement.record_index`](#stim.FlippedMeasurement.record_index) - [`stim.Flow`](#stim.Flow) - [`stim.Flow.__eq__`](#stim.Flow.__eq__) - [`stim.Flow.__init__`](#stim.Flow.__init__) - [`stim.Flow.__mul__`](#stim.Flow.__mul__) - [`stim.Flow.__ne__`](#stim.Flow.__ne__) - [`stim.Flow.__repr__`](#stim.Flow.__repr__) - [`stim.Flow.__str__`](#stim.Flow.__str__) - [`stim.Flow.included_observables_copy`](#stim.Flow.included_observables_copy) - [`stim.Flow.input_copy`](#stim.Flow.input_copy) - [`stim.Flow.measurements_copy`](#stim.Flow.measurements_copy) - [`stim.Flow.output_copy`](#stim.Flow.output_copy) - [`stim.GateData`](#stim.GateData) - [`stim.GateData.__eq__`](#stim.GateData.__eq__) - [`stim.GateData.__init__`](#stim.GateData.__init__) - [`stim.GateData.__ne__`](#stim.GateData.__ne__) - [`stim.GateData.__repr__`](#stim.GateData.__repr__) - [`stim.GateData.__str__`](#stim.GateData.__str__) - [`stim.GateData.aliases`](#stim.GateData.aliases) - [`stim.GateData.flows`](#stim.GateData.flows) - [`stim.GateData.generalized_inverse`](#stim.GateData.generalized_inverse) - [`stim.GateData.hadamard_conjugated`](#stim.GateData.hadamard_conjugated) - [`stim.GateData.inverse`](#stim.GateData.inverse) - [`stim.GateData.is_noisy_gate`](#stim.GateData.is_noisy_gate) - [`stim.GateData.is_reset`](#stim.GateData.is_reset) - [`stim.GateData.is_single_qubit_gate`](#stim.GateData.is_single_qubit_gate) - [`stim.GateData.is_symmetric_gate`](#stim.GateData.is_symmetric_gate) - [`stim.GateData.is_two_qubit_gate`](#stim.GateData.is_two_qubit_gate) - [`stim.GateData.is_unitary`](#stim.GateData.is_unitary) - [`stim.GateData.name`](#stim.GateData.name) - [`stim.GateData.num_parens_arguments_range`](#stim.GateData.num_parens_arguments_range) - [`stim.GateData.produces_measurements`](#stim.GateData.produces_measurements) - [`stim.GateData.tableau`](#stim.GateData.tableau) - [`stim.GateData.takes_measurement_record_targets`](#stim.GateData.takes_measurement_record_targets) - [`stim.GateData.takes_pauli_targets`](#stim.GateData.takes_pauli_targets) - [`stim.GateData.unitary_matrix`](#stim.GateData.unitary_matrix) - [`stim.GateTarget`](#stim.GateTarget) - [`stim.GateTarget.__eq__`](#stim.GateTarget.__eq__) - [`stim.GateTarget.__init__`](#stim.GateTarget.__init__) - [`stim.GateTarget.__ne__`](#stim.GateTarget.__ne__) - [`stim.GateTarget.__repr__`](#stim.GateTarget.__repr__) - [`stim.GateTarget.is_combiner`](#stim.GateTarget.is_combiner) - [`stim.GateTarget.is_inverted_result_target`](#stim.GateTarget.is_inverted_result_target) - [`stim.GateTarget.is_measurement_record_target`](#stim.GateTarget.is_measurement_record_target) - [`stim.GateTarget.is_qubit_target`](#stim.GateTarget.is_qubit_target) - [`stim.GateTarget.is_sweep_bit_target`](#stim.GateTarget.is_sweep_bit_target) - [`stim.GateTarget.is_x_target`](#stim.GateTarget.is_x_target) - [`stim.GateTarget.is_y_target`](#stim.GateTarget.is_y_target) - [`stim.GateTarget.is_z_target`](#stim.GateTarget.is_z_target) - [`stim.GateTarget.pauli_type`](#stim.GateTarget.pauli_type) - [`stim.GateTarget.qubit_value`](#stim.GateTarget.qubit_value) - [`stim.GateTarget.value`](#stim.GateTarget.value) - [`stim.GateTargetWithCoords`](#stim.GateTargetWithCoords) - [`stim.GateTargetWithCoords.__init__`](#stim.GateTargetWithCoords.__init__) - [`stim.GateTargetWithCoords.coords`](#stim.GateTargetWithCoords.coords) - [`stim.GateTargetWithCoords.gate_target`](#stim.GateTargetWithCoords.gate_target) - [`stim.PauliString`](#stim.PauliString) - [`stim.PauliString.__add__`](#stim.PauliString.__add__) - [`stim.PauliString.__eq__`](#stim.PauliString.__eq__) - [`stim.PauliString.__getitem__`](#stim.PauliString.__getitem__) - [`stim.PauliString.__iadd__`](#stim.PauliString.__iadd__) - [`stim.PauliString.__imul__`](#stim.PauliString.__imul__) - [`stim.PauliString.__init__`](#stim.PauliString.__init__) - [`stim.PauliString.__itruediv__`](#stim.PauliString.__itruediv__) - [`stim.PauliString.__len__`](#stim.PauliString.__len__) - [`stim.PauliString.__mul__`](#stim.PauliString.__mul__) - [`stim.PauliString.__ne__`](#stim.PauliString.__ne__) - [`stim.PauliString.__neg__`](#stim.PauliString.__neg__) - [`stim.PauliString.__pos__`](#stim.PauliString.__pos__) - [`stim.PauliString.__repr__`](#stim.PauliString.__repr__) - [`stim.PauliString.__rmul__`](#stim.PauliString.__rmul__) - [`stim.PauliString.__setitem__`](#stim.PauliString.__setitem__) - [`stim.PauliString.__str__`](#stim.PauliString.__str__) - [`stim.PauliString.__truediv__`](#stim.PauliString.__truediv__) - [`stim.PauliString.after`](#stim.PauliString.after) - [`stim.PauliString.before`](#stim.PauliString.before) - [`stim.PauliString.commutes`](#stim.PauliString.commutes) - [`stim.PauliString.copy`](#stim.PauliString.copy) - [`stim.PauliString.from_numpy`](#stim.PauliString.from_numpy) - [`stim.PauliString.from_unitary_matrix`](#stim.PauliString.from_unitary_matrix) - [`stim.PauliString.iter_all`](#stim.PauliString.iter_all) - [`stim.PauliString.pauli_indices`](#stim.PauliString.pauli_indices) - [`stim.PauliString.random`](#stim.PauliString.random) - [`stim.PauliString.sign`](#stim.PauliString.sign) - [`stim.PauliString.to_numpy`](#stim.PauliString.to_numpy) - [`stim.PauliString.to_tableau`](#stim.PauliString.to_tableau) - [`stim.PauliString.to_unitary_matrix`](#stim.PauliString.to_unitary_matrix) - [`stim.PauliString.weight`](#stim.PauliString.weight) - [`stim.PauliStringIterator`](#stim.PauliStringIterator) - [`stim.PauliStringIterator.__iter__`](#stim.PauliStringIterator.__iter__) - [`stim.PauliStringIterator.__next__`](#stim.PauliStringIterator.__next__) - [`stim.Tableau`](#stim.Tableau) - [`stim.Tableau.__add__`](#stim.Tableau.__add__) - [`stim.Tableau.__call__`](#stim.Tableau.__call__) - [`stim.Tableau.__eq__`](#stim.Tableau.__eq__) - [`stim.Tableau.__iadd__`](#stim.Tableau.__iadd__) - [`stim.Tableau.__init__`](#stim.Tableau.__init__) - [`stim.Tableau.__len__`](#stim.Tableau.__len__) - [`stim.Tableau.__mul__`](#stim.Tableau.__mul__) - [`stim.Tableau.__ne__`](#stim.Tableau.__ne__) - [`stim.Tableau.__pow__`](#stim.Tableau.__pow__) - [`stim.Tableau.__repr__`](#stim.Tableau.__repr__) - [`stim.Tableau.__str__`](#stim.Tableau.__str__) - [`stim.Tableau.append`](#stim.Tableau.append) - [`stim.Tableau.copy`](#stim.Tableau.copy) - [`stim.Tableau.from_circuit`](#stim.Tableau.from_circuit) - [`stim.Tableau.from_conjugated_generators`](#stim.Tableau.from_conjugated_generators) - [`stim.Tableau.from_named_gate`](#stim.Tableau.from_named_gate) - [`stim.Tableau.from_numpy`](#stim.Tableau.from_numpy) - [`stim.Tableau.from_stabilizers`](#stim.Tableau.from_stabilizers) - [`stim.Tableau.from_state_vector`](#stim.Tableau.from_state_vector) - [`stim.Tableau.from_unitary_matrix`](#stim.Tableau.from_unitary_matrix) - [`stim.Tableau.inverse`](#stim.Tableau.inverse) - [`stim.Tableau.inverse_x_output`](#stim.Tableau.inverse_x_output) - [`stim.Tableau.inverse_x_output_pauli`](#stim.Tableau.inverse_x_output_pauli) - [`stim.Tableau.inverse_y_output`](#stim.Tableau.inverse_y_output) - [`stim.Tableau.inverse_y_output_pauli`](#stim.Tableau.inverse_y_output_pauli) - [`stim.Tableau.inverse_z_output`](#stim.Tableau.inverse_z_output) - [`stim.Tableau.inverse_z_output_pauli`](#stim.Tableau.inverse_z_output_pauli) - [`stim.Tableau.iter_all`](#stim.Tableau.iter_all) - [`stim.Tableau.prepend`](#stim.Tableau.prepend) - [`stim.Tableau.random`](#stim.Tableau.random) - [`stim.Tableau.then`](#stim.Tableau.then) - [`stim.Tableau.to_circuit`](#stim.Tableau.to_circuit) - [`stim.Tableau.to_numpy`](#stim.Tableau.to_numpy) - [`stim.Tableau.to_pauli_string`](#stim.Tableau.to_pauli_string) - [`stim.Tableau.to_stabilizers`](#stim.Tableau.to_stabilizers) - [`stim.Tableau.to_state_vector`](#stim.Tableau.to_state_vector) - [`stim.Tableau.to_unitary_matrix`](#stim.Tableau.to_unitary_matrix) - [`stim.Tableau.x_output`](#stim.Tableau.x_output) - [`stim.Tableau.x_output_pauli`](#stim.Tableau.x_output_pauli) - [`stim.Tableau.x_sign`](#stim.Tableau.x_sign) - [`stim.Tableau.y_output`](#stim.Tableau.y_output) - [`stim.Tableau.y_output_pauli`](#stim.Tableau.y_output_pauli) - [`stim.Tableau.y_sign`](#stim.Tableau.y_sign) - [`stim.Tableau.z_output`](#stim.Tableau.z_output) - [`stim.Tableau.z_output_pauli`](#stim.Tableau.z_output_pauli) - [`stim.Tableau.z_sign`](#stim.Tableau.z_sign) - [`stim.TableauIterator`](#stim.TableauIterator) - [`stim.TableauIterator.__iter__`](#stim.TableauIterator.__iter__) - [`stim.TableauIterator.__next__`](#stim.TableauIterator.__next__) - [`stim.TableauSimulator`](#stim.TableauSimulator) - [`stim.TableauSimulator.__init__`](#stim.TableauSimulator.__init__) - [`stim.TableauSimulator.c_xyz`](#stim.TableauSimulator.c_xyz) - [`stim.TableauSimulator.c_zyx`](#stim.TableauSimulator.c_zyx) - [`stim.TableauSimulator.canonical_stabilizers`](#stim.TableauSimulator.canonical_stabilizers) - [`stim.TableauSimulator.cnot`](#stim.TableauSimulator.cnot) - [`stim.TableauSimulator.copy`](#stim.TableauSimulator.copy) - [`stim.TableauSimulator.current_inverse_tableau`](#stim.TableauSimulator.current_inverse_tableau) - [`stim.TableauSimulator.current_measurement_record`](#stim.TableauSimulator.current_measurement_record) - [`stim.TableauSimulator.cx`](#stim.TableauSimulator.cx) - [`stim.TableauSimulator.cy`](#stim.TableauSimulator.cy) - [`stim.TableauSimulator.cz`](#stim.TableauSimulator.cz) - [`stim.TableauSimulator.depolarize1`](#stim.TableauSimulator.depolarize1) - [`stim.TableauSimulator.depolarize2`](#stim.TableauSimulator.depolarize2) - [`stim.TableauSimulator.do`](#stim.TableauSimulator.do) - [`stim.TableauSimulator.do_circuit`](#stim.TableauSimulator.do_circuit) - [`stim.TableauSimulator.do_pauli_string`](#stim.TableauSimulator.do_pauli_string) - [`stim.TableauSimulator.do_tableau`](#stim.TableauSimulator.do_tableau) - [`stim.TableauSimulator.h`](#stim.TableauSimulator.h) - [`stim.TableauSimulator.h_xy`](#stim.TableauSimulator.h_xy) - [`stim.TableauSimulator.h_xz`](#stim.TableauSimulator.h_xz) - [`stim.TableauSimulator.h_yz`](#stim.TableauSimulator.h_yz) - [`stim.TableauSimulator.iswap`](#stim.TableauSimulator.iswap) - [`stim.TableauSimulator.iswap_dag`](#stim.TableauSimulator.iswap_dag) - [`stim.TableauSimulator.measure`](#stim.TableauSimulator.measure) - [`stim.TableauSimulator.measure_kickback`](#stim.TableauSimulator.measure_kickback) - [`stim.TableauSimulator.measure_many`](#stim.TableauSimulator.measure_many) - [`stim.TableauSimulator.measure_observable`](#stim.TableauSimulator.measure_observable) - [`stim.TableauSimulator.num_qubits`](#stim.TableauSimulator.num_qubits) - [`stim.TableauSimulator.peek_bloch`](#stim.TableauSimulator.peek_bloch) - [`stim.TableauSimulator.peek_observable_expectation`](#stim.TableauSimulator.peek_observable_expectation) - [`stim.TableauSimulator.peek_x`](#stim.TableauSimulator.peek_x) - [`stim.TableauSimulator.peek_y`](#stim.TableauSimulator.peek_y) - [`stim.TableauSimulator.peek_z`](#stim.TableauSimulator.peek_z) - [`stim.TableauSimulator.postselect_observable`](#stim.TableauSimulator.postselect_observable) - [`stim.TableauSimulator.postselect_x`](#stim.TableauSimulator.postselect_x) - [`stim.TableauSimulator.postselect_y`](#stim.TableauSimulator.postselect_y) - [`stim.TableauSimulator.postselect_z`](#stim.TableauSimulator.postselect_z) - [`stim.TableauSimulator.reset`](#stim.TableauSimulator.reset) - [`stim.TableauSimulator.reset_x`](#stim.TableauSimulator.reset_x) - [`stim.TableauSimulator.reset_y`](#stim.TableauSimulator.reset_y) - [`stim.TableauSimulator.reset_z`](#stim.TableauSimulator.reset_z) - [`stim.TableauSimulator.s`](#stim.TableauSimulator.s) - [`stim.TableauSimulator.s_dag`](#stim.TableauSimulator.s_dag) - [`stim.TableauSimulator.set_inverse_tableau`](#stim.TableauSimulator.set_inverse_tableau) - [`stim.TableauSimulator.set_num_qubits`](#stim.TableauSimulator.set_num_qubits) - [`stim.TableauSimulator.set_state_from_stabilizers`](#stim.TableauSimulator.set_state_from_stabilizers) - [`stim.TableauSimulator.set_state_from_state_vector`](#stim.TableauSimulator.set_state_from_state_vector) - [`stim.TableauSimulator.sqrt_x`](#stim.TableauSimulator.sqrt_x) - [`stim.TableauSimulator.sqrt_x_dag`](#stim.TableauSimulator.sqrt_x_dag) - [`stim.TableauSimulator.sqrt_y`](#stim.TableauSimulator.sqrt_y) - [`stim.TableauSimulator.sqrt_y_dag`](#stim.TableauSimulator.sqrt_y_dag) - [`stim.TableauSimulator.state_vector`](#stim.TableauSimulator.state_vector) - [`stim.TableauSimulator.swap`](#stim.TableauSimulator.swap) - [`stim.TableauSimulator.x`](#stim.TableauSimulator.x) - [`stim.TableauSimulator.x_error`](#stim.TableauSimulator.x_error) - [`stim.TableauSimulator.xcx`](#stim.TableauSimulator.xcx) - [`stim.TableauSimulator.xcy`](#stim.TableauSimulator.xcy) - [`stim.TableauSimulator.xcz`](#stim.TableauSimulator.xcz) - [`stim.TableauSimulator.y`](#stim.TableauSimulator.y) - [`stim.TableauSimulator.y_error`](#stim.TableauSimulator.y_error) - [`stim.TableauSimulator.ycx`](#stim.TableauSimulator.ycx) - [`stim.TableauSimulator.ycy`](#stim.TableauSimulator.ycy) - [`stim.TableauSimulator.ycz`](#stim.TableauSimulator.ycz) - [`stim.TableauSimulator.z`](#stim.TableauSimulator.z) - [`stim.TableauSimulator.z_error`](#stim.TableauSimulator.z_error) - [`stim.TableauSimulator.zcx`](#stim.TableauSimulator.zcx) - [`stim.TableauSimulator.zcy`](#stim.TableauSimulator.zcy) - [`stim.TableauSimulator.zcz`](#stim.TableauSimulator.zcz) - [`stim.gate_data`](#stim.gate_data) - [`stim.main`](#stim.main) - [`stim.read_shot_data_file`](#stim.read_shot_data_file) - [`stim.target_combined_paulis`](#stim.target_combined_paulis) - [`stim.target_combiner`](#stim.target_combiner) - [`stim.target_inv`](#stim.target_inv) - [`stim.target_logical_observable_id`](#stim.target_logical_observable_id) - [`stim.target_pauli`](#stim.target_pauli) - [`stim.target_rec`](#stim.target_rec) - [`stim.target_relative_detector_id`](#stim.target_relative_detector_id) - [`stim.target_separator`](#stim.target_separator) - [`stim.target_sweep_bit`](#stim.target_sweep_bit) - [`stim.target_x`](#stim.target_x) - [`stim.target_y`](#stim.target_y) - [`stim.target_z`](#stim.target_z) - [`stim.write_shot_data_file`](#stim.write_shot_data_file) ```python # Types used by the method definitions. from typing import overload, TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union import io import pathlib import numpy as np ``` ```python # stim.Circuit # (at top-level in the stim module) class Circuit: """A mutable stabilizer circuit. The stim.Circuit class is arguably the most important object in the entire library. It is the interface through which you explain a noisy quantum computation to Stim, in order to do fast bulk sampling or fast error analysis. For example, suppose you want to use a matching-based decoder on a new quantum error correction construction. Stim can help you do this but the very first step is to create a circuit implementing the construction. Once you have the circuit you can then use methods like stim.Circuit.detector_error_model() to create an object that can be used to configure the decoder, or like stim.Circuit.compile_detector_sampler() to produce problems for the decoder to solve, or like stim.Circuit.shortest_graphlike_error() to check for mistakes in the implementation of the code. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append("X", 0) >>> c.append("M", 0) >>> c.compile_sampler().sample(shots=1) array([[ True]]) >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''').compile_detector_sampler().sample(shots=1) array([[False]]) """ ``` ```python # stim.Circuit.__add__ # (in class stim.Circuit) def __add__( self, second: stim.Circuit, ) -> stim.Circuit: """Creates a circuit by appending two circuits. Examples: >>> import stim >>> c1 = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c2 = stim.Circuit(''' ... M 0 1 2 ... ''') >>> c1 + c2 stim.Circuit(''' X 0 Y 1 2 M 0 1 2 ''') """ ``` ```python # stim.Circuit.__eq__ # (in class stim.Circuit) def __eq__( self, arg0: stim.Circuit, ) -> bool: """Determines if two circuits have identical contents. """ ``` ```python # stim.Circuit.__getitem__ # (in class stim.Circuit) @overload def __getitem__( self, index_or_slice: int, ) -> Union[stim.CircuitInstruction, stim.CircuitRepeatBlock]: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.Circuit: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns copies of instructions from the circuit. Args: index_or_slice: An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a circuit. Returns: If the index was an integer, then an instruction from the circuit. If the index was a slice, then a circuit made up of the instructions in that slice. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X 0 ... X_ERROR(0.5) 2 ... REPEAT 100 { ... X 0 ... Y 1 2 ... } ... TICK ... M 0 ... DETECTOR rec[-1] ... ''') >>> circuit[1] stim.CircuitInstruction('X_ERROR', [stim.GateTarget(2)], [0.5]) >>> circuit[2] stim.CircuitRepeatBlock(100, stim.Circuit(''' X 0 Y 1 2 ''')) >>> circuit[1::2] stim.Circuit(''' X_ERROR(0.5) 2 TICK DETECTOR rec[-1] ''') """ ``` ```python # stim.Circuit.__iadd__ # (in class stim.Circuit) def __iadd__( self, second: stim.Circuit, ) -> stim.Circuit: """Appends a circuit into the receiving circuit (mutating it). Examples: >>> import stim >>> c1 = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c2 = stim.Circuit(''' ... M 0 1 2 ... ''') >>> c1 += c2 >>> print(repr(c1)) stim.Circuit(''' X 0 Y 1 2 M 0 1 2 ''') """ ``` ```python # stim.Circuit.__imul__ # (in class stim.Circuit) def __imul__( self, repetitions: int, ) -> stim.Circuit: """Mutates the circuit by putting its contents into a REPEAT block. Special case: if the repetition count is 0, the circuit is cleared. Special case: if the repetition count is 1, nothing happens. Args: repetitions: The number of times the REPEAT block should repeat. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c *= 3 >>> print(repr(c)) stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ ``` ```python # stim.Circuit.__init__ # (in class stim.Circuit) def __init__( self, stim_program_text: str = '', ) -> None: """Creates a stim.Circuit. Args: stim_program_text: Defaults to empty. Describes operations to append into the circuit. Examples: >>> import stim >>> empty = stim.Circuit() >>> not_empty = stim.Circuit(''' ... X 0 ... CNOT 0 1 ... M 1 ... ''') """ ``` ```python # stim.Circuit.__len__ # (in class stim.Circuit) def __len__( self, ) -> int: """Returns the number of top-level instructions and blocks in the circuit. Instructions inside of blocks are not included in this count. Examples: >>> import stim >>> len(stim.Circuit()) 0 >>> len(stim.Circuit(''' ... X 0 ... X_ERROR(0.5) 1 2 ... TICK ... M 0 ... DETECTOR rec[-1] ... ''')) 5 >>> len(stim.Circuit(''' ... REPEAT 100 { ... X 0 ... Y 1 2 ... } ... ''')) 1 """ ``` ```python # stim.Circuit.__mul__ # (in class stim.Circuit) def __mul__( self, repetitions: int, ) -> stim.Circuit: """Repeats the circuit using a REPEAT block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the REPEAT block should repeat. Returns: repetitions=0: An empty circuit. repetitions=1: A copy of this circuit. repetitions>=2: A circuit with a single REPEAT block, where the contents of that repeat block are this circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c * 3 stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ ``` ```python # stim.Circuit.__ne__ # (in class stim.Circuit) def __ne__( self, arg0: stim.Circuit, ) -> bool: """Determines if two circuits have non-identical contents. """ ``` ```python # stim.Circuit.__repr__ # (in class stim.Circuit) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.Circuit`. """ ``` ```python # stim.Circuit.__rmul__ # (in class stim.Circuit) def __rmul__( self, repetitions: int, ) -> stim.Circuit: """Repeats the circuit using a REPEAT block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the REPEAT block should repeat. Returns: repetitions=0: An empty circuit. repetitions=1: A copy of this circuit. repetitions>=2: A circuit with a single REPEAT block, where the contents of that repeat block are this circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> 3 * c stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ ``` ```python # stim.Circuit.__str__ # (in class stim.Circuit) def __str__( self, ) -> str: """Returns stim instructions (that can be saved to a file and parsed by stim) for the current circuit. """ ``` ```python # stim.Circuit.append # (in class stim.Circuit) @overload def append( self, name: str, targets: Union[int, stim.GateTarget, stim.PauliString, Iterable[Union[int, stim.GateTarget, stim.PauliString]]], arg: Union[float, Iterable[float], None] = None, *, tag: str = "", ) -> None: pass @overload def append( self, name: Union[stim.CircuitInstruction, stim.CircuitRepeatBlock, stim.Circuit], ) -> None: pass def append( self, name: object, targets: object = (), arg: object = None, *, tag: str = '', ) -> None: """Appends an operation into the circuit. Note: `stim.Circuit.append_operation` is an alias of `stim.Circuit.append`. Args: name: The name of the operation's gate (e.g. "H" or "M" or "CNOT"). This argument can also be set to a `stim.CircuitInstruction` or `stim.CircuitInstructionBlock`, which results in the instruction or block being appended to the circuit. The other arguments (targets and arg) can't be specified when doing so. (The argument being called `name` is no longer quite right, but is being kept for backwards compatibility.) targets: The objects operated on by the gate. This can be either a single target or an iterable of multiple targets. Each target can be: An int: The index of a targeted qubit. A `stim.GateTarget`: Could be a variety of things. Methods like `stim.target_rec`, `stim.target_sweet`, `stim.target_x`, and `stim.CircuitInstruction.__getitem__` all return this type. A `stim.PauliString`: This will automatically be expanded into a product of pauli targets like `X1*Y2*Z3`. arg: The "parens arguments" for the gate, such as the probability for a noise operation. A double or list of doubles parameterizing the gate. Different gates take different parens arguments. For example, X_ERROR takes a probability, OBSERVABLE_INCLUDE takes an observable index, and PAULI_CHANNEL_1 takes three disjoint probabilities. Note: Defaults to no parens arguments. Except, for backwards compatibility reasons, `cirq.append_operation` (but not `cirq.append`) will default to a single 0.0 argument for gates that take exactly one argument. tag: A customizable string attached to the instruction. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append("X", 0) >>> c.append("H", [0, 1]) >>> c.append("M", [0, stim.target_inv(1)]) >>> c.append("CNOT", [stim.target_rec(-1), 0]) >>> c.append("X_ERROR", [0], 0.125) >>> c.append("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(2)], 0.25) >>> c.append("MPP", [stim.PauliString("X1*Y2"), stim.GateTarget("Z3")]) >>> print(repr(c)) stim.Circuit(''' X 0 H 0 1 M 0 !1 CX rec[-1] 0 X_ERROR(0.125) 0 E(0.25) X0 Y2 MPP X1*Y2 Z3 ''') """ ``` ```python # stim.Circuit.append_from_stim_program_text # (in class stim.Circuit) def append_from_stim_program_text( self, stim_program_text: str, ) -> None: """Appends operations described by a STIM format program into the circuit. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append_from_stim_program_text(''' ... H 0 # comment ... CNOT 0 2 ... ... M 2 ... CNOT rec[-1] 1 ... ''') >>> print(c) H 0 CX 0 2 M 2 CX rec[-1] 1 Args: stim_program_text: The STIM program text containing the circuit operations to append. """ ``` ```python # stim.Circuit.approx_equals # (in class stim.Circuit) def approx_equals( self, other: object, *, atol: float, ) -> bool: """Checks if a circuit is approximately equal to another circuit. Two circuits are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example, `X_ERROR(0.100) 0` is approximately equal to `X_ERROR(0.099)` within an absolute tolerance of 0.002. All other details of the circuits (such as the ordering of instructions and targets) must be exactly the same. Args: other: The circuit, or other object, to compare to this one. atol: The absolute error tolerance. The maximum amount each probability may have been perturbed by. Returns: True if the given object is a circuit approximately equal up to the receiving circuit up to the given tolerance, otherwise False. Examples: >>> import stim >>> base = stim.Circuit(''' ... X_ERROR(0.099) 0 1 2 ... M 0 1 2 ... ''') >>> base.approx_equals(base, atol=0) True >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0) False >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0.0001) False >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0.01) True >>> base.approx_equals(stim.Circuit(''' ... DEPOLARIZE1(0.099) 0 1 2 ... MRX 0 1 2 ... '''), atol=9999) False """ ``` ```python # stim.Circuit.clear # (in class stim.Circuit) def clear( self, ) -> None: """Clears the contents of the circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c.clear() >>> c stim.Circuit() """ ``` ```python # stim.Circuit.compile_detector_sampler # (in class stim.Circuit) def compile_detector_sampler( self, *, seed: object = None, ) -> stim.CompiledDetectorSampler: """Returns an object that can batch sample detection events from the circuit. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[False]]) """ ``` ```python # stim.Circuit.compile_m2d_converter # (in class stim.Circuit) def compile_m2d_converter( self, *, skip_reference_sample: bool = False, ) -> stim.CompiledMeasurementsToDetectionEventsConverter: """Creates a measurement-to-detection-event converter for the given circuit. The converter can efficiently compute detection events and observable flips from raw measurement data. The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is. Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit. Args: skip_reference_sample: Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution). Returns: An initialized stim.CompiledMeasurementsToDetectionEventsConverter. Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> converter.convert( ... measurements=np.array([[0], [1]], dtype=np.bool_), ... append_observables=False, ... ) array([[ True], [False]]) """ ``` ```python # stim.Circuit.compile_sampler # (in class stim.Circuit) def compile_sampler( self, *, skip_reference_sample: bool = False, seed: Optional[int] = None, reference_sample: Optional[np.ndarray] = None, ) -> stim.CompiledMeasurementSampler: """Returns an object that can quickly batch sample measurements from the circuit. Args: skip_reference_sample: Defaults to False. When set to True, the reference sample used by the sampler is initialized to all-zeroes instead of being collected from the circuit. This means that the results returned by the sampler are actually whether or not each measurement was *flipped*, instead of true measurement results. Forcing an all-zero reference sample is useful when you are only interested in error propagation and don't want to have to deal with the fact that some measurements want to be On when no errors occur. It is also useful when you know for sure that the all-zero result is actually a possible result from the circuit (under noiseless execution), meaning it is a valid reference sample as good as any other. Computing the reference sample is the most time consuming and memory intensive part of simulating the circuit, so promising that the simulator can safely skip that step is an effective optimization. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. reference_sample: The data to xor into the measurement flips produced by the frame simulator, in order to produce proper measurement results. This can either be specified as an `np.bool_` array or a bit packed `np.uint8` array (little endian). Under normal conditions, the reference sample should be a valid noiseless sample of the circuit, such as the one returned by `circuit.reference_sample()`. If this argument is not provided, the reference sample will be set to `circuit.reference_sample()`, unless `skip_reference_sample=True` is used, in which case it will be set to all-zeros. Raises: ValueError: skip_reference_sample is True and reference_sample is not None. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 2 ... M 0 1 2 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[False, False, True]]) """ ``` ```python # stim.Circuit.copy # (in class stim.Circuit) def copy( self, ) -> stim.Circuit: """Returns a copy of the circuit. An independent circuit with the same contents. Examples: >>> import stim >>> c1 = stim.Circuit("H 0") >>> c2 = c1.copy() >>> c2 is c1 False >>> c2 == c1 True """ ``` ```python # stim.Circuit.count_determined_measurements # (in class stim.Circuit) def count_determined_measurements( self, *, unknown_input: bool = False, ) -> int: """Counts the number of predictable measurements in the circuit. This method ignores any noise in the circuit. This method works by performing a tableau stabilizer simulation of the circuit and, before each measurement is simulated, checking if its expectation is non-zero. A measurement is predictable if its result can be predicted by using other measurements that have already been performed, assuming the circuit is executed without any noise. Note that, when multiple measurements occur at the same time, re-ordering the order they are resolved can change which specific measurements are predictable but won't change how many of them were predictable in total. The number of predictable measurements is a useful quantity because it's related to the number of detectors and observables that a circuit should declare. If circuit.num_detectors + circuit.num_observables is less than circuit.count_determined_measurements(), this is a warning sign that you've missed some detector declarations. The exact relationship between the number of determined measurements and the number of detectors and observables can differ from code to code. For example, the toric code has an extra redundant measurement compared to the surface code because in the toric code the last X stabilizer to be measured is equal to the product of all other X stabilizers even in the first round when initializing in the Z basis. Typically this relationship is not declared as a detector, because it's not local, or as an observable, because it doesn't store a qubit. Args: unknown_input: Defaults to False (inputs assumed to be in the |0> state). When set to True, the inputs are instead treated as being in unknown random states. For example, this means that Z-basis measurements at the very beginning of the circuit will be considered random rather than determined. Returns: The number of measurements that were predictable. Examples: >>> import stim >>> stim.Circuit(''' ... R 0 ... M 0 ... ''').count_determined_measurements() 1 >>> stim.Circuit(''' ... R 0 ... H 0 ... M 0 ... ''').count_determined_measurements() 0 >>> stim.Circuit(''' ... M 0 ... ''').count_determined_measurements() 1 >>> stim.Circuit(''' ... M 0 ... ''').count_determined_measurements(unknown_input=True) 0 >>> stim.Circuit(''' ... M 0 ... M 0 1 ... M 0 1 2 ... M 0 1 2 3 ... ''').count_determined_measurements(unknown_input=True) 6 >>> stim.Circuit(''' ... R 0 1 ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... ''').count_determined_measurements() 2 >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... distance=5, ... rounds=9, ... ) >>> circuit.count_determined_measurements() 217 >>> circuit.num_detectors + circuit.num_observables 217 """ ``` ```python # stim.Circuit.decomposed # (in class stim.Circuit) def decomposed( self, ) -> stim.Circuit: """Recreates the circuit using (mostly) the {H,S,CX,M,R} gate set. The intent of this method is to simplify the circuit to use fewer gate types, so it's easier for other tools to consume. Currently, this method performs the following simplifications: - Single qubit cliffords are decomposed into {H,S}. - Multi-qubit cliffords are decomposed into {H,S,CX}. - Single qubit dissipative gates are decomposed into {H,S,M,R}. - Multi-qubit dissipative gates are decomposed into {H,S,CX,M,R}. Currently, the following types of gate *aren't* simplified, but they may be in the future: - Noise instructions (like X_ERROR, DEPOLARIZE2, and E). - Annotations (like TICK, DETECTOR, and SHIFT_COORDS). - The MPAD instruction. - Repeat blocks are not flattened. Returns: A `stim.Circuit` whose function is equivalent to the original circuit, but with most gates decomposed into the {H,S,CX,M,R} gate set. Examples: >>> import stim >>> stim.Circuit(''' ... SWAP 0 1 ... ''').decomposed() stim.Circuit(''' CX 0 1 1 0 0 1 ''') >>> stim.Circuit(''' ... ISWAP 0 1 2 1 ... TICK ... MPP !X1*Y2*Z3 ... ''').decomposed() stim.Circuit(''' H 0 CX 0 1 1 0 H 1 S 1 0 H 2 CX 2 1 1 2 H 1 S 1 2 TICK H 1 2 S 2 H 2 S 2 2 CX 2 1 3 1 M !1 CX 2 1 3 1 H 2 S 2 H 2 S 2 2 H 1 ''') """ ``` ```python # stim.Circuit.detecting_regions # (in class stim.Circuit) def detecting_regions( self, *, targets: Optional[Iterable[stim.DemTarget | str | Iterable[float]]] = None, ticks: Optional[Iterable[int]] = None, ) -> Dict[stim.DemTarget, Dict[int, stim.PauliString]]: """Records where detectors and observables are sensitive to errors over time. The result of this method is a nested dictionary, mapping detectors/observables and ticks to Pauli sensitivities for that detector/observable at that time. For example, if observable 2 has Z-type sensitivity on qubits 5 and 6 during tick 3, then `result[stim.target_logical_observable_id(2)][3]` will be equal to `stim.PauliString("Z5*Z6")`. If you want sensitivities from more places in the circuit, besides just at the TICK instructions, you can work around this by making a version of the circuit with more TICKs. Args: targets: Defaults to everything (None). When specified, this should be an iterable of filters where items matching any one filter are included. A variety of filters are supported: stim.DemTarget: Includes the targeted detector or observable. Iterable[float]: Coordinate prefix match. Includes detectors whose coordinate data begins with the same floats. "D": Includes all detectors. "L": Includes all observables. "D#" (e.g. "D5"): Includes the detector with the specified index. "L#" (e.g. "L5"): Includes the observable with the specified index. ticks: Defaults to everything (None). When specified, this should be a list of integers corresponding to the tick indices to report sensitivities for. ignore_anticommutation_errors: Defaults to False. When set to False, invalid detecting regions that anticommute with a reset will cause the method to raise an exception. When set to True, the offending component will simply be silently dropped. This can result in broken detectors having apparently enormous detecting regions. Returns: Nested dictionaries keyed first by a `stim.DemTarget` identifying the detector or observable, then by the index of the tick, leading to a PauliString with that target's error sensitivity at that tick. Note you can use `stim.PauliString.pauli_indices` to quickly get to the non-identity terms in the sensitivity. Examples: >>> import stim >>> detecting_regions = stim.Circuit(''' ... R 0 ... TICK ... H 0 ... TICK ... CX 0 1 ... TICK ... MX 0 1 ... DETECTOR rec[-1] rec[-2] ... ''').detecting_regions() >>> for target, tick_regions in detecting_regions.items(): ... print("target", target) ... for tick, sensitivity in tick_regions.items(): ... print(" tick", tick, "=", sensitivity) target D0 tick 0 = +Z_ tick 1 = +X_ tick 2 = +XX >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... rounds=5, ... distance=4, ... ) >>> detecting_regions = circuit.detecting_regions( ... targets=["L0", (2, 4), stim.DemTarget.relative_detector_id(5)], ... ticks=range(5, 15), ... ) >>> for target, tick_regions in detecting_regions.items(): ... print("target", target) ... for tick, sensitivity in tick_regions.items(): ... print(" tick", tick, "=", sensitivity) target D1 tick 5 = +____________________X______________________ tick 6 = +____________________Z______________________ target D5 tick 5 = +______X____________________________________ tick 6 = +______Z____________________________________ target D14 tick 5 = +__________X_X______XXX_____________________ tick 6 = +__________X_X______XZX_____________________ tick 7 = +__________X_X______XZX_____________________ tick 8 = +__________X_X______XXX_____________________ tick 9 = +__________XXX_____XXX______________________ tick 10 = +__________XXX_______X______________________ tick 11 = +__________X_________X______________________ tick 12 = +____________________X______________________ tick 13 = +____________________Z______________________ target D29 tick 7 = +____________________Z______________________ tick 8 = +____________________X______________________ tick 9 = +____________________XX_____________________ tick 10 = +___________________XXX_______X_____________ tick 11 = +____________X______XXXX______X_____________ tick 12 = +__________X_X______XXX_____________________ tick 13 = +__________X_X______XZX_____________________ tick 14 = +__________X_X______XZX_____________________ target D44 tick 14 = +____________________Z______________________ target L0 tick 5 = +_X________X________X________X______________ tick 6 = +_X________X________X________X______________ tick 7 = +_X________X________X________X______________ tick 8 = +_X________X________X________X______________ tick 9 = +_X________X_______XX________X______________ tick 10 = +_X________X________X________X______________ tick 11 = +_X________XX_______X________XX_____________ tick 12 = +_X________X________X________X______________ tick 13 = +_X________X________X________X______________ tick 14 = +_X________X________X________X______________ """ ``` ```python # stim.Circuit.detector_error_model # (in class stim.Circuit) def detector_error_model( self, *, decompose_errors: bool = False, flatten_loops: bool = False, allow_gauge_detectors: bool = False, approximate_disjoint_errors: float = False, ignore_decomposition_failures: bool = False, block_decomposition_from_introducing_remnant_edges: bool = False, ) -> stim.DetectorErrorModel: """Returns a stim.DetectorErrorModel describing the error processes in the circuit. Args: decompose_errors: Defaults to false. When set to true, the error analysis attempts to decompose the components of composite error mechanisms (such as depolarization errors) into simpler errors, and suggest this decomposition via `stim.target_separator()` between the components. For example, in an XZ surface code, single qubit depolarization has a Y error term which can be decomposed into simpler X and Z error terms. Decomposition fails (causing this method to throw) if it's not possible to decompose large errors into simple errors that affect at most two detectors. flatten_loops: Defaults to false. When set to True, the output will not contain any `repeat` blocks. When set to False, the error analysis watches for loops in the circuit reaching a periodic steady state with respect to the detectors being introduced, the error mechanisms that affect them, and the locations of the logical observables. When it identifies such a steady state, it outputs a repeat block. This is massively more efficient than flattening for circuits that contain loops, but creates a more complex output. allow_gauge_detectors: Defaults to false. When set to false, the error analysis verifies that detectors in the circuit are actually deterministic under noiseless execution of the circuit. When set to True, these detectors are instead considered to be part of degrees freedom that can be removed from the error model. For example, if detectors D1 and D3 both anti-commute with a reset, then the error model has a gauge `error(0.5) D1 D3`. When gauges are identified, one of the involved detectors is removed from the system using Gaussian elimination. Note that logical observables are still verified to be deterministic, even if this option is set. approximate_disjoint_errors: Defaults to false. When set to false, composite error mechanisms with disjoint components (such as `PAULI_CHANNEL_1(0.1, 0.2, 0.0)`) can cause the error analysis to throw exceptions (because detector error models can only contain independent error mechanisms). When set to true, the probabilities of the disjoint cases are instead assumed to be independent probabilities. For example, a `PAULI_CHANNEL_1(0.1, 0.2, 0.0)` becomes equivalent to an `X_ERROR(0.1)` followed by a `Y_ERROR(0.2)`. This assumption is an approximation, but it is a good approximation for small probabilities. This argument can also be set to a probability between 0 and 1, setting a threshold below which the approximation is acceptable. Any error mechanisms that have a component probability above the threshold will cause an exception to be thrown. ignore_decomposition_failures: Defaults to False. When this is set to True, circuit errors that fail to decompose into graphlike detector error model errors no longer cause the conversion process to abort. Instead, the undecomposed error is inserted into the output. Whatever tool the detector error model is then given to is responsible for dealing with the undecomposed errors (e.g. a tool may choose to simply ignore them). Irrelevant unless decompose_errors=True. block_decomposition_from_introducing_remnant_edges: Defaults to False. Requires that both A B and C D be present elsewhere in the detector error model in order to decompose A B C D into A B ^ C D. Normally, only one of A B or C D needs to appear to allow this decomposition. Remnant edges can be a useful feature for ensuring decomposition succeeds, but they can also reduce the effective code distance by giving the decoder single edges that actually represent multiple errors in the circuit (resulting in the decoder making misinformed choices when decoding). Irrelevant unless decompose_errors=True. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model() stim.DetectorErrorModel(''' error(0.125) D0 error(0.375) D0 D1 error(0.25) D1 ''') """ ``` ```python # stim.Circuit.diagram # (in class stim.Circuit) def diagram( self, type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "interactive", "interactive-html"] = 'timeline-text', *, tick: Union[None, int, range] = None, filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), rows: int | None = None, ) -> 'stim._DiagramHelper': """Returns a diagram of the circuit, from a variety of options. Args: type: The type of diagram. Available types are: "timeline-text" (default): An ASCII diagram of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-svg": An SVG image of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-svg-html": A resizable SVG image viewer of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-3d": A 3d model, in GLTF format, of the operations applied by the circuit over time. "timeline-3d-html": Same 3d model as 'timeline-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. "detslice-text": An ASCII diagram of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. "detslice-svg": An SVG image of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. For example, a detector slice diagram of a CSS surface code circuit during the TICK between a measurement layer and a reset layer will produce the usual diagram of a surface code. Uses the Pauli color convention XYZ=RGB. "detslice-svg-html": Same as detslice-svg but the SVG image is inside a resizable HTML iframe. "matchgraph-svg": An SVG image of the match graph extracted from the circuit by stim.Circuit.detector_error_model. "matchgraph-svg-html": Same as matchgraph-svg but the SVG image is inside a resizable HTML iframe. "matchgraph-3d": An 3D model of the match graph extracted from the circuit by stim.Circuit.detector_error_model. "matchgraph-3d-html": Same 3d model as 'match-graph-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. "timeslice-svg": An SVG image of the operations applied between two TICK instructions in the circuit, with the operations laid out in 2d. "timeslice-svg-html": Same as timeslice-svg but the SVG image is inside a resizable HTML iframe. "detslice-with-ops-svg": A combination of timeslice-svg and detslice-svg, with the operations overlaid over the detector slices taken from the TICK after the operations were applied. "detslice-with-ops-svg-html": Same as detslice-with-ops-svg but the SVG image is inside a resizable HTML iframe. "interactive" or "interactive-html": An HTML web page containing Crumble (an interactive editor for 2D stabilizer circuits) initialized with the given circuit as its default contents. tick: Required for detector and time slice diagrams. Specifies which TICK instruction, or range of TICK instructions, to slice at. Note that the first TICK instruction in the circuit corresponds tick=1. The value tick=0 refers to the very start of the circuit. Passing `range(A, B)` for a detector slice will show the slices for ticks A through B including A but excluding B. Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. rows: In diagrams that have multiple separate pieces, such as timeslice diagrams and detslice diagrams, this controls how many rows of pieces there will be. If not specified, a number of rows that creates a roughly square layout will be chosen. filter_coords: A list of things to include in the diagram. Different effects depending on the diagram. For detslice diagrams, the filter defaults to showing all detectors and no observables. When specified, each list entry can be a collection of floats (detectors whose coordinates start with the same numbers will be included), a stim.DemTarget (specifying a detector or observable to include), a string like "D5" or "L0" specifying a detector or observable to include. Returns: An object whose `__str__` method returns the diagram, so that writing the diagram to a file works correctly. The returned object may also define methods such as `_repr_html_`, so that ipython notebooks recognize it can be shown using a specialized viewer instead of as raw text. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... CNOT 0 1 1 2 ... ''') >>> print(circuit.diagram()) q0: -H-@--- | q1: ---X-@- | q2: -----X- >>> circuit = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... TICK ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.diagram("detslice-text", tick=1)) q0: -Z:D0- | q1: -Z:D0- """ ``` ```python # stim.Circuit.explain_detector_error_model_errors # (in class stim.Circuit) def explain_detector_error_model_errors( self, *, dem_filter: object = None, reduce_to_one_representative_error: bool = False, ) -> List[stim.ExplainedError]: """Explains how detector error model errors are produced by circuit errors. Args: dem_filter: Defaults to None (unused). When used, the output will only contain detector error model errors that appear in the given `stim.DetectorErrorModel`. Any error mechanisms from the detector error model that can't be reproduced using one error from the circuit will also be included in the result, but with an empty list of associated circuit error mechanisms. reduce_to_one_representative_error: Defaults to False. When True, the items in the result will contain at most one circuit error mechanism. Returns: A `List[stim.ExplainedError]` (see `stim.ExplainedError` for more information). Each item in the list describes how a detector error model error can be produced by individual circuit errors. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... # Create Bell pair. ... H 0 ... CNOT 0 1 ... ... # Noise. ... DEPOLARIZE1(0.01) 0 ... ... # Bell basis measurement. ... CNOT 0 1 ... H 0 ... M 0 1 ... ... # Both measurements should be False under noiseless execution. ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... ''') >>> explained_errors = circuit.explain_detector_error_model_errors( ... dem_filter=stim.DetectorErrorModel('error(1) D0 D1'), ... reduce_to_one_representative_error=True, ... ) >>> print(explained_errors[0].circuit_error_locations[0]) CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 0 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.01) 0 } """ ``` ```python # stim.Circuit.flattened # (in class stim.Circuit) def flattened( self, ) -> stim.Circuit: """Creates an equivalent circuit without REPEAT or SHIFT_COORDS. Returns: A `stim.Circuit` with the same instructions in the same order, but with loops flattened into repeated instructions and with all coordinate shifts inlined. Examples: >>> import stim >>> stim.Circuit(''' ... REPEAT 5 { ... MR 0 1 ... DETECTOR(0, 0) rec[-2] ... DETECTOR(1, 0) rec[-1] ... SHIFT_COORDS(0, 1) ... } ... ''').flattened() stim.Circuit(''' MR 0 1 DETECTOR(0, 0) rec[-2] DETECTOR(1, 0) rec[-1] MR 0 1 DETECTOR(0, 1) rec[-2] DETECTOR(1, 1) rec[-1] MR 0 1 DETECTOR(0, 2) rec[-2] DETECTOR(1, 2) rec[-1] MR 0 1 DETECTOR(0, 3) rec[-2] DETECTOR(1, 3) rec[-1] MR 0 1 DETECTOR(0, 4) rec[-2] DETECTOR(1, 4) rec[-1] ''') """ ``` ```python # stim.Circuit.flow_generators # (in class stim.Circuit) def flow_generators( self, ) -> List[stim.Flow]: """Returns a list of flows that generate all of the circuit's flows. Every stabilizer flow that the circuit implements is a product of some subset of the returned generators. Every returned flow will be a flow of the circuit. Returns: A list of flow generators for the circuit. Examples: >>> import stim >>> stim.Circuit("H 0").flow_generators() [stim.Flow("X -> Z"), stim.Flow("Z -> X")] >>> stim.Circuit("M 0").flow_generators() [stim.Flow("1 -> Z xor rec[0]"), stim.Flow("Z -> rec[0]")] >>> stim.Circuit("RX 0").flow_generators() [stim.Flow("1 -> X")] >>> for flow in stim.Circuit("MXX 0 1").flow_generators(): ... print(flow) 1 -> XX xor rec[0] _X -> _X X_ -> _X xor rec[0] ZZ -> ZZ >>> for flow in stim.Circuit.generated( ... "repetition_code:memory", ... rounds=2, ... distance=3, ... after_clifford_depolarization=1e-3, ... ).flow_generators(): ... print(flow) 1 -> rec[0] 1 -> rec[1] 1 -> rec[2] 1 -> rec[3] 1 -> rec[4] 1 -> rec[5] 1 -> rec[6] 1 -> ____Z 1 -> ___Z_ 1 -> __Z__ 1 -> _Z___ 1 -> Z____ """ ``` ```python # stim.Circuit.from_file # (in class stim.Circuit) @staticmethod def from_file( file: Union[io.TextIOBase, str, pathlib.Path], ) -> stim.Circuit: """Reads a stim circuit from a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md Args: file: A file path or open file object to read from. Returns: The circuit parsed from the file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('H 5', file=f) ... circuit = stim.Circuit.from_file(path) >>> circuit stim.Circuit(''' H 5 ''') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('CNOT 4 5', file=f) ... with open(path) as f: ... circuit = stim.Circuit.from_file(f) >>> circuit stim.Circuit(''' CX 4 5 ''') """ ``` ```python # stim.Circuit.generated # (in class stim.Circuit) @staticmethod def generated( code_task: str, *, distance: int, rounds: int, after_clifford_depolarization: float = 0.0, before_round_data_depolarization: float = 0.0, before_measure_flip_probability: float = 0.0, after_reset_flip_probability: float = 0.0, ) -> stim.Circuit: """Generates common circuits. The generated circuits can include configurable noise. The generated circuits include DETECTOR and OBSERVABLE_INCLUDE annotations so that their detection events and logical observables can be sampled. The generated circuits include TICK annotations to mark the progression of time. (E.g. so that converting them using `stimcirq.stim_circuit_to_cirq_circuit` will produce a `cirq.Circuit` with the intended moment structure.) Args: code_task: A string identifying the type of circuit to generate. Available code tasks are: - "repetition_code:memory" - "surface_code:rotated_memory_x" - "surface_code:rotated_memory_z" - "surface_code:unrotated_memory_x" - "surface_code:unrotated_memory_z" - "color_code:memory_xyz" distance: The desired code distance of the generated circuit. The code distance is the minimum number of physical errors needed to cause a logical error. This parameter indirectly determines how many qubits the generated circuit uses. rounds: How many times the measurement qubits in the generated circuit will be measured. Indirectly determines the duration of the generated circuit. after_clifford_depolarization: Defaults to 0. The probability (p) of `DEPOLARIZE1(p)` operations to add after every single-qubit Clifford operation and `DEPOLARIZE2(p)` operations to add after every two-qubit Clifford operation. The after-Clifford depolarizing operations are only included if this probability is not 0. before_round_data_depolarization: Defaults to 0. The probability (p) of `DEPOLARIZE1(p)` operations to apply to every data qubit at the start of a round of stabilizer measurements. The start-of-round depolarizing operations are only included if this probability is not 0. before_measure_flip_probability: Defaults to 0. The probability (p) of `X_ERROR(p)` operations applied to qubits before each measurement (X basis measurements use `Z_ERROR(p)` instead). The before-measurement flips are only included if this probability is not 0. after_reset_flip_probability: Defaults to 0. The probability (p) of `X_ERROR(p)` operations applied to qubits after each reset (X basis resets use `Z_ERROR(p)` instead). The after-reset flips are only included if this probability is not 0. Returns: The generated circuit. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... distance=4, ... rounds=10000, ... after_clifford_depolarization=0.0125) >>> print(circuit) R 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.0125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.0125) 2 1 4 3 6 5 TICK MR 1 3 5 DETECTOR(1, 0) rec[-3] DETECTOR(3, 0) rec[-2] DETECTOR(5, 0) rec[-1] REPEAT 9999 { TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.0125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.0125) 2 1 4 3 6 5 TICK MR 1 3 5 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-3] rec[-6] DETECTOR(3, 0) rec[-2] rec[-5] DETECTOR(5, 0) rec[-1] rec[-4] } M 0 2 4 6 DETECTOR(1, 1) rec[-3] rec[-4] rec[-7] DETECTOR(3, 1) rec[-2] rec[-3] rec[-6] DETECTOR(5, 1) rec[-1] rec[-2] rec[-5] OBSERVABLE_INCLUDE(0) rec[-1] """ ``` ```python # stim.Circuit.get_detector_coordinates # (in class stim.Circuit) def get_detector_coordinates( self, only: object = None, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of detectors in the circuit. Args: only: Defaults to None (meaning include all detectors). A list of detector indices to include in the result. Detector indices beyond the end of the detector error model of the circuit cause an error. Returns: A dictionary mapping integers (detector indices) to lists of floats (coordinates). Detectors with no specified coordinate data are mapped to an empty tuple. If `only` is specified, then `set(result.keys()) == set(only)`. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... M 0 ... DETECTOR rec[-1] ... DETECTOR(1, 2, 3) rec[-1] ... REPEAT 3 { ... DETECTOR(42) rec[-1] ... SHIFT_COORDS(100) ... } ... ''') >>> circuit.get_detector_coordinates() {0: [], 1: [1.0, 2.0, 3.0], 2: [42.0], 3: [142.0], 4: [242.0]} >>> circuit.get_detector_coordinates(only=[1]) {1: [1.0, 2.0, 3.0]} """ ``` ```python # stim.Circuit.get_final_qubit_coordinates # (in class stim.Circuit) def get_final_qubit_coordinates( self, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of qubits in the circuit. If a qubit's coordinates are specified multiple times, only the last specified coordinates are returned. Returns: A dictionary mapping qubit indices (integers) to coordinates (lists of floats). Qubits that never had their coordinates specified are not included in the result. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... QUBIT_COORDS(1, 2, 3) 1 ... ''') >>> circuit.get_final_qubit_coordinates() {1: [1.0, 2.0, 3.0]} """ ``` ```python # stim.Circuit.has_all_flows # (in class stim.Circuit) def has_all_flows( self, flows: Iterable[stim.Flow], *, unsigned: bool = False, ) -> bool: """Determines if the circuit has all the given stabilizer flow or not. This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster because, behind the scenes, the circuit can be iterated once instead of once per flow. This method ignores any noise in the circuit. Args: flows: An iterable of `stim.Flow` instances representing the flows to check. unsigned: Defaults to False. When False, the flows must be correct including the sign of the Pauli strings. When True, only the Pauli terms need to be correct; the signs are permitted to be inverted. In effect, this requires the circuit to be correct up to Pauli gates. Returns: True if the circuit has the given flow; False otherwise. Examples: >>> import stim >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> Y'), ... stim.Flow('Z -> X'), ... ]) False >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> -Y'), ... stim.Flow('Z -> X'), ... ]) True >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> Y'), ... stim.Flow('Z -> X'), ... ], unsigned=True) True Caveats: Currently, the unsigned=False version of this method is implemented by performing 256 randomized tests. Each test has a 50% chance of a false positive, and a 0% chance of a false negative. So, when the method returns True, there is technically still a 2^-256 chance the circuit doesn't have the flow. This is lower than the chance of a cosmic ray flipping the result. """ ``` ```python # stim.Circuit.has_flow # (in class stim.Circuit) def has_flow( self, flow: stim.Flow, *, unsigned: bool = False, ) -> bool: """Determines if the circuit has the given stabilizer flow or not. A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer P at the start of the circuit to the instantaneous stabilizer Q at the end of the circuit. The flow may be mediated by certain measurements. For example, a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and the CNOT flows implemented by the circuit involve these measurements. A flow like P -> Q means the circuit transforms P into Q. A flow like 1 -> P means the circuit prepares P. A flow like P -> 1 means the circuit measures P. A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). This method ignores any noise in the circuit. Args: flow: The flow to check for. unsigned: Defaults to False. When False, the flows must be correct including the sign of the Pauli strings. When True, only the Pauli terms need to be correct; the signs are permitted to be inverted. In effect, this requires the circuit to be correct up to Pauli gates. Returns: True if the circuit has the given flow; False otherwise. Examples: >>> import stim >>> m = stim.Circuit('M 0') >>> m.has_flow(stim.Flow('Z -> Z')) True >>> m.has_flow(stim.Flow('X -> X')) False >>> m.has_flow(stim.Flow('Z -> I')) False >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) True >>> m.has_flow(stim.Flow('Z -> rec[-1]')) True >>> cx58 = stim.Circuit('CX 5 8') >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) True >>> cx58.has_flow(stim.Flow('X_ -> XX')) False >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) True >>> stim.Circuit(''' ... RY 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("Y"), ... )) True >>> stim.Circuit(''' ... RY 0 ... X_ERROR(0.1) 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("Y"), ... )) True >>> stim.Circuit(''' ... RY 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("X"), ... )) False >>> stim.Circuit(''' ... CX 0 1 ... ''').has_flow(stim.Flow( ... input=stim.PauliString("+X_"), ... output=stim.PauliString("+XX"), ... )) True >>> stim.Circuit(''' ... # Lattice surgery CNOT ... R 1 ... MXX 0 1 ... MZZ 1 2 ... MX 1 ... ''').has_flow(stim.Flow( ... input=stim.PauliString("+X_X"), ... output=stim.PauliString("+__X"), ... measurements=[0, 2], ... )) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( ... stim.Flow("Y -> Y"), ... unsigned=True, ... ) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( ... stim.Flow("Y -> Y"), ... unsigned=False, ... ) False Caveats: Currently, the unsigned=False version of this method is implemented by performing 256 randomized tests. Each test has a 50% chance of a false positive, and a 0% chance of a false negative. So, when the method returns True, there is technically still a 2^-256 chance the circuit doesn't have the flow. This is lower than the chance of a cosmic ray flipping the result. """ ``` ```python # stim.Circuit.insert # (in class stim.Circuit) def insert( self, index: int, operation: Union[stim.CircuitInstruction, stim.Circuit], ) -> None: """Inserts an operation at the given index, pushing existing operations forward. Beware that inserted operations are automatically fused with the preceding and following operations, if possible. This can make it complex to reason about how the indices of operations change in response to insertions. Args: index: The index to insert at. Must satisfy -len(circuit) <= index < len(circuit). Negative indices are made non-negative by adding len(circuit) to them, so they refer to indices relative to the end of the circuit instead of the start. Instructions before the index are not shifted. Instructions that were at or after the index are shifted forwards as needed. operation: The object to insert. This can be a single stim.CircuitInstruction or an entire stim.Circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... S 1 ... X 2 ... ''') >>> c.insert(1, stim.CircuitInstruction("Y", [3, 4, 5])) >>> c stim.Circuit(''' H 0 Y 3 4 5 S 1 X 2 ''') >>> c.insert(-1, stim.Circuit("S 999\nCX 0 1\nCZ 2 3")) >>> c stim.Circuit(''' H 0 Y 3 4 5 S 1 999 CX 0 1 CZ 2 3 X 2 ''') """ ``` ```python # stim.Circuit.inverse # (in class stim.Circuit) def inverse( self, ) -> stim.Circuit: """Returns a circuit that applies the same operations but inverted and in reverse. If circuit starts with QUBIT_COORDS instructions, the returned circuit will still have the same QUBIT_COORDS instructions in the same order at the start. Returns: A `stim.Circuit` that applies inverted operations in the reverse order. Raises: ValueError: The circuit contains operations that don't have an inverse, such as measurements. There are also some unsupported operations such as SHIFT_COORDS. Examples: >>> import stim >>> stim.Circuit(''' ... S 0 1 ... ISWAP 0 1 1 2 ... ''').inverse() stim.Circuit(''' ISWAP_DAG 1 2 0 1 S_DAG 1 0 ''') >>> stim.Circuit(''' ... QUBIT_COORDS(1, 2) 0 ... QUBIT_COORDS(4, 3) 1 ... QUBIT_COORDS(9, 5) 2 ... H 0 1 ... REPEAT 100 { ... CX 0 1 1 2 ... TICK ... S 1 2 ... } ... ''').inverse() stim.Circuit(''' QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(4, 3) 1 QUBIT_COORDS(9, 5) 2 REPEAT 100 { S_DAG 2 1 TICK CX 1 2 0 1 } H 1 0 ''') """ ``` ```python # stim.Circuit.likeliest_error_sat_problem # (in class stim.Circuit) def likeliest_error_sat_problem( self, *, quantization: int = 100, format: str = 'WDIMACS', ) -> str: """Makes a maxSAT problem for the circuit's likeliest undetectable logical error. The output is a string describing the maxSAT problem in WDIMACS format (see https://jix.github.io/varisat/manual/0.2.0/formats/dimacs.html). The optimal solution to the problem is the highest likelihood set of error mechanisms that combine to flip any logical observable while producing no detection events). If there are any errors with probability p > 0.5, they are inverted so that the resulting weight ends up being positive. If there are errors with weight close or equal to 0.5, they can end up with 0 weight meaning that they can be included or not in the solution with no affect on the likelihood. There are many tools that can solve maxSAT problems in WDIMACS format. One quick way to get started is to install pysat by running this BASH terminal command: pip install python-sat Afterwards, you can run the included maxSAT solver "RC2" with this Python code: from pysat.examples.rc2 import RC2 from pysat.formula import WCNF wcnf = WCNF(from_string="p wcnf 1 2 3\n3 -1 0\n3 1 0\n") with RC2(wcnf) as rc2: print(rc2.compute()) print(rc2.cost) Much faster solvers are available online. For example, you can download one of the entries in the 2023 maxSAT competition (see https://maxsat-evaluations.github.io/2023) and run it on your problem by running these BASH terminal commands: wget https://maxsat-evaluations.github.io/2023/mse23-solver-src/exact/CASHWMaxSAT-CorePlus.zip unzip CASHWMaxSAT-CorePlus.zip ./CASHWMaxSAT-CorePlus/bin/cashwmaxsatcoreplus -bm -m your_problem.wcnf Args: format: Defaults to "WDIMACS", corresponding to WDIMACS format which is described here: http://www.maxhs.org/docs/wdimacs.html quantization: Defaults to 10. Error probabilities are converted to log-odds and scaled/rounded to be positive integers at most this large. Setting this argument to a larger number results in more accurate quantization such that the returned error set should have a likelihood closer to the true most likely solution. This comes at the cost of making some maxSAT solvers slower. Returns: A string corresponding to the contents of a maxSAT problem file in the requested format. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X_ERROR(0.1) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... X_ERROR(0.4) 0 ... M 0 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.likeliest_error_sat_problem( ... quantization=1000 ... ), end='') p wcnf 2 4 4001 185 -1 0 1000 -2 0 4001 -1 0 4001 2 0 """ ``` ```python # stim.Circuit.missing_detectors # (in class stim.Circuit) def missing_detectors( self, *, unknown_input: bool = False, ) -> int: """Finds deterministic measurements independent of declared detectors/observables. This method is useful for debugging missing detectors in a circuit, because it identifies generators for uncovered degrees of freedom. It's not recommended to use this method to solve for the detectors of a circuit. The returned detectors are not guaranteed to be stable across versions, and aren't optimized to be "good" (e.g. form a low weight basis or be matchable if possible). It will also identify things that are technically determined but that the user may not want to use as a detector, such as the fact that in the first round after transversal Z basis initialization of a toric code the product of all X stabilizer measurements is deterministic even though the individual measurements are all random. Args: unknown_input: Defaults to False (inputs assumed to be in the |0> state). When set to True, the inputs are instead treated as being in unknown random states. For example, this means that Z-basis measurements at the very beginning of the circuit will be considered random rather than determined. Returns: A circuit containing DETECTOR instructions that specify the uncovered degrees of freedom in the deterministic measurement sets of the input circuit. The returned circuit can be appended to the input circuit to get a circuit with no missing detectors. Examples: >>> import stim >>> stim.Circuit(''' ... R 0 ... M 0 ... ''').missing_detectors() stim.Circuit(''' DETECTOR rec[-1] ''') >>> stim.Circuit(''' ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... DEPOLARIZE1(0.1) 0 1 ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... DETECTOR rec[-1] rec[-4] ... DETECTOR rec[-2] rec[-5] ... DETECTOR rec[-3] rec[-6] ... ''').missing_detectors(unknown_input=True) stim.Circuit(''' DETECTOR rec[-3] rec[-2] rec[-1] ''') """ ``` ```python # stim.Circuit.num_detectors # (in class stim.Circuit) @property def num_detectors( self, ) -> int: """Counts the number of bits produced when sampling the circuit's detectors. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... DETECTOR rec[-1] ... REPEAT 100 { ... M 0 1 2 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... } ... ''') >>> c.num_detectors 201 """ ``` ```python # stim.Circuit.num_measurements # (in class stim.Circuit) @property def num_measurements( self, ) -> int: """Counts the number of bits produced when sampling the circuit's measurements. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... REPEAT 100 { ... M 0 1 ... } ... ''') >>> c.num_measurements 201 """ ``` ```python # stim.Circuit.num_observables # (in class stim.Circuit) @property def num_observables( self, ) -> int: """Counts the number of logical observables defined by the circuit. This is one more than the largest index that appears as an argument to an OBSERVABLE_INCLUDE instruction. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... OBSERVABLE_INCLUDE(2) rec[-1] ... OBSERVABLE_INCLUDE(5) rec[-1] ... ''') >>> c.num_observables 6 """ ``` ```python # stim.Circuit.num_qubits # (in class stim.Circuit) @property def num_qubits( self, ) -> int: """Counts the number of qubits used when simulating the circuit. This is always one more than the largest qubit index used by the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... X 0 ... M 0 1 ... ''').num_qubits 2 >>> stim.Circuit(''' ... X 0 ... M 0 1 ... H 100 ... ''').num_qubits 101 """ ``` ```python # stim.Circuit.num_sweep_bits # (in class stim.Circuit) @property def num_sweep_bits( self, ) -> int: """Returns the number of sweep bits needed to completely configure the circuit. This is always one more than the largest sweep bit index used by the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... CX sweep[2] 0 ... ''').num_sweep_bits 3 >>> stim.Circuit(''' ... CZ sweep[5] 0 ... CX sweep[2] 0 ... ''').num_sweep_bits 6 """ ``` ```python # stim.Circuit.num_ticks # (in class stim.Circuit) @property def num_ticks( self, ) -> int: """Counts the number of TICK instructions executed when running the circuit. TICKs in loops are counted once per iteration. Returns: The number of ticks executed by the circuit. Examples: >>> import stim >>> stim.Circuit().num_ticks 0 >>> stim.Circuit(''' ... TICK ... ''').num_ticks 1 >>> stim.Circuit(''' ... H 0 ... TICK ... CX 0 1 ... TICK ... ''').num_ticks 2 >>> stim.Circuit(''' ... H 0 ... TICK ... REPEAT 100 { ... CX 0 1 ... TICK ... } ... ''').num_ticks 101 """ ``` ```python # stim.Circuit.pop # (in class stim.Circuit) def pop( self, index: int = -1, ) -> Union[stim.CircuitInstruction, stim.CircuitRepeatBlock]: """Pops an operation from the end of the circuit, or at the given index. Args: index: Defaults to -1 (end of circuit). The index to pop from. Returns: The popped instruction. Raises: IndexError: The given index is outside the bounds of the circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... S 1 ... X 2 ... Y 3 ... ''') >>> c.pop() stim.CircuitInstruction('Y', [stim.GateTarget(3)], []) >>> c.pop(1) stim.CircuitInstruction('S', [stim.GateTarget(1)], []) >>> c stim.Circuit(''' H 0 X 2 ''') """ ``` ```python # stim.Circuit.reference_detector_and_observable_signs # (in class stim.Circuit) def reference_detector_and_observable_signs( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: """Determines noiseless parities of the measurement sets of detectors/observables. BEWARE: the returned values are NOT the "expected value of the detector/observable". Stim consistently defines the value of a detector/observable as whether or not it flipped, so the expected value of a detector/observable is vacuously always 0 (not flipped). This method instead returns the "sign"; the expected parity of the measurement set declared by the detector/observable. The sign is the baseline used to determine if a flip occurred. A detector/observable's value is whether its sign disagrees with the measured parity of its measurement set. Note that this method doesn't account for sweep bits. It will effectively ignore instructions like `CX sweep[0] 0`. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: A (det, obs) tuple with numpy arrays containing the reference parities. if bit_packed: det.shape == (math.ceil(num_detectors / 8),) det.dtype == np.uint8 obs.shape == (math.ceil(num_observables / 8),) obs.dtype == np.uint8 else: det.shape == (num_detectors,) det.dtype == np.bool_ obs.shape == (num_observables,) obs.dtype == np.bool_ Examples: >>> import stim >>> stim.Circuit(''' ... X 1 ... M 0 1 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... OBSERVABLE_INCLUDE(3) rec[-1] rec[-2] ... ''').reference_detector_and_observable_signs() (array([ True, False]), array([False, False, False, True])) """ ``` ```python # stim.Circuit.reference_sample # (in class stim.Circuit) def reference_sample( self, *, bit_packed: bool = False, ) -> np.ndarray: """Samples the given circuit in a deterministic fashion. Discards all noisy operations, and biases all collapse events towards +Z instead of randomly +Z/-Z. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: A numpy array containing the reference sample. if bit_packed: shape == (math.ceil(num_measurements / 8),) dtype == np.uint8 else: shape == (num_measurements,) dtype == np.bool_ Examples: >>> import stim >>> stim.Circuit(''' ... X 1 ... M 0 1 ... ''').reference_sample() array([False, True]) """ ``` ```python # stim.Circuit.search_for_undetectable_logical_errors # (in class stim.Circuit) def search_for_undetectable_logical_errors( self, *, dont_explore_detection_event_sets_with_size_above: int, dont_explore_edges_with_degree_above: int, dont_explore_edges_increasing_symptom_degree: bool, canonicalize_circuit_errors: bool = False, ) -> List[stim.ExplainedError]: """Searches for small sets of errors that form an undetectable logical error. THIS IS A HEURISTIC METHOD. It does not guarantee that it will find errors of particular sizes, or with particular properties. The errors it finds are a tangled combination of the truncation parameters you specify, internal optimizations which are correct when not truncating, and minutia of the circuit being considered. If you want a well behaved method that does provide guarantees of finding errors of a particular type, use `stim.Circuit.shortest_graphlike_error`. This method is more thorough than that (assuming you don't truncate so hard you omit graphlike edges), but exactly how thorough is difficult to describe. It's also not guaranteed that the behavior of this method will not be changed in the future in a way that permutes which logical errors are found and which are missed. This search method considers hyper errors, so it has worst case exponential runtime. It is important to carefully consider the arguments you are providing, which truncate the search space and trade cost for quality. The search progresses by starting from each error that crosses a logical observable, noting which detection events each error produces, and then iteratively adding in errors touching those detection events attempting to cancel out the detection event with the lowest index. Beware that the choice of logical observable can interact with the truncation options. Using different observables can change whether or not the search succeeds, even if those observables are equal modulo the stabilizers of the code. This is because the edges crossing logical observables are used as starting points for the search, and starting from different places along a path will result in different numbers of symptoms in intermediate states as the search progresses. For example, if the logical observable is next to a boundary, then the starting edges are likely boundary edges (degree 1) with 'room to grow', whereas if the observable was running through the bulk then the starting edges will have degree at least 2. Args: dont_explore_detection_event_sets_with_size_above: Truncates the search space by refusing to cross an edge (i.e. add an error) when doing so would produce an intermediate state that has more detection events than this limit. dont_explore_edges_with_degree_above: Truncates the search space by refusing to consider errors that cause a lot of detection events. For example, you may only want to consider graphlike errors which have two or fewer detection events. dont_explore_edges_increasing_symptom_degree: Truncates the search space by refusing to cross an edge (i.e. add an error) when doing so would produce an intermediate state that has more detection events that the previous intermediate state. This massively improves the efficiency of the search because instead of, for example, exploring all n^4 possible detection event sets with 4 symptoms, the search will attempt to cancel out symptoms one by one. canonicalize_circuit_errors: Whether or not to use one representative for equal-symptom circuit errors. False (default): Each DEM error lists every possible circuit error that single handedly produces those symptoms as a potential match. This is verbose but gives complete information. True: Each DEM error is matched with one possible circuit error that single handedly produces those symptoms, with a preference towards errors that are simpler (e.g. apply Paulis to fewer qubits). This discards mostly-redundant information about different ways to produce the same symptoms in order to give a succinct result. Returns: A list of error mechanisms that cause an undetected logical error. Each entry in the list is a `stim.ExplainedError` detailing the location and effects of a single physical error. The effects of the entire list combine to produce a logical frame change without any detection events. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... rounds=5, ... distance=5, ... after_clifford_depolarization=0.001) >>> print(len(circuit.search_for_undetectable_logical_errors( ... dont_explore_detection_event_sets_with_size_above=4, ... dont_explore_edges_with_degree_above=4, ... dont_explore_edges_increasing_symptom_degree=True, ... ))) 5 """ ``` ```python # stim.Circuit.shortest_error_sat_problem # (in class stim.Circuit) def shortest_error_sat_problem( self, *, format: str = 'WDIMACS', ) -> str: """Makes a maxSAT problem of the circuit's distance, that other tools can solve. The output is a string describing the maxSAT problem in WDIMACS format (see https://jix.github.io/varisat/manual/0.2.0/formats/dimacs.html). The optimal solution to the problem is the fault distance of the circuit (the minimum number of error mechanisms that combine to flip any logical observable while producing no detection events). This method ignores the probabilities of the error mechanisms since it only cares about minimizing the number of errors triggered. There are many tools that can solve maxSAT problems in WDIMACS format. One quick way to get started is to install pysat by running this BASH terminal command: pip install python-sat Afterwards, you can run the included maxSAT solver "RC2" with this Python code: from pysat.examples.rc2 import RC2 from pysat.formula import WCNF wcnf = WCNF(from_string="p wcnf 1 2 3\n3 -1 0\n3 1 0\n") with RC2(wcnf) as rc2: print(rc2.compute()) print(rc2.cost) Much faster solvers are available online. For example, you can download one of the entries in the 2023 maxSAT competition (see https://maxsat-evaluations.github.io/2023) and run it on your problem by running these BASH terminal commands: wget https://maxsat-evaluations.github.io/2023/mse23-solver-src/exact/CASHWMaxSAT-CorePlus.zip unzip CASHWMaxSAT-CorePlus.zip ./CASHWMaxSAT-CorePlus/bin/cashwmaxsatcoreplus -bm -m your_problem.wcnf Args: format: Defaults to "WDIMACS", corresponding to WDIMACS format which is described here: http://www.maxhs.org/docs/wdimacs.html Returns: A string corresponding to the contents of a maxSAT problem file in the requested format. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X_ERROR(0.1) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... X_ERROR(0.4) 0 ... M 0 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.shortest_error_sat_problem(), end='') p wcnf 2 4 5 1 -1 0 1 -2 0 5 -1 0 5 2 0 """ ``` ```python # stim.Circuit.shortest_graphlike_error # (in class stim.Circuit) def shortest_graphlike_error( self, *, ignore_ungraphlike_errors: bool = True, canonicalize_circuit_errors: bool = False, ) -> List[stim.ExplainedError]: """Finds a minimum set of graphlike errors to produce an undetected logical error. A "graphlike error" is an error that creates at most two detection events (causes a change in the parity of the measurement sets of at most two DETECTOR annotations). Note that this method does not pay attention to error probabilities (other than ignoring errors with probability 0). It searches for a logical error with the minimum *number* of physical errors, not the maximum probability of those physical errors all occurring. This method works by converting the circuit into a `stim.DetectorErrorModel` using `circuit.detector_error_model(...)`, computing the shortest graphlike error of the error model, and then converting the physical errors making up that logical error back into representative circuit errors. Args: ignore_ungraphlike_errors: False: Attempt to decompose any ungraphlike errors in the circuit into graphlike parts. If this fails, raise an exception instead of continuing. Note: in some cases, graphlike errors only appear as parts of decomposed ungraphlike errors. This can produce a result that lists DEM errors with zero matching circuit errors, because the only way to achieve those errors is by combining a decomposed error with a graphlike error. As a result, when using this option it is NOT guaranteed that the length of the result is an upper bound on the true code distance. That is only the case if every item in the result lists at least one matching circuit error. True (default): Ungraphlike errors are simply skipped as if they weren't present, even if they could become graphlike if decomposed. This guarantees the length of the result is an upper bound on the true code distance. canonicalize_circuit_errors: Whether or not to use one representative for equal-symptom circuit errors. False (default): Each DEM error lists every possible circuit error that single handedly produces those symptoms as a potential match. This is verbose but gives complete information. True: Each DEM error is matched with one possible circuit error that single handedly produces those symptoms, with a preference towards errors that are simpler (e.g. apply Paulis to fewer qubits). This discards mostly-redundant information about different ways to produce the same symptoms in order to give a succinct result. Returns: A list of error mechanisms that cause an undetected logical error. Each entry in the list is a `stim.ExplainedError` detailing the location and effects of a single physical error. The effects of the entire list combine to produce a logical frame change without any detection events. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... before_round_data_depolarization=0.01) >>> len(circuit.shortest_graphlike_error()) 7 """ ``` ```python # stim.Circuit.solve_flow_measurements # (in class stim.Circuit) def solve_flow_measurements( self, flows: List[stim.Flow], ) -> List[Optional[List[int]]]: """Finds measurements to explain the starts/ends of the given flows, ignoring sign. CAUTION: it's not guaranteed that the solutions returned by this method are minimal. It may use 20 measurements when only 2 are needed. The method applies some simple heuristics that attempt to reduce the size, but these heuristics aren't perfect and don't make any strong guarantees. The recommended way to use this method is on small parts of a circuit, such as a single surface code round. The ideal use case is when there is exactly one solution for each flow, because then the method behaves predictably and consistently. When there are multiple solutions, the method has no real way to pick out a "good" solution rather than a "cataclysmic trash fire of a" solution. For example, if you have a multi-round surface code circuit with open time boundaries and solve the flow 1 -> Z1*Z2*Z3*Z4, then there's a good solution (the Z1*Z2*Z3*Z4 measurement from the last round), various mediocre solutions (a Z1*Z2*Z3*Z4 measurement from a different round), and lots of terrible solutions (a combination of multiple Z1*Z2*Z3*Z4 measurements from an odd number of rounds, times a random combination of unrelated detectors). The method is permitted to return any of those solutions. Args: flows: A list of flows, each of which to be solved. Measurements and signs are entirely ignored. An error is raised if one of the given flows has an identity pauli string as its input and as its output, despite the fact that this case has a vacuous solution (no measurements). This error is only present as a safety check that catches some possible bugs in the calling code, such as accidentally applying this method to detector flows. This error may be removed in the future, so that the vacuous case succeeds vacuously. Returns: A list of solutions for each given flow. If no solution exists for flows[k], then solutions[k] is None. Otherwise, solutions[k] is a list of measurement indices for flows[k]. When solutions[k] is not None, it's guaranteed that circuit.has_flow(stim.Flow( input=flows[k].input, output=flows[k].output, measurements=solutions[k], ), unsigned=True) Raises: ValueError: A flow had an empty input and output. Examples: >>> import stim >>> stim.Circuit(''' ... M 2 ... ''').solve_flow_measurements([ ... stim.Flow("Z2 -> 1"), ... ]) [[0]] >>> stim.Circuit(''' ... M 2 ... ''').solve_flow_measurements([ ... stim.Flow("X2 -> X2"), ... ]) [None] >>> stim.Circuit(''' ... MXX 0 1 ... ''').solve_flow_measurements([ ... stim.Flow("YY -> ZZ"), ... ]) [[0]] >>> # Rep code cycle >>> stim.Circuit(''' ... R 1 3 ... CX 0 1 2 3 ... CX 4 3 2 1 ... M 1 3 ... ''').solve_flow_measurements([ ... stim.Flow("1 -> Z0*Z4"), ... stim.Flow("Z0 -> Z2"), ... stim.Flow("X0*X2*X4 -> X0*X2*X4"), ... stim.Flow("Y0 -> Y0"), ... ]) [[0, 1], [0], [], None] """ ``` ```python # stim.Circuit.time_reversed_for_flows # (in class stim.Circuit) def time_reversed_for_flows( self, flows: Iterable[stim.Flow], *, dont_turn_measurements_into_resets: bool = False, ) -> Tuple[stim.Circuit, List[stim.Flow]]: """Time-reverses the circuit while preserving error correction structure. This method returns a circuit that has the same internal detecting regions as the given circuit, as well as the same internal-to-external flows given in the `flows` argument, except they are all time-reversed. For example, if you pass a fault tolerant preparation circuit into this method (1 -> Z), the result will be a fault tolerant *measurement* circuit (Z -> 1). Or, if you pass a fault tolerant C_XYZ circuit into this method (X->Y, Y->Z, and Z->X), the result will be a fault tolerant C_ZYX circuit (X->Z, Y->X, and Z->Y). Note that this method doesn't guarantee that it will preserve the *sign* of the detecting regions or stabilizer flows. For example, inverting a memory circuit that preserves a logical observable (X->X and Z->Z) may produce a memory circuit that always bit flips the logical observable (X->X and Z->-Z) or that dynamically adjusts the logical observable in response to measurements (like "X -> X xor rec[-1]" and "Z -> Z xor rec[-2]"). This method will turn time-reversed resets into measurements, and attempts to turn time-reversed measurements into resets. A measurement will time-reverse into a reset if some annotated detectors, annotated observables, or given flows have detecting regions with sensitivity just before the measurement but none have detecting regions with sensitivity after the measurement. In some cases this method will have to introduce new operations. In particular, when a measurement-reset operation has a noisy result, time-reversing this measurement noise produces reset noise. But the measure-reset operations don't have built-in reset noise, so the reset noise is specified by adding an X_ERROR or Z_ERROR noise instruction after the time-reversed measure-reset operation. Args: flows: Flows you care about, that reach past the start/end of the given circuit. The result will contain an inverted flow for each of these given flows. You need this information because it reveals the measurements needed to produce the inverted flows that you care about. An exception will be raised if the circuit doesn't have all these flows. The inverted circuit will have the inverses of these flows (ignoring sign). dont_turn_measurements_into_resets: Defaults to False. When set to True, measurements will time-reverse into measurements even if nothing is sensitive to the measured qubit after the measurement completes. This guarantees the output circuit has *all* flows that the input circuit has (up to sign and feedback), even ones that aren't annotated. Returns: An (inverted_circuit, inverted_flows) tuple. inverted_circuit is the qec inverse of the given circuit. inverted_flows is a list of flows, matching up by index with the flows given as arguments to the method. The input, output, and sign fields of these flows are boring. The useful field is measurement_indices, because it's difficult to predict which measurements are needed for the inverted flow due to effects such as implicitly-included resets inverting into explicitly-included measurements. Caveats: Currently, this method doesn't compute the sign of the inverted flows. It unconditionally sets the sign to False. Examples: >>> import stim >>> inv_circuit, inv_flows = stim.Circuit(''' ... R 0 ... H 0 ... S 0 ... MY 0 ... DETECTOR rec[-1] ... ''').time_reversed_for_flows([]) >>> inv_circuit stim.Circuit(''' RY 0 S_DAG 0 H 0 M 0 DETECTOR rec[-1] ''') >>> inv_flows [] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows([ ... stim.Flow("Z -> rec[-1]"), ... ]) >>> inv_circuit stim.Circuit(''' R 0 ''') >>> inv_flows [stim.Flow("1 -> Z")] >>> inv_circuit.has_all_flows(inv_flows, unsigned=True) True >>> inv_circuit, inv_flows = stim.Circuit(''' ... R 0 ... ''').time_reversed_for_flows([ ... stim.Flow("1 -> Z"), ... ]) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("Z -> rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows([ ... stim.Flow("1 -> Z xor rec[-1]"), ... ]) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("Z -> rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows( ... flows=[stim.Flow("Z -> rec[-1]")], ... dont_turn_measurements_into_resets=True, ... ) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("1 -> Z xor rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... MR(0.125) 0 ... ''').time_reversed_for_flows([]) >>> inv_circuit stim.Circuit(''' MR 0 X_ERROR(0.125) 0 ''') >>> inv_flows [] >>> inv_circuit, inv_flows = stim.Circuit(''' ... MXX 0 1 ... H 0 ... ''').time_reversed_for_flows([ ... stim.Flow("ZZ -> YY xor rec[-1]"), ... stim.Flow("ZZ -> XZ"), ... ]) >>> inv_circuit stim.Circuit(''' H 0 MXX 0 1 ''') >>> inv_flows [stim.Flow("YY -> ZZ xor rec[-1]"), stim.Flow("XZ -> ZZ")] >>> stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... distance=2, ... rounds=1, ... ).time_reversed_for_flows([])[0] stim.Circuit(''' QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 1) 3 QUBIT_COORDS(1, 3) 6 QUBIT_COORDS(2, 2) 7 QUBIT_COORDS(3, 3) 8 QUBIT_COORDS(2, 4) 12 RX 8 6 3 1 MR 12 7 2 TICK H 12 2 TICK CX 1 7 12 6 TICK CX 6 7 12 8 TICK CX 3 7 2 1 TICK CX 8 7 2 3 TICK H 12 2 TICK M 12 7 2 DETECTOR(2, 0, 1) rec[-1] DETECTOR(2, 4, 1) rec[-3] MX 8 6 3 1 DETECTOR(2, 0, 0) rec[-5] rec[-2] rec[-1] DETECTOR(2, 4, 0) rec[-7] rec[-4] rec[-3] OBSERVABLE_INCLUDE(0) rec[-3] rec[-1] ''') """ ``` ```python # stim.Circuit.to_crumble_url # (in class stim.Circuit) def to_crumble_url( self, *, skip_detectors: bool = False, mark: Optional[Dict[int, List[stim.ExplainedError]]] = None, ) -> str: """Returns a URL that opens up crumble and loads this circuit into it. Crumble is a tool for editing stabilizer circuits, and visualizing their stabilizer flows. Its source code is in the `glue/crumble` directory of the stim code repository on github. A prebuilt version is made available at https://algassert.com/crumble, which is what the URL returned by this method will point to. Args: skip_detectors: Defaults to False. If set to True, detectors from the circuit aren't included in the crumble URL. This can reduce visual clutter in crumble, and improve its performance, since it doesn't need to indicate or track the sensitivity regions of detectors. mark: Defaults to None (no marks). If set to a dictionary from int to errors, such as `mark={1: circuit.shortest_graphlike_error()}`, then the errors will be highlighted and tracked forward by crumble. Returns: A URL that can be opened in a web browser. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... S 1 ... ''').to_crumble_url() 'https://algassert.com/crumble#circuit=H_0;CX_0_1;S_1_' >>> circuit = stim.Circuit(''' ... M(0.25) 0 1 2 ... DETECTOR rec[-1] rec[-2] ... DETECTOR rec[-2] rec[-3] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''') >>> err = circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) >>> circuit.to_crumble_url(skip_detectors=True, mark={1: err}) 'https://algassert.com/crumble#circuit=;TICK;MARKX(1)1;MARKX(1)2;MARKX(1)0;TICK;M(0.25)0_1_2;OI(0)rec[-1]_' """ ``` ```python # stim.Circuit.to_file # (in class stim.Circuit) def to_file( self, file: Union[io.TextIOBase, str, pathlib.Path], ) -> None: """Writes the stim circuit to a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md Args: file: A file path or an open file to write to. Examples: >>> import stim >>> import tempfile >>> c = stim.Circuit('H 5\nX 0') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... c.to_file(f) ... with open(path) as f: ... contents = f.read() >>> contents 'H 5\nX 0\n' >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... c.to_file(path) ... with open(path) as f: ... contents = f.read() >>> contents 'H 5\nX 0\n' """ ``` ```python # stim.Circuit.to_qasm # (in class stim.Circuit) def to_qasm( self, *, open_qasm_version: int, skip_dets_and_obs: bool = False, ) -> str: """Creates an equivalent OpenQASM implementation of the circuit. Args: open_qasm_version: The version of OpenQASM to target. This should be set to 2 or to 3. Differences between the versions are: - Support for operations on classical bits (only version 3). This means DETECTOR and OBSERVABLE_INCLUDE only work with version 3. - Support for feedback operations (only version 3). - Support for subroutines (only version 3). Without subroutines, non-standard dissipative gates like MR and RX need to decompose inline every single time they're used. - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc). skip_dets_and_obs: Defaults to False. When set to False, the output will include a `dets` register and an `obs` register (assuming the circuit has detectors and observables). These registers will be computed as part of running the circuit. This requires performing a simulation of the circuit, in order to correctly account for the expected value of measurements. When set to True, the `dets` and `obs` registers are not included in the output, and no simulation of the circuit is performed. Returns: The OpenQASM code as a string. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... R 0 1 ... X 1 ... H 0 ... CX 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... '''); >>> qasm = circuit.to_qasm(open_qasm_version=3); >>> print(qasm.strip().replace('\n\n', '\n')) OPENQASM 3.0; include "stdgates.inc"; qreg q[2]; creg rec[2]; creg dets[1]; reset q[0]; reset q[1]; x q[1]; h q[0]; cx q[0], q[1]; measure q[0] -> rec[0]; measure q[1] -> rec[1]; dets[0] = rec[1] ^ rec[0] ^ 1; """ ``` ```python # stim.Circuit.to_quirk_url # (in class stim.Circuit) def to_quirk_url( self, ) -> str: """Returns a URL that opens up quirk and loads this circuit into it. Quirk is an open source drag and drop circuit editor with support for up to 16 qubits. Its source code is available at https://github.com/strilanc/quirk and a prebuilt version is available at https://algassert.com/quirk, which is what the URL returned by this method will point to. Quirk doesn't support features like noise, feedback, or detectors. This method will simply drop any unsupported operations from the circuit when producing the URL. Returns: A URL that can be opened in a web browser. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... S 1 ... ''').to_quirk_url() 'https://algassert.com/quirk#circuit={"cols":[["H"],["•","X"],[1,"Z^½"]]}' """ ``` ```python # stim.Circuit.to_tableau # (in class stim.Circuit) def to_tableau( self, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False, ) -> stim.Tableau: """Converts the circuit into an equivalent stabilizer tableau. Args: ignore_noise: Defaults to False. When False, any noise operations in the circuit will cause the conversion to fail with an exception. When True, noise operations are skipped over as if they weren't even present in the circuit. ignore_measurement: Defaults to False. When False, any measurement operations in the circuit will cause the conversion to fail with an exception. When True, measurement operations are skipped over as if they weren't even present in the circuit. ignore_reset: Defaults to False. When False, any reset operations in the circuit will cause the conversion to fail with an exception. When True, reset operations are skipped over as if they weren't even present in the circuit. Returns: A tableau equivalent to the circuit (up to global phase). Raises: ValueError: The circuit contains noise operations but ignore_noise=False. OR The circuit contains measurement operations but ignore_measurement=False. OR The circuit contains reset operations but ignore_reset=False. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''').to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ ``` ```python # stim.Circuit.with_inlined_feedback # (in class stim.Circuit) def with_inlined_feedback( self, ) -> stim.Circuit: """Returns a circuit without feedback with rewritten detectors/observables. When a feedback operation affects the expected parity of a detector or observable, the measurement controlling that feedback operation is implicitly part of the measurement set that defines the detector or observable. This method removes all feedback, but avoids changing the meaning of detectors or observables by turning these implicit measurement dependencies into explicit measurement dependencies added to the observable or detector. This method guarantees that the detector error model derived from the original circuit, and the transformed circuit, will be equivalent (modulo floating point rounding errors and variations in where loops are placed). Specifically, the following should be true for any circuit: dem1 = circuit.flattened().detector_error_model() dem2 = circuit.with_inlined_feedback().flattened().detector_error_model() assert dem1.approx_equals(dem2, 1e-5) Returns: A `stim.Circuit` with feedback operations removed, with rewritten DETECTOR instructions (as needed to avoid changing the meaning of each detector), and with additional OBSERVABLE_INCLUDE instructions (as needed to avoid changing the meaning of each observable). The circuit's function is permitted to differ from the original in that any feedback operation can be pushed to the end of the circuit and discarded. All non-feedback operations must stay where they are, preserving the structure of the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... CX 0 1 # copy to measure qubit ... M 1 # measure first time ... CX rec[-1] 1 # use feedback to reset measurement qubit ... CX 0 1 # copy to measure qubit ... M 1 # measure second time ... DETECTOR rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').with_inlined_feedback() stim.Circuit(''' CX 0 1 M 1 OBSERVABLE_INCLUDE(0) rec[-1] CX 0 1 M 1 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''') """ ``` ```python # stim.Circuit.without_noise # (in class stim.Circuit) def without_noise( self, ) -> stim.Circuit: """Returns a copy of the circuit with all noise processes removed. Pure noise instructions, such as X_ERROR and DEPOLARIZE2, are not included in the result. Noisy measurement instructions, like `M(0.001)`, have their noise parameter removed. Returns: A `stim.Circuit` with the same instructions except all noise processes have been removed. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.25) 0 ... CNOT 0 1 ... M(0.125) 0 ... ''').without_noise() stim.Circuit(''' CX 0 1 M 0 ''') """ ``` ```python # stim.Circuit.without_tags # (in class stim.Circuit) def without_tags( self, ) -> stim.Circuit: """Returns a copy of the circuit with all tags removed. Returns: A `stim.Circuit` with the same instructions except all tags have been removed. Examples: >>> import stim >>> stim.Circuit(''' ... X[test-tag] 0 ... M[test-tag-2](0.125) 0 ... ''').without_tags() stim.Circuit(''' X 0 M(0.125) 0 ''') """ ``` ```python # stim.CircuitErrorLocation # (at top-level in the stim module) class CircuitErrorLocation: """Describes the location of an error mechanism from a stim circuit. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... distance=5, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ) >>> logical_error = circuit.shortest_graphlike_error() >>> error_location = logical_error[0].circuit_error_locations[0] >>> print(error_location) CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } """ ``` ```python # stim.CircuitErrorLocation.__init__ # (in class stim.CircuitErrorLocation) def __init__( self, *, tick_offset: int, flipped_pauli_product: List[stim.GateTargetWithCoords], flipped_measurement: object, instruction_targets: stim.CircuitTargetsInsideInstruction, stack_frames: List[stim.CircuitErrorLocationStackFrame], noise_tag: str = '', ) -> None: """Creates a stim.CircuitErrorLocation. Examples: >>> import stim >>> err = stim.CircuitErrorLocation( ... tick_offset=1, ... flipped_pauli_product=( ... stim.GateTargetWithCoords( ... gate_target=stim.target_x(0), ... coords=[], ... ), ... ), ... flipped_measurement=stim.FlippedMeasurement( ... record_index=None, ... observable=(), ... ), ... instruction_targets=stim.CircuitTargetsInsideInstruction( ... gate='DEPOLARIZE1', ... args=[0.001], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=(stim.GateTargetWithCoords( ... gate_target=0, ... coords=[], ... ),) ... ), ... stack_frames=( ... stim.CircuitErrorLocationStackFrame( ... instruction_offset=2, ... iteration_index=0, ... instruction_repetitions_arg=0, ... ), ... ), ... noise_tag='test-tag', ... ) >>> print(err) CircuitErrorLocation { noise_tag: test-tag flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } """ ``` ```python # stim.CircuitErrorLocation.flipped_measurement # (in class stim.CircuitErrorLocation) @property def flipped_measurement( self, ) -> Optional[stim.FlippedMeasurement]: """The measurement that was flipped by the error mechanism. If the error isn't a measurement error, this will be None. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... M(0.125) 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement stim.FlippedMeasurement( record_index=0, observable=(stim.GateTargetWithCoords(stim.target_z(0), []),), ) """ ``` ```python # stim.CircuitErrorLocation.flipped_pauli_product # (in class stim.CircuitErrorLocation) @property def flipped_pauli_product( self, ) -> List[stim.GateTargetWithCoords]: """The Pauli errors that the error mechanism applied to qubits. When the error is a measurement error, this will be an empty list. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_pauli_product [stim.GateTargetWithCoords(stim.target_y(0), [])] """ ``` ```python # stim.CircuitErrorLocation.instruction_targets # (in class stim.CircuitErrorLocation) @property def instruction_targets( self, ) -> stim.CircuitTargetsInsideInstruction: """Within the error instruction, which may have hundreds of targets, which specific targets were being executed to produce the error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> targets = err[0].circuit_error_locations[0].instruction_targets >>> targets == stim.CircuitTargetsInsideInstruction( ... gate='Y_ERROR', ... args=[0.125], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=(stim.GateTargetWithCoords(0, []),), ... ) True """ ``` ```python # stim.CircuitErrorLocation.noise_tag # (in class stim.CircuitErrorLocation) @property def noise_tag( self, ) -> str: """The tag on the noise instruction that caused the error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... Y_ERROR[test-tag](0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].noise_tag 'test-tag' """ ``` ```python # stim.CircuitErrorLocation.stack_frames # (in class stim.CircuitErrorLocation) @property def stack_frames( self, ) -> List[stim.CircuitErrorLocationStackFrame]: """Describes where in the circuit's execution the error happened. Multiple frames are needed because the error may occur within a loop, or a loop nested inside a loop, or etc. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames [stim.CircuitErrorLocationStackFrame( instruction_offset=2, iteration_index=0, instruction_repetitions_arg=0, )] """ ``` ```python # stim.CircuitErrorLocation.tick_offset # (in class stim.CircuitErrorLocation) @property def tick_offset( self, ) -> int: """The number of TICKs that executed before the error happened. This counts TICKs occurring multiple times during loops. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... TICK ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].tick_offset 3 """ ``` ```python # stim.CircuitErrorLocationStackFrame # (at top-level in the stim module) class CircuitErrorLocationStackFrame: """Describes the location of an instruction being executed within a circuit or loop, distinguishing between separate loop iterations. The full location of an instruction is a list of these frames, drilling down from the top level circuit to the inner-most loop that the instruction is within. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames[0] stim.CircuitErrorLocationStackFrame( instruction_offset=0, iteration_index=0, instruction_repetitions_arg=5, ) >>> err[0].circuit_error_locations[0].stack_frames[1] stim.CircuitErrorLocationStackFrame( instruction_offset=1, iteration_index=4, instruction_repetitions_arg=0, ) """ ``` ```python # stim.CircuitErrorLocationStackFrame.__init__ # (in class stim.CircuitErrorLocationStackFrame) def __init__( self, *, instruction_offset: int, iteration_index: int, instruction_repetitions_arg: int, ) -> None: """Creates a stim.CircuitErrorLocationStackFrame. Examples: >>> import stim >>> frame = stim.CircuitErrorLocationStackFrame( ... instruction_offset=1, ... iteration_index=2, ... instruction_repetitions_arg=3, ... ) """ ``` ```python # stim.CircuitErrorLocationStackFrame.instruction_offset # (in class stim.CircuitErrorLocationStackFrame) @property def instruction_offset( self, ) -> int: """The index of the instruction within the circuit, or within the instruction's parent REPEAT block. This is slightly different from the line number, because blank lines and commented lines don't count and also because the offset of the first instruction is 0 instead of 1. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames[0].instruction_offset 2 """ ``` ```python # stim.CircuitErrorLocationStackFrame.instruction_repetitions_arg # (in class stim.CircuitErrorLocationStackFrame) @property def instruction_repetitions_arg( self, ) -> int: """If the instruction being referred to is a REPEAT block, this is the repetition count of that REPEAT block. Otherwise this field defaults to 0. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> full = err[0].circuit_error_locations[0].stack_frames[0] >>> loop = err[0].circuit_error_locations[0].stack_frames[1] >>> full.instruction_repetitions_arg 5 >>> loop.instruction_repetitions_arg 0 """ ``` ```python # stim.CircuitErrorLocationStackFrame.iteration_index # (in class stim.CircuitErrorLocationStackFrame) @property def iteration_index( self, ) -> int: """Disambiguates which iteration of the loop containing this instruction is being referred to. If the instruction isn't in a REPEAT block, this field defaults to 0. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> full = err[0].circuit_error_locations[0].stack_frames[0] >>> loop = err[0].circuit_error_locations[0].stack_frames[1] >>> full.iteration_index 0 >>> loop.iteration_index 4 """ ``` ```python # stim.CircuitInstruction # (at top-level in the stim module) class CircuitInstruction: """An instruction, like `H 0 1` or `CNOT rec[-1] 5`, from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... M 0 1 ... X_ERROR(0.125) 5 ... ''') >>> circuit[0] stim.CircuitInstruction('H', [stim.GateTarget(0)], []) >>> circuit[1] stim.CircuitInstruction('M', [stim.GateTarget(0), stim.GateTarget(1)], []) >>> circuit[2] stim.CircuitInstruction('X_ERROR', [stim.GateTarget(5)], [0.125]) """ ``` ```python # stim.CircuitInstruction.__eq__ # (in class stim.CircuitInstruction) def __eq__( self, arg0: stim.CircuitInstruction, ) -> bool: """Determines if two `stim.CircuitInstruction`s are identical. """ ``` ```python # stim.CircuitInstruction.__init__ # (in class stim.CircuitInstruction) def __init__( self, name: str, targets: Optional[Iterable[Union[int, stim.GateTarget]]] = None, gate_args: Optional[Iterable[float]] = None, *, tag: str = "", ) -> None: """Creates or parses a `stim.CircuitInstruction`. Args: name: The name of the instruction being applied. If `targets` and `gate_args` aren't specified, this can be a full instruction line from a stim Circuit file, like "CX 0 1". targets: The targets the instruction is being applied to. These can be raw values like `0` and `stim.target_rec(-1)`, or instances of `stim.GateTarget`. gate_args: The sequence of numeric arguments parameterizing a gate. For noise gates this is their probabilities. For `OBSERVABLE_INCLUDE` instructions it's the index of the logical observable to affect. tag: Defaults to "". A custom string attached to the instruction. For example, for a TICK instruction, this could a string specifying an amount of time which is used by custom code for adding noise to a circuit. In general, stim will attempt to propagate tags across circuit transformations but will otherwise completely ignore them. Examples: >>> import stim >>> print(stim.CircuitInstruction('DEPOLARIZE1', [5], [0.25])) DEPOLARIZE1(0.25) 5 >>> stim.CircuitInstruction('CX rec[-1] 5 # comment') stim.CircuitInstruction('CX', [stim.target_rec(-1), stim.GateTarget(5)], []) >>> print(stim.CircuitInstruction('I', [2], tag='100ns')) I[100ns] 2 """ ``` ```python # stim.CircuitInstruction.__ne__ # (in class stim.CircuitInstruction) def __ne__( self, arg0: stim.CircuitInstruction, ) -> bool: """Determines if two `stim.CircuitInstruction`s are different. """ ``` ```python # stim.CircuitInstruction.__repr__ # (in class stim.CircuitInstruction) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CircuitInstruction`. """ ``` ```python # stim.CircuitInstruction.__str__ # (in class stim.CircuitInstruction) def __str__( self, ) -> str: """Returns a text description of the instruction as a stim circuit file line. """ ``` ```python # stim.CircuitInstruction.gate_args_copy # (in class stim.CircuitInstruction) def gate_args_copy( self, ) -> List[float]: """Returns the gate's arguments (numbers parameterizing the instruction). For noisy gates this typically a list of probabilities. For OBSERVABLE_INCLUDE it's a singleton list containing the logical observable index. Examples: >>> import stim >>> instruction = stim.CircuitInstruction('X_ERROR', [2, 3], [0.125]) >>> instruction.gate_args_copy() [0.125] >>> instruction.gate_args_copy() == instruction.gate_args_copy() True >>> instruction.gate_args_copy() is instruction.gate_args_copy() False """ ``` ```python # stim.CircuitInstruction.name # (in class stim.CircuitInstruction) @property def name( self, ) -> str: """The name of the instruction (e.g. `H` or `X_ERROR` or `DETECTOR`). """ ``` ```python # stim.CircuitInstruction.num_measurements # (in class stim.CircuitInstruction) @property def num_measurements( self, ) -> int: """Returns the number of bits produced when running this instruction. Examples: >>> import stim >>> stim.CircuitInstruction('H', [0]).num_measurements 0 >>> stim.CircuitInstruction('M', [0]).num_measurements 1 >>> stim.CircuitInstruction('M', [2, 3, 5, 7, 11]).num_measurements 5 >>> stim.CircuitInstruction('MXX', [0, 1, 4, 5, 11, 13]).num_measurements 3 >>> stim.Circuit('MPP X0*X1 X0*Z1*Y2')[0].num_measurements 2 >>> stim.CircuitInstruction('HERALDED_ERASE', [0], [0.25]).num_measurements 1 """ ``` ```python # stim.CircuitInstruction.tag # (in class stim.CircuitInstruction) @property def tag( self, ) -> str: """The custom tag attached to the instruction. The tag is an arbitrary string. The default tag, when none is specified, is the empty string. Examples: >>> import stim >>> stim.Circuit("H[test] 0")[0].tag 'test' >>> stim.Circuit("H 0")[0].tag '' """ ``` ```python # stim.CircuitInstruction.target_groups # (in class stim.CircuitInstruction) def target_groups( self, ) -> List[List[stim.GateTarget]]: """Splits the instruction's targets into groups depending on the type of gate. Single qubit gates like H get one group per target. Two qubit gates like CX get one group per pair of targets. Pauli product gates like MPP get one group per combined product. Returns: A list of groups of targets. Examples: >>> import stim >>> for g in stim.Circuit('H 0 1 2')[0].target_groups(): ... print(repr(g)) [stim.GateTarget(0)] [stim.GateTarget(1)] [stim.GateTarget(2)] >>> for g in stim.Circuit('CX 0 1 2 3')[0].target_groups(): ... print(repr(g)) [stim.GateTarget(0), stim.GateTarget(1)] [stim.GateTarget(2), stim.GateTarget(3)] >>> for g in stim.Circuit('MPP X0*Y1*Z2 X5*X6')[0].target_groups(): ... print(repr(g)) [stim.target_x(0), stim.target_y(1), stim.target_z(2)] [stim.target_x(5), stim.target_x(6)] >>> for g in stim.Circuit('DETECTOR rec[-1] rec[-2]')[0].target_groups(): ... print(repr(g)) [stim.target_rec(-1)] [stim.target_rec(-2)] >>> for g in stim.Circuit('CORRELATED_ERROR(0.1) X0 Y1')[0].target_groups(): ... print(repr(g)) [stim.target_x(0), stim.target_y(1)] """ ``` ```python # stim.CircuitInstruction.targets_copy # (in class stim.CircuitInstruction) def targets_copy( self, ) -> List[stim.GateTarget]: """Returns a copy of the targets of the instruction. Examples: >>> import stim >>> instruction = stim.CircuitInstruction('X_ERROR', [2, 3], [0.125]) >>> instruction.targets_copy() [stim.GateTarget(2), stim.GateTarget(3)] >>> instruction.targets_copy() == instruction.targets_copy() True >>> instruction.targets_copy() is instruction.targets_copy() False """ ``` ```python # stim.CircuitRepeatBlock # (at top-level in the stim module) class CircuitRepeatBlock: """A REPEAT block from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 >>> repeat_block.body_copy() stim.Circuit(''' CX 0 1 CZ 1 2 ''') """ ``` ```python # stim.CircuitRepeatBlock.__eq__ # (in class stim.CircuitRepeatBlock) def __eq__( self, arg0: stim.CircuitRepeatBlock, ) -> bool: """Determines if two `stim.CircuitRepeatBlock`s are identical. """ ``` ```python # stim.CircuitRepeatBlock.__init__ # (in class stim.CircuitRepeatBlock) def __init__( self, repeat_count: int, body: stim.Circuit, *, tag: str = '', ) -> None: """Initializes a `stim.CircuitRepeatBlock`. Args: repeat_count: The number of times to repeat the block. body: The body of the block, as a circuit. tag: Defaults to empty. A custom string attached to the REPEAT instruction. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append(stim.CircuitRepeatBlock(100, stim.Circuit("M 0"))) >>> c stim.Circuit(''' REPEAT 100 { M 0 } ''') """ ``` ```python # stim.CircuitRepeatBlock.__ne__ # (in class stim.CircuitRepeatBlock) def __ne__( self, arg0: stim.CircuitRepeatBlock, ) -> bool: """Determines if two `stim.CircuitRepeatBlock`s are different. """ ``` ```python # stim.CircuitRepeatBlock.__repr__ # (in class stim.CircuitRepeatBlock) def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.CircuitRepeatBlock`. """ ``` ```python # stim.CircuitRepeatBlock.body_copy # (in class stim.CircuitRepeatBlock) def body_copy( self, ) -> stim.Circuit: """Returns a copy of the body of the repeat block. (Making a copy is enforced to make it clear that editing the result won't change the block's body.) Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.body_copy() stim.Circuit(''' CX 0 1 CZ 1 2 ''') """ ``` ```python # stim.CircuitRepeatBlock.name # (in class stim.CircuitRepeatBlock) @property def name( self, ) -> str: """Returns the name "REPEAT". This is a duck-typing convenience method. It exists so that code that doesn't know whether it has a `stim.CircuitInstruction` or a `stim.CircuitRepeatBlock` can check the object's name without having to do an `instanceof` check first. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 1 2 ... } ... S 1 ... ''') >>> [instruction.name for instruction in circuit] ['H', 'REPEAT', 'S'] """ ``` ```python # stim.CircuitRepeatBlock.num_measurements # (in class stim.CircuitRepeatBlock) @property def num_measurements( self, ) -> int: """Returns the number of bits produced when running this loop. Examples: >>> import stim >>> stim.CircuitRepeatBlock( ... body=stim.Circuit("M 0 1"), ... repeat_count=25, ... ).num_measurements 50 """ ``` ```python # stim.CircuitRepeatBlock.repeat_count # (in class stim.CircuitRepeatBlock) @property def repeat_count( self, ) -> int: """The repetition count of the repeat block. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 """ ``` ```python # stim.CircuitRepeatBlock.tag # (in class stim.CircuitRepeatBlock) @property def tag( self, ) -> str: """The custom tag attached to the REPEAT instruction. The tag is an arbitrary string. The default tag, when none is specified, is the empty string. Examples: >>> import stim >>> stim.Circuit(''' ... REPEAT[test] 5 { ... H 0 ... } ... ''')[0].tag 'test' >>> stim.Circuit(''' ... REPEAT 5 { ... H 0 ... } ... ''')[0].tag '' """ ``` ```python # stim.CircuitTargetsInsideInstruction # (at top-level in the stim module) class CircuitTargetsInsideInstruction: """Describes a range of targets within a circuit instruction. """ ``` ```python # stim.CircuitTargetsInsideInstruction.__init__ # (in class stim.CircuitTargetsInsideInstruction) def __init__( self, *, gate: str, tag: str = '', args: List[float], target_range_start: int, target_range_end: int, targets_in_range: List[stim.GateTargetWithCoords], ) -> None: """Creates a stim.CircuitTargetsInsideInstruction. Examples: >>> import stim >>> val = stim.CircuitTargetsInsideInstruction( ... gate='X_ERROR', ... tag='', ... args=[0.25], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=[stim.GateTargetWithCoords(0, [])], ... ) """ ``` ```python # stim.CircuitTargetsInsideInstruction.args # (in class stim.CircuitTargetsInsideInstruction) @property def args( self, ) -> List[float]: """Returns parens arguments of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.args [0.25] """ ``` ```python # stim.CircuitTargetsInsideInstruction.gate # (in class stim.CircuitTargetsInsideInstruction) @property def gate( self, ) -> Optional[str]: """Returns the name of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.gate 'X_ERROR' """ ``` ```python # stim.CircuitTargetsInsideInstruction.tag # (in class stim.CircuitTargetsInsideInstruction) @property def tag( self, ) -> str: """Returns the tag of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR[look-at-me-imma-tag](0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.tag 'look-at-me-imma-tag' """ ``` ```python # stim.CircuitTargetsInsideInstruction.target_range_end # (in class stim.CircuitTargetsInsideInstruction) @property def target_range_end( self, ) -> int: """Returns the exclusive end of the range of targets that were executing within the gate / instruction. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.target_range_start 0 >>> loc.instruction_targets.target_range_end 1 """ ``` ```python # stim.CircuitTargetsInsideInstruction.target_range_start # (in class stim.CircuitTargetsInsideInstruction) @property def target_range_start( self, ) -> int: """Returns the inclusive start of the range of targets that were executing within the gate / instruction. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.target_range_start 0 >>> loc.instruction_targets.target_range_end 1 """ ``` ```python # stim.CircuitTargetsInsideInstruction.targets_in_range # (in class stim.CircuitTargetsInsideInstruction) @property def targets_in_range( self, ) -> List[stim.GateTargetWithCoords]: """Returns the subset of targets of the gate/instruction that were being executed. Includes coordinate data with the targets. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.targets_in_range [stim.GateTargetWithCoords(0, [])] """ ``` ```python # stim.CliffordString # (at top-level in the stim module) class CliffordString: """A tensor product of single qubit Clifford gates (e.g. "H \u2297 X \u2297 S"). Represents a collection of Clifford operations applied pairwise to a collection of qubits. Ignores global phase. Examples: >>> import stim >>> stim.CliffordString("H,S,C_XYZ") * stim.CliffordString("H,H,H") stim.CliffordString("I,C_ZYX,SQRT_X_DAG") """ ``` ```python # stim.CliffordString.__add__ # (in class stim.CliffordString) def __add__( self, rhs: stim.CliffordString, ) -> stim.CliffordString: """Concatenates two CliffordStrings. Args: rhs: The suffix of the concatenation. Returns: The concatenated Clifford string. Examples: >>> import stim >>> stim.CliffordString("I,X,H") + stim.CliffordString("Y,S") stim.CliffordString("I,X,H,Y,S") """ ``` ```python # stim.CliffordString.__eq__ # (in class stim.CliffordString) def __eq__( self, arg0: stim.CliffordString, ) -> bool: """Determines if two Clifford strings have identical contents. """ ``` ```python # stim.CliffordString.__getitem__ # (in class stim.CliffordString) @overload def __getitem__( self, index_or_slice: int, ) -> stim.GateData: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.CliffordString: pass def __getitem__( self, index_or_slice: Union[int, slice], ) -> Union[stim.GateData, stim.CliffordString]: """Returns a Clifford or substring from the CliffordString. Args: index_or_slice: The index of the Clifford to return, or the slice corresponding to the sub CliffordString to return. Returns: The indexed Clifford (as a stim.GateData instance) or the sliced CliffordString. Examples: >>> import stim >>> s = stim.CliffordString("I,X,Y,Z,H") >>> s[2] stim.gate_data('Y') >>> s[-1] stim.gate_data('H') >>> s[:-1] stim.CliffordString("I,X,Y,Z") >>> s[::2] stim.CliffordString("I,Y,H") """ ``` ```python # stim.CliffordString.__iadd__ # (in class stim.CliffordString) def __iadd__( self, rhs: stim.CliffordString, ) -> stim.CliffordString: """Mutates the CliffordString by concatenating onto it. Args: rhs: The suffix to concatenate onto the target CliffordString. Returns: The mutated Clifford string. Examples: >>> import stim >>> c = stim.CliffordString("I,X,H") >>> alias = c >>> alias += stim.CliffordString("Y,S") >>> c stim.CliffordString("I,X,H,Y,S") """ ``` ```python # stim.CliffordString.__imul__ # (in class stim.CliffordString) def __imul__( self, rhs: Union[stim.CliffordString, int], ) -> stim.CliffordString: """Inplace CliffordString multiplication. Mutates the CliffordString into itself multiplied by another CliffordString (via pairwise Clifford multipliation) or by an integer (via repeating the contents). Args: rhs: Either a stim.CliffordString or an int. If rhs is a stim.CliffordString, then the Cliffords from each string are multiplied pairwise. If rhs is an int, it is the number of times to repeat the Clifford string's contents. Returns: The mutated Clifford string. Examples: >>> import stim >>> c = stim.CliffordString("S,X,X") >>> alias = c >>> alias *= stim.CliffordString("S,Z,H,Z") >>> c stim.CliffordString("Z,Y,SQRT_Y,Z") >>> c = stim.CliffordString("I,X,H") >>> alias = c >>> alias *= 2 >>> c stim.CliffordString("I,X,H,I,X,H") """ ``` ```python # stim.CliffordString.__init__ # (in class stim.CliffordString) def __init__( self, arg: Union[int, str, stim.CliffordString, stim.PauliString, stim.Circuit], /, ) -> None: """Initializes a stim.CliffordString from the given argument. Args: arg [position-only]: This can be a variety of types, including: int: initializes an identity Clifford string of the given length. str: initializes by parsing a comma-separated list of gate names. stim.CliffordString: initializes by copying the given Clifford string. stim.PauliString: initializes by copying from the given Pauli string (ignores the sign of the Pauli string). stim.Circuit: initializes a CliffordString equivalent to the action of the circuit (as long as the circuit only contains single qubit unitary operations and annotations). Iterable: initializes by interpreting each item as a Clifford. Each item can be a single-qubit Clifford gate name (like "SQRT_X") or stim.GateData instance. Examples: >>> import stim >>> stim.CliffordString(5) stim.CliffordString("I,I,I,I,I") >>> stim.CliffordString("X,Y,Z,SQRT_X") stim.CliffordString("X,Y,Z,SQRT_X") >>> stim.CliffordString(["H", stim.gate_data("S")]) stim.CliffordString("H,S") >>> stim.CliffordString(stim.PauliString("XYZ")) stim.CliffordString("X,Y,Z") >>> stim.CliffordString(stim.CliffordString("X,Y,Z")) stim.CliffordString("X,Y,Z") >>> stim.CliffordString(stim.Circuit(''' ... H 0 1 2 ... S 2 3 ... TICK ... S 3 ... I 6 ... ''')) stim.CliffordString("H,H,C_ZYX,Z,I,I,I") """ ``` ```python # stim.CliffordString.__ipow__ # (in class stim.CliffordString) def __ipow__( self, num_qubits: int, ) -> object: """Mutates the CliffordString into itself raised to a power. Args: power: The power to raise the CliffordString's Cliffords to. This value can be negative (e.g. -1 inverts the string). Returns: The mutated Clifford string. Examples: >>> import stim >>> p = stim.CliffordString("I,X,H,S,C_XYZ") >>> p **= 3 >>> p stim.CliffordString("I,X,H,S_DAG,I") >>> p **= 2 >>> p stim.CliffordString("I,I,I,Z,I") >>> alias = p >>> alias **= 2 >>> p stim.CliffordString("I,I,I,I,I") """ ``` ```python # stim.CliffordString.__len__ # (in class stim.CliffordString) def __len__( self, ) -> int: """Returns the number of Clifford operations in the string. Examples: >>> import stim >>> len(stim.CliffordString("I,X,Y,Z,H")) 5 """ ``` ```python # stim.CliffordString.__mul__ # (in class stim.CliffordString) def __mul__( self, rhs: Union[stim.CliffordString, int], ) -> stim.CliffordString: """CliffordString multiplication. Args: rhs: Either a stim.CliffordString or an int. If rhs is a stim.CliffordString, then the Cliffords from each string are multiplied pairwise. If rhs is an int, it is the number of times to repeat the Clifford string's contents. Examples: >>> import stim >>> stim.CliffordString("S,X,X") * stim.CliffordString("S,Z,H,Z") stim.CliffordString("Z,Y,SQRT_Y,Z") >>> stim.CliffordString("I,X,H") * 3 stim.CliffordString("I,X,H,I,X,H,I,X,H") """ ``` ```python # stim.CliffordString.__ne__ # (in class stim.CliffordString) def __ne__( self, arg0: stim.CliffordString, ) -> bool: """Determines if two Clifford strings have non-identical contents. """ ``` ```python # stim.CliffordString.__pow__ # (in class stim.CliffordString) def __pow__( self, power: int, ) -> stim.CliffordString: """Returns the CliffordString raised to a power. Args: power: The power to raise the CliffordString's Cliffords to. This value can be negative (e.g. -1 returns the inverse string). Returns: The Clifford string raised to the power. Examples: >>> import stim >>> p = stim.CliffordString("I,X,H,S,C_XYZ") >>> p**0 stim.CliffordString("I,I,I,I,I") >>> p**1 stim.CliffordString("I,X,H,S,C_XYZ") >>> p**12000001 stim.CliffordString("I,X,H,S,C_XYZ") >>> p**2 stim.CliffordString("I,I,I,Z,C_ZYX") >>> p**3 stim.CliffordString("I,X,H,S_DAG,I") >>> p**-1 stim.CliffordString("I,X,H,S_DAG,C_ZYX") """ ``` ```python # stim.CliffordString.__repr__ # (in class stim.CliffordString) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CliffordString`. """ ``` ```python # stim.CliffordString.__rmul__ # (in class stim.CliffordString) def __rmul__( self, lhs: int, ) -> stim.CliffordString: """CliffordString left-multiplication. Args: lhs: The number of times to repeat the Clifford string's contents. Returns: The repeated Clifford string. Examples: >>> import stim >>> 2 * stim.CliffordString("I,X,H") stim.CliffordString("I,X,H,I,X,H") >>> 0 * stim.CliffordString("I,X,H") stim.CliffordString("") >>> 5 * stim.CliffordString("I") stim.CliffordString("I,I,I,I,I") """ ``` ```python # stim.CliffordString.__setitem__ # (in class stim.CliffordString) def __setitem__( self, index_or_slice: Union[int, slice], new_value: Union[str, stim.GateData, stim.CliffordString, stim.PauliString, stim.Tableau], ) -> None: """Overwrites an indexed Clifford, or slice of Cliffords, with the given value. Args: index_or_slice: The index of the Clifford to overwrite, or the slice of Cliffords to overwrite. new_value: Specifies the value to write into the Clifford string. This can be set to a few different types of values: - str: Name of the single qubit Clifford gate to write to the index or broadcast over the slice. - stim.GateData: The single qubit Clifford gate to write to the index or broadcast over the slice. - stim.Tableau: Must be a single qubit tableau. Specifies the single qubit Clifford gate to write to the index or broadcast over the slice. - stim.CliffordString: String of Cliffords to write into the slice. Examples: >>> import stim >>> s = stim.CliffordString("I,I,I,I,I") >>> s[1] = 'H' >>> s stim.CliffordString("I,H,I,I,I") >>> s[2:] = 'SQRT_X' >>> s stim.CliffordString("I,H,SQRT_X,SQRT_X,SQRT_X") >>> s[0] = stim.gate_data('S_DAG').inverse >>> s stim.CliffordString("S,H,SQRT_X,SQRT_X,SQRT_X") >>> s[:] = 'I' >>> s stim.CliffordString("I,I,I,I,I") >>> s[::2] = stim.CliffordString("X,Y,Z") >>> s stim.CliffordString("X,I,Y,I,Z") >>> s[0] = stim.Tableau.from_named_gate("H") >>> s stim.CliffordString("H,I,Y,I,Z") >>> s[:] = stim.Tableau.from_named_gate("S") >>> s stim.CliffordString("S,S,S,S,S") >>> s[:4] = stim.PauliString("IXYZ") >>> s stim.CliffordString("I,X,Y,Z,S") """ ``` ```python # stim.CliffordString.__str__ # (in class stim.CliffordString) def __str__( self, ) -> str: """Returns a string representation of the CliffordString's operations. """ ``` ```python # stim.CliffordString.all_cliffords_string # (in class stim.CliffordString) @staticmethod def all_cliffords_string( ) -> stim.CliffordString: """Returns a stim.CliffordString containing each single qubit Clifford once. Useful for things like testing that a method works on every single Clifford. Examples: >>> import stim >>> cliffords = stim.CliffordString.all_cliffords_string() >>> len(cliffords) 24 >>> print(cliffords[:8]) I,X,Y,Z,H_XY,S,S_DAG,H_NXY >>> print(cliffords[8:16]) H,SQRT_Y_DAG,H_NXZ,SQRT_Y,H_YZ,H_NYZ,SQRT_X,SQRT_X_DAG >>> print(cliffords[16:]) C_XYZ,C_XYNZ,C_NXYZ,C_XNYZ,C_ZYX,C_ZNYX,C_NZYX,C_ZYNX """ ``` ```python # stim.CliffordString.copy # (in class stim.CliffordString) def copy( self, ) -> stim.CliffordString: """Returns a copy of the CliffordString. Returns: The copy. Examples: >>> import stim >>> c = stim.CliffordString("H,X") >>> alias = c >>> copy = c.copy() >>> c *= 5 >>> alias stim.CliffordString("H,X,H,X,H,X,H,X,H,X") >>> copy stim.CliffordString("H,X") """ ``` ```python # stim.CliffordString.random # (in class stim.CliffordString) @staticmethod def random( num_qubits: int, ) -> stim.CliffordString: """Samples a uniformly random CliffordString. Args: num_qubits: The number of qubits the CliffordString should act upon. Examples: >>> import stim >>> p = stim.CliffordString.random(5) >>> len(p) 5 Returns: The sampled Clifford string. """ ``` ```python # stim.CliffordString.x_outputs # (in class stim.CliffordString) def x_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates an X input into. For example, H conjugates X into +Z and S_DAG conjugates X into -Y. Combined with `z_outputs`, the results of this method completely specify the single qubit Clifford applied to each qubit. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> x_paulis, x_signs = stim.CliffordString("I,Y,H,S").x_outputs() >>> x_paulis stim.PauliString("+XXZY") >>> x_signs array([False, True, False, False]) >>> stim.CliffordString("I,Y,H,S").x_outputs(bit_packed_signs=True)[1] array([2], dtype=uint8) """ ``` ```python # stim.CliffordString.y_outputs # (in class stim.CliffordString) def y_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates a Y input into. For example, H conjugates Y into -Y and S_DAG conjugates Y into +X. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> y_paulis, y_signs = stim.CliffordString("I,X,H,S").y_outputs() >>> y_paulis stim.PauliString("+YYYX") >>> y_signs array([False, True, True, True]) >>> stim.CliffordString("I,X,H,S").y_outputs(bit_packed_signs=True)[1] array([14], dtype=uint8) """ ``` ```python # stim.CliffordString.z_outputs # (in class stim.CliffordString) def z_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates a Z input into. For example, H conjugates Z into +X and SQRT_X conjugates Z into -Y. Combined with `x_outputs`, the results of this method completely specify the single qubit Clifford applied to each qubit. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> z_paulis, z_signs = stim.CliffordString("I,Y,H,S").z_outputs() >>> z_paulis stim.PauliString("+ZZXZ") >>> z_signs array([False, True, False, False]) >>> stim.CliffordString("I,Y,H,S").z_outputs(bit_packed_signs=True)[1] array([2], dtype=uint8) """ ``` ```python # stim.CompiledDemSampler # (at top-level in the stim module) class CompiledDemSampler: """A helper class for efficiently sampler from a detector error model. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) """ ``` ```python # stim.CompiledDemSampler.sample # (in class stim.CompiledDemSampler) def sample( self, shots: int, *, bit_packed: bool = False, return_errors: bool = False, recorded_errors_to_replay: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray]]: """Samples the detector error model's error mechanisms to produce sample data. Args: shots: The number of times to sample from the model. bit_packed: Defaults to false. False: the returned numpy arrays have dtype=np.bool_. True: the returned numpy arrays have dtype=np.uint8 and pack 8 bits into each byte. Setting this to True is equivalent to running `np.packbits(data, bitorder='little', axis=1)` on each output value, but has the performance benefit of the data never being expanded into an unpacked form. return_errors: Defaults to False. False: the third entry of the returned tuple is None. True: the third entry of the returned tuple is a numpy array recording which errors were sampled. recorded_errors_to_replay: Defaults to None, meaning sample errors randomly. If not None, this is expected to be a 2d numpy array specifying which errors to apply (e.g. one returned from a previous call to the sample method). The array must have dtype=np.bool_ and shape=(num_shots, num_errors) or dtype=np.uint8 and shape=(num_shots, math.ceil(num_errors / 8)). Returns: A tuple (detector_data, obs_data, error_data). Assuming bit_packed is False and return_errors is True: - If error_data[s, k] is True, then the error with index k fired in the shot with index s. - If detector_data[s, k] is True, then the detector with index k ended up flipped in the shot with index s. - If obs_data[s, k] is True, then the observable with index k ended up flipped in the shot with index s. The dtype and shape of the data depends on the arguments: if bit_packed: detector_data.shape == (num_shots, math.ceil(num_detectors / 8)) detector_data.dtype == np.uint8 obs_data.shape == (num_shots, math.ceil(num_observables / 8)) obs_data.dtype == np.uint8 if return_errors: error_data.shape = (num_shots, math.ceil(num_errors / 8)) error_data.dtype = np.uint8 else: error_data is None else: detector_data.shape == (num_shots, num_detectors) detector_data.dtype == np.bool_ obs_data.shape == (num_shots, num_observables) obs_data.dtype == np.bool_ if return_errors: error_data.shape = (num_shots, num_errors) error_data.dtype = np.bool_ else: error_data is None Note that bit packing is done using little endian order on the last axis (i.e. like `np.packbits(data, bitorder='little', axis=1)`). Examples: >>> import stim >>> import numpy as np >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> # Taking samples. >>> det_data, obs_data, err_data_not_requested = sampler.sample(shots=4) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data_not_requested is None True >>> # Recording errors. >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) >>> # Bit packing. >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True, ... bit_packed=True) >>> det_data array([[6], [6], [6], [6]], dtype=uint8) >>> obs_data array([[1], [1], [1], [1]], dtype=uint8) >>> err_data array([[2], [2], [2], [2]], dtype=uint8) >>> # Recording and replaying errors. >>> noisy_dem = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.25) D1 ... ''') >>> noisy_sampler = noisy_dem.compile_sampler() >>> det_data, obs_data, err_data = noisy_sampler.sample( ... shots=100, ... return_errors=True) >>> replay_det_data, replay_obs_data, _ = noisy_sampler.sample( ... shots=100, ... recorded_errors_to_replay=err_data) >>> np.array_equal(det_data, replay_det_data) True >>> np.array_equal(obs_data, replay_obs_data) True """ ``` ```python # stim.CompiledDemSampler.sample_write # (in class stim.CompiledDemSampler) def sample_write( self, shots: int, *, det_out_file: Union[None, str, pathlib.Path], det_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', obs_out_file: Union[None, str, pathlib.Path], obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', err_out_file: Union[None, str, pathlib.Path] = None, err_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', replay_err_in_file: Union[None, str, pathlib.Path] = None, replay_err_in_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Samples the detector error model and writes the results to disk. Args: shots: The number of times to sample from the model. det_out_file: Where to write detection event data. If None: detection event data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase det_out_format: The format to write the detection event data in (e.g. "01" or "b8"). obs_out_file: Where to write observable flip data. If None: observable flip data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase obs_out_format: The format to write the observable flip data in (e.g. "01" or "b8"). err_out_file: Where to write errors-that-occurred data. If None: errors-that-occurred data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase err_out_format: The format to write the errors-that-occurred data in (e.g. "01" or "b8"). replay_err_in_file: If this is specified, errors are replayed from data instead of generated randomly. The following types are supported: - None: errors are generated randomly according to the probabilities in the detector error model. - str or pathlib.Path: the file at the given path is opened and errors-to-apply data is read from there. - io.IOBase: NOT IMPLEMENTED replay_err_in_format: The format to write the errors-that-occurred data in (e.g. "01" or "b8"). Returns: Nothing. Results are written to disk. Examples: >>> import stim >>> import tempfile >>> import pathlib >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(0) D1 ... error(0) D0 ... error(1) D1 D2 L0 ... error(0) D0 ... ''') >>> sampler = dem.compile_sampler() >>> with tempfile.TemporaryDirectory() as d: ... d = pathlib.Path(d) ... sampler.sample_write( ... shots=1, ... det_out_file=d / 'dets.01', ... det_out_format='01', ... obs_out_file=d / 'obs.01', ... obs_out_format='01', ... err_out_file=d / 'err.hits', ... err_out_format='hits', ... ) ... with open(d / 'dets.01') as f: ... assert f.read() == "011\n" ... with open(d / 'obs.01') as f: ... assert f.read() == "1\n" ... with open(d / 'err.hits') as f: ... assert f.read() == "3\n" """ ``` ```python # stim.CompiledDetectorSampler # (at top-level in the stim module) class CompiledDetectorSampler: """An analyzed stabilizer circuit whose detection events can be sampled quickly. """ ``` ```python # stim.CompiledDetectorSampler.__init__ # (in class stim.CompiledDetectorSampler) def __init__( self, circuit: stim.Circuit, *, seed: object = None, ) -> None: """Creates an object that can sample the detection events from a circuit. Args: circuit: The circuit to sample from. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Returns: An initialized stim.CompiledDetectorSampler. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(1.0) 0 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[ True]]) """ ``` ```python # stim.CompiledDetectorSampler.__repr__ # (in class stim.CompiledDetectorSampler) def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.CompiledDetectorSampler`. """ ``` ```python # stim.CompiledDetectorSampler.sample # (in class stim.CompiledDetectorSampler) def sample( self, shots: int, *, prepend_observables: bool = False, append_observables: bool = False, separate_observables: bool = False, bit_packed: bool = False, dets_out: Optional[np.ndarray] = None, obs_out: Optional[np.ndarray] = None, ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: """Returns a numpy array containing a batch of detector samples from the circuit. The circuit must define the detectors using DETECTOR instructions. Observables defined by OBSERVABLE_INCLUDE instructions can also be included in the results as honorary detectors. Args: shots: The number of times to sample every detector in the circuit. separate_observables: Defaults to False. When set to True, the return value is a (detection_events, observable_flips) tuple instead of a flat detection_events array. prepend_observables: Defaults to false. When set, observables are included with the detectors and are placed at the start of the results. append_observables: Defaults to false. When set, observables are included with the detectors and are placed at the end of the results. bit_packed: Returns a uint8 numpy array with 8 bits per byte, instead of a bool_ numpy array with 1 bit per byte. Uses little endian packing. dets_out: Defaults to None. Specifies a pre-allocated numpy array to write the detection event data into. This array must have the correct shape and dtype. obs_out: Defaults to None. Specifies a pre-allocated numpy array to write the observable flip data into. This array must have the correct shape and dtype. Returns: A numpy array or tuple of numpy arrays containing the samples. if separate_observables=False and bit_packed=False: A single numpy array. dtype=bool_ shape=( shots, num_detectors + num_observables * ( append_observables + prepend_observables), ) The bit for detection event `m` in shot `s` is at result[s, m] if separate_observables=False and bit_packed=True: A single numpy array. dtype=uint8 shape=( shots, math.ceil((num_detectors + num_observables * ( append_observables + prepend_observables)) / 8), ) The bit for detection event `m` in shot `s` is at (result[s, m // 8] >> (m % 8)) & 1 if separate_observables=True and bit_packed=False: A (dets, obs) tuple. dets.dtype=bool_ dets.shape=(shots, num_detectors) obs.dtype=bool_ obs.shape=(shots, num_observables) The bit for detection event `m` in shot `s` is at dets[s, m] The bit for observable `m` in shot `s` is at obs[s, m] if separate_observables=True and bit_packed=True: A (dets, obs) tuple. dets.dtype=uint8 dets.shape=(shots, math.ceil(num_detectors / 8)) obs.dtype=uint8 obs.shape=(shots, math.ceil(num_observables / 8)) The bit for detection event `m` in shot `s` is at (dets[s, m // 8] >> (m % 8)) & 1 The bit for observable `m` in shot `s` is at (obs[s, m // 8] >> (m % 8)) & 1 Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(1.0) 0 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[ True]]) """ ``` ```python # stim.CompiledDetectorSampler.sample_write # (in class stim.CompiledDetectorSampler) def sample_write( self, shots: int, *, filepath: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', obs_out_filepath: Optional[Union[str, pathlib.Path]] = None, obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', prepend_observables: bool = False, append_observables: bool = False, ) -> None: """Samples detection events from the circuit and writes them to a file. Args: shots: The number of times to sample every measurement in the circuit. filepath: The file to write the results to. format: The output format to write the results with. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". obs_out_filepath: Sample observables as part of each shot, and write them to this file. This keeps the observable data separate from the detector data. obs_out_format: If writing the observables to a file, this is the format to write them in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". prepend_observables: Sample observables as part of each shot, and put them at the start of the detector data. append_observables: Sample observables as part of each shot, and put them at the end of the detector data. Returns: None. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f"{d}/tmp.dat" ... c = stim.Circuit(''' ... X_ERROR(1) 0 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''') ... c.compile_detector_sampler().sample_write( ... shots=3, ... filepath=path, ... format="dets") ... with open(path) as f: ... print(f.read(), end='') shot D0 shot D0 shot D0 """ ``` ```python # stim.CompiledMeasurementSampler # (at top-level in the stim module) class CompiledMeasurementSampler: """An analyzed stabilizer circuit whose measurements can be sampled quickly. """ ``` ```python # stim.CompiledMeasurementSampler.__init__ # (in class stim.CompiledMeasurementSampler) def __init__( self, circuit: stim.Circuit, *, skip_reference_sample: bool = False, seed: object = None, reference_sample: object = None, ) -> None: """Creates a measurement sampler for the given circuit. The sampler uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the sampler, as a baseline for deriving more samples using an error propagation simulator. Args: circuit: The stim circuit to sample from. skip_reference_sample: Defaults to False. When set to True, the reference sample used by the sampler is initialized to all-zeroes instead of being collected from the circuit. This means that the results returned by the sampler are actually whether or not each measurement was *flipped*, instead of true measurement results. Forcing an all-zero reference sample is useful when you are only interested in error propagation and don't want to have to deal with the fact that some measurements want to be On when no errors occur. It is also useful when you know for sure that the all-zero result is actually a possible result from the circuit (under noiseless execution), meaning it is a valid reference sample as good as any other. Computing the reference sample is the most time consuming and memory intensive part of simulating the circuit, so promising that the simulator can safely skip that step is an effective optimization. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. reference_sample: The data to xor into the measurement flips produced by the frame simulator, in order to produce proper measurement results. This can either be specified as an `np.bool_` array or a bit packed `np.uint8` array (little endian). Under normal conditions, the reference sample should be a valid noiseless sample of the circuit, such as the one returned by `circuit.reference_sample()`. If this argument is not provided, the reference sample will be set to `circuit.reference_sample()`, unless `skip_reference_sample=True` is used, in which case it will be set to all-zeros. Returns: An initialized stim.CompiledMeasurementSampler. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[ True, False, True, True]]) """ ``` ```python # stim.CompiledMeasurementSampler.__repr__ # (in class stim.CompiledMeasurementSampler) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CompiledMeasurementSampler`. """ ``` ```python # stim.CompiledMeasurementSampler.sample # (in class stim.CompiledMeasurementSampler) def sample( self, shots: int, *, bit_packed: bool = False, ) -> np.ndarray: """Samples a batch of measurement samples from the circuit. Args: shots: The number of times to sample every measurement in the circuit. bit_packed: Returns a uint8 numpy array with 8 bits per byte, instead of a bool_ numpy array with 1 bit per byte. Uses little endian packing. Returns: A numpy array containing the samples. If bit_packed=False: dtype=bool_ shape=(shots, circuit.num_measurements) The bit for measurement `m` in shot `s` is at result[s, m] If bit_packed=True: dtype=uint8 shape=(shots, math.ceil(circuit.num_measurements / 8)) The bit for measurement `m` in shot `s` is at (result[s, m // 8] >> (m % 8)) & 1 Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[ True, False, True, True]]) """ ``` ```python # stim.CompiledMeasurementSampler.sample_write # (in class stim.CompiledMeasurementSampler) def sample_write( self, shots: int, *, filepath: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Samples measurements from the circuit and writes them to a file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f"{d}/tmp.dat" ... c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') ... c.compile_sampler().sample_write(5, filepath=path, format="01") ... with open(path) as f: ... print(f.read(), end='') 1011 1011 1011 1011 1011 Args: shots: The number of times to sample every measurement in the circuit. filepath: The file to write the results to. format: The output format to write the results with. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". Returns: None. """ ``` ```python # stim.CompiledMeasurementsToDetectionEventsConverter # (at top-level in the stim module) class CompiledMeasurementsToDetectionEventsConverter: """A tool for quickly converting measurements from an analyzed stabilizer circuit into detection events. """ ``` ```python # stim.CompiledMeasurementsToDetectionEventsConverter.__init__ # (in class stim.CompiledMeasurementsToDetectionEventsConverter) def __init__( self, circuit: stim.Circuit, *, skip_reference_sample: bool = False, ) -> None: """Creates a measurement-to-detection-events converter for the given circuit. The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is. Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit. Args: circuit: The stim circuit to use for conversions. skip_reference_sample: Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution). Returns: An initialized stim.CompiledMeasurementsToDetectionEventsConverter. Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> converter.convert( ... measurements=np.array([[0], [1]], dtype=np.bool_), ... append_observables=False, ... ) array([[ True], [False]]) """ ``` ```python # stim.CompiledMeasurementsToDetectionEventsConverter.__repr__ # (in class stim.CompiledMeasurementsToDetectionEventsConverter) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CompiledMeasurementsToDetectionEventsConverter`. """ ``` ```python # stim.CompiledMeasurementsToDetectionEventsConverter.convert # (in class stim.CompiledMeasurementsToDetectionEventsConverter) @overload def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, append_observables: bool = False, bit_packed: bool = False, ) -> np.ndarray: pass @overload def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, separate_observables: Literal[True], append_observables: bool = False, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: pass def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, separate_observables: bool = False, append_observables: bool = False, bit_packed: bool = False, ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: """Converts measurement data into detection event data. Args: measurements: A numpy array containing measurement data. The dtype of the array is used to determine if it is bit packed or not. dtype=np.bool_ (unpacked data): shape=(num_shots, circuit.num_measurements) dtype=np.uint8 (bit packed data): shape=(num_shots, math.ceil(circuit.num_measurements / 8)) sweep_bits: Optional. A numpy array containing sweep data for the `sweep[k]` controls in the circuit. The dtype of the array is used to determine if it is bit packed or not. dtype=np.bool_ (unpacked data): shape=(num_shots, circuit.num_sweep_bits) dtype=np.uint8 (bit packed data): shape=(num_shots, math.ceil(circuit.num_sweep_bits / 8)) separate_observables: Defaults to False. When set to True, two numpy arrays are returned instead of one, with the second array containing the observable flip data. append_observables: Defaults to False. When set to True, the observables in the circuit are treated as if they were additional detectors. Their results are appended to the end of the detection event data. bit_packed: Defaults to False. When set to True, the returned numpy array contains bit packed data (dtype=np.uint8 with 8 bits per item) instead of unpacked data (dtype=np.bool_). Returns: The detection event data and (optionally) observable data. The result is a single numpy array if separate_observables is false, otherwise it's a tuple of two numpy arrays. When returning two numpy arrays, the first array is the detection event data and the second is the observable flip data. The dtype of the returned arrays is np.bool_ if bit_packed is false, otherwise they're np.uint8 arrays. shape[0] of the array(s) is the number of shots. shape[1] of the array(s) is the number of bits per shot (divided by 8 if bit packed) (e.g. for just detection event data it would be circuit.num_detectors). Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 1 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-2] ... ''').compile_m2d_converter() >>> dets, obs = converter.convert( ... measurements=np.array([[1, 0], ... [1, 0], ... [1, 0], ... [0, 0], ... [1, 0]], dtype=np.bool_), ... separate_observables=True, ... ) >>> dets array([[False, False], [False, False], [False, False], [False, True], [False, False]]) >>> obs array([[False], [False], [False], [ True], [False]]) """ ``` ```python # stim.CompiledMeasurementsToDetectionEventsConverter.convert_file # (in class stim.CompiledMeasurementsToDetectionEventsConverter) def convert_file( self, *, measurements_filepath: Union[str, pathlib.Path], measurements_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', sweep_bits_filepath: Optional[Union[str, pathlib.Path]] = None, sweep_bits_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', detection_events_filepath: Union[str, pathlib.Path], detection_events_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', append_observables: bool = False, obs_out_filepath: Optional[Union[str, pathlib.Path]] = None, obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Reads measurement data from a file and writes detection events to another file. Args: measurements_filepath: A file containing measurement data to be converted. measurements_format: The format the measurement data is stored in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". detection_events_filepath: Where to save detection event data to. detection_events_format: The format to save the detection event data in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". sweep_bits_filepath: Defaults to None. A file containing sweep data, or None. When specified, sweep data (used for `sweep[k]` controls in the circuit, which can vary from shot to shot) will be read from the given file. When not specified, all sweep bits default to False and no sweep-controlled operations occur. sweep_bits_format: The format the sweep data is stored in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". obs_out_filepath: Sample observables as part of each shot, and write them to this file. This keeps the observable data separate from the detector data. obs_out_format: If writing the observables to a file, this is the format to write them in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". append_observables: When True, the observables in the circuit are included as part of the detection event data. Specifically, they are treated as if they were additional detectors at the end of the circuit. When False, observable data is not output. Examples: >>> import stim >>> import tempfile >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> with tempfile.TemporaryDirectory() as d: ... with open(f"{d}/measurements.01", "w") as f: ... print("0", file=f) ... print("1", file=f) ... converter.convert_file( ... measurements_filepath=f"{d}/measurements.01", ... detection_events_filepath=f"{d}/detections.01", ... append_observables=False, ... ) ... with open(f"{d}/detections.01") as f: ... print(f.read(), end="") 1 0 """ ``` ```python # stim.DemInstruction # (at top-level in the stim module) class DemInstruction: """An instruction from a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 L0 ... error(0.125) D1 D2 ... error(0.125) D2 D3 ... error(0.125) D3 ... ''') >>> instruction = model[0] >>> instruction stim.DemInstruction('error', [0.125], [stim.target_relative_detector_id(0)]) """ ``` ```python # stim.DemInstruction.__eq__ # (in class stim.DemInstruction) def __eq__( self, arg0: stim.DemInstruction, ) -> bool: """Determines if two instructions have identical contents. """ ``` ```python # stim.DemInstruction.__init__ # (in class stim.DemInstruction) def __init__( self, type: str, args: Optional[Iterable[float]] = None, targets: Optional[Iterable[stim.DemTarget]] = None, *, tag: str = "", ) -> None: """Creates or parses a stim.DemInstruction. Args: type: The name of the instruction type (e.g. "error" or "shift_detectors"). If `args` and `targets` aren't specified, this can also be set to a full line of text from a dem file, like "error(0.25) D0". args: Numeric values parameterizing the instruction (e.g. the 0.1 in "error(0.1)"). targets: The objects the instruction involves (e.g. the "D0" and "L1" in "error(0.1) D0 L1"). tag: An arbitrary piece of text attached to the instruction. Examples: >>> import stim >>> instruction = stim.DemInstruction( ... 'error', ... [0.125], ... [stim.target_relative_detector_id(5)], ... tag='test-tag', ... ) >>> print(instruction) error[test-tag](0.125) D5 >>> print(stim.DemInstruction('error(0.125) D5 L6 ^ D4 # comment')) error(0.125) D5 L6 ^ D4 """ ``` ```python # stim.DemInstruction.__ne__ # (in class stim.DemInstruction) def __ne__( self, arg0: stim.DemInstruction, ) -> bool: """Determines if two instructions have non-identical contents. """ ``` ```python # stim.DemInstruction.__repr__ # (in class stim.DemInstruction) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.DetectorErrorModel`. """ ``` ```python # stim.DemInstruction.__str__ # (in class stim.DemInstruction) def __str__( self, ) -> str: """Returns detector error model (.dem) instructions (that can be parsed by stim) for the model. """ ``` ```python # stim.DemInstruction.args_copy # (in class stim.DemInstruction) def args_copy( self, ) -> List[float]: """Returns a copy of the list of numbers parameterizing the instruction. For example, this would be coordinates of a detector instruction or the probability of an error instruction. The result is a copy, meaning that editing it won't change the instruction's targets or future copies. Examples: >>> import stim >>> instruction = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''')[0] >>> instruction.args_copy() [0.125] >>> instruction.args_copy() == instruction.args_copy() True >>> instruction.args_copy() is instruction.args_copy() False """ ``` ```python # stim.DemInstruction.tag # (in class stim.DemInstruction) @property def tag( self, ) -> str: """Returns the arbitrary text tag attached to the instruction. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error[test-tag](0.125) D0 ... error(0.125) D0 ... ''') >>> dem[0].tag 'test-tag' >>> dem[1].tag '' """ ``` ```python # stim.DemInstruction.target_groups # (in class stim.DemInstruction) def target_groups( self, ) -> List[List[stim.DemTarget]]: """Returns a copy of the instruction's targets, split by target separators. When a detector error model instruction contains a suggested decomposition, its targets contain separators (`stim.DemTarget("^")`). This method splits the targets into groups based the separators, similar to how `str.split` works. Returns: A list of groups of targets. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.01) D0 D1 ^ D2 ... error(0.01) D0 L0 ... error(0.01) ... ''') >>> dem[0].target_groups() [[stim.DemTarget('D0'), stim.DemTarget('D1')], [stim.DemTarget('D2')]] >>> dem[1].target_groups() [[stim.DemTarget('D0'), stim.DemTarget('L0')]] >>> dem[2].target_groups() [[]] """ ``` ```python # stim.DemInstruction.targets_copy # (in class stim.DemInstruction) def targets_copy( self, ) -> List[Union[int, stim.DemTarget]]: """Returns a copy of the instruction's targets. The result is a copy, meaning that editing it won't change the instruction's targets or future copies. Examples: >>> import stim >>> instruction = stim.DetectorErrorModel(''' ... error(0.125) D0 L2 ... ''')[0] >>> instruction.targets_copy() [stim.DemTarget('D0'), stim.DemTarget('L2')] >>> instruction.targets_copy() == instruction.targets_copy() True >>> instruction.targets_copy() is instruction.targets_copy() False """ ``` ```python # stim.DemInstruction.type # (in class stim.DemInstruction) @property def type( self, ) -> str: """The name of the instruction type (e.g. "error" or "shift_detectors"). """ ``` ```python # stim.DemRepeatBlock # (at top-level in the stim module) class DemRepeatBlock: """A repeat block from a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... repeat 100 { ... error(0.125) D0 D1 ... shift_detectors 1 ... } ... ''') >>> model[0] stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' error(0.125) D0 D1 shift_detectors 1 ''')) """ ``` ```python # stim.DemRepeatBlock.__eq__ # (in class stim.DemRepeatBlock) def __eq__( self, arg0: stim.DemRepeatBlock, ) -> bool: """Determines if two repeat blocks are identical. """ ``` ```python # stim.DemRepeatBlock.__init__ # (in class stim.DemRepeatBlock) def __init__( self, repeat_count: int, block: stim.DetectorErrorModel, ) -> None: """Creates a stim.DemRepeatBlock. Args: repeat_count: The number of times the repeat block's body is supposed to execute. block: The body of the repeat block as a DetectorErrorModel containing the instructions to repeat. Examples: >>> import stim >>> repeat_block = stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' ... error(0.125) D0 D1 ... shift_detectors 1 ... ''')) """ ``` ```python # stim.DemRepeatBlock.__ne__ # (in class stim.DemRepeatBlock) def __ne__( self, arg0: stim.DemRepeatBlock, ) -> bool: """Determines if two repeat blocks are different. """ ``` ```python # stim.DemRepeatBlock.__repr__ # (in class stim.DemRepeatBlock) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.DemRepeatBlock`. """ ``` ```python # stim.DemRepeatBlock.body_copy # (in class stim.DemRepeatBlock) def body_copy( self, ) -> stim.DetectorErrorModel: """Returns a copy of the block's body, as a stim.DetectorErrorModel. Examples: >>> import stim >>> body = stim.DetectorErrorModel(''' ... error(0.125) D0 D1 ... shift_detectors 1 ... ''') >>> repeat_block = stim.DemRepeatBlock(100, body) >>> repeat_block.body_copy() == body True >>> repeat_block.body_copy() is repeat_block.body_copy() False """ ``` ```python # stim.DemRepeatBlock.repeat_count # (in class stim.DemRepeatBlock) @property def repeat_count( self, ) -> int: """The number of times the repeat block's body is supposed to execute. """ ``` ```python # stim.DemRepeatBlock.type # (in class stim.DemRepeatBlock) @property def type( self, ) -> object: """Returns the type name "repeat". This is a duck-typing convenience method. It exists so that code that doesn't know whether it has a `stim.DemInstruction` or a `stim.DemRepeatBlock` can check the type field without having to do an `instanceof` check first. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.1) D0 L0 ... repeat 5 { ... error(0.1) D0 D1 ... shift_detectors 1 ... } ... logical_observable L0 ... ''') >>> [instruction.type for instruction in dem] ['error', 'repeat', 'logical_observable'] """ ``` ```python # stim.DemTarget # (at top-level in the stim module) class DemTarget: """An instruction target from a detector error model (.dem) file. """ ``` ```python # stim.DemTarget.__eq__ # (in class stim.DemTarget) def __eq__( self, arg0: stim.DemTarget, ) -> bool: """Determines if two `stim.DemTarget`s are identical. """ ``` ```python # stim.DemTarget.__init__ # (in class stim.DemTarget) def __init__( self, arg: object, /, ) -> None: """Creates a stim.DemTarget from the given object. Args: arg: A string to parse as a stim.DemTarget, or some other object to convert into a stim.DemTarget. Examples: >>> import stim >>> stim.DemTarget("D5") == stim.target_relative_detector_id(5) True >>> stim.DemTarget("L2") == stim.target_logical_observable_id(2) True >>> stim.DemTarget("^") == stim.target_separator() True """ ``` ```python # stim.DemTarget.__ne__ # (in class stim.DemTarget) def __ne__( self, arg0: stim.DemTarget, ) -> bool: """Determines if two `stim.DemTarget`s are different. """ ``` ```python # stim.DemTarget.__repr__ # (in class stim.DemTarget) def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.DemTarget`. """ ``` ```python # stim.DemTarget.__str__ # (in class stim.DemTarget) def __str__( self, ) -> str: """Returns a text description of the detector error model target. """ ``` ```python # stim.DemTarget.is_logical_observable_id # (in class stim.DemTarget) def is_logical_observable_id( self, ) -> bool: """Determines if the detector error model target is a logical observable id target. In a detector error model file, observable targets are prefixed by `L`. For example, in `error(0.25) D0 L1` the `L1` is an observable target. Examples: >>> import stim >>> stim.DemTarget("L2").is_logical_observable_id() True >>> stim.DemTarget("D3").is_logical_observable_id() False >>> stim.DemTarget("^").is_logical_observable_id() False """ ``` ```python # stim.DemTarget.is_relative_detector_id # (in class stim.DemTarget) def is_relative_detector_id( self, ) -> bool: """Determines if the detector error model target is a relative detector id target. In a detector error model file, detectors are prefixed by `D`. For example, in `error(0.25) D0 L1` the `D0` is a relative detector target. Examples: >>> import stim >>> stim.DemTarget("L2").is_relative_detector_id() False >>> stim.DemTarget("D3").is_relative_detector_id() True >>> stim.DemTarget("^").is_relative_detector_id() False """ ``` ```python # stim.DemTarget.is_separator # (in class stim.DemTarget) def is_separator( self, ) -> bool: """Determines if the detector error model target is a separator. Separates separate the components of a suggested decompositions within an error. For example, the `^` in `error(0.25) D1 D2 ^ D3 D4` is the separator. Examples: >>> import stim >>> stim.DemTarget("L2").is_separator() False >>> stim.DemTarget("D3").is_separator() False >>> stim.DemTarget("^").is_separator() True """ ``` ```python # stim.DemTarget.logical_observable_id # (in class stim.DemTarget) @staticmethod def logical_observable_id( index: int, ) -> stim.DemTarget: """Returns a logical observable id identifying a frame change. Args: index: The index of the observable. Returns: The logical observable target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.logical_observable_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) L13 ''') """ ``` ```python # stim.DemTarget.relative_detector_id # (in class stim.DemTarget) @staticmethod def relative_detector_id( index: int, ) -> stim.DemTarget: """Returns a relative detector id (e.g. "D5" in a .dem file). Args: index: The index of the detector, relative to the current detector offset. Returns: The relative detector target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D13 ''') """ ``` ```python # stim.DemTarget.separator # (in class stim.DemTarget) @staticmethod def separator( ) -> stim.DemTarget: """Returns a target separator (e.g. "^" in a .dem file). Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(1), ... stim.DemTarget.separator(), ... stim.DemTarget.relative_detector_id(2), ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D1 ^ D2 ''') """ ``` ```python # stim.DemTarget.val # (in class stim.DemTarget) @property def val( self, ) -> int: """Returns the target's integer value. Example: >>> import stim >>> stim.DemTarget("D5").val 5 >>> stim.DemTarget("L6").val 6 """ ``` ```python # stim.DemTargetWithCoords # (at top-level in the stim module) class DemTargetWithCoords: """A detector error model instruction target with associated coords. It is also guaranteed that, if the type of the DEM target is a relative detector id, it is actually absolute (i.e. relative to 0). For example, if the DEM target is a detector from a circuit with coordinate arguments given to detectors, the coords field will contain the coordinate data for the detector. This is helpful information to have available when debugging a problem in a circuit, instead of having to constantly manually look up the coordinates of a detector index in order to understand what is happening. Examples: >>> import stim >>> t = stim.DemTargetWithCoords(stim.DemTarget("D1"), [1.5, 2.0]) >>> t.dem_target stim.DemTarget('D1') >>> t.coords [1.5, 2.0] """ ``` ```python # stim.DemTargetWithCoords.__init__ # (in class stim.DemTargetWithCoords) def __init__( self, dem_target: stim.DemTarget, coords: List[float], ) -> None: """Creates a stim.DemTargetWithCoords. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0] stim.DemTargetWithCoords(dem_target=stim.DemTarget('D0'), coords=[2, 3]) """ ``` ```python # stim.DemTargetWithCoords.coords # (in class stim.DemTargetWithCoords) @property def coords( self, ) -> List[float]: """Returns the associated coordinate information as a list of floats. If there is no coordinate information, returns an empty list. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0].coords [2.0, 3.0] """ ``` ```python # stim.DemTargetWithCoords.dem_target # (in class stim.DemTargetWithCoords) @property def dem_target( self, ) -> stim.DemTarget: """Returns the actual DEM target as a `stim.DemTarget`. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0].dem_target stim.DemTarget('D0') """ ``` ```python # stim.DetectorErrorModel # (at top-level in the stim module) class DetectorErrorModel: """An error model built out of independent error mechanics. This class is one of the most important classes in Stim, because it is the mechanism used to explain circuits to decoders. A typical workflow would look something like: 1. Create a quantum error correction circuit annotated with detectors and observables. 2. Fail at configuring your favorite decoder using the circuit, because it's a pain to convert circuit error mechanisms into a format understood by the decoder. 2a. Call circuit.detector_error_model(), with decompose_errors=True if working with a matching-based code. This converts the circuit errors into a straightforward list of independent "with probability p these detectors and observables get flipped" terms. 3. Write tedious but straightforward glue code to create whatever graph-like object the decoder needs from the detector error model. 3a. Actually, ideally, someone has already done that for you. For example, pymatching can take detector error models directly and sinter knows how to explain a detector error model to fusion_blossom. 4. Get samples using circuit.compile_detector_sampler(), feed them to the decoder, and compare its observable flip predictions to the actual flips recorded in the samples. 4a. Actually, sinter will basically handle steps 2 through 4 for you. So you should probably have just generated your circuits, called `sinter collect` on them, then `sinter plot` on the results. 5. Write the paper. Error mechanisms are described in terms of the visible detection events and the hidden observable frame changes that they causes. Error mechanisms can also suggest decompositions of their effects into components, which can be helpful for decoders that want to work with a simpler decomposed error model instead of the full error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 L0 ... error(0.125) D1 D2 ... error(0.125) D2 D3 ... error(0.125) D3 ... ''') >>> len(model) 5 >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model() stim.DetectorErrorModel(''' error(0.125) D0 error(0.375) D0 D1 error(0.25) D1 ''') """ ``` ```python # stim.DetectorErrorModel.__add__ # (in class stim.DetectorErrorModel) def __add__( self, second: stim.DetectorErrorModel, ) -> stim.DetectorErrorModel: """Creates a detector error model by appending two models. Examples: >>> import stim >>> m1 = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''') >>> m2 = stim.DetectorErrorModel(''' ... error(0.25) D1 ... ''') >>> m1 + m2 stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D1 ''') """ ``` ```python # stim.DetectorErrorModel.__eq__ # (in class stim.DetectorErrorModel) def __eq__( self, arg0: stim.DetectorErrorModel, ) -> bool: """Determines if two detector error models have identical contents. """ ``` ```python # stim.DetectorErrorModel.__getitem__ # (in class stim.DetectorErrorModel) @overload def __getitem__( self, index_or_slice: int, ) -> Union[stim.DemInstruction, stim.DemRepeatBlock]: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.DetectorErrorModel: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns copies of instructions from the detector error model. Args: index_or_slice: An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D1 L1 ... repeat 100 { ... error(0.125) D1 D2 ... shift_detectors 1 ... } ... error(0.125) D2 ... logical_observable L0 ... detector D5 ... ''') >>> model[0] stim.DemInstruction('error', [0.125], [stim.target_relative_detector_id(0)]) >>> model[2] stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' error(0.125) D1 D2 shift_detectors 1 ''')) >>> model[1::2] stim.DetectorErrorModel(''' error(0.125) D1 L1 error(0.125) D2 detector D5 ''') """ ``` ```python # stim.DetectorErrorModel.__iadd__ # (in class stim.DetectorErrorModel) def __iadd__( self, second: stim.DetectorErrorModel, ) -> stim.DetectorErrorModel: """Appends a detector error model into the receiving model (mutating it). Examples: >>> import stim >>> m1 = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''') >>> m2 = stim.DetectorErrorModel(''' ... error(0.25) D1 ... ''') >>> m1 += m2 >>> print(repr(m1)) stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D1 ''') """ ``` ```python # stim.DetectorErrorModel.__imul__ # (in class stim.DetectorErrorModel) def __imul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Mutates the detector error model by putting its contents into a repeat block. Special case: if the repetition count is 0, the model is cleared. Special case: if the repetition count is 1, nothing happens. Args: repetitions: The number of times the repeat block should repeat. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> m *= 3 >>> print(m) repeat 3 { error(0.25) D0 shift_detectors 1 } """ ``` ```python # stim.DetectorErrorModel.__init__ # (in class stim.DetectorErrorModel) def __init__( self, detector_error_model_text: str = '', ) -> None: """Creates a stim.DetectorErrorModel. Args: detector_error_model_text: Defaults to empty. Describes instructions to append into the circuit in the detector error model (.dem) format. Examples: >>> import stim >>> empty = stim.DetectorErrorModel() >>> not_empty = stim.DetectorErrorModel(''' ... error(0.125) D0 L0 ... ''') """ ``` ```python # stim.DetectorErrorModel.__len__ # (in class stim.DetectorErrorModel) def __len__( self, ) -> int: """Returns the number of top-level instructions/blocks in the detector error model. Instructions inside of blocks are not included in this count. Examples: >>> import stim >>> len(stim.DetectorErrorModel()) 0 >>> len(stim.DetectorErrorModel(''' ... error(0.1) D0 D1 ... shift_detectors 100 ... logical_observable L5 ... ''')) 3 >>> len(stim.DetectorErrorModel(''' ... repeat 100 { ... error(0.1) D0 D1 ... error(0.1) D1 D2 ... } ... ''')) 1 """ ``` ```python # stim.DetectorErrorModel.__mul__ # (in class stim.DetectorErrorModel) def __mul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Repeats the detector error model using a repeat block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the repeat block should repeat. Returns: repetitions=0: An empty detector error model. repetitions=1: A copy of this detector error model. repetitions>=2: A detector error model with a single repeat block, where the contents of that repeat block are this detector error model. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> m * 3 stim.DetectorErrorModel(''' repeat 3 { error(0.25) D0 shift_detectors 1 } ''') """ ``` ```python # stim.DetectorErrorModel.__ne__ # (in class stim.DetectorErrorModel) def __ne__( self, arg0: stim.DetectorErrorModel, ) -> bool: """Determines if two detector error models have non-identical contents. """ ``` ```python # stim.DetectorErrorModel.__repr__ # (in class stim.DetectorErrorModel) def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.DetectorErrorModel`. """ ``` ```python # stim.DetectorErrorModel.__rmul__ # (in class stim.DetectorErrorModel) def __rmul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Repeats the detector error model using a repeat block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the repeat block should repeat. Returns: repetitions=0: An empty detector error model. repetitions=1: A copy of this detector error model. repetitions>=2: A detector error model with a single repeat block, where the contents of that repeat block are this detector error model. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> 3 * m stim.DetectorErrorModel(''' repeat 3 { error(0.25) D0 shift_detectors 1 } ''') """ ``` ```python # stim.DetectorErrorModel.__str__ # (in class stim.DetectorErrorModel) def __str__( self, ) -> str: """Returns the contents of a detector error model file (.dem) encoding the model. """ ``` ```python # stim.DetectorErrorModel.append # (in class stim.DetectorErrorModel) def append( self, instruction: object, parens_arguments: object = None, targets: List[object] = (), *, tag: str = '', ) -> None: """Appends an instruction to the detector error model. Args: instruction: Either the name of an instruction, a stim.DemInstruction, a stim.DemRepeatBlock. or a stim.DetectorErrorModel. The `parens_arguments`, `targets`, and 'tag' arguments should be given iff the instruction is a name. parens_arguments: Numeric values parameterizing the instruction. The numbers inside parentheses in a detector error model file (eg. the `0.25` in `error(0.25) D0`). This argument can be given either a list of doubles, or a single double (which will be implicitly wrapped into a list). targets: The instruction targets, such as the `D0` in `error(0.25) D0`. tag: An arbitrary piece of text attached to the repeat instruction. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.125, [ ... stim.DemTarget.relative_detector_id(1), ... ]) >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(1), ... stim.DemTarget.separator(), ... stim.DemTarget.relative_detector_id(2), ... stim.DemTarget.logical_observable_id(3), ... ], tag='test-tag') >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 ''') >>> m.append("shift_detectors", (1, 2, 3), [5]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 ''') >>> m += m * 3 >>> m.append(m[0]) >>> m.append(m[-2]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 repeat 3 { error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } error(0.125) D1 repeat 3 { error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } ''') """ ``` ```python # stim.DetectorErrorModel.approx_equals # (in class stim.DetectorErrorModel) def approx_equals( self, other: object, *, atol: float, ) -> bool: """Checks if detector error models are approximately equal. Two detector error model are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example `error(0.100) D0` is approximately equal to `error(0.099) D0` within an absolute tolerance of 0.002. All other details of the models (such as the ordering of errors and their targets) must be exactly the same. Args: other: The detector error model, or other object, to compare to this one. atol: The absolute error tolerance. The maximum amount each probability may have been perturbed by. Returns: True if the given object is a detector error model approximately equal up to the receiving circuit up to the given tolerance, otherwise False. Examples: >>> import stim >>> base = stim.DetectorErrorModel(''' ... error(0.099) D0 D1 ... ''') >>> base.approx_equals(base, atol=0) True >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0) False >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0.0001) False >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0.01) True >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.099) D0 D1 L0 L1 L2 L3 L4 ... '''), atol=9999) False """ ``` ```python # stim.DetectorErrorModel.clear # (in class stim.DetectorErrorModel) def clear( self, ) -> None: """Clears the contents of the detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.1) D0 D1 ... ''') >>> model.clear() >>> model stim.DetectorErrorModel() """ ``` ```python # stim.DetectorErrorModel.compile_sampler # (in class stim.DetectorErrorModel) def compile_sampler( self, *, seed: object = None, ) -> stim.CompiledDemSampler: """Returns a CompiledDemSampler that can batch sample from detector error models. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Returns: A seeded stim.CompiledDemSampler for the given detector error model. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) """ ``` ```python # stim.DetectorErrorModel.copy # (in class stim.DetectorErrorModel) def copy( self, ) -> stim.DetectorErrorModel: """Returns a copy of the detector error model. The copy is an independent detector error model with the same contents. Examples: >>> import stim >>> c1 = stim.DetectorErrorModel("error(0.1) D0 D1") >>> c2 = c1.copy() >>> c2 is c1 False >>> c2 == c1 True """ ``` ```python # stim.DetectorErrorModel.diagram # (in class stim.DetectorErrorModel) def diagram( self, type: Literal["matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html"] = 'matchgraph-svg', ) -> Any: """Returns a diagram of the circuit, from a variety of options. Args: type: The type of diagram. Available types are: "matchgraph-svg": An image of the decoding graph of the detector error model. Red lines are errors crossing a logical observable. Blue lines are undecomposed hyper errors. "matchgraph-svg-html": Same as matchgraph-svg but with the SVG wrapped in a resizable HTML iframe. "matchgraph-3d": A 3d model of the decoding graph of the detector error model. Red lines are errors crossing a logical observable. Blue lines are undecomposed hyper errors. GLTF files can be opened with a variety of programs, or opened online in viewers such as https://gltf-viewer.donmccurdy.com/ . Red lines are errors crossing a logical observable. "matchgraph-3d-html": Same 3d model as 'match-graph-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. Returns: An object whose `__str__` method returns the diagram, so that writing the diagram to a file works correctly. The returned object also defines a `_repr_html_` method, so that ipython notebooks recognize it can be shown using a specialized viewer instead of as raw text. Examples: >>> import stim >>> import tempfile >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... after_clifford_depolarization=0.01) >>> dem = circuit.detector_error_model(decompose_errors=True) >>> with tempfile.TemporaryDirectory() as d: ... diagram = circuit.diagram("match-graph-svg") ... with open(f"{d}/dem_image.svg", "w") as f: ... print(diagram, file=f) >>> with tempfile.TemporaryDirectory() as d: ... diagram = circuit.diagram("match-graph-3d") ... with open(f"{d}/dem_3d_model.gltf", "w") as f: ... print(diagram, file=f) """ ``` ```python # stim.DetectorErrorModel.flattened # (in class stim.DetectorErrorModel) def flattened( self, ) -> stim.DetectorErrorModel: """Returns the detector error model without repeat or detector_shift instructions. Returns: A `stim.DetectorErrorModel` with the same errors in the same order, but with repeat loops flattened into actually repeated instructions and with all coordinate/index shifts inlined. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... REPEAT 5 { ... error(0.25) D0 D1 ... shift_detectors 1 ... } ... error(0.125) D0 L0 ... ''').flattened() stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D0 D1 error(0.25) D1 D2 error(0.25) D2 D3 error(0.25) D3 D4 error(0.25) D4 D5 error(0.125) D5 L0 ''') """ ``` ```python # stim.DetectorErrorModel.from_file # (in class stim.DetectorErrorModel) @staticmethod def from_file( file: Union[io.TextIOBase, str, pathlib.Path], ) -> stim.DetectorErrorModel: """Reads a detector error model from a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md Args: file: A file path or open file object to read from. Returns: The circuit parsed from the file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('error(0.25) D2 D3', file=f) ... circuit = stim.DetectorErrorModel.from_file(path) >>> circuit stim.DetectorErrorModel(''' error(0.25) D2 D3 ''') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('error(0.25) D2 D3', file=f) ... with open(path) as f: ... circuit = stim.DetectorErrorModel.from_file(f) >>> circuit stim.DetectorErrorModel(''' error(0.25) D2 D3 ''') """ ``` ```python # stim.DetectorErrorModel.get_detector_coordinates # (in class stim.DetectorErrorModel) def get_detector_coordinates( self, only: object = None, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of detectors in the detector error model. Args: only: Defaults to None (meaning include all detectors). A list of detector indices to include in the result. Detector indices beyond the end of the detector error model cause an error. Returns: A dictionary mapping integers (detector indices) to lists of floats (coordinates). Detectors with no specified coordinate data are mapped to an empty tuple. If `only` is specified, then `set(result.keys()) == set(only)`. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.25) D0 D1 ... detector(1, 2, 3) D1 ... shift_detectors(5) 1 ... detector(1, 2) D2 ... ''') >>> dem.get_detector_coordinates() {0: [], 1: [1.0, 2.0, 3.0], 2: [], 3: [6.0, 2.0]} >>> dem.get_detector_coordinates(only=[1]) {1: [1.0, 2.0, 3.0]} """ ``` ```python # stim.DetectorErrorModel.num_detectors # (in class stim.DetectorErrorModel) @property def num_detectors( self, ) -> int: """Counts the number of detectors (e.g. `D2`) in the error model. Detector indices are assumed to be contiguous from 0 up to whatever the maximum detector id is. If the largest detector's absolute id is n-1, then the number of detectors is n. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model().num_detectors 2 >>> stim.DetectorErrorModel(''' ... error(0.1) D0 D199 ... ''').num_detectors 200 >>> stim.DetectorErrorModel(''' ... shift_detectors 1000 ... error(0.1) D0 D199 ... ''').num_detectors 1200 """ ``` ```python # stim.DetectorErrorModel.num_errors # (in class stim.DetectorErrorModel) @property def num_errors( self, ) -> int: """Counts the number of errors (e.g. `error(0.1) D0`) in the error model. Error instructions inside repeat blocks count once per repetition. Redundant errors with the same targets count as separate errors. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... repeat 100 { ... repeat 5 { ... error(0.25) D1 ... } ... } ... ''').num_errors 501 """ ``` ```python # stim.DetectorErrorModel.num_observables # (in class stim.DetectorErrorModel) @property def num_observables( self, ) -> int: """Counts the number of frame changes (e.g. `L2`) in the error model. Observable indices are assumed to be contiguous from 0 up to whatever the maximum observable id is. If the largest observable's id is n-1, then the number of observables is n. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(99) rec[-1] ... ''').detector_error_model().num_observables 100 >>> stim.DetectorErrorModel(''' ... error(0.1) L399 ... ''').num_observables 400 """ ``` ```python # stim.DetectorErrorModel.rounded # (in class stim.DetectorErrorModel) def rounded( self, arg0: int, ) -> stim.DetectorErrorModel: """Creates an equivalent detector error model but with rounded error probabilities. Args: digits: The number of digits to round to. Returns: A `stim.DetectorErrorModel` with the same instructions in the same order, but with the parens arguments of error instructions rounded to the given precision. Instructions whose error probability was rounded to zero are still included in the output. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.019499) D0 ... error(0.000001) D0 D1 ... ''') >>> dem.rounded(2) stim.DetectorErrorModel(''' error(0.02) D0 error(0) D0 D1 ''') >>> dem.rounded(3) stim.DetectorErrorModel(''' error(0.019) D0 error(0) D0 D1 ''') """ ``` ```python # stim.DetectorErrorModel.shortest_graphlike_error # (in class stim.DetectorErrorModel) def shortest_graphlike_error( self, ignore_ungraphlike_errors: bool = True, ) -> stim.DetectorErrorModel: """Finds a minimum set of graphlike errors to produce an undetected logical error. Note that this method does not pay attention to error probabilities (other than ignoring errors with probability 0). It searches for a logical error with the minimum *number* of physical errors, not the maximum probability of those physical errors all occurring. This method works by looking for errors that have frame changes (eg. "error(0.1) D0 D1 L5" flips the frame of observable 5). These errors are converted into one or two symptoms and a net frame change. The symptoms can then be moved around by following errors touching that symptom. Each symptom is moved until it disappears into a boundary or cancels against another remaining symptom, while leaving the other symptoms alone (ensuring only one symptom is allowed to move significantly reduces waste in the search space). Eventually a path or cycle of errors is found that cancels out the symptoms, and if there is still a frame change at that point then that path or cycle is a logical error (otherwise all that was found was a stabilizer of the system; a dead end). The search process advances like a breadth first search, seeded from all the frame-change errors and branching them outward in tandem, until one of them wins the race to find a solution. Args: ignore_ungraphlike_errors: Defaults to True. When False, an exception is raised if there are any errors in the model that are not graphlike. When True, those errors are skipped as if they weren't present. A graphlike error is an error with less than two symptoms. For the purposes of this method, errors are also considered graphlike if they are decomposed into graphlike components: graphlike: error(0.1) D0 error(0.1) D0 D1 error(0.1) D0 D1 L0 not graphlike but decomposed into graphlike components: error(0.1) D0 D1 ^ D2 not graphlike, not decomposed into graphlike components: error(0.1) D0 D1 D2 error(0.1) D0 D1 D2 ^ D3 Returns: A detector error model containing just the error instructions corresponding to an undetectable logical error. There will be no other kinds of instructions (no `repeat`s, no `shift_detectors`, etc). The error probabilities will all be set to 1. The `len` of the returned model is the graphlike code distance of the circuit. But beware that in general the true code distance may be smaller. For example, in the XZ surface code with twists, the true minimum sized logical error is likely to use Y errors. But each Y error decomposes into two graphlike components (the X part and the Z part). As a result, the graphlike code distance in that context is likely to be nearly twice as large as the true code distance. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 ... error(0.125) D1 L55 ... error(0.125) D1 ... ''').shortest_graphlike_error() stim.DetectorErrorModel(''' error(1) D1 error(1) D1 L55 ''') >>> stim.DetectorErrorModel(''' ... error(0.125) D0 D1 D2 ... error(0.125) L0 ... ''').shortest_graphlike_error(ignore_ungraphlike_errors=True) stim.DetectorErrorModel(''' error(1) L0 ''') >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... before_round_data_depolarization=0.01) >>> model = circuit.detector_error_model(decompose_errors=True) >>> len(model.shortest_graphlike_error()) 7 """ ``` ```python # stim.DetectorErrorModel.to_file # (in class stim.DetectorErrorModel) def to_file( self, file: Union[io.TextIOBase, str, pathlib.Path], ) -> None: """Writes the detector error model to a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md Args: file: A file path or an open file to write to. Examples: >>> import stim >>> import tempfile >>> c = stim.DetectorErrorModel('error(0.25) D2 D3') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... c.to_file(f) ... with open(path) as f: ... contents = f.read() >>> contents 'error(0.25) D2 D3\n' >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... c.to_file(path) ... with open(path) as f: ... contents = f.read() >>> contents 'error(0.25) D2 D3\n' """ ``` ```python # stim.DetectorErrorModel.without_tags # (in class stim.DetectorErrorModel) def without_tags( self, ) -> stim.DetectorErrorModel: """Returns a copy of the detector error model with all tags removed. Returns: A `stim.DetectorErrorModel` with the same instructions except all tags have been removed. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error[test-tag](0.25) D0 ... ''').without_tags() stim.DetectorErrorModel(''' error(0.25) D0 ''') """ ``` ```python # stim.ExplainedError # (at top-level in the stim module) class ExplainedError: """Describes the location of an error mechanism from a stim circuit. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0]) ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } } """ ``` ```python # stim.ExplainedError.__init__ # (in class stim.ExplainedError) def __init__( self, *, dem_error_terms: List[stim.DemTargetWithCoords], circuit_error_locations: List[stim.CircuitErrorLocation], ) -> None: """Creates a stim.ExplainedError. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0]) ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } } """ ``` ```python # stim.ExplainedError.circuit_error_locations # (in class stim.ExplainedError) @property def circuit_error_locations( self, ) -> List[stim.CircuitErrorLocation]: """The locations of circuit errors that produce the symptoms in dem_error_terms. Note: if this list contains a single entry, it may be because a result with a single representative error was requested (as opposed to all possible errors). Note: if this list is empty, it may be because there was a DEM error decomposed into parts where one of the parts is impossible to make on its own from a single circuit error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0].circuit_error_locations[0]) CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } """ ``` ```python # stim.ExplainedError.dem_error_terms # (in class stim.ExplainedError) @property def dem_error_terms( self, ) -> List[stim.DemTargetWithCoords]: """The detectors and observables flipped by this error mechanism. """ ``` ```python # stim.FlipSimulator # (at top-level in the stim module) class FlipSimulator: """A simulator that tracks whether things are flipped, instead of what they are. Tracking flips is significantly cheaper than tracking actual values, requiring O(1) work per gate (compared to O(n) for unitary operations and O(n^2) for collapsing operations in the tableau simulator, where n is the qubit count). Supports interactive usage, where gates and measurements are applied on demand. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) """ ``` ```python # stim.FlipSimulator.__init__ # (in class stim.FlipSimulator) def __init__( self, *, batch_size: int, disable_stabilizer_randomization: bool = False, num_qubits: int = 0, seed: Optional[int] = None, ) -> None: """Initializes a stim.FlipSimulator. Args: batch_size: For speed, the flip simulator simulates many instances in parallel. This argument determines the number of parallel instances. It's recommended to use a multiple of 256, because internally the state of the instances is striped across SSE (128 bit) or AVX (256 bit) words with one bit in the word belonging to each instance. The result is that, even if you only ask for 1 instance, probably the same amount of work is being done as if you'd asked for 256 instances. The extra results just aren't being used, creating waste. disable_stabilizer_randomization: Determines whether or not the flip simulator uses stabilizer randomization. Defaults to False (stabilizer randomization used). Set to True to disable stabilizer randomization. Stabilizer randomization means that, when a qubit is initialized or measured in the Z basis, a Z error is added to the qubit with 50% probability. More generally, anytime a stabilizer is introduced into the system by any means, an error equal to that stabilizer is applied with 50% probability. This ensures that observables anticommuting with stabilizers of the system must be maximally uncertain. In other words, this feature enforces Heisenberg's uncertainty principle. This is a safety feature that you should not turn off unless you have a reason to do so. Stabilizer randomization is turned on by default because it catches mistakes. For example, suppose you are trying to create a stabilizer code but you accidentally have the code measure two anticommuting stabilizers. With stabilizer randomization turned off, it will look like this code works. With stabilizer randomization turned on, the two measurements will correctly randomize each other revealing that the code doesn't work. In some use cases, stabilizer randomization is a hindrance instead of helpful. For example, if you are using the flip simulator to understand how an error propagates through the system, the stabilizer randomization will be introducing error terms that you don't want. num_qubits: Sets the initial number of qubits tracked by the simulation. The simulator will still automatically resize as needed when qubits beyond this limit are touched. This parameter exists as a way to hint at the desired size of the simulator's state for performance, and to ensure methods that peek at the size have the expected size from the start instead of only after the relevant qubits have been touched. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: An initialized stim.FlipSimulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) """ ``` ```python # stim.FlipSimulator.append_measurement_flips # (in class stim.FlipSimulator) def append_measurement_flips( self, measurement_flip_data: np.ndarray, ) -> None: """Appends measurement flip data to the simulator's measurement record. Args: measurement_flip_data: The flip data to append. The following shape/dtype combinations are supported. Single measurement without bit packing: shape=(self.batch_size,) dtype=np.bool_ Single measurement with bit packing: shape=(math.ceil(self.batch_size / 8),) dtype=np.uint8 Multiple measurements without bit packing: shape=(num_measurements, self.batch_size) dtype=np.bool_ Multiple measurements with bit packing: shape=(num_measurements, math.ceil(self.batch_size / 8)) dtype=np.uint8 Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.append_measurement_flips(np.array( ... [0, 1, 0, 0, 1, 0, 0, 1, 1], ... dtype=np.bool_, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True]]) >>> sim.append_measurement_flips(np.array( ... [0b11001001, 0], ... dtype=np.uint8, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False]]) >>> sim.append_measurement_flips(np.array( ... [[0b11111111, 0b1], [0b00000000, 0b0], [0b11111111, 0b1]], ... dtype=np.uint8, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False], [ True, True, True, True, True, True, True, True, True], [False, False, False, False, False, False, False, False, False], [ True, True, True, True, True, True, True, True, True]]) >>> sim.append_measurement_flips(np.array( ... [[1, 0, 1, 0, 1, 0, 1, 0, 1], [0, 1, 0, 1, 0, 1, 0, 1, 0]], ... dtype=np.bool_, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False], [ True, True, True, True, True, True, True, True, True], [False, False, False, False, False, False, False, False, False], [ True, True, True, True, True, True, True, True, True], [ True, False, True, False, True, False, True, False, True], [False, True, False, True, False, True, False, True, False]]) """ ``` ```python # stim.FlipSimulator.batch_size # (in class stim.FlipSimulator) @property def batch_size( self, ) -> int: """Returns the number of instances being simulated by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.batch_size 256 >>> sim = stim.FlipSimulator(batch_size=42) >>> sim.batch_size 42 """ ``` ```python # stim.FlipSimulator.broadcast_pauli_errors # (in class stim.FlipSimulator) def broadcast_pauli_errors( self, *, pauli: Union[str, int], mask: np.ndarray, p: float = 1, ) -> None: """Applies a pauli error to all qubits in all instances, filtered by a mask. Args: pauli: The pauli, specified as an integer or string. Uses the convention 0=I, 1=X, 2=Y, 3=Z. Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. mask: A 2d numpy array specifying where to apply errors. The first axis is qubits, the second axis is simulation instances. The first axis can have a length less than the current number of qubits (or more, which adds qubits to the simulation). The length of the second axis must match the simulator's `batch_size`. The array must satisfy mask.dtype == np.bool_ len(mask.shape) == 2 mask.shape[1] == flip_sim.batch_size The error is only applied to qubit q in instance k when mask[q, k] == True. p: Defaults to 1 (no effect). When specified, the error is applied probabilistically instead of deterministically to each (instance, qubit) pair matching the mask. This argument specifies the probability. Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator( ... batch_size=2, ... num_qubits=3, ... disable_stabilizer_randomization=True, ... ) >>> sim.broadcast_pauli_errors( ... pauli='X', ... mask=np.asarray([[True, False],[False, False],[True, True]]), ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+X_X"), stim.PauliString("+__X")] >>> sim.broadcast_pauli_errors( ... pauli='Z', ... mask=np.asarray([[False, True],[False, False],[True, True]]), ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] """ ``` ```python # stim.FlipSimulator.clear # (in class stim.FlipSimulator) def clear( self, ) -> None: """Clears the simulator's state, so it can be reused for another simulation. This clears the measurement flip history, clears the detector flip history, and zeroes the observable flip state. It also resets all qubits to |0>. If stabilizer randomization is disabled, this zeros all pauli flip data. Otherwise it randomizes all pauli flips to be I or Z with equal probability. Behind the scenes, this doesn't free memory or resize the simulator. So, repeating the same simulation with calls to `clear` in between will be faster than allocating a new simulator each time (by avoiding re-allocations). Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.do(stim.Circuit("M(0.1) 9")) >>> sim.num_qubits 10 >>> sim.get_measurement_flips().shape (1, 256) >>> sim.clear() >>> sim.num_qubits 10 >>> sim.get_measurement_flips().shape (0, 256) """ ``` ```python # stim.FlipSimulator.copy # (in class stim.FlipSimulator) def copy( self, *, copy_rng: bool = False, seed: Optional[int] = None, ) -> stim.FlipSimulator: """Returns a simulator with the same internal state, except perhaps its prng. Args: copy_rng: Defaults to False. When False, the copy's pseudo random number generator is reinitialized with a random seed instead of being a copy of the original simulator's pseudo random number generator. This causes the copy and the original to sample independent randomness, instead of identical randomness, for future random operations. When set to true, the copy will have the exact same pseudo random number generator state as the original, and so will produce identical results if told to do the same noisy operations. This argument is incompatible with the `seed` argument. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng state is either copied from the original simulator or reseeded from system entropy, depending on the copy_rng argument. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: The copy of the simulator. Examples: >>> import stim >>> import numpy as np >>> s1 = stim.FlipSimulator(batch_size=256) >>> s1.set_pauli_flip('X', qubit_index=2, instance_index=3) >>> s2 = s1.copy() >>> s2 is s1 False >>> s2.peek_pauli_flips() == s1.peek_pauli_flips() True >>> s1 = stim.FlipSimulator(batch_size=256) >>> s2 = s1.copy(copy_rng=True) >>> s1.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) >>> s2.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) >>> np.array_equal(s1.get_measurement_flips(), s2.get_measurement_flips()) True """ ``` ```python # stim.FlipSimulator.do # (in class stim.FlipSimulator) def do( self, obj: Union[stim.Circuit, stim.CircuitInstruction, stim.CircuitRepeatBlock], ) -> None: """Applies a circuit or circuit instruction to the simulator's state. The results of any measurements performed can be retrieved using the `get_measurement_flips` method. Args: obj: The circuit or instruction to apply to the simulator's state. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=1, ... disable_stabilizer_randomization=True, ... ) >>> circuit = stim.Circuit(''' ... X_ERROR(1) 0 1 3 ... REPEAT 5 { ... H 0 ... C_XYZ 1 ... } ... ''') >>> sim.do(circuit) >>> sim.peek_pauli_flips() [stim.PauliString("+ZZ_X")] >>> sim.do(circuit[0]) >>> sim.peek_pauli_flips() [stim.PauliString("+YY__")] >>> sim.do(circuit[1]) >>> sim.peek_pauli_flips() [stim.PauliString("+YX__")] """ ``` ```python # stim.FlipSimulator.generate_bernoulli_samples # (in class stim.FlipSimulator) def generate_bernoulli_samples( self, num_samples: int, *, p: float, bit_packed: bool = False, out: Optional[np.ndarray] = None, ) -> np.ndarray: """Uses the simulator's random number generator to produce biased coin flips. This method has best performance when specifying `bit_packed=True` and when specifying an `out=` parameter pointing to a numpy array that has contiguous data aligned to a 64 bit boundary. (If `out` isn't specified, the returned numpy array will have this property.) Args: num_samples: The number of samples to produce. p: The probability of each sample being True instead of False. bit_packed: Defaults to False (no bit packing). When True, the result has type np.uint8 instead of np.bool_ and 8 samples are packed into each byte as if by np.packbits(bitorder='little'). (The bit order is relevant when producing a number of samples that isn't a multiple of 8.) out: Defaults to None (allocate new). A numpy array to write the samples into. Must have the correct size and dtype. Returns: A numpy array containing the samples. The shape and dtype depends on the bit_packed argument: if not bit_packed: shape = (num_samples,) dtype = np.bool_ elif not transpose and bit_packed: shape = (math.ceil(num_samples / 8),) dtype = np.uint8 Raises: ValueError: The given `out` argument had a shape or dtype inconsistent with the requested data. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> r = sim.generate_bernoulli_samples(1001, p=0.25) >>> r.dtype dtype('bool') >>> r.shape (1001,) >>> r = sim.generate_bernoulli_samples(53, p=0.1, bit_packed=True) >>> r.dtype dtype('uint8') >>> r.shape (7,) >>> r[6] & 0b1110_0000 # zero'd padding bits np.uint8(0) >>> r2 = sim.generate_bernoulli_samples(53, p=0.2, bit_packed=True, out=r) >>> r is r2 # Check request to reuse r worked. True """ ``` ```python # stim.FlipSimulator.get_detector_flips # (in class stim.FlipSimulator) def get_detector_flips( self, *, detector_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves detector flip data from the simulator's detection event record. Args: record_index: Identifies a detector to read results from. Setting this to None (default) returns results from all detectors. Otherwise this should be an integer in range(0, self.num_detectors). instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_detectors, self.batch_size), where the first index is the detector_index and the second index is the instance_index and the dtype is np.bool_. Specifying detector_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a detector_index (or a 0d array, a boolean, if detector_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit(''' ... M 0 0 0 ... DETECTOR rec[-2] rec[-3] ... DETECTOR rec[-1] rec[-2] ... ''')) >>> sim.get_detector_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_detector_flips(bit_packed=True) array([[0, 0], [0, 0]], dtype=uint8) >>> sim.get_detector_flips(instance_index=2) array([False, False]) >>> sim.get_detector_flips(detector_index=1) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_detector_flips(instance_index=2, detector_index=1) array(False) """ ``` ```python # stim.FlipSimulator.get_measurement_flips # (in class stim.FlipSimulator) def get_measurement_flips( self, *, record_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves measurement flip data from the simulator's measurement record. Args: record_index: Identifies a measurement to read results from. Setting this to None (default) returns results from all measurements. Setting this to a non-negative integer indexes measurements by the order they occurred. For example, record index 0 is the first measurement. Setting this to a negative integer indexes measurements by recency. For example, recording index -1 is the most recent measurement. instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be set to an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_measurements, self.batch_size), where the first index is the measurement_index and the second index is the instance_index and the dtype is np.bool_. Specifying record_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a measurement_index (or a 0d array, a boolean, if record_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit('M 0 1 2')) >>> sim.get_measurement_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_measurement_flips(bit_packed=True) array([[0, 0], [0, 0], [0, 0]], dtype=uint8) >>> sim.get_measurement_flips(instance_index=1) array([False, False, False]) >>> sim.get_measurement_flips(record_index=2) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_measurement_flips(instance_index=1, record_index=2) array(False) """ ``` ```python # stim.FlipSimulator.get_observable_flips # (in class stim.FlipSimulator) def get_observable_flips( self, *, observable_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves observable flip data from the simulator's detection event record. Args: record_index: Identifies a observable to read results from. Setting this to None (default) returns results from all observables. Otherwise this should be an integer in range(0, self.num_observables). instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_observables, self.batch_size), where the first index is the observable_index and the second index is the instance_index and the dtype is np.bool_. Specifying observable_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a observable_index (or a 0d array, a boolean, if observable_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit(''' ... M 0 0 0 ... OBSERVABLE_INCLUDE(0) rec[-2] ... OBSERVABLE_INCLUDE(1) rec[-1] ... ''')) >>> sim.get_observable_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_observable_flips(bit_packed=True) array([[0, 0], [0, 0]], dtype=uint8) >>> sim.get_observable_flips(instance_index=2) array([False, False]) >>> sim.get_observable_flips(observable_index=1) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_observable_flips(instance_index=2, observable_index=1) array(False) """ ``` ```python # stim.FlipSimulator.num_detectors # (in class stim.FlipSimulator) @property def num_detectors( self, ) -> int: """Returns the number of detectors that have been simulated and stored. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_detectors 0 >>> sim.do(stim.Circuit(''' ... M 0 0 ... DETECTOR rec[-1] rec[-2] ... ''')) >>> sim.num_detectors 1 """ ``` ```python # stim.FlipSimulator.num_measurements # (in class stim.FlipSimulator) @property def num_measurements( self, ) -> int: """Returns the number of measurements that have been simulated and stored. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_measurements 0 >>> sim.do(stim.Circuit('M 3 5')) >>> sim.num_measurements 2 """ ``` ```python # stim.FlipSimulator.num_observables # (in class stim.FlipSimulator) @property def num_observables( self, ) -> int: """Returns the number of observables currently tracked by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_observables 0 >>> sim.do(stim.Circuit(''' ... M 0 ... OBSERVABLE_INCLUDE(4) rec[-1] ... ''')) >>> sim.num_observables 5 """ ``` ```python # stim.FlipSimulator.num_qubits # (in class stim.FlipSimulator) @property def num_qubits( self, ) -> int: """Returns the number of qubits currently tracked by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_qubits 0 >>> sim = stim.FlipSimulator(batch_size=256, num_qubits=4) >>> sim.num_qubits 4 >>> sim.do(stim.Circuit('H 5')) >>> sim.num_qubits 6 """ ``` ```python # stim.FlipSimulator.peek_pauli_flips # (in class stim.FlipSimulator) @overload def peek_pauli_flips( self, ) -> List[stim.PauliString]: pass @overload def peek_pauli_flips( self, *, instance_index: int, ) -> stim.PauliString: pass def peek_pauli_flips( self, *, instance_index: Optional[int] = None, ) -> Union[stim.PauliString, List[stim.PauliString]]: """Returns the current pauli errors packed into stim.PauliString instances. Args: instance_index: Defaults to None. When set to None, the pauli errors from all instances are returned as a list of `stim.PauliString`. When set to an integer, a single `stim.PauliString` is returned containing the errors for the indexed instance. Returns: if instance_index is None: A list of stim.PauliString, with the k'th entry being the errors from the k'th simulation instance. else: A stim.PauliString with the errors from the k'th simulation instance. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=2, ... disable_stabilizer_randomization=True, ... num_qubits=10, ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+__________"), stim.PauliString("+__________")] >>> sim.peek_pauli_flips(instance_index=0) stim.PauliString("+__________") >>> sim.do(stim.Circuit(''' ... X_ERROR(1) 0 3 5 ... Z_ERROR(1) 3 6 ... ''')) >>> sim.peek_pauli_flips() [stim.PauliString("+X__Y_XZ___"), stim.PauliString("+X__Y_XZ___")] >>> sim = stim.FlipSimulator( ... batch_size=1, ... num_qubits=100, ... ) >>> flips: stim.PauliString = sim.peek_pauli_flips(instance_index=0) >>> sorted(set(str(flips))) # Should have Zs from stabilizer randomization ['+', 'Z', '_'] """ ``` ```python # stim.FlipSimulator.set_pauli_flip # (in class stim.FlipSimulator) def set_pauli_flip( self, pauli: Union[str, int], *, qubit_index: int, instance_index: int, ) -> None: """Sets the pauli flip on a given qubit in a given simulation instance. Args: pauli: The pauli, specified as an integer or string. Uses the convention 0=I, 1=X, 2=Y, 3=Z. Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. qubit_index: The qubit to put the error on. Must be non-negative. The state will automatically expand as needed to store the error. instance_index: The simulation index to put the error inside. Use negative indices to index from the end of the list. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=2, ... num_qubits=3, ... disable_stabilizer_randomization=True, ... ) >>> sim.set_pauli_flip('X', qubit_index=2, instance_index=1) >>> sim.peek_pauli_flips() [stim.PauliString("+___"), stim.PauliString("+__X")] """ ``` ```python # stim.FlipSimulator.to_numpy # (in class stim.FlipSimulator) def to_numpy( self, *, bit_packed: bool = False, transpose: bool = False, output_xs: Union[bool, np.ndarray] = False, output_zs: Union[bool, np.ndarray] = False, output_measure_flips: Union[bool, np.ndarray] = False, output_detector_flips: Union[bool, np.ndarray] = False, output_observable_flips: Union[bool, np.ndarray] = False, ) -> Optional[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]]: """Writes the simulator state into numpy arrays. Args: bit_packed: Whether or not the result is bit packed, storing 8 bits per byte instead of 1 bit per byte. Bit packing always applies to the second index of the result. Bits are packed in little endian order (as if by `np.packbits(X, axis=1, order='little')`). transpose: Defaults to False. When set to False, the second index of the returned array (the index affected by bit packing) is the shot index (meaning the first index is the qubit index or measurement index or etc). When set to True, results are transposed so that the first index is the shot index. output_xs: Defaults to False. When set to False, the X flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the X flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_zs: Defaults to False. When set to False, the Z flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the Z flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_measure_flips: Defaults to False. When set to False, the measure flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the measure flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_detector_flips: Defaults to False. When set to False, the detector flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the detector flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_observable_flips: Defaults to False. When set to False, the obs flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the obs flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). Returns: A tuple (xs, zs, ms, ds, os) of numpy arrays. The xs and zs arrays are the pauli flip data specified using XZ encoding (00=I, 10=X, 11=Y, 01=Z). The ms array is the measure flip data, the ds array is the detector flip data, and the os array is the obs flip data. The arrays default to `None` when the corresponding `output_*` argument was left False. The shape and dtype of the data depends on arguments given to the function. The following specifies each array's shape and dtype for each case: if not transpose and not bit_packed: xs.shape = (sim.batch_size, sim.num_qubits) zs.shape = (sim.batch_size, sim.num_qubits) ms.shape = (sim.batch_size, sim.num_measurements) ds.shape = (sim.batch_size, sim.num_detectors) os.shape = (sim.batch_size, sim.num_observables) xs.dtype = np.bool_ zs.dtype = np.bool_ ms.dtype = np.bool_ ds.dtype = np.bool_ os.dtype = np.bool_ elif not transpose and bit_packed: xs.shape = (sim.batch_size, math.ceil(sim.num_qubits / 8)) zs.shape = (sim.batch_size, math.ceil(sim.num_qubits / 8)) ms.shape = (sim.batch_size, math.ceil(sim.num_measurements / 8)) ds.shape = (sim.batch_size, math.ceil(sim.num_detectors / 8)) os.shape = (sim.batch_size, math.ceil(sim.num_observables / 8)) xs.dtype = np.uint8 zs.dtype = np.uint8 ms.dtype = np.uint8 ds.dtype = np.uint8 os.dtype = np.uint8 elif transpose and not bit_packed: xs.shape = (sim.num_qubits, sim.batch_size) zs.shape = (sim.num_qubits, sim.batch_size) ms.shape = (sim.num_measurements, sim.batch_size) ds.shape = (sim.num_detectors, sim.batch_size) os.shape = (sim.num_observables, sim.batch_size) xs.dtype = np.bool_ zs.dtype = np.bool_ ms.dtype = np.bool_ ds.dtype = np.bool_ os.dtype = np.bool_ elif transpose and bit_packed: xs.shape = (sim.num_qubits, math.ceil(sim.batch_size / 8)) zs.shape = (sim.num_qubits, math.ceil(sim.batch_size / 8)) ms.shape = (sim.num_measurements, math.ceil(sim.batch_size / 8)) ds.shape = (sim.num_detectors, math.ceil(sim.batch_size / 8)) os.shape = (sim.num_observables, math.ceil(sim.batch_size / 8)) xs.dtype = np.uint8 zs.dtype = np.uint8 ms.dtype = np.uint8 ds.dtype = np.uint8 os.dtype = np.uint8 Raises: ValueError: All the `output_*` arguments were False, or an `output_*` argument had a shape or dtype inconsistent with the requested data. Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit('M(1) 0 1 2')) >>> ms_buf = np.empty(shape=(9, 1), dtype=np.uint8) >>> xs, zs, ms, ds, os = sim.to_numpy( ... transpose=True, ... bit_packed=True, ... output_xs=True, ... output_measure_flips=ms_buf, ... ) >>> assert ms is ms_buf >>> xs array([[0], [0], [0], [0], [0], [0], [0], [0], [0]], dtype=uint8) >>> zs >>> ms array([[7], [7], [7], [7], [7], [7], [7], [7], [7]], dtype=uint8) >>> ds >>> os """ ``` ```python # stim.FlippedMeasurement # (at top-level in the stim module) class FlippedMeasurement: """Describes a measurement that was flipped. Gives the measurement's index in the measurement record, and also the observable of the measurement. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement stim.FlippedMeasurement( record_index=1, observable=(stim.GateTargetWithCoords(stim.target_z(10), []),), ) """ ``` ```python # stim.FlippedMeasurement.__init__ # (in class stim.FlippedMeasurement) def __init__( self, measurement_record_index: Optional[int], measured_observable: Iterable[stim.GateTargetWithCoords], ): """Creates a stim.FlippedMeasurement. Examples: >>> import stim >>> print(stim.FlippedMeasurement( ... record_index=5, ... observable=[], ... )) stim.FlippedMeasurement( record_index=5, observable=(), ) """ ``` ```python # stim.FlippedMeasurement.observable # (in class stim.FlippedMeasurement) @property def observable( self, ) -> List[stim.GateTargetWithCoords]: """Returns the observable of the flipped measurement. For example, an `MX 5` measurement will have the observable X5. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement.observable [stim.GateTargetWithCoords(stim.target_z(10), [])] """ ``` ```python # stim.FlippedMeasurement.record_index # (in class stim.FlippedMeasurement) @property def record_index( self, ) -> int: """The measurement record index of the flipped measurement. For example, the fifth measurement in a circuit has a measurement record index of 4. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement.record_index 1 """ ``` ```python # stim.Flow # (at top-level in the stim module) class Flow: """A stabilizer flow (e.g. "XI -> XX xor rec[-1]"). Stabilizer circuits implement, and can be defined by, how they turn input stabilizers into output stabilizers mediated by measurements. These relationships are called stabilizer flows, and `stim.Flow` is a representation of such a flow. For example, a `stim.Flow` can be given to `stim.Circuit.has_flow` to verify that a circuit implements the flow. A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer P at the start of the circuit to the instantaneous stabilizer Q at the end of the circuit. The flow may be mediated by certain measurements. For example, a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and the CNOT flows implemented by the circuit involve these measurements. A flow like P -> Q means the circuit transforms P into Q. A flow like 1 -> P means the circuit prepares P. A flow like P -> 1 means the circuit measures P. A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). References: Stim's gate documentation includes the stabilizer flows of each gate. Appendix A of https://arxiv.org/abs/2302.02192 describes how flows are defined and provides a circuit construction for experimentally verifying their presence. Examples: >>> import stim >>> c = stim.Circuit("CNOT 2 4") >>> c.has_flow(stim.Flow("__X__ -> __X_X")) True >>> c.has_flow(stim.Flow("X2*X4 -> X2")) True >>> c.has_flow(stim.Flow("Z4 -> Z4")) False """ ``` ```python # stim.Flow.__eq__ # (in class stim.Flow) def __eq__( self, arg0: stim.Flow, ) -> bool: """Determines if two flows have identical contents. """ ``` ```python # stim.Flow.__init__ # (in class stim.Flow) def __init__( self, arg: Union[None, str, stim.Flow] = None, /, *, input: Optional[stim.PauliString] = None, output: Optional[stim.PauliString] = None, measurements: Optional[Iterable[Union[int, GateTarget]]] = None, included_observables: Optional[Iterable[int]] = None, ) -> None: """Initializes a stim.Flow. When given a string, the string is parsed as flow shorthand. For example, the string "X_ -> ZZ xor rec[-1]" will result in a flow with input pauli string "X_", output pauli string "ZZ", and measurement indices [-1]. Args: arg [position-only]: Defaults to None. Must be specified by itself if used. str: Initializes a flow by parsing the given shorthand text. stim.Flow: Initializes a copy of the given flow. None (default): Initializes an empty flow. input: Defaults to None. Can be set to a stim.PauliString to directly specify the flow's input stabilizer. output: Defaults to None. Can be set to a stim.PauliString to directly specify the flow's output stabilizer. measurements: Defaults to None. Can be set to a list of integers or gate targets like `stim.target_rec(-1)`, to specify the measurements that mediate the flow. Negative and positive measurement indices are allowed. Indexes follow the python convention where -1 is the last measurement in a circuit and 0 is the first measurement in a circuit. included_observables: Defaults to None. `OBSERVABLE_INCLUDE` instructions that target an observable index from this list will be implicitly included in the flow. This allows flows to refer to observables. For example, the flow "X5 -> obs[3]" says "At the start of the circuit, observable 3 should be an X term on qubit 5. By the end of the circuit it will be measured. The `OBSERVABLE_INCLUDE(3)` instructions in the circuit should explain how this happened.". Examples: >>> import stim >>> stim.Flow("X2 -> -Y2*Z4 xor rec[-1]") stim.Flow("__X -> -__Y_Z xor rec[-1]") >>> stim.Flow("Z -> 1 xor rec[-1]") stim.Flow("Z -> rec[-1]") >>> stim.Flow( ... input=stim.PauliString("XX"), ... output=stim.PauliString("_X"), ... measurements=[], ... ) stim.Flow("XX -> _X") >>> # Identical terms cancel. >>> stim.Flow("X2 -> Y2*Y2 xor rec[-2] xor rec[-2]") stim.Flow("__X -> ___") >>> stim.Flow("X -> Y xor obs[3] xor obs[3] xor obs[3]") stim.Flow("X -> Y xor obs[3]") """ ``` ```python # stim.Flow.__mul__ # (in class stim.Flow) def __mul__( self, rhs: stim.Flow, ) -> stim.Flow: """Computes the product of two flows. Args: rhs: The right hand side of the multiplication. Returns: The product of the two flows. Raises: ValueError: The inputs anti-commute (their product would be anti-Hermitian). For example, 1 -> X times 1 -> Y fails because it would give 1 -> iZ. Examples: >>> import stim >>> stim.Flow("X -> X") * stim.Flow("Z -> Z") stim.Flow("Y -> Y") >>> stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ") stim.Flow("1 -> -YY") >>> stim.Flow("X -> rec[-1]") * stim.Flow("X -> rec[-2]") stim.Flow("_ -> rec[-2] xor rec[-1]") """ ``` ```python # stim.Flow.__ne__ # (in class stim.Flow) def __ne__( self, arg0: stim.Flow, ) -> bool: """Determines if two flows have non-identical contents. """ ``` ```python # stim.Flow.__repr__ # (in class stim.Flow) def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.Flow`. """ ``` ```python # stim.Flow.__str__ # (in class stim.Flow) def __str__( self, ) -> str: """Returns a shorthand description of the flow. """ ``` ```python # stim.Flow.included_observables_copy # (in class stim.Flow) def included_observables_copy( self, ) -> List[int]: """Returns a copy of the flow's included observable indices. When an observable is included in a flow, the flow implicitly includes all measurements and pauli terms from `OBSERVABLE_INCLUDE` instructions targeting that observable index. Examples: >>> import stim >>> f = stim.Flow(included_observables=[3, 2]) >>> f.included_observables_copy() [2, 3] >>> f.included_observables_copy() is f.included_observables_copy() False >>> f = stim.Flow("X2 -> obs[3]") >>> f.included_observables_copy() [3] >>> stim.Circuit("OBSERVABLE_INCLUDE(3) X2").has_flow(f) True """ ``` ```python # stim.Flow.input_copy # (in class stim.Flow) def input_copy( self, ) -> stim.PauliString: """Returns a copy of the flow's input stabilizer. Examples: >>> import stim >>> f = stim.Flow(input=stim.PauliString('XX')) >>> f.input_copy() stim.PauliString("+XX") >>> f.input_copy() is f.input_copy() False """ ``` ```python # stim.Flow.measurements_copy # (in class stim.Flow) def measurements_copy( self, ) -> List[int]: """Returns a copy of the flow's measurement indices. Examples: >>> import stim >>> f = stim.Flow(measurements=[-1, 2]) >>> f.measurements_copy() [-1, 2] >>> f.measurements_copy() is f.measurements_copy() False """ ``` ```python # stim.Flow.output_copy # (in class stim.Flow) def output_copy( self, ) -> stim.PauliString: """Returns a copy of the flow's output stabilizer. Examples: >>> import stim >>> f = stim.Flow(output=stim.PauliString('XX')) >>> f.output_copy() stim.PauliString("+XX") >>> f.output_copy() is f.output_copy() False """ ``` ```python # stim.GateData # (at top-level in the stim module) class GateData: """Details about a gate supported by stim. Examples: >>> import stim >>> stim.gate_data('h').name 'H' >>> stim.gate_data('h').is_unitary True >>> stim.gate_data('h').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) """ ``` ```python # stim.GateData.__eq__ # (in class stim.GateData) def __eq__( self, arg0: stim.GateData, ) -> bool: """Determines if two GateData instances are identical. """ ``` ```python # stim.GateData.__init__ # (in class stim.GateData) def __init__( self, name: str, ) -> None: """Finds gate data for the named gate. Examples: >>> import stim >>> stim.GateData('H').is_unitary True """ ``` ```python # stim.GateData.__ne__ # (in class stim.GateData) def __ne__( self, arg0: stim.GateData, ) -> bool: """Determines if two GateData instances are not identical. """ ``` ```python # stim.GateData.__repr__ # (in class stim.GateData) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.GateData`. """ ``` ```python # stim.GateData.__str__ # (in class stim.GateData) def __str__( self, ) -> str: """Returns text describing the gate data. """ ``` ```python # stim.GateData.aliases # (in class stim.GateData) @property def aliases( self, ) -> List[str]: """Returns all aliases that can be used to name the gate. Although gates can be referred to by lower case and mixed case named, the result only includes upper cased aliases. Examples: >>> import stim >>> stim.gate_data('H').aliases ['H', 'H_XZ'] >>> stim.gate_data('cnot').aliases ['CNOT', 'CX', 'ZCX'] """ ``` ```python # stim.GateData.flows # (in class stim.GateData) @property def flows( self, ) -> Optional[List[stim.Flow]]: """Returns stabilizer flow generators for the gate, or else None. A stabilizer flow describes an input-output relationship that the gate satisfies, where an input pauli string is transformed into an output pauli string mediated by certain measurement results. Caution: this method returns None for variable-target-count gates like MPP. Not because MPP has no stabilizer flows, but because its stabilizer flows depend on how many qubits it targets and what basis it targets them in. Returns: A list of stim.Flow instances representing the generators. Examples: >>> import stim >>> stim.gate_data('H').flows [stim.Flow("X -> Z"), stim.Flow("Z -> X")] >>> for e in stim.gate_data('ISWAP').flows: ... print(e) X_ -> ZY Z_ -> _Z _X -> YZ _Z -> Z_ >>> for e in stim.gate_data('MXX').flows: ... print(e) X_ -> X_ _X -> _X ZZ -> ZZ XX -> rec[-1] """ ``` ```python # stim.GateData.generalized_inverse # (in class stim.GateData) @property def generalized_inverse( self, ) -> stim.GateData: """The closest-thing-to-an-inverse for the gate, if forced to pick something. The generalized inverse of a unitary gate U is its actual inverse U^-1. The generalized inverse of a reset or measurement gate U is a gate V such that, for every stabilizer flow that U has, V has the time reverse of that flow (up to Pauli feedback, with potentially more flows). For example, the time-reverse of R is MR because R has the single flow 1 -> Z and MR has the time reversed flow Z -> rec[-1]. The generalized inverse of noise like X_ERROR is just the same noise. The generalized inverse of an annotation like TICK is just the same annotation. Examples: >>> import stim >>> stim.gate_data('H').generalized_inverse stim.gate_data('H') >>> stim.gate_data('CXSWAP').generalized_inverse stim.gate_data('SWAPCX') >>> stim.gate_data('X_ERROR').generalized_inverse stim.gate_data('X_ERROR') >>> stim.gate_data('MX').generalized_inverse stim.gate_data('MX') >>> stim.gate_data('MRY').generalized_inverse stim.gate_data('MRY') >>> stim.gate_data('R').generalized_inverse stim.gate_data('M') >>> stim.gate_data('DETECTOR').generalized_inverse stim.gate_data('DETECTOR') >>> stim.gate_data('TICK').generalized_inverse stim.gate_data('TICK') """ ``` ```python # stim.GateData.hadamard_conjugated # (in class stim.GateData) def hadamard_conjugated( self, *, unsigned: bool = False, ) -> Optional[stim.GateData]: """Returns a stim gate equivalent to this gate conjugated by Hadamard gates. The Hadamard conjugate can be thought of as the XZ dual of the gate; the gate you get by exchanging the X and Z bases. For example, a SQRT_X will become a SQRT_Z and a CX gate will switch directions into an XCZ. If stim doesn't define a gate equivalent to conjugating this gate by Hadamards, the value `None` is returned. Args: unsigned: Defaults to False. When False, the returned gate must be *exactly* the Hadamard conjugation of this gate. When True, the returned gate must have the same flows but the sign of the flows can be different (i.e. the returned gate must be the Hadamard conjugate up to Pauli gate differences). Returns: A stim.GateData instance of the Hadamard conjugate, if it exists in stim. None, if stim doesn't define a gate equal to the Hadamard conjugate. Examples: >>> import stim >>> stim.gate_data('X').hadamard_conjugated() stim.gate_data('Z') >>> stim.gate_data('CX').hadamard_conjugated() stim.gate_data('XCZ') >>> stim.gate_data('RY').hadamard_conjugated() is None True >>> stim.gate_data('RY').hadamard_conjugated(unsigned=True) stim.gate_data('RY') >>> stim.gate_data('ISWAP').hadamard_conjugated(unsigned=True) is None True >>> stim.gate_data('SWAP').hadamard_conjugated() stim.gate_data('SWAP') >>> stim.gate_data('CXSWAP').hadamard_conjugated() stim.gate_data('SWAPCX') >>> stim.gate_data('MXX').hadamard_conjugated() stim.gate_data('MZZ') >>> stim.gate_data('DEPOLARIZE1').hadamard_conjugated() stim.gate_data('DEPOLARIZE1') >>> stim.gate_data('X_ERROR').hadamard_conjugated() stim.gate_data('Z_ERROR') >>> stim.gate_data('H_XY').hadamard_conjugated() stim.gate_data('H_NYZ') >>> stim.gate_data('DETECTOR').hadamard_conjugated(unsigned=True) stim.gate_data('DETECTOR') """ ``` ```python # stim.GateData.inverse # (in class stim.GateData) @property def inverse( self, ) -> Optional[stim.GateData]: """The inverse of the gate, or None if it has no inverse. The inverse V of a gate U must have the property that V undoes the effects of U and that U undoes the effects of V. In particular, the circuit U 0 1 V 0 1 should be equivalent to doing nothing at all. Examples: >>> import stim >>> stim.gate_data('H').inverse stim.gate_data('H') >>> stim.gate_data('CX').inverse stim.gate_data('CX') >>> stim.gate_data('S').inverse stim.gate_data('S_DAG') >>> stim.gate_data('CXSWAP').inverse stim.gate_data('SWAPCX') >>> stim.gate_data('X_ERROR').inverse is None True >>> stim.gate_data('M').inverse is None True >>> stim.gate_data('R').inverse is None True >>> stim.gate_data('DETECTOR').inverse is None True >>> stim.gate_data('TICK').inverse is None True """ ``` ```python # stim.GateData.is_noisy_gate # (in class stim.GateData) @property def is_noisy_gate( self, ) -> bool: """Returns whether or not the gate can produce noise. Note that measurement operations are considered noisy, because for example `M(0.001) 2 3 5` will include noise that flips its result 0.1% of the time. Examples: >>> import stim >>> stim.gate_data('M').is_noisy_gate True >>> stim.gate_data('MXX').is_noisy_gate True >>> stim.gate_data('X_ERROR').is_noisy_gate True >>> stim.gate_data('CORRELATED_ERROR').is_noisy_gate True >>> stim.gate_data('MPP').is_noisy_gate True >>> stim.gate_data('H').is_noisy_gate False >>> stim.gate_data('CX').is_noisy_gate False >>> stim.gate_data('R').is_noisy_gate False >>> stim.gate_data('DETECTOR').is_noisy_gate False """ ``` ```python # stim.GateData.is_reset # (in class stim.GateData) @property def is_reset( self, ) -> bool: """Returns whether or not the gate resets qubits in any basis. Examples: >>> import stim >>> stim.gate_data('R').is_reset True >>> stim.gate_data('RX').is_reset True >>> stim.gate_data('MR').is_reset True >>> stim.gate_data('M').is_reset False >>> stim.gate_data('MXX').is_reset False >>> stim.gate_data('MPP').is_reset False >>> stim.gate_data('H').is_reset False >>> stim.gate_data('CX').is_reset False >>> stim.gate_data('HERALDED_ERASE').is_reset False >>> stim.gate_data('DEPOLARIZE2').is_reset False >>> stim.gate_data('X_ERROR').is_reset False >>> stim.gate_data('CORRELATED_ERROR').is_reset False >>> stim.gate_data('DETECTOR').is_reset False """ ``` ```python # stim.GateData.is_single_qubit_gate # (in class stim.GateData) @property def is_single_qubit_gate( self, ) -> bool: """Returns whether or not the gate is a single qubit gate. Single qubit gates apply separately to each of their targets. Variable-qubit gates like CORRELATED_ERROR and MPP are not considered single qubit gates. Examples: >>> import stim >>> stim.gate_data('H').is_single_qubit_gate True >>> stim.gate_data('R').is_single_qubit_gate True >>> stim.gate_data('M').is_single_qubit_gate True >>> stim.gate_data('X_ERROR').is_single_qubit_gate True >>> stim.gate_data('CX').is_single_qubit_gate False >>> stim.gate_data('MXX').is_single_qubit_gate False >>> stim.gate_data('CORRELATED_ERROR').is_single_qubit_gate False >>> stim.gate_data('MPP').is_single_qubit_gate False >>> stim.gate_data('DETECTOR').is_single_qubit_gate False >>> stim.gate_data('TICK').is_single_qubit_gate False >>> stim.gate_data('REPEAT').is_single_qubit_gate False """ ``` ```python # stim.GateData.is_symmetric_gate # (in class stim.GateData) @property def is_symmetric_gate( self, ) -> bool: """Returns whether or not the gate is the same when its targets are swapped. A two qubit gate is symmetric if it doesn't matter if you swap its targets. It is unaffected when conjugated by the SWAP gate. Single qubit gates are vacuously symmetric. A multi-qubit gate is symmetric if swapping any two of its targets has no effect. Note that this method is for symmetry *without broadcasting*. For example, SWAP is symmetric even though SWAP 1 2 3 4 isn't equal to SWAP 1 3 2 4. Returns: True if the gate is symmetric. False if the gate isn't symmetric. Examples: >>> import stim >>> stim.gate_data('CX').is_symmetric_gate False >>> stim.gate_data('CZ').is_symmetric_gate True >>> stim.gate_data('ISWAP').is_symmetric_gate True >>> stim.gate_data('CXSWAP').is_symmetric_gate False >>> stim.gate_data('MXX').is_symmetric_gate True >>> stim.gate_data('DEPOLARIZE2').is_symmetric_gate True >>> stim.gate_data('PAULI_CHANNEL_2').is_symmetric_gate False >>> stim.gate_data('H').is_symmetric_gate True >>> stim.gate_data('R').is_symmetric_gate True >>> stim.gate_data('X_ERROR').is_symmetric_gate True >>> stim.gate_data('CORRELATED_ERROR').is_symmetric_gate False >>> stim.gate_data('MPP').is_symmetric_gate False >>> stim.gate_data('DETECTOR').is_symmetric_gate False """ ``` ```python # stim.GateData.is_two_qubit_gate # (in class stim.GateData) @property def is_two_qubit_gate( self, ) -> bool: """Returns whether or not the gate is a two qubit gate. Two qubit gates must be given an even number of targets. Variable-qubit gates like CORRELATED_ERROR and MPP are not considered two qubit gates. Returns: True if the gate is a two qubit gate. False if the gate isn't a two qubit gate. Examples: >>> import stim >>> stim.gate_data('CX').is_two_qubit_gate True >>> stim.gate_data('MXX').is_two_qubit_gate True >>> stim.gate_data('H').is_two_qubit_gate False >>> stim.gate_data('R').is_two_qubit_gate False >>> stim.gate_data('M').is_two_qubit_gate False >>> stim.gate_data('X_ERROR').is_two_qubit_gate False >>> stim.gate_data('CORRELATED_ERROR').is_two_qubit_gate False >>> stim.gate_data('MPP').is_two_qubit_gate False >>> stim.gate_data('DETECTOR').is_two_qubit_gate False """ ``` ```python # stim.GateData.is_unitary # (in class stim.GateData) @property def is_unitary( self, ) -> bool: """Returns whether or not the gate is a unitary gate. Examples: >>> import stim >>> stim.gate_data('H').is_unitary True >>> stim.gate_data('CX').is_unitary True >>> stim.gate_data('R').is_unitary False >>> stim.gate_data('M').is_unitary False >>> stim.gate_data('MXX').is_unitary False >>> stim.gate_data('X_ERROR').is_unitary False >>> stim.gate_data('CORRELATED_ERROR').is_unitary False >>> stim.gate_data('MPP').is_unitary False >>> stim.gate_data('DETECTOR').is_unitary False """ ``` ```python # stim.GateData.name # (in class stim.GateData) @property def name( self, ) -> str: """Returns the canonical name of the gate. Examples: >>> import stim >>> stim.gate_data('H').name 'H' >>> stim.gate_data('cnot').name 'CX' """ ``` ```python # stim.GateData.num_parens_arguments_range # (in class stim.GateData) @property def num_parens_arguments_range( self, ) -> range: """Returns the min/max parens arguments taken by the gate, as a python range. Examples: >>> import stim >>> stim.gate_data('M').num_parens_arguments_range range(0, 2) >>> list(stim.gate_data('M').num_parens_arguments_range) [0, 1] >>> list(stim.gate_data('R').num_parens_arguments_range) [0] >>> list(stim.gate_data('H').num_parens_arguments_range) [0] >>> list(stim.gate_data('X_ERROR').num_parens_arguments_range) [1] >>> list(stim.gate_data('PAULI_CHANNEL_1').num_parens_arguments_range) [3] >>> list(stim.gate_data('PAULI_CHANNEL_2').num_parens_arguments_range) [15] >>> stim.gate_data('DETECTOR').num_parens_arguments_range range(0, 256) >>> list(stim.gate_data('OBSERVABLE_INCLUDE').num_parens_arguments_range) [1] """ ``` ```python # stim.GateData.produces_measurements # (in class stim.GateData) @property def produces_measurements( self, ) -> bool: """Returns whether or not the gate produces measurement results. Examples: >>> import stim >>> stim.gate_data('M').produces_measurements True >>> stim.gate_data('MRY').produces_measurements True >>> stim.gate_data('MXX').produces_measurements True >>> stim.gate_data('MPP').produces_measurements True >>> stim.gate_data('HERALDED_ERASE').produces_measurements True >>> stim.gate_data('H').produces_measurements False >>> stim.gate_data('CX').produces_measurements False >>> stim.gate_data('R').produces_measurements False >>> stim.gate_data('X_ERROR').produces_measurements False >>> stim.gate_data('CORRELATED_ERROR').produces_measurements False >>> stim.gate_data('DETECTOR').produces_measurements False """ ``` ```python # stim.GateData.tableau # (in class stim.GateData) @property def tableau( self, ) -> Optional[stim.Tableau]: """Returns the gate's tableau, or None if the gate has no tableau. Examples: >>> import stim >>> print(stim.gate_data('M').tableau) None >>> stim.gate_data('H').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> stim.gate_data('ISWAP').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZY"), stim.PauliString("+YZ"), ], zs=[ stim.PauliString("+_Z"), stim.PauliString("+Z_"), ], ) """ ``` ```python # stim.GateData.takes_measurement_record_targets # (in class stim.GateData) @property def takes_measurement_record_targets( self, ) -> bool: """Returns whether or not the gate can accept rec targets. For example, `CX` can take a measurement record target like `CX rec[-1] 1`. Examples: >>> import stim >>> stim.gate_data('CX').takes_measurement_record_targets True >>> stim.gate_data('DETECTOR').takes_measurement_record_targets True >>> stim.gate_data('H').takes_measurement_record_targets False >>> stim.gate_data('SWAP').takes_measurement_record_targets False >>> stim.gate_data('R').takes_measurement_record_targets False >>> stim.gate_data('M').takes_measurement_record_targets False >>> stim.gate_data('MRY').takes_measurement_record_targets False >>> stim.gate_data('MXX').takes_measurement_record_targets False >>> stim.gate_data('X_ERROR').takes_measurement_record_targets False >>> stim.gate_data('CORRELATED_ERROR').takes_measurement_record_targets False >>> stim.gate_data('MPP').takes_measurement_record_targets False """ ``` ```python # stim.GateData.takes_pauli_targets # (in class stim.GateData) @property def takes_pauli_targets( self, ) -> bool: """Returns whether or not the gate expects pauli targets. For example, `CORRELATED_ERROR` takes targets like `X0` and `Y1` instead of `0` or `1`. Examples: >>> import stim >>> stim.gate_data('CORRELATED_ERROR').takes_pauli_targets True >>> stim.gate_data('MPP').takes_pauli_targets True >>> stim.gate_data('H').takes_pauli_targets False >>> stim.gate_data('CX').takes_pauli_targets False >>> stim.gate_data('R').takes_pauli_targets False >>> stim.gate_data('M').takes_pauli_targets False >>> stim.gate_data('MRY').takes_pauli_targets False >>> stim.gate_data('MXX').takes_pauli_targets False >>> stim.gate_data('X_ERROR').takes_pauli_targets False >>> stim.gate_data('DETECTOR').takes_pauli_targets False """ ``` ```python # stim.GateData.unitary_matrix # (in class stim.GateData) @property def unitary_matrix( self, ) -> Optional[np.ndarray]: """Returns the gate's unitary matrix, or None if the gate isn't unitary. Examples: >>> import stim >>> print(stim.gate_data('M').unitary_matrix) None >>> stim.gate_data('X').unitary_matrix array([[0.+0.j, 1.+0.j], [1.+0.j, 0.+0.j]], dtype=complex64) >>> stim.gate_data('ISWAP').unitary_matrix array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j], [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]], dtype=complex64) """ ``` ```python # stim.GateTarget # (at top-level in the stim module) class GateTarget: """Represents a gate target, like `0` or `rec[-1]`, from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... M 0 !1 ... ''') >>> circuit[0].targets_copy()[0] stim.GateTarget(0) >>> circuit[0].targets_copy()[1] stim.target_inv(1) """ ``` ```python # stim.GateTarget.__eq__ # (in class stim.GateTarget) def __eq__( self, arg0: stim.GateTarget, ) -> bool: """Determines if two `stim.GateTarget`s are identical. """ ``` ```python # stim.GateTarget.__init__ # (in class stim.GateTarget) def __init__( self, value: object, ) -> None: """Initializes a `stim.GateTarget`. Args: value: A value to convert into a gate target, like an integer to interpret as a qubit target or a string to parse. Examples: >>> import stim >>> stim.GateTarget(stim.GateTarget(5)) stim.GateTarget(5) >>> stim.GateTarget("X7") stim.target_x(7) >>> stim.GateTarget("rec[-3]") stim.target_rec(-3) >>> stim.GateTarget("!Z7") stim.target_z(7, invert=True) >>> stim.GateTarget("*") stim.GateTarget.combiner() """ ``` ```python # stim.GateTarget.__ne__ # (in class stim.GateTarget) def __ne__( self, arg0: stim.GateTarget, ) -> bool: """Determines if two `stim.GateTarget`s are different. """ ``` ```python # stim.GateTarget.__repr__ # (in class stim.GateTarget) def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.GateTarget`. """ ``` ```python # stim.GateTarget.is_combiner # (in class stim.GateTarget) @property def is_combiner( self, ) -> bool: """Returns whether or not this is a combiner target like `*`. Examples: >>> import stim >>> stim.GateTarget(6).is_combiner False >>> stim.target_inv(7).is_combiner False >>> stim.target_x(8).is_combiner False >>> stim.target_y(2).is_combiner False >>> stim.target_z(3).is_combiner False >>> stim.target_sweep_bit(9).is_combiner False >>> stim.target_rec(-5).is_combiner False >>> stim.target_combiner().is_combiner True """ ``` ```python # stim.GateTarget.is_inverted_result_target # (in class stim.GateTarget) @property def is_inverted_result_target( self, ) -> bool: """Returns whether or not this is an inverted target like `!5` or `!X4`. Examples: >>> import stim >>> stim.GateTarget(6).is_inverted_result_target False >>> stim.target_inv(7).is_inverted_result_target True >>> stim.target_x(8).is_inverted_result_target False >>> stim.target_x(8, invert=True).is_inverted_result_target True >>> stim.target_y(2).is_inverted_result_target False >>> stim.target_z(3).is_inverted_result_target False >>> stim.target_sweep_bit(9).is_inverted_result_target False >>> stim.target_rec(-5).is_inverted_result_target False """ ``` ```python # stim.GateTarget.is_measurement_record_target # (in class stim.GateTarget) @property def is_measurement_record_target( self, ) -> bool: """Returns whether or not this is a measurement record target like `rec[-5]`. Examples: >>> import stim >>> stim.GateTarget(6).is_measurement_record_target False >>> stim.target_inv(7).is_measurement_record_target False >>> stim.target_x(8).is_measurement_record_target False >>> stim.target_y(2).is_measurement_record_target False >>> stim.target_z(3).is_measurement_record_target False >>> stim.target_sweep_bit(9).is_measurement_record_target False >>> stim.target_rec(-5).is_measurement_record_target True """ ``` ```python # stim.GateTarget.is_qubit_target # (in class stim.GateTarget) @property def is_qubit_target( self, ) -> bool: """Returns whether or not this is a qubit target like `5` or `!6`. Examples: >>> import stim >>> stim.GateTarget(6).is_qubit_target True >>> stim.target_inv(7).is_qubit_target True >>> stim.target_x(8).is_qubit_target False >>> stim.target_y(2).is_qubit_target False >>> stim.target_z(3).is_qubit_target False >>> stim.target_sweep_bit(9).is_qubit_target False >>> stim.target_rec(-5).is_qubit_target False """ ``` ```python # stim.GateTarget.is_sweep_bit_target # (in class stim.GateTarget) @property def is_sweep_bit_target( self, ) -> bool: """Returns whether or not this is a sweep bit target like `sweep[4]`. Examples: >>> import stim >>> stim.GateTarget(6).is_sweep_bit_target False >>> stim.target_inv(7).is_sweep_bit_target False >>> stim.target_x(8).is_sweep_bit_target False >>> stim.target_y(2).is_sweep_bit_target False >>> stim.target_z(3).is_sweep_bit_target False >>> stim.target_sweep_bit(9).is_sweep_bit_target True >>> stim.target_rec(-5).is_sweep_bit_target False """ ``` ```python # stim.GateTarget.is_x_target # (in class stim.GateTarget) @property def is_x_target( self, ) -> bool: """Returns whether or not this is an X pauli target like `X2` or `!X7`. Examples: >>> import stim >>> stim.GateTarget(6).is_x_target False >>> stim.target_inv(7).is_x_target False >>> stim.target_x(8).is_x_target True >>> stim.target_y(2).is_x_target False >>> stim.target_z(3).is_x_target False >>> stim.target_sweep_bit(9).is_x_target False >>> stim.target_rec(-5).is_x_target False """ ``` ```python # stim.GateTarget.is_y_target # (in class stim.GateTarget) @property def is_y_target( self, ) -> bool: """Returns whether or not this is a Y pauli target like `Y2` or `!Y7`. Examples: >>> import stim >>> stim.GateTarget(6).is_y_target False >>> stim.target_inv(7).is_y_target False >>> stim.target_x(8).is_y_target False >>> stim.target_y(2).is_y_target True >>> stim.target_z(3).is_y_target False >>> stim.target_sweep_bit(9).is_y_target False >>> stim.target_rec(-5).is_y_target False """ ``` ```python # stim.GateTarget.is_z_target # (in class stim.GateTarget) @property def is_z_target( self, ) -> bool: """Returns whether or not this is a Z pauli target like `Z2` or `!Z7`. Examples: >>> import stim >>> stim.GateTarget(6).is_z_target False >>> stim.target_inv(7).is_z_target False >>> stim.target_x(8).is_z_target False >>> stim.target_y(2).is_z_target False >>> stim.target_z(3).is_z_target True >>> stim.target_sweep_bit(9).is_z_target False >>> stim.target_rec(-5).is_z_target False """ ``` ```python # stim.GateTarget.pauli_type # (in class stim.GateTarget) @property def pauli_type( self, ) -> str: """Returns whether this is an 'X', 'Y', or 'Z' target. For non-pauli targets, this property evaluates to 'I'. Examples: >>> import stim >>> stim.GateTarget(6).pauli_type 'I' >>> stim.target_inv(7).pauli_type 'I' >>> stim.target_x(8).pauli_type 'X' >>> stim.target_y(2).pauli_type 'Y' >>> stim.target_z(3).pauli_type 'Z' >>> stim.target_sweep_bit(9).pauli_type 'I' >>> stim.target_rec(-5).pauli_type 'I' """ ``` ```python # stim.GateTarget.qubit_value # (in class stim.GateTarget) @property def qubit_value( self, ) -> Optional[int]: """Returns the integer value of the targeted qubit, or else None. Examples: >>> import stim >>> stim.GateTarget(6).qubit_value 6 >>> stim.target_inv(7).qubit_value 7 >>> stim.target_x(8).qubit_value 8 >>> stim.target_y(2).qubit_value 2 >>> stim.target_z(3).qubit_value 3 >>> print(stim.target_sweep_bit(9).qubit_value) None >>> print(stim.target_rec(-5).qubit_value) None """ ``` ```python # stim.GateTarget.value # (in class stim.GateTarget) @property def value( self, ) -> int: """The numeric part of the target. This is non-negative integer for qubit targets, and a negative integer for measurement record targets. Examples: >>> import stim >>> stim.GateTarget(6).value 6 >>> stim.target_inv(7).value 7 >>> stim.target_x(8).value 8 >>> stim.target_y(2).value 2 >>> stim.target_z(3).value 3 >>> stim.target_sweep_bit(9).value 9 >>> stim.target_rec(-5).value -5 """ ``` ```python # stim.GateTargetWithCoords # (at top-level in the stim module) class GateTargetWithCoords: """A gate target with associated coordinate information. For example, if the gate target is a qubit from a circuit with QUBIT_COORDS instructions, the coords field will contain the coordinate data from the QUBIT_COORDS instruction for the qubit. This is helpful information to have available when debugging a problem in a circuit, instead of having to constantly manually look up the coordinates of a qubit index in order to understand what is happening. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) >>> t.coords [1.5, 2.0] """ ``` ```python # stim.GateTargetWithCoords.__init__ # (in class stim.GateTargetWithCoords) def __init__( self, gate_target: object, coords: List[float], ) -> None: """Creates a stim.GateTargetWithCoords. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) >>> t.coords [1.5, 2.0] """ ``` ```python # stim.GateTargetWithCoords.coords # (in class stim.GateTargetWithCoords) @property def coords( self, ) -> List[float]: """Returns the associated coordinate information as a list of floats. If there is no coordinate information, returns an empty list. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.coords [1.5, 2.0] """ ``` ```python # stim.GateTargetWithCoords.gate_target # (in class stim.GateTargetWithCoords) @property def gate_target( self, ) -> stim.GateTarget: """Returns the actual gate target as a `stim.GateTarget`. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) """ ``` ```python # stim.PauliString # (at top-level in the stim module) class PauliString: """A signed Pauli tensor product (e.g. "+X \u2297 X \u2297 X" or "-Y \u2297 Z". Represents a collection of Pauli operations (I, X, Y, Z) applied pairwise to a collection of qubits. Examples: >>> import stim >>> stim.PauliString("XX") * stim.PauliString("YY") stim.PauliString("-ZZ") >>> print(stim.PauliString(5)) +_____ """ ``` ```python # stim.PauliString.__add__ # (in class stim.PauliString) def __add__( self, rhs: stim.PauliString, ) -> stim.PauliString: """Returns the tensor product of two Pauli strings. Concatenates the Pauli strings and multiplies their signs. Args: rhs: A second stim.PauliString. Examples: >>> import stim >>> stim.PauliString("X") + stim.PauliString("YZ") stim.PauliString("+XYZ") >>> stim.PauliString("iX") + stim.PauliString("-X") stim.PauliString("-iXX") Returns: The tensor product. """ ``` ```python # stim.PauliString.__eq__ # (in class stim.PauliString) def __eq__( self, arg0: stim.PauliString, ) -> bool: """Determines if two Pauli strings have identical contents. """ ``` ```python # stim.PauliString.__getitem__ # (in class stim.PauliString) @overload def __getitem__( self, index_or_slice: int, ) -> int: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.PauliString: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns an individual Pauli or Pauli string slice from the pauli string. Individual Paulis are returned as an int using the encoding 0=I, 1=X, 2=Y, 3=Z. Slices are returned as a stim.PauliString (always with positive sign). Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p[2] 2 >>> p[-1] 3 >>> p[:2] stim.PauliString("+_X") >>> p[::-1] stim.PauliString("+ZYX_") Args: index_or_slice: The index of the pauli to return, or the slice of paulis to return. Returns: 0: Identity. 1: Pauli X. 2: Pauli Y. 3: Pauli Z. """ ``` ```python # stim.PauliString.__iadd__ # (in class stim.PauliString) def __iadd__( self, rhs: stim.PauliString, ) -> stim.PauliString: """Performs an inplace tensor product. Concatenates the given Pauli string onto the receiving string and multiplies their signs. Args: rhs: A second stim.PauliString. Examples: >>> import stim >>> p = stim.PauliString("iX") >>> alias = p >>> p += stim.PauliString("-YY") >>> p stim.PauliString("-iXYY") >>> alias is p True Returns: The mutated pauli string. """ ``` ```python # stim.PauliString.__imul__ # (in class stim.PauliString) def __imul__( self, rhs: object, ) -> stim.PauliString: """Inplace right-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: rhs: The right hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term into the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply into the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> p = stim.PauliString("X") >>> p *= 1j >>> p stim.PauliString("+iX") >>> p = stim.PauliString("iXY_") >>> p *= 3 >>> p stim.PauliString("-iXY_XY_XY_") >>> p = stim.PauliString("X") >>> alias = p >>> p *= stim.PauliString("Y") >>> alias stim.PauliString("+iZ") >>> p = stim.PauliString("X") >>> p *= stim.PauliString("_YY") >>> p stim.PauliString("+XYY") Returns: The mutated Pauli string. """ ``` ```python # stim.PauliString.__init__ # (in class stim.PauliString) def __init__( self, arg: Union[None, int, str, stim.PauliString, Iterable[Union[int, Literal["_", "I", "X", "Y", "Z"]]]] = None, /, ) -> None: """Initializes a stim.PauliString from the given argument. When given a string, the string is parsed as a pauli string. The string can optionally start with a sign ('+', '-', 'i', '+i', or '-i'). The rest of the string should be either a dense pauli string or a sparse pauli string. A dense pauli string is made up of characters from '_IXYZ' where '_' and 'I' mean identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z. A sparse pauli string is a series of integers seperated by '*' and prefixed by 'I', 'X', 'Y', or 'Z'. Args: arg [position-only]: This can be a variety of types, including: None (default): initializes an empty Pauli string. int: initializes an identity Pauli string of the given length. str: initializes by parsing the given text. stim.PauliString: initializes a copy of the given Pauli string. Iterable: initializes by interpreting each item as a Pauli. Each item can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Dict[int, Union[int, str]]: initializes by interpreting keys as the qubit index and values as the Pauli for that index. Each value can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Dict[Union[int, str], Iterable[int]]: initializes by interpreting keys as Pauli operators and values as the qubit indices for that Pauli. Each key can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Examples: >>> import stim >>> stim.PauliString("-XYZ") stim.PauliString("-XYZ") >>> stim.PauliString() stim.PauliString("+") >>> stim.PauliString(5) stim.PauliString("+_____") >>> stim.PauliString(stim.PauliString("XX")) stim.PauliString("+XX") >>> stim.PauliString([0, 1, 3, 2]) stim.PauliString("+_XZY") >>> stim.PauliString("X" for _ in range(4)) stim.PauliString("+XXXX") >>> stim.PauliString("-X2*Y6") stim.PauliString("-__X___Y") >>> stim.PauliString("X6*Y6") stim.PauliString("+i______Z") >>> stim.PauliString({0: "X", 2: "Y", 3: "X"}) stim.PauliString("+X_YX") >>> stim.PauliString({0: "X", 2: 2, 3: 1}) stim.PauliString("+X_YX") >>> stim.PauliString({"X": [1], 2: [4], "Z": [0, 3]}) stim.PauliString("+ZX_ZY") """ ``` ```python # stim.PauliString.__itruediv__ # (in class stim.PauliString) def __itruediv__( self, rhs: complex, ) -> stim.PauliString: """Inplace divides the Pauli string by a complex unit. Args: rhs: The divisor. Can be 1, -1, 1j, or -1j. Examples: >>> import stim >>> p = stim.PauliString("X") >>> p /= 1j >>> p stim.PauliString("-iX") Returns: The mutated Pauli string. Raises: ValueError: The divisor isn't 1, -1, 1j, or -1j. """ ``` ```python # stim.PauliString.__len__ # (in class stim.PauliString) def __len__( self, ) -> int: """Returns the length the pauli string; the number of qubits it operates on. Examples: >>> import stim >>> len(stim.PauliString("XY_ZZ")) 5 >>> len(stim.PauliString("X0*Z99")) 100 """ ``` ```python # stim.PauliString.__mul__ # (in class stim.PauliString) def __mul__( self, rhs: object, ) -> stim.PauliString: """Right-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: rhs: The right hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term with the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply with the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> stim.PauliString("X") * 1 stim.PauliString("+X") >>> stim.PauliString("X") * -1 stim.PauliString("-X") >>> stim.PauliString("X") * 1j stim.PauliString("+iX") >>> stim.PauliString("X") * 2 stim.PauliString("+XX") >>> stim.PauliString("-X") * 2 stim.PauliString("+XX") >>> stim.PauliString("iX") * 2 stim.PauliString("-XX") >>> stim.PauliString("X") * 3 stim.PauliString("+XXX") >>> stim.PauliString("iX") * 3 stim.PauliString("-iXXX") >>> stim.PauliString("X") * stim.PauliString("Y") stim.PauliString("+iZ") >>> stim.PauliString("X") * stim.PauliString("XX_") stim.PauliString("+_X_") >>> stim.PauliString("XXXX") * stim.PauliString("_XYZ") stim.PauliString("+X_ZY") Returns: The product or tensor power. Raises: TypeError: The right hand side isn't a stim.PauliString, a non-negative integer, or a complex unit (1, -1, 1j, or -1j). """ ``` ```python # stim.PauliString.__ne__ # (in class stim.PauliString) def __ne__( self, arg0: stim.PauliString, ) -> bool: """Determines if two Pauli strings have non-identical contents. """ ``` ```python # stim.PauliString.__neg__ # (in class stim.PauliString) def __neg__( self, ) -> stim.PauliString: """Returns the negation of the pauli string. Examples: >>> import stim >>> -stim.PauliString("X") stim.PauliString("-X") >>> -stim.PauliString("-Y") stim.PauliString("+Y") >>> -stim.PauliString("iZZZ") stim.PauliString("-iZZZ") """ ``` ```python # stim.PauliString.__pos__ # (in class stim.PauliString) def __pos__( self, ) -> stim.PauliString: """Returns a pauli string with the same contents. Examples: >>> import stim >>> +stim.PauliString("+X") stim.PauliString("+X") >>> +stim.PauliString("-YY") stim.PauliString("-YY") >>> +stim.PauliString("iZZZ") stim.PauliString("+iZZZ") """ ``` ```python # stim.PauliString.__repr__ # (in class stim.PauliString) def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.PauliString`. """ ``` ```python # stim.PauliString.__rmul__ # (in class stim.PauliString) def __rmul__( self, lhs: object, ) -> stim.PauliString: """Left-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: lhs: The left hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term with the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply with the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> 1 * stim.PauliString("X") stim.PauliString("+X") >>> -1 * stim.PauliString("X") stim.PauliString("-X") >>> 1j * stim.PauliString("X") stim.PauliString("+iX") >>> 2 * stim.PauliString("X") stim.PauliString("+XX") >>> 2 * stim.PauliString("-X") stim.PauliString("+XX") >>> 2 * stim.PauliString("iX") stim.PauliString("-XX") >>> 3 * stim.PauliString("X") stim.PauliString("+XXX") >>> 3 * stim.PauliString("iX") stim.PauliString("-iXXX") >>> stim.PauliString("X") * stim.PauliString("Y") stim.PauliString("+iZ") >>> stim.PauliString("X") * stim.PauliString("XX_") stim.PauliString("+_X_") >>> stim.PauliString("XXXX") * stim.PauliString("_XYZ") stim.PauliString("+X_ZY") Returns: The product. Raises: ValueError: The scalar phase factor isn't 1, -1, 1j, or -1j. """ ``` ```python # stim.PauliString.__setitem__ # (in class stim.PauliString) def __setitem__( self, index: int, new_pauli: object, ) -> None: """Mutates an entry in the pauli string using the encoding 0=I, 1=X, 2=Y, 3=Z. Args: index: The index of the pauli to overwrite. new_pauli: Either a character from '_IXYZ' or an integer from range(4). Examples: >>> import stim >>> p = stim.PauliString(4) >>> p[2] = 1 >>> print(p) +__X_ >>> p[0] = 3 >>> p[1] = 2 >>> p[3] = 0 >>> print(p) +ZYX_ >>> p[0] = 'I' >>> p[1] = 'X' >>> p[2] = 'Y' >>> p[3] = 'Z' >>> print(p) +_XYZ >>> p[-1] = 'Y' >>> print(p) +_XYY """ ``` ```python # stim.PauliString.__str__ # (in class stim.PauliString) def __str__( self, ) -> str: """Returns a text description. """ ``` ```python # stim.PauliString.__truediv__ # (in class stim.PauliString) def __truediv__( self, rhs: complex, ) -> stim.PauliString: """Divides the Pauli string by a complex unit. Args: rhs: The divisor. Can be 1, -1, 1j, or -1j. Examples: >>> import stim >>> stim.PauliString("X") / 1j stim.PauliString("-iX") Returns: The quotient. Raises: ValueError: The divisor isn't 1, -1, 1j, or -1j. """ ``` ```python # stim.PauliString.after # (in class stim.PauliString) @overload def after( self, operation: Union[stim.Circuit, stim.CircuitInstruction], ) -> stim.PauliString: pass @overload def after( self, operation: stim.Tableau, targets: Iterable[int], ) -> stim.PauliString: pass def after( self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None, ) -> stim.PauliString: """Returns the result of conjugating the Pauli string by an operation. Args: operation: A circuit, tableau, or circuit instruction to conjugate the Pauli string by. Must be Clifford (e.g. if it's a circuit, the circuit can't have noise or measurements). targets: Required if and only if the operation is a tableau. Specifies which qubits to target. Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p.after(stim.CircuitInstruction("H", [1])) stim.PauliString("+_ZYZ") >>> p.after(stim.Circuit(''' ... C_XYZ 1 2 3 ... ''')) stim.PauliString("+_YZX") >>> p.after(stim.Tableau.from_named_gate('CZ'), targets=[0, 1]) stim.PauliString("+ZXYZ") Returns: The conjugated Pauli string. The Pauli string after the operation that is exactly equivalent to the given Pauli string before the operation. """ ``` ```python # stim.PauliString.before # (in class stim.PauliString) @overload def before( self, operation: Union[stim.Circuit, stim.CircuitInstruction], ) -> stim.PauliString: pass @overload def before( self, operation: stim.Tableau, targets: Iterable[int], ) -> stim.PauliString: pass def before( self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None, ) -> stim.PauliString: """Returns the result of conjugating the Pauli string by an operation. Args: operation: A circuit, tableau, or circuit instruction to anti-conjugate the Pauli string by. Must be Clifford (e.g. if it's a circuit, the circuit can't have noise or measurements). targets: Required if and only if the operation is a tableau. Specifies which qubits to target. Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p.before(stim.CircuitInstruction("H", [1])) stim.PauliString("+_ZYZ") >>> p.before(stim.Circuit(''' ... C_XYZ 1 2 3 ... ''')) stim.PauliString("+_ZXY") >>> p.before(stim.Tableau.from_named_gate('CZ'), targets=[0, 1]) stim.PauliString("+ZXYZ") Returns: The conjugated Pauli string. The Pauli string before the operation that is exactly equivalent to the given Pauli string after the operation. """ ``` ```python # stim.PauliString.commutes # (in class stim.PauliString) def commutes( self, other: stim.PauliString, ) -> bool: """Determines if two Pauli strings commute or not. Two Pauli strings commute if they have an even number of matched non-equal non-identity Pauli terms. Otherwise they anticommute. Args: other: The other Pauli string. Examples: >>> import stim >>> xx = stim.PauliString("XX") >>> xx.commutes(stim.PauliString("X_")) True >>> xx.commutes(stim.PauliString("XX")) True >>> xx.commutes(stim.PauliString("XY")) False >>> xx.commutes(stim.PauliString("XZ")) False >>> xx.commutes(stim.PauliString("ZZ")) True >>> xx.commutes(stim.PauliString("X_Y__")) True >>> xx.commutes(stim.PauliString("")) True Returns: True if the Pauli strings commute, False if they anti-commute. """ ``` ```python # stim.PauliString.copy # (in class stim.PauliString) def copy( self, ) -> stim.PauliString: """Returns a copy of the pauli string. The copy is an independent pauli string with the same contents. Examples: >>> import stim >>> p1 = stim.PauliString.random(2) >>> p2 = p1.copy() >>> p2 is p1 False >>> p2 == p1 True """ ``` ```python # stim.PauliString.from_numpy # (in class stim.PauliString) @staticmethod def from_numpy( *, xs: np.ndarray, zs: np.ndarray, sign: Union[int, float, complex] = +1, num_qubits: Optional[int] = None, ) -> stim.PauliString: """Creates a pauli string from X bit and Z bit numpy arrays, using the encoding: x=0 and z=0 -> P=I x=1 and z=0 -> P=X x=1 and z=1 -> P=Y x=0 and z=1 -> P=Z Args: xs: The X bits of the pauli string. This array can either be a 1-dimensional numpy array with dtype=np.bool_, or a bit packed 1-dimensional numpy array with dtype=np.uint8. If the dtype is np.uint8 then the array is assumed to be bit packed in little endian order and the "num_qubits" argument must be specified. When bit packed, the x bit with offset k is stored at (xs[k // 8] >> (k % 8)) & 1. zs: The Z bits of the pauli string. This array can either be a 1-dimensional numpy array with dtype=np.bool_, or a bit packed 1-dimensional numpy array with dtype=np.uint8. If the dtype is np.uint8 then the array is assumed to be bit packed in little endian order and the "num_qubits" argument must be specified. When bit packed, the x bit with offset k is stored at (xs[k // 8] >> (k % 8)) & 1. sign: Defaults to +1. Set to +1, -1, 1j, or -1j to control the sign of the returned Pauli string. num_qubits: Must be specified if xs or zs is a bit packed array. Specifies the expected length of the Pauli string. Returns: The created pauli string. Examples: >>> import stim >>> import numpy as np >>> xs = np.array([1, 1, 1, 1, 1, 1, 1, 0, 0], dtype=np.bool_) >>> zs = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1], dtype=np.bool_) >>> stim.PauliString.from_numpy(xs=xs, zs=zs, sign=-1) stim.PauliString("-XXXXYYYZZ") >>> xs = np.array([127, 0], dtype=np.uint8) >>> zs = np.array([240, 1], dtype=np.uint8) >>> stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=9) stim.PauliString("+XXXXYYYZZ") """ ``` ```python # stim.PauliString.from_unitary_matrix # (in class stim.PauliString) @staticmethod def from_unitary_matrix( matrix: Iterable[Iterable[Union[int, float, complex]]], *, endian: Literal["little", "big"] = 'little', unsigned: bool = False, ) -> stim.PauliString: """Creates a stim.PauliString from the unitary matrix of a Pauli group member. Args: matrix: A unitary matrix specified as an iterable of rows, with each row is an iterable of amplitudes. The unitary matrix must correspond to a Pauli string, including global phase. endian: "little": matrix entries are in little endian order, where higher index qubits correspond to larger changes in row/col indices. "big": matrix entries are in big endian order, where higher index qubits correspond to smaller changes in row/col indices. unsigned: When False, the input must only contain the values [0, 1, -1, 1j, -1j] and the output will have the correct global phase. When True, the input is permitted to be scaled by an arbitrary unit complex value and the output will always have positive sign. False is stricter but provides more information, while True is more flexible but provides less information. Returns: The pauli string equal to the given unitary matrix. Raises: ValueError: The given matrix isn't the unitary matrix of a Pauli string. Examples: >>> import stim >>> stim.PauliString.from_unitary_matrix([ ... [1j, 0], ... [0, -1j], ... ], endian='little') stim.PauliString("+iZ") >>> stim.PauliString.from_unitary_matrix([ ... [1j**0.1, 0], ... [0, -(1j**0.1)], ... ], endian='little', unsigned=True) stim.PauliString("+Z") >>> stim.PauliString.from_unitary_matrix([ ... [0, 1, 0, 0], ... [1, 0, 0, 0], ... [0, 0, 0, -1], ... [0, 0, -1, 0], ... ], endian='little') stim.PauliString("+XZ") """ ``` ```python # stim.PauliString.iter_all # (in class stim.PauliString) @staticmethod def iter_all( num_qubits: int, *, min_weight: int = 0, max_weight: object = None, allowed_paulis: str = 'XYZ', ) -> stim.PauliStringIterator: """Returns an iterator that iterates over all matching pauli strings. Args: num_qubits: The desired number of qubits in the pauli strings. min_weight: Defaults to 0. The minimum number of non-identity terms that must be present in each yielded pauli string. max_weight: Defaults to None (unused). The maximum number of non-identity terms that must be present in each yielded pauli string. allowed_paulis: Defaults to "XYZ". Set this to a string containing the non-identity paulis that are allowed to appear in each yielded pauli string. This argument must be a string made up of only "X", "Y", and "Z" characters. A non-identity Pauli is allowed if it appears in the string, and not allowed if it doesn't. Identity Paulis are always allowed. Returns: An Iterable[stim.PauliString] that yields the requested pauli strings. Examples: >>> import stim >>> pauli_string_iterator = stim.PauliString.iter_all( ... num_qubits=3, ... min_weight=1, ... max_weight=2, ... allowed_paulis="XZ", ... ) >>> for p in pauli_string_iterator: ... print(p) +X__ +Z__ +_X_ +_Z_ +__X +__Z +XX_ +XZ_ +ZX_ +ZZ_ +X_X +X_Z +Z_X +Z_Z +_XX +_XZ +_ZX +_ZZ """ ``` ```python # stim.PauliString.pauli_indices # (in class stim.PauliString) def pauli_indices( self, included_paulis: str = "XYZ", ) -> List[int]: """Returns the indices of non-identity Paulis, or of specified Paulis. Args: include: A string containing the Pauli types to include. X type Pauli indices are included if "X" or "x" is in the string. Y type Pauli indices are included if "Y" or "y" is in the string. Z type Pauli indices are included if "Z" or "z" is in the string. I type Pauli indices are included if "I" or "_" is in the string. An exception is thrown if other characters are in the string. Returns: A list containing the ascending indices of matching Pauli terms. Examples: >>> import stim >>> stim.PauliString("_____X___Y____Z___").pauli_indices() [5, 9, 14] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("XZ") [5, 14] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("X") [5] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("Y") [9] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("IY") [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17] >>> stim.PauliString("-X103*Y100").pauli_indices() [100, 103] """ ``` ```python # stim.PauliString.random # (in class stim.PauliString) @staticmethod def random( num_qubits: int, *, allow_imaginary: bool = False, ) -> stim.PauliString: """Samples a uniformly random Hermitian Pauli string. Args: num_qubits: The number of qubits the Pauli string should act on. allow_imaginary: Defaults to False. If True, the sign of the result may be 1j or -1j in addition to +1 or -1. In other words, setting this to True allows the result to be non-Hermitian. Examples: >>> import stim >>> p = stim.PauliString.random(5) >>> len(p) 5 >>> p.sign in [-1, +1] True >>> p2 = stim.PauliString.random(3, allow_imaginary=True) >>> len(p2) 3 >>> p2.sign in [-1, +1, 1j, -1j] True Returns: The sampled Pauli string. """ ``` ```python # stim.PauliString.sign # (in class stim.PauliString) @property def sign( self, ) -> complex: """The sign of the Pauli string. Can be +1, -1, 1j, or -1j. Examples: >>> import stim >>> stim.PauliString("X").sign (1+0j) >>> stim.PauliString("-X").sign (-1+0j) >>> stim.PauliString("iX").sign 1j >>> stim.PauliString("-iX").sign (-0-1j) """ @sign.setter def sign(self, value: complex): pass ``` ```python # stim.PauliString.to_numpy # (in class stim.PauliString) def to_numpy( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: """Decomposes the contents of the pauli string into X bit and Z bit numpy arrays. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: An (xs, zs) tuple encoding the paulis from the string. The k'th Pauli from the string is encoded into k'th bit of xs and the k'th bit of zs using the "xz" encoding: P=I -> x=0 and z=0 P=X -> x=1 and z=0 P=Y -> x=1 and z=1 P=Z -> x=0 and z=1 The dtype and shape of the result depends on the bit_packed argument. If bit_packed=False: Each bit gets its own byte. xs.dtype = zs.dtype = np.bool_ xs.shape = zs.shape = len(self) xs_k = xs[k] zs_k = zs[k] If bit_packed=True: Equivalent to applying np.packbits(bitorder='little') to the result. xs.dtype = zs.dtype = np.uint8 xs.shape = zs.shape = math.ceil(len(self) / 8) xs_k = (xs[k // 8] >> (k % 8)) & 1 zs_k = (zs[k // 8] >> (k % 8)) & 1 Examples: >>> import stim >>> xs, zs = stim.PauliString("XXXXYYYZZ").to_numpy() >>> xs array([ True, True, True, True, True, True, True, False, False]) >>> zs array([False, False, False, False, True, True, True, True, True]) >>> xs, zs = stim.PauliString("XXXXYYYZZ").to_numpy(bit_packed=True) >>> xs array([127, 0], dtype=uint8) >>> zs array([240, 1], dtype=uint8) """ ``` ```python # stim.PauliString.to_tableau # (in class stim.PauliString) def to_tableau( self, ) -> stim.Tableau: """Creates a Tableau equivalent to this Pauli string. The tableau represents a Clifford operation that multiplies qubits by the corresponding Pauli operations from this Pauli string. The global phase of the pauli operation is lost in the conversion. Returns: The created tableau. Examples: >>> import stim >>> p = stim.PauliString("ZZ") >>> p.to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-X_"), stim.PauliString("-_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+_Z"), ], ) >>> q = stim.PauliString("YX_Z") >>> q.to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-X___"), stim.PauliString("+_X__"), stim.PauliString("+__X_"), stim.PauliString("-___X"), ], zs=[ stim.PauliString("-Z___"), stim.PauliString("-_Z__"), stim.PauliString("+__Z_"), stim.PauliString("+___Z"), ], ) """ ``` ```python # stim.PauliString.to_unitary_matrix # (in class stim.PauliString) def to_unitary_matrix( self, *, endian: Literal["little", "big"], ) -> np.ndarray[np.complex64]: """Converts the pauli string into a unitary matrix. Args: endian: "little": The first qubit is the least significant (corresponds to an offset of 1 in the matrix). "big": The first qubit is the most significant (corresponds to an offset of 2**(n - 1) in the matrix). Returns: A numpy array with dtype=np.complex64 and shape=(1 << len(pauli_string), 1 << len(pauli_string)). Example: >>> import stim >>> stim.PauliString("-YZ").to_unitary_matrix(endian="little") array([[0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.-1.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 0.-1.j], [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j]], dtype=complex64) """ ``` ```python # stim.PauliString.weight # (in class stim.PauliString) @property def weight( self, ) -> int: """Returns the number of non-identity pauli terms in the pauli string. Examples: >>> import stim >>> stim.PauliString("+___").weight 0 >>> stim.PauliString("+__X").weight 1 >>> stim.PauliString("+XYZ").weight 3 >>> stim.PauliString("-XXX___XXYZ").weight 7 """ ``` ```python # stim.PauliStringIterator # (at top-level in the stim module) class PauliStringIterator: """Iterates over all pauli strings matching specified patterns. Examples: >>> import stim >>> pauli_string_iterator = stim.PauliString.iter_all( ... 2, ... min_weight=1, ... max_weight=1, ... allowed_paulis="XZ", ... ) >>> for p in pauli_string_iterator: ... print(p) +X_ +Z_ +_X +_Z """ ``` ```python # stim.PauliStringIterator.__iter__ # (in class stim.PauliStringIterator) def __iter__( self, ) -> stim.PauliStringIterator: """Returns an independent copy of the pauli string iterator. Since for-loops and loop-comprehensions call `iter` on things they iterate, this effectively allows the iterator to be iterated multiple times. """ ``` ```python # stim.PauliStringIterator.__next__ # (in class stim.PauliStringIterator) def __next__( self, ) -> stim.PauliString: """Returns the next iterated pauli string. """ ``` ```python # stim.Tableau # (at top-level in the stim module) class Tableau: """A stabilizer tableau. Represents a Clifford operation by explicitly storing how that operation conjugates a list of Pauli group generators into composite Pauli products. Examples: >>> import stim >>> stim.Tableau.from_named_gate("H") stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> t = stim.Tableau.random(5) >>> t_inv = t**-1 >>> print(t * t_inv) +-xz-xz-xz-xz-xz- | ++ ++ ++ ++ ++ | XZ __ __ __ __ | __ XZ __ __ __ | __ __ XZ __ __ | __ __ __ XZ __ | __ __ __ __ XZ >>> x2z3 = t.x_output(2) * t.z_output(3) >>> t_inv(x2z3) stim.PauliString("+__XZ_") """ ``` ```python # stim.Tableau.__add__ # (in class stim.Tableau) def __add__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Returns the direct sum (diagonal concatenation) of two Tableaus. Args: rhs: A second stim.Tableau. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> cz = stim.Tableau.from_named_gate("CZ") >>> print(s + cz) +-xz-xz-xz- | ++ ++ ++ | YZ __ __ | __ XZ Z_ | __ Z_ XZ Returns: The direct sum. """ ``` ```python # stim.Tableau.__call__ # (in class stim.Tableau) def __call__( self, pauli_string: stim.PauliString, ) -> stim.PauliString: """Returns the equivalent PauliString after the Tableau's Clifford operation. If P is a Pauli product before a Clifford operation C, then this method returns Q = C * P * C**-1 (the conjugation of P by C). Q is the equivalent Pauli product after C. This works because: C*P = C*P * I = C*P * (C**-1 * C) = (C*P*C**-1) * C = Q*C (Keep in mind that A*B means first B is applied, then A is applied.) Args: pauli_string: The pauli string to conjugate. Returns: The new conjugated pauli string. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> p = stim.PauliString("XX") >>> result = t(p) >>> print(result) +X_ """ ``` ```python # stim.Tableau.__eq__ # (in class stim.Tableau) def __eq__( self, arg0: stim.Tableau, ) -> bool: """Determines if two tableaus have identical contents. """ ``` ```python # stim.Tableau.__iadd__ # (in class stim.Tableau) def __iadd__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Performs an inplace direct sum (diagonal concatenation). Args: rhs: A second stim.Tableau. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> cz = stim.Tableau.from_named_gate("CZ") >>> alias = s >>> s += cz >>> alias is s True >>> print(s) +-xz-xz-xz- | ++ ++ ++ | YZ __ __ | __ XZ Z_ | __ Z_ XZ Returns: The mutated tableau. """ ``` ```python # stim.Tableau.__init__ # (in class stim.Tableau) def __init__( self, num_qubits: int, ) -> None: """Creates an identity tableau over the given number of qubits. Examples: >>> import stim >>> t = stim.Tableau(3) >>> print(t) +-xz-xz-xz- | ++ ++ ++ | XZ __ __ | __ XZ __ | __ __ XZ Args: num_qubits: The number of qubits the tableau's operation acts on. """ ``` ```python # stim.Tableau.__len__ # (in class stim.Tableau) def __len__( self, ) -> int: """Returns the number of qubits operated on by the tableau. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> len(t) 2 """ ``` ```python # stim.Tableau.__mul__ # (in class stim.Tableau) def __mul__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Returns the product of two tableaus. If the tableau T1 represents the Clifford operation with unitary C1, and the tableau T2 represents the Clifford operation with unitary C2, then the tableau T1*T2 represents the Clifford operation with unitary C1*C2. Args: rhs: The tableau on the right hand side of the multiplication. Examples: >>> import stim >>> t1 = stim.Tableau.random(4) >>> t2 = stim.Tableau.random(4) >>> t3 = t2 * t1 >>> p = stim.PauliString.random(4) >>> t3(p) == t2(t1(p)) True """ ``` ```python # stim.Tableau.__ne__ # (in class stim.Tableau) def __ne__( self, arg0: stim.Tableau, ) -> bool: """Determines if two tableaus have non-identical contents. """ ``` ```python # stim.Tableau.__pow__ # (in class stim.Tableau) def __pow__( self, exponent: int, ) -> stim.Tableau: """Raises the tableau to an integer power. Large powers are reached efficiently using repeated squaring. Negative powers are reached by inverting the tableau. Args: exponent: The power to raise to. Can be negative, zero, or positive. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> s**0 == stim.Tableau(1) True >>> s**1 == s True >>> s**2 == stim.Tableau.from_named_gate("Z") True >>> s**-1 == s**3 == stim.Tableau.from_named_gate("S_DAG") True >>> s**5 == s True >>> s**(400000000 + 1) == s True >>> s**(-400000000 + 1) == s True """ ``` ```python # stim.Tableau.__repr__ # (in class stim.Tableau) def __repr__( self, ) -> str: """Returns valid python code evaluating to an equal `stim.Tableau`. """ ``` ```python # stim.Tableau.__str__ # (in class stim.Tableau) def __str__( self, ) -> str: """Returns a text description. """ ``` ```python # stim.Tableau.append # (in class stim.Tableau) def append( self, gate: stim.Tableau, targets: Sequence[int], ) -> None: """Appends an operation's effect into this tableau, mutating this tableau. Time cost is O(n*m*m) where n=len(self) and m=len(gate). Args: gate: The tableau of the operation being appended into this tableau. targets: The qubits being targeted by the gate. Examples: >>> import stim >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> t = stim.Tableau(2) >>> t.append(cnot, [0, 1]) >>> t.append(cnot, [1, 0]) >>> t.append(cnot, [0, 1]) >>> t == stim.Tableau.from_named_gate("SWAP") True """ ``` ```python # stim.Tableau.copy # (in class stim.Tableau) def copy( self, ) -> stim.Tableau: """Returns a copy of the tableau. An independent tableau with the same contents. Examples: >>> import stim >>> t1 = stim.Tableau.random(2) >>> t2 = t1.copy() >>> t2 is t1 False >>> t2 == t1 True """ ``` ```python # stim.Tableau.from_circuit # (in class stim.Tableau) @staticmethod def from_circuit( circuit: stim.Circuit, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False, ) -> stim.Tableau: """Converts a circuit into an equivalent stabilizer tableau. Args: circuit: The circuit to compile into a tableau. ignore_noise: Defaults to False. When False, any noise operations in the circuit will cause the conversion to fail with an exception. When True, noise operations are skipped over as if they weren't even present in the circuit. ignore_measurement: Defaults to False. When False, any measurement operations in the circuit will cause the conversion to fail with an exception. When True, measurement operations are skipped over as if they weren't even present in the circuit. ignore_reset: Defaults to False. When False, any reset operations in the circuit will cause the conversion to fail with an exception. When True, reset operations are skipped over as if they weren't even present in the circuit. Returns: The tableau equivalent to the given circuit (up to global phase). Raises: ValueError: The circuit contains noise operations but ignore_noise=False. OR The circuit contains measurement operations but ignore_measurement=False. OR The circuit contains reset operations but ignore_reset=False. Examples: >>> import stim >>> stim.Tableau.from_circuit(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''')) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ ``` ```python # stim.Tableau.from_conjugated_generators # (in class stim.Tableau) @staticmethod def from_conjugated_generators( *, xs: List[stim.PauliString], zs: List[stim.PauliString], ) -> stim.Tableau: """Creates a tableau from the given outputs for each generator. Verifies that the tableau is well formed. Args: xs: A List[stim.PauliString] with the results of conjugating X0, X1, etc. zs: A List[stim.PauliString] with the results of conjugating Z0, Z1, etc. Returns: The created tableau. Raises: ValueError: The given outputs are malformed. Their lengths are inconsistent, or they don't satisfy the required commutation relationships. Examples: >>> import stim >>> identity3 = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("X__"), ... stim.PauliString("_X_"), ... stim.PauliString("__X"), ... ], ... zs=[ ... stim.PauliString("Z__"), ... stim.PauliString("_Z_"), ... stim.PauliString("__Z"), ... ], ... ) >>> identity3 == stim.Tableau(3) True """ ``` ```python # stim.Tableau.from_named_gate # (in class stim.Tableau) @staticmethod def from_named_gate( name: str, ) -> stim.Tableau: """Returns the tableau of a named Clifford gate. Args: name: The name of the Clifford gate. Returns: The gate's tableau. Examples: >>> import stim >>> print(stim.Tableau.from_named_gate("H")) +-xz- | ++ | ZX >>> print(stim.Tableau.from_named_gate("CNOT")) +-xz-xz- | ++ ++ | XZ _Z | X_ XZ >>> print(stim.Tableau.from_named_gate("S")) +-xz- | ++ | YZ """ ``` ```python # stim.Tableau.from_numpy # (in class stim.Tableau) def from_numpy( self, *, x2x: np.ndarray, x2z: np.ndarray, z2x: np.ndarray, z2z: np.ndarray, x_signs: Optional[np.ndarray] = None, z_signs: Optional[np.ndarray] = None, ) -> stim.Tableau: """Creates a tableau from numpy arrays x2x, x2z, z2x, z2z, x_signs, and z_signs. The x2x, x2z, z2x, z2z arrays are the four quadrants of the table defined in Aaronson and Gottesman's "Improved Simulation of Stabilizer Circuits" ( https://arxiv.org/abs/quant-ph/0406196 ). Args: x2x: A 2d numpy array containing the x-to-x coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.x_output_pauli(i, j) in [1, 2] == x2x[i, j]. Bit packing must be in little endian order and only applies to the second axis. x2z: A 2d numpy array containing the x-to-z coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.x_output_pauli(i, j) in [2, 3] == x2z[i, j]. Bit packing must be in little endian order and only applies to the second axis. z2x: A 2d numpy array containing the z-to-x coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.z_output_pauli(i, j) in [1, 2] == z2x[i, j]. Bit packing must be in little endian order and only applies to the second axis. z2z: A 2d numpy array containing the z-to-z coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.z_output_pauli(i, j) in [2, 3] == z2z[i, j]. Bit packing must be in little endian order and only applies to the second axis. x_signs: Defaults to all-positive if not specified. A 1d numpy array containing the sign bits for the X generator outputs. False means positive and True means negative. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). Bit packing must be in little endian order. z_signs: Defaults to all-positive if not specified. A 1d numpy array containing the sign bits for the Z generator outputs. False means positive and True means negative. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). Bit packing must be in little endian order. Returns: The tableau created from the numpy data. Examples: >>> import stim >>> import numpy as np >>> tableau = stim.Tableau.from_numpy( ... x2x=np.array([[1, 1], [0, 1]], dtype=np.bool_), ... z2x=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... x2z=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... z2z=np.array([[1, 0], [1, 1]], dtype=np.bool_), ... ) >>> tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+ZZ"), ], ) >>> tableau == stim.Tableau.from_named_gate("CNOT") True >>> stim.Tableau.from_numpy( ... x2x=np.array([[9], [5], [7], [6]], dtype=np.uint8), ... x2z=np.array([[13], [13], [0], [3]], dtype=np.uint8), ... z2x=np.array([[8], [5], [9], [15]], dtype=np.uint8), ... z2z=np.array([[6], [11], [2], [3]], dtype=np.uint8), ... x_signs=np.array([7], dtype=np.uint8), ... z_signs=np.array([9], dtype=np.uint8), ... ) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-Y_ZY"), stim.PauliString("-Y_YZ"), stim.PauliString("-XXX_"), stim.PauliString("+ZYX_"), ], zs=[ stim.PauliString("-_ZZX"), stim.PauliString("+YZXZ"), stim.PauliString("+XZ_X"), stim.PauliString("-YYXX"), ], ) """ ``` ```python # stim.Tableau.from_stabilizers # (in class stim.Tableau) @staticmethod def from_stabilizers( stabilizers: Iterable[stim.PauliString], *, allow_redundant: bool = False, allow_underconstrained: bool = False, ) -> stim.Tableau: """Creates a tableau representing a state with the given stabilizers. Args: stabilizers: A list of `stim.PauliString`s specifying the stabilizers that the state must have. It is permitted for stabilizers to have different lengths. All stabilizers are padded up to the length of the longest stabilizer by appending identity terms. allow_redundant: Defaults to False. If set to False, then the given stabilizers must all be independent. If any one of them is a product of the others (including the empty product), an exception will be raised. If set to True, then redundant stabilizers are simply ignored. allow_underconstrained: Defaults to False. If set to False, then the given stabilizers must form a complete set of generators. They must exactly specify the desired stabilizer state, with no degrees of freedom left over. For an n-qubit state there must be n independent stabilizers. If set to True, then there can be leftover degrees of freedom which can be set arbitrarily. Returns: A tableau which, when applied to the all-zeroes state, produces a state with the given stabilizers. Guarantees that result.z_output(k) will be equal to the k'th independent stabilizer from the `stabilizers` argument. Raises: ValueError: A stabilizer is redundant but allow_redundant=True wasn't set. OR The given stabilizers are contradictory (e.g. "+Z" and "-Z" both specified). OR The given stabilizers anticommute (e.g. "+Z" and "+X" both specified). OR The stabilizers left behind a degree of freedom but allow_underconstrained=True wasn't set. OR A stabilizer has an imaginary sign (i or -i). Examples: >>> import stim >>> stim.Tableau.from_stabilizers([ ... stim.PauliString("XX"), ... stim.PauliString("ZZ"), ... ]) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) >>> stim.Tableau.from_stabilizers([ ... stim.PauliString("XX_"), ... stim.PauliString("ZZ_"), ... stim.PauliString("-YY_"), ... stim.PauliString(""), ... ], allow_underconstrained=True, allow_redundant=True) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z__"), stim.PauliString("+_X_"), stim.PauliString("+__X"), ], zs=[ stim.PauliString("+XX_"), stim.PauliString("+ZZ_"), stim.PauliString("+__Z"), ], ) """ ``` ```python # stim.Tableau.from_state_vector # (in class stim.Tableau) @staticmethod def from_state_vector( state_vector: Iterable[float], *, endian: Literal["little", "big"], ) -> stim.Tableau: """Creates a tableau representing the stabilizer state of the given state vector. Args: state_vector: A list of complex amplitudes specifying a superposition. The vector must correspond to a state that is reachable using Clifford operations, and can be unnormalized. endian: "little": state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A tableau which, when applied to the all-zeroes state, produces a state with the given state vector. Raises: ValueError: The given state vector isn't a list of complex values specifying a stabilizer state. OR The given endian value isn't 'little' or 'big'. Examples: >>> import stim >>> stim.Tableau.from_state_vector([ ... 0.5**0.5, ... 0.5**0.5 * 1j, ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+Y"), ], ) >>> stim.Tableau.from_state_vector([ ... 0.5**0.5, ... 0, ... 0, ... 0.5**0.5, ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ ``` ```python # stim.Tableau.from_unitary_matrix # (in class stim.Tableau) @staticmethod def from_unitary_matrix( matrix: Iterable[Iterable[float]], *, endian: Literal["little", "big"] = 'little', ) -> stim.Tableau: """Creates a tableau from the unitary matrix of a Clifford operation. Args: matrix: A unitary matrix specified as an iterable of rows, with each row is an iterable of amplitudes. The unitary matrix must correspond to a Clifford operation. endian: "little": matrix entries are in little endian order, where higher index qubits correspond to larger changes in row/col indices. "big": matrix entries are in big endian order, where higher index qubits correspond to smaller changes in row/col indices. Returns: The tableau equivalent to the given unitary matrix (up to global phase). Raises: ValueError: The given matrix isn't the unitary matrix of a Clifford operation. Examples: >>> import stim >>> stim.Tableau.from_unitary_matrix([ ... [1, 0], ... [0, 1j], ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Y"), ], zs=[ stim.PauliString("+Z"), ], ) >>> stim.Tableau.from_unitary_matrix([ ... [1, 0, 0, 0], ... [0, 1, 0, 0], ... [0, 0, 0, -1j], ... [0, 0, 1j, 0], ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XZ"), stim.PauliString("+YX"), ], zs=[ stim.PauliString("+ZZ"), stim.PauliString("+_Z"), ], ) """ ``` ```python # stim.Tableau.inverse # (in class stim.Tableau) def inverse( self, *, unsigned: bool = False, ) -> stim.Tableau: """Computes the inverse of the tableau. The inverse T^-1 of a tableau T is the unique tableau with the property that T * T^-1 = T^-1 * T = I where I is the identity tableau. Args: unsigned: Defaults to false. When set to true, skips computing the signs of the output observables and instead just set them all to be positive. This is beneficial because computing the signs takes O(n^3) time and the rest of the inverse computation is O(n^2) where n is the number of qubits in the tableau. So, if you only need the Pauli terms (not the signs), it is significantly cheaper. Returns: The inverse tableau. Examples: >>> import stim >>> # Check that the inverse agrees with hard-coded tableaus. >>> s = stim.Tableau.from_named_gate("S") >>> s_dag = stim.Tableau.from_named_gate("S_DAG") >>> s.inverse() == s_dag True >>> z = stim.Tableau.from_named_gate("Z") >>> z.inverse() == z True >>> # Check that multiplying by the inverse produces the identity. >>> t = stim.Tableau.random(10) >>> t_inv = t.inverse() >>> identity = stim.Tableau(10) >>> t * t_inv == t_inv * t == identity True >>> # Check a manual case. >>> t = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("-__Z"), ... stim.PauliString("+XZ_"), ... stim.PauliString("+_ZZ"), ... ], ... zs=[ ... stim.PauliString("-YYY"), ... stim.PauliString("+Z_Z"), ... stim.PauliString("-ZYZ") ... ], ... ) >>> print(t.inverse()) +-xz-xz-xz- | -- +- -- | XX XX YX | XZ Z_ X_ | X_ YX Y_ >>> print(t.inverse(unsigned=True)) +-xz-xz-xz- | ++ ++ ++ | XX XX YX | XZ Z_ X_ | X_ YX Y_ """ ``` ```python # stim.Tableau.inverse_x_output # (in class stim.Tableau) def inverse_x_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit X Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).x_output(input_index)`. Args: input_index: Identifies the column (the qubit of the X generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating an X generator by the inverse of the tableau. Examples: >>> import stim # Check equivalence with the inverse's x_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().x_output(0) >>> t.inverse_x_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_x_output(0, unsigned=True) == expected True """ ``` ```python # stim.Tableau.inverse_x_output_pauli # (in class stim.Tableau) def inverse_x_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().x_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input X generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_x_output_pauli(0, 0) 2 >>> t_inv.inverse_x_output_pauli(0, 1) 0 >>> t_inv.inverse_x_output_pauli(1, 0) 2 >>> t_inv.inverse_x_output_pauli(1, 1) 3 """ ``` ```python # stim.Tableau.inverse_y_output # (in class stim.Tableau) def inverse_y_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit Y Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).y_output(input_index)`. Args: input_index: Identifies the column (the qubit of the Y generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating a Y generator by the inverse of the tableau. Examples: >>> import stim # Check equivalence with the inverse's y_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().y_output(0) >>> t.inverse_y_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_y_output(0, unsigned=True) == expected True """ ``` ```python # stim.Tableau.inverse_y_output_pauli # (in class stim.Tableau) def inverse_y_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().y_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input Y generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_y_output_pauli(0, 0) 1 >>> t_inv.inverse_y_output_pauli(0, 1) 2 >>> t_inv.inverse_y_output_pauli(1, 0) 0 >>> t_inv.inverse_y_output_pauli(1, 1) 2 """ ``` ```python # stim.Tableau.inverse_z_output # (in class stim.Tableau) def inverse_z_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit Z Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).z_output(input_index)`. Args: input_index: Identifies the column (the qubit of the Z generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating a Z generator by the inverse of the tableau. Examples: >>> import stim >>> import stim # Check equivalence with the inverse's z_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().z_output(0) >>> t.inverse_z_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_z_output(0, unsigned=True) == expected True """ ``` ```python # stim.Tableau.inverse_z_output_pauli # (in class stim.Tableau) def inverse_z_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().z_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input Z generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_z_output_pauli(0, 0) 3 >>> t_inv.inverse_z_output_pauli(0, 1) 2 >>> t_inv.inverse_z_output_pauli(1, 0) 2 >>> t_inv.inverse_z_output_pauli(1, 1) 1 """ ``` ```python # stim.Tableau.iter_all # (in class stim.Tableau) @staticmethod def iter_all( num_qubits: int, *, unsigned: bool = False, ) -> stim.TableauIterator: """Returns an iterator that iterates over all Tableaus of a given size. Args: num_qubits: The size of tableau to iterate over. unsigned: Defaults to False. If set to True, only tableaus where all columns have positive sign are yielded by the iterator. This substantially reduces the total number of tableaus to iterate over. Returns: An Iterable[stim.Tableau] that yields the requested tableaus. Examples: >>> import stim >>> single_qubit_gate_reprs = set() >>> for t in stim.Tableau.iter_all(1): ... single_qubit_gate_reprs.add(repr(t)) >>> len(single_qubit_gate_reprs) 24 >>> num_2q_gates_mod_paulis = 0 >>> for _ in stim.Tableau.iter_all(2, unsigned=True): ... num_2q_gates_mod_paulis += 1 >>> num_2q_gates_mod_paulis 720 """ ``` ```python # stim.Tableau.prepend # (in class stim.Tableau) def prepend( self, gate: stim.Tableau, targets: Sequence[int], ) -> None: """Prepends an operation's effect into this tableau, mutating this tableau. Time cost is O(n*m*m) where n=len(self) and m=len(gate). Args: gate: The tableau of the operation being prepended into this tableau. targets: The qubits being targeted by the gate. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("H") >>> t.prepend(stim.Tableau.from_named_gate("X"), [0]) >>> t == stim.Tableau.from_named_gate("SQRT_Y_DAG") True """ ``` ```python # stim.Tableau.random # (in class stim.Tableau) @staticmethod def random( num_qubits: int, ) -> stim.Tableau: """Samples a uniformly random Clifford operation and returns its tableau. Args: num_qubits: The number of qubits the tableau should act on. Returns: The sampled tableau. Examples: >>> import stim >>> t = stim.Tableau.random(42) References: "Hadamard-free circuits expose the structure of the Clifford group" Sergey Bravyi, Dmitri Maslov https://arxiv.org/abs/2003.09412 """ ``` ```python # stim.Tableau.then # (in class stim.Tableau) def then( self, second: stim.Tableau, ) -> stim.Tableau: """Returns the result of composing two tableaus. If the tableau T1 represents the Clifford operation with unitary C1, and the tableau T2 represents the Clifford operation with unitary C2, then the tableau T1.then(T2) represents the Clifford operation with unitary C2*C1. Args: second: The result is equivalent to applying the second tableau after the receiving tableau. Examples: >>> import stim >>> t1 = stim.Tableau.random(4) >>> t2 = stim.Tableau.random(4) >>> t3 = t1.then(t2) >>> p = stim.PauliString.random(4) >>> t3(p) == t2(t1(p)) True """ ``` ```python # stim.Tableau.to_circuit # (in class stim.Tableau) def to_circuit( self, method: Literal["elimination", "graph_state"] = 'elimination', ) -> stim.Circuit: """Synthesizes a circuit that implements the tableau's Clifford operation. The circuits returned by this method are not guaranteed to be stable from version to version, and may be produced using randomization. Args: method: The method to use when synthesizing the circuit. Available values: "elimination": Cancels off-diagonal terms using Gaussian elimination. Gate set: H, S, CX Circuit qubit count: n Circuit operation count: O(n^2) Circuit depth: O(n^2) "graph_state": Prepares the tableau's state using a graph state circuit. Gate set: RX, CZ, H, S, X, Y, Z Circuit qubit count: n Circuit operation count: O(n^2) The circuit will be made up of three layers: 1. An RX layer initializing all qubits. 2. A CZ layer coupling the qubits. (Each CZ is an edge in the graph state.) 3. A single qubit rotation layer. Note: "graph_state" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. "mpp_state": Prepares the tableau's state using MPP and feedback. Gate set: MPP, CX rec, CY rec, CZ rec Circuit qubit count: n Circuit operation count: O(n^2) The circuit will be made up of two layers: 1. An MPP layer measuring each of the tableau's stabilizers. 2. A feedback layer using the measurement results to control whether or not to apply each of the tableau's destabilizers in order to get the correct sign for each stabilizer. Note: "mpp_state" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. "mpp_state_unsigned": Prepares the tableau's state up to sign using MPP. Gate set: MPP Circuit qubit count: n Circuit operation count: O(n^2) The circuit will contain a series of MPP measurements measuring each of the tableau's stabilizers. The stabilizers are measured in the order used by the tableau (i.e. tableau.z_output(k) is the k'th stabilizer measured). Note: "mpp_state_unsigned" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. Returns: The synthesized circuit. Example: >>> import stim >>> tableau = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("+YZ__"), ... stim.PauliString("-Y_XY"), ... stim.PauliString("+___Y"), ... stim.PauliString("+YZX_"), ... ], ... zs=[ ... stim.PauliString("+XZYY"), ... stim.PauliString("-XYX_"), ... stim.PauliString("-ZXXZ"), ... stim.PauliString("+XXZ_"), ... ], ... ) >>> tableau.to_circuit() stim.Circuit(''' S 0 H 0 1 3 CX 0 1 0 2 0 3 S 1 3 H 1 3 CX 1 0 3 0 3 1 1 3 3 1 H 1 S 1 CX 1 3 H 2 3 CX 2 1 3 1 3 2 2 3 3 2 H 3 CX 2 3 S 3 H 3 0 1 2 S 0 0 1 1 2 2 H 0 1 2 S 3 3 ''') >>> tableau.to_circuit("graph_state") stim.Circuit(''' RX 0 1 2 3 TICK CZ 0 3 1 2 1 3 TICK X 0 1 Z 2 S 2 3 H 3 S 3 ''') >>> tableau.to_circuit("mpp_state_unsigned") stim.Circuit(''' MPP X0*Z1*Y2*Y3 !X0*Y1*X2 !Z0*X1*X2*Z3 X0*X1*Z2 ''') >>> tableau.to_circuit("mpp_state") stim.Circuit(''' MPP X0*Z1*Y2*Y3 !X0*Y1*X2 !Z0*X1*X2*Z3 X0*X1*Z2 CX rec[-3] 2 rec[-1] 2 CY rec[-4] 0 rec[-3] 0 rec[-3] 3 rec[-2] 3 rec[-1] 0 CZ rec[-4] 1 rec[-1] 1 ''') """ ``` ```python # stim.Tableau.to_numpy # (in class stim.Tableau) def to_numpy( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Decomposes the contents of the tableau into six numpy arrays. The first four numpy arrays correspond to the four quadrants of the table defined in Aaronson and Gottesman's "Improved Simulation of Stabilizer Circuits" ( https://arxiv.org/abs/quant-ph/0406196 ). The last two numpy arrays are the X and Z sign bit vectors of the tableau. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: An (x2x, x2z, z2x, z2z, x_signs, z_signs) tuple encoding the tableau. x2x: A 2d table of whether tableau(X_i)_j is X or Y (instead of I or Z). x2z: A 2d table of whether tableau(X_i)_j is Z or Y (instead of I or X). z2x: A 2d table of whether tableau(Z_i)_j is X or Y (instead of I or Z). z2z: A 2d table of whether tableau(Z_i)_j is Z or Y (instead of I or X). x_signs: A vector of whether tableau(X_i) is negative. z_signs: A vector of whether tableau(Z_i) is negative. If bit_packed=False then: x2x.dtype = np.bool_ x2z.dtype = np.bool_ z2x.dtype = np.bool_ z2z.dtype = np.bool_ x_signs.dtype = np.bool_ z_signs.dtype = np.bool_ x2x.shape = (len(tableau), len(tableau)) x2z.shape = (len(tableau), len(tableau)) z2x.shape = (len(tableau), len(tableau)) z2z.shape = (len(tableau), len(tableau)) x_signs.shape = len(tableau) z_signs.shape = len(tableau) x2x[i, j] = tableau.x_output_pauli(i, j) in [1, 2] x2z[i, j] = tableau.x_output_pauli(i, j) in [2, 3] z2x[i, j] = tableau.z_output_pauli(i, j) in [1, 2] z2z[i, j] = tableau.z_output_pauli(i, j) in [2, 3] If bit_packed=True then: x2x.dtype = np.uint8 x2z.dtype = np.uint8 z2x.dtype = np.uint8 z2z.dtype = np.uint8 x_signs.dtype = np.uint8 z_signs.dtype = np.uint8 x2x.shape = (len(tableau), math.ceil(len(tableau) / 8)) x2z.shape = (len(tableau), math.ceil(len(tableau) / 8)) z2x.shape = (len(tableau), math.ceil(len(tableau) / 8)) z2z.shape = (len(tableau), math.ceil(len(tableau) / 8)) x_signs.shape = math.ceil(len(tableau) / 8) z_signs.shape = math.ceil(len(tableau) / 8) (x2x[i, j // 8] >> (j % 8)) & 1 = tableau.x_output_pauli(i, j) in [1, 2] (x2z[i, j // 8] >> (j % 8)) & 1 = tableau.x_output_pauli(i, j) in [2, 3] (z2x[i, j // 8] >> (j % 8)) & 1 = tableau.z_output_pauli(i, j) in [1, 2] (z2z[i, j // 8] >> (j % 8)) & 1 = tableau.z_output_pauli(i, j) in [2, 3] Examples: >>> import stim >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> print(repr(cnot)) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+ZZ"), ], ) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = cnot.to_numpy() >>> x2x array([[ True, True], [False, True]]) >>> x2z array([[False, False], [False, False]]) >>> z2x array([[False, False], [False, False]]) >>> z2z array([[ True, False], [ True, True]]) >>> x_signs array([False, False]) >>> z_signs array([False, False]) >>> t = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("-Y_ZY"), ... stim.PauliString("-Y_YZ"), ... stim.PauliString("-XXX_"), ... stim.PauliString("+ZYX_"), ... ], ... zs=[ ... stim.PauliString("-_ZZX"), ... stim.PauliString("+YZXZ"), ... stim.PauliString("+XZ_X"), ... stim.PauliString("-YYXX"), ... ], ... ) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy() >>> x2x array([[ True, False, False, True], [ True, False, True, False], [ True, True, True, False], [False, True, True, False]]) >>> x2z array([[ True, False, True, True], [ True, False, True, True], [False, False, False, False], [ True, True, False, False]]) >>> z2x array([[False, False, False, True], [ True, False, True, False], [ True, False, False, True], [ True, True, True, True]]) >>> z2z array([[False, True, True, False], [ True, True, False, True], [False, True, False, False], [ True, True, False, False]]) >>> x_signs array([ True, True, True, False]) >>> z_signs array([ True, False, False, True]) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy(bit_packed=True) >>> x2x array([[9], [5], [7], [6]], dtype=uint8) >>> x2z array([[13], [13], [ 0], [ 3]], dtype=uint8) >>> z2x array([[ 8], [ 5], [ 9], [15]], dtype=uint8) >>> z2z array([[ 6], [11], [ 2], [ 3]], dtype=uint8) >>> x_signs array([7], dtype=uint8) >>> z_signs array([9], dtype=uint8) """ ``` ```python # stim.Tableau.to_pauli_string # (in class stim.Tableau) def to_pauli_string( self, ) -> stim.PauliString: """Return a Pauli string equivalent to the tableau. If the tableau is equivalent to a pauli product, creates an equivalent pauli string. If not, then an error is raised. Returns: The created pauli string Raises: ValueError: The Tableau isn't equivalent to a Pauli product. Example: >>> import stim >>> t = (stim.Tableau.from_named_gate("Z") + ... stim.Tableau.from_named_gate("Y") + ... stim.Tableau.from_named_gate("I") + ... stim.Tableau.from_named_gate("X")) >>> print(t) +-xz-xz-xz-xz- | -+ -- ++ +- | XZ __ __ __ | __ XZ __ __ | __ __ XZ __ | __ __ __ XZ >>> print(t.to_pauli_string()) +ZY_X """ ``` ```python # stim.Tableau.to_stabilizers # (in class stim.Tableau) def to_stabilizers( self, *, canonicalize: bool = False, ) -> List[stim.PauliString]: """Returns the stabilizer generators of the tableau, optionally canonicalized. The stabilizer generators of the tableau are its Z outputs. Canonicalizing standardizes the generators, so that states that are equal will produce the same generators. For example, [ZI, IZ], [ZI, ZZ], amd [ZZ, ZI] describe equal states and all canonicalize to [ZI, IZ]. The canonical form is computed as follows: 1. Get a list of stabilizers using `tableau.z_output(k)` for each k. 2. Perform Gaussian elimination. pivoting on standard generators. 2a) Pivot on g=X0 first, then Z0, X1, Z1, X2, Z2, etc. 2b) Find a stabilizer that uses the generator g. If there are none, go to the next g. 2c) Multiply that stabilizer into all other stabilizers that use the generator g. 2d) Swap that stabilizer with the stabilizer at position `r` then increment `r`. `r` starts at 0. Args: canonicalize: Defaults to False. When False, the tableau's Z outputs are returned unchanged. When True, the Z outputs are rewritten into a standard form. Two stabilizer states have the same standard form if and only if they describe equivalent quantum states. Returns: A List[stim.PauliString] of the tableau's stabilizer generators. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> raw_stabilizers = t.to_stabilizers() >>> for e in raw_stabilizers: ... print(repr(e)) stim.PauliString("+Z_") stim.PauliString("+ZZ") >>> canonical_stabilizers = t.to_stabilizers(canonicalize=True) >>> for e in canonical_stabilizers: ... print(repr(e)) stim.PauliString("+Z_") stim.PauliString("+_Z") """ ``` ```python # stim.Tableau.to_state_vector # (in class stim.Tableau) def to_state_vector( self, *, endian: Literal["little", "big"] = 'little', ) -> np.ndarray[np.complex64]: """Returns the state vector produced by applying the tableau to the |0..0> state. This function takes O(n * 2**n) time and O(2**n) space, where n is the number of qubits. The computation is done by initialization a random state vector and iteratively projecting it into the +1 eigenspace of each stabilizer of the state. The state is then canonicalized so that zero values are actually exactly 0, and so that the first non-zero entry is positive. Args: endian: "little" (default): state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A `numpy.ndarray[numpy.complex64]` of computational basis amplitudes. If the result is in little endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_0, the qubit with index 1 is storing the bit b_1, etc. If the result is in big endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_{n-1}, the qubit with index 1 is storing the bit b_{n-2}, etc. Examples: >>> import stim >>> import numpy as np >>> i2 = stim.Tableau.from_named_gate('I') >>> x = stim.Tableau.from_named_gate('X') >>> h = stim.Tableau.from_named_gate('H') >>> (x + i2).to_state_vector(endian='little') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> (i2 + x).to_state_vector(endian='little') array([0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], dtype=complex64) >>> (i2 + x).to_state_vector(endian='big') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> (h + h).to_state_vector(endian='little') array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], dtype=complex64) """ ``` ```python # stim.Tableau.to_unitary_matrix # (in class stim.Tableau) def to_unitary_matrix( self, *, endian: Literal["little", "big"], ) -> np.ndarray[np.complex64]: """Converts the tableau into a unitary matrix. For an n-qubit tableau, this method performs O(n 4^n) work. It uses the state channel duality to transform the tableau into a list of stabilizers, then generates a random state vector and projects it into the +1 eigenspace of each stabilizer. Note that tableaus don't have a defined global phase, so the result's global phase may be different from what you expect. For example, the square of SQRT_X's unitary might equal -X instead of +X. Args: endian: "little": The first qubit is the least significant (corresponds to an offset of 1 in the state vector). "big": The first qubit is the most significant (corresponds to an offset of 2**(n - 1) in the state vector). Returns: A numpy array with dtype=np.complex64 and shape=(1 << len(tableau), 1 << len(tableau)). Example: >>> import stim >>> cnot = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("XX"), ... stim.PauliString("_X"), ... ], ... zs=[ ... stim.PauliString("Z_"), ... stim.PauliString("ZZ"), ... ], ... ) >>> cnot.to_unitary_matrix(endian='big') array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64) """ ``` ```python # stim.Tableau.x_output # (in class stim.Tableau) def x_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli X by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli X operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.x_output(0) stim.PauliString("+Z") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.x_output(0) stim.PauliString("+XX") >>> cnot.x_output(1) stim.PauliString("+_X") """ ``` ```python # stim.Tableau.x_output_pauli # (in class stim.Tableau) def x_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.x_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input X generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.x_output_pauli(0, 0) 2 >>> t.x_output_pauli(0, 1) 0 >>> t.x_output_pauli(1, 0) 2 >>> t.x_output_pauli(1, 1) 3 """ ``` ```python # stim.Tableau.x_sign # (in class stim.Tableau) def x_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating an X generator. This operation runs in constant time. Args: target: The qubit the X generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("S_DAG").x_sign(0) -1 >>> stim.Tableau.from_named_gate("S").x_sign(0) 1 """ ``` ```python # stim.Tableau.y_output # (in class stim.Tableau) def y_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli Y by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli Y operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.y_output(0) stim.PauliString("-Y") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.y_output(0) stim.PauliString("+YX") >>> cnot.y_output(1) stim.PauliString("+ZY") """ ``` ```python # stim.Tableau.y_output_pauli # (in class stim.Tableau) def y_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.y_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input Y generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.y_output_pauli(0, 0) 1 >>> t.y_output_pauli(0, 1) 2 >>> t.y_output_pauli(1, 0) 0 >>> t.y_output_pauli(1, 1) 2 """ ``` ```python # stim.Tableau.y_sign # (in class stim.Tableau) def y_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating a Y generator. Unlike x_sign and z_sign, this operation runs in linear time. The Y generator has to be computed by multiplying the X and Z outputs and the sign depends on all terms. Args: target: The qubit the Y generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("S_DAG").y_sign(0) 1 >>> stim.Tableau.from_named_gate("S").y_sign(0) -1 """ ``` ```python # stim.Tableau.z_output # (in class stim.Tableau) def z_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli Z by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli Z operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.z_output(0) stim.PauliString("+X") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.z_output(0) stim.PauliString("+Z_") >>> cnot.z_output(1) stim.PauliString("+ZZ") """ ``` ```python # stim.Tableau.z_output_pauli # (in class stim.Tableau) def z_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.z_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input Z generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.z_output_pauli(0, 0) 3 >>> t.z_output_pauli(0, 1) 2 >>> t.z_output_pauli(1, 0) 2 >>> t.z_output_pauli(1, 1) 1 """ ``` ```python # stim.Tableau.z_sign # (in class stim.Tableau) def z_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating a Z generator. This operation runs in constant time. Args: target: The qubit the Z generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("SQRT_X_DAG").z_sign(0) 1 >>> stim.Tableau.from_named_gate("SQRT_X").z_sign(0) -1 """ ``` ```python # stim.TableauIterator # (at top-level in the stim module) class TableauIterator: """Iterates over all stabilizer tableaus of a specified size. Examples: >>> import stim >>> tableau_iterator = stim.Tableau.iter_all(1) >>> n = 0 >>> for single_qubit_clifford in tableau_iterator: ... n += 1 >>> n 24 """ ``` ```python # stim.TableauIterator.__iter__ # (in class stim.TableauIterator) def __iter__( self, ) -> stim.TableauIterator: """Returns an independent copy of the tableau iterator. Since for-loops and loop-comprehensions call `iter` on things they iterate, this effectively allows the iterator to be iterated multiple times. """ ``` ```python # stim.TableauIterator.__next__ # (in class stim.TableauIterator) def __next__( self, ) -> stim.Tableau: """Returns the next iterated tableau. """ ``` ```python # stim.TableauSimulator # (at top-level in the stim module) class TableauSimulator: """A stabilizer circuit simulator that tracks an inverse stabilizer tableau. Supports interactive usage, where gates and measurements are applied on demand. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> if s.measure(0): ... s.h(1) ... s.cnot(1, 2) >>> s.measure(1) == s.measure(2) True >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+X_"), stim.PauliString("+XZ"), ], ) """ ``` ```python # stim.TableauSimulator.__init__ # (in class stim.TableauSimulator) def __init__( self, *, seed: Optional[int] = None, ) -> None: """Initializes a stim.TableauSimulator. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: An initialized stim.TableauSimulator. Examples: >>> import stim >>> s = stim.TableauSimulator(seed=0) >>> s2 = stim.TableauSimulator(seed=0) >>> s.h(0) >>> s2.h(0) >>> s.measure(0) == s2.measure(0) True """ ``` ```python # stim.TableauSimulator.c_xyz # (in class stim.TableauSimulator) def c_xyz( self, *targets, ) -> None: """Applies a C_XYZ gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.c_xyz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y +Z +X """ ``` ```python # stim.TableauSimulator.c_zyx # (in class stim.TableauSimulator) def c_zyx( self, *targets, ) -> None: """Applies a C_ZYX gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.c_zyx(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z +X +Y """ ``` ```python # stim.TableauSimulator.canonical_stabilizers # (in class stim.TableauSimulator) def canonical_stabilizers( self, ) -> List[stim.PauliString]: """Returns a standardized list of the simulator's current stabilizer generators. Two simulators have the same canonical stabilizers if and only if their current quantum state is equal (and tracking the same number of qubits). The canonical form is computed as follows: 1. Get a list of stabilizers using the `z_output`s of `simulator.current_inverse_tableau()**-1`. 2. Perform Gaussian elimination on each generator g. 2a) The generators are considered in order X0, Z0, X1, Z1, X2, Z2, etc. 2b) Pick any stabilizer that uses the generator g. If there are none, go to the next g. 2c) Multiply that stabilizer into all other stabilizers that use the generator g. 2d) Swap that stabilizer with the stabilizer at position `next_output` then increment `next_output`. Returns: A List[stim.PauliString] of the simulator's state's stabilizers. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.x(2) >>> for e in s.canonical_stabilizers(): ... print(repr(e)) stim.PauliString("+XX_") stim.PauliString("+ZZ_") stim.PauliString("-__Z") >>> # Scramble the stabilizers then check the canonical form is unchanged. >>> s.set_inverse_tableau(s.current_inverse_tableau()**-1) >>> s.cnot(0, 1) >>> s.cz(0, 2) >>> s.s(0, 2) >>> s.cy(2, 1) >>> s.set_inverse_tableau(s.current_inverse_tableau()**-1) >>> for e in s.canonical_stabilizers(): ... print(repr(e)) stim.PauliString("+XX_") stim.PauliString("+ZZ_") stim.PauliString("-__Z") """ ``` ```python # stim.TableauSimulator.cnot # (in class stim.TableauSimulator) def cnot( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cnot(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ ``` ```python # stim.TableauSimulator.copy # (in class stim.TableauSimulator) def copy( self, *, copy_rng: bool = False, seed: Optional[int] = None, ) -> stim.TableauSimulator: """Returns a simulator with the same internal state, except perhaps its prng. Args: copy_rng: By default, new simulator's prng is reinitialized with a random seed. However, one can set this argument to True in order to have the prng state copied together with the rest of the original simulator's state. Consequently, in this case the two simulators will produce the same measurement outcomes for the same quantum circuits. If both seed and copy_rng are set, an exception is raised. Defaults to False. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng state is either copied from the original simulator or reseeded from system entropy, depending on the copy_rng argument. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Examples: >>> import stim >>> s1 = stim.TableauSimulator() >>> s1.set_inverse_tableau(stim.Tableau.random(1)) >>> s2 = s1.copy() >>> s2 is s1 False >>> s2.current_inverse_tableau() == s1.current_inverse_tableau() True >>> s1 = stim.TableauSimulator() >>> s2 = s1.copy(copy_rng=True) >>> s1.h(0) >>> s2.h(0) >>> assert s1.measure(0) == s2.measure(0) >>> s = stim.TableauSimulator() >>> def brute_force_post_select(qubit, desired_result): ... global s ... while True: ... s2 = s.copy() ... if s2.measure(qubit) == desired_result: ... s = s2 ... break >>> s.h(0) >>> brute_force_post_select(qubit=0, desired_result=True) >>> s.measure(0) True """ ``` ```python # stim.TableauSimulator.current_inverse_tableau # (in class stim.TableauSimulator) def current_inverse_tableau( self, ) -> stim.Tableau: """Returns a copy of the internal state of the simulator as a stim.Tableau. Returns: A stim.Tableau copy of the simulator's state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> s.cnot(0, 1) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+X_"), stim.PauliString("+XZ"), ], ) """ ``` ```python # stim.TableauSimulator.current_measurement_record # (in class stim.TableauSimulator) def current_measurement_record( self, ) -> List[bool]: """Returns a copy of the record of all measurements performed by the simulator. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.current_measurement_record() [] >>> s.measure(0) False >>> s.x(0) >>> s.measure(0) True >>> s.current_measurement_record() [False, True] >>> s.do(stim.Circuit("M 0")) >>> s.current_measurement_record() [False, True, True] Returns: A list of booleans containing the result of every measurement performed by the simulator so far. """ ``` ```python # stim.TableauSimulator.cx # (in class stim.TableauSimulator) def cx( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ ``` ```python # stim.TableauSimulator.cy # (in class stim.TableauSimulator) def cy( self, *targets, ) -> None: """Applies a controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ ``` ```python # stim.TableauSimulator.cz # (in class stim.TableauSimulator) def cz( self, *targets, ) -> None: """Applies a controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ ``` ```python # stim.TableauSimulator.depolarize1 # (in class stim.TableauSimulator) def depolarize1( self, *targets: int, p: float, ): """Probabilistically applies single-qubit depolarization to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.depolarize1(0, 1, 2, p=0.01) """ ``` ```python # stim.TableauSimulator.depolarize2 # (in class stim.TableauSimulator) def depolarize2( self, *targets: int, p: float, ): """Probabilistically applies two-qubit depolarization to targets. Args: *targets: The indices of the qubits to target with the noise. The pairs of qubits are formed by zip(targets[::1], targets[1::2]). p: The chance of the error being applied, independently, to each qubit pair. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.depolarize1(0, 1, 4, 5, p=0.01) """ ``` ```python # stim.TableauSimulator.do # (in class stim.TableauSimulator) def do( self, circuit_or_pauli_string: Union[stim.Circuit, stim.PauliString, stim.CircuitInstruction, stim.CircuitRepeatBlock], ) -> None: """Applies a circuit or pauli string to the simulator's state. Args: circuit_or_pauli_string: A stim.Circuit, stim.PauliString, stim.CircuitInstruction, or stim.CircuitRepeatBlock with operations to apply to the simulator's state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do(stim.Circuit(''' ... X 0 ... M 0 ... ''')) >>> s.current_measurement_record() [True] >>> s = stim.TableauSimulator() >>> s.do(stim.PauliString("IXYZ")) >>> s.measure_many(0, 1, 2, 3) [False, True, True, False] """ ``` ```python # stim.TableauSimulator.do_circuit # (in class stim.TableauSimulator) def do_circuit( self, circuit: stim.Circuit, ) -> None: """Applies a circuit to the simulator's state. Args: circuit: A stim.Circuit containing operations to apply. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do_circuit(stim.Circuit(''' ... X 0 ... M 0 ... ''')) >>> s.current_measurement_record() [True] """ ``` ```python # stim.TableauSimulator.do_pauli_string # (in class stim.TableauSimulator) def do_pauli_string( self, pauli_string: stim.PauliString, ) -> None: """Applies the paulis from a pauli string to the simulator's state. Args: pauli_string: A stim.PauliString containing Paulis to apply. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do_pauli_string(stim.PauliString("IXYZ")) >>> s.measure_many(0, 1, 2, 3) [False, True, True, False] """ ``` ```python # stim.TableauSimulator.do_tableau # (in class stim.TableauSimulator) def do_tableau( self, tableau: stim.Tableau, targets: List[int], ) -> None: """Applies a custom tableau operation to qubits in the simulator. Note that this method has to compute the inverse of the tableau, because the simulator's internal state is an inverse tableau. Args: tableau: A stim.Tableau representing the Clifford operation to apply. targets: The indices of the qubits to operate on. Examples: >>> import stim >>> sim = stim.TableauSimulator() >>> sim.h(1) >>> sim.h_yz(2) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+X', '+Y', '+Z'] >>> rot3 = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("_X_"), ... stim.PauliString("__X"), ... stim.PauliString("X__"), ... ], ... zs=[ ... stim.PauliString("_Z_"), ... stim.PauliString("__Z"), ... stim.PauliString("Z__"), ... ], ... ) >>> sim.do_tableau(rot3, [1, 2, 3]) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+Z', '+X', '+Y'] >>> sim.do_tableau(rot3, [1, 2, 3]) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+Y', '+Z', '+X'] """ ``` ```python # stim.TableauSimulator.h # (in class stim.TableauSimulator) def h( self, *targets, ) -> None: """Applies a Hadamard gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z -Y +X """ ``` ```python # stim.TableauSimulator.h_xy # (in class stim.TableauSimulator) def h_xy( self, *targets, ) -> None: """Applies an operation that swaps the X and Y axes to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_xy(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y +X -Z """ ``` ```python # stim.TableauSimulator.h_xz # (in class stim.TableauSimulator) def h_xz( self, *targets, ) -> None: """Applies a Hadamard gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_xz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z -Y +X """ ``` ```python # stim.TableauSimulator.h_yz # (in class stim.TableauSimulator) def h_yz( self, *targets, ) -> None: """Applies an operation that swaps the Y and Z axes to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_yz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X +Z +Y """ ``` ```python # stim.TableauSimulator.iswap # (in class stim.TableauSimulator) def iswap( self, *targets, ) -> None: """Applies an ISWAP gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.iswap(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Y +Z """ ``` ```python # stim.TableauSimulator.iswap_dag # (in class stim.TableauSimulator) def iswap_dag( self, *targets, ) -> None: """Applies an ISWAP_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.iswap_dag(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ -Y +Z """ ``` ```python # stim.TableauSimulator.measure # (in class stim.TableauSimulator) def measure( self, target: int, ) -> bool: """Measures a single qubit. Unlike the other methods on TableauSimulator, this one does not broadcast over multiple targets. This is to avoid returning a list, which would create a pitfall where typing `if sim.measure(qubit)` would be a bug. To measure multiple qubits, use `TableauSimulator.measure_many`. Args: target: The index of the qubit to measure. Returns: The measurement result as a bool. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(1) >>> s.measure(0) False >>> s.measure(1) True """ ``` ```python # stim.TableauSimulator.measure_kickback # (in class stim.TableauSimulator) def measure_kickback( self, target: int, ) -> tuple: """Measures a qubit and returns the result as well as its Pauli kickback (if any). The "Pauli kickback" of a stabilizer circuit measurement is a set of Pauli operations that flip the post-measurement system state between the two possible post-measurement states. For example, consider measuring one of the qubits in the state |00>+|11> in the Z basis. If the measurement result is False, then the system projects into the state |00>. If the measurement result is True, then the system projects into the state |11>. Applying a Pauli X operation to both qubits flips between |00> and |11>. Therefore the Pauli kickback of the measurement is `stim.PauliString("XX")`. Note that there are often many possible equivalent Pauli kickbacks. For example, if in the previous example there was a third qubit in the |0> state, then both `stim.PauliString("XX_")` and `stim.PauliString("XXZ")` are valid kickbacks. Measurements with deterministic results don't have a Pauli kickback. Args: target: The index of the qubit to measure. Returns: A (result, kickback) tuple. The result is a bool containing the measurement's output. The kickback is either None (meaning the measurement was deterministic) or a stim.PauliString (meaning the measurement was random, and the operations in the Pauli string flip between the two possible post-measurement states). Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.measure_kickback(0) (False, None) >>> s.h(0) >>> s.measure_kickback(0)[1] stim.PauliString("+X") >>> def pseudo_post_select(qubit, desired_result): ... m, kick = s.measure_kickback(qubit) ... if m != desired_result: ... if kick is None: ... raise ValueError("Post-selected the impossible!") ... s.do(kick) >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.cnot(0, 2) >>> pseudo_post_select(qubit=2, desired_result=True) >>> s.measure_many(0, 1, 2) [True, True, True] """ ``` ```python # stim.TableauSimulator.measure_many # (in class stim.TableauSimulator) def measure_many( self, *targets, ) -> List[bool]: """Measures multiple qubits. Args: *targets: The indices of the qubits to measure. Returns: The measurement results as a list of bools. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(1) >>> s.measure_many(0, 1) [False, True] """ ``` ```python # stim.TableauSimulator.measure_observable # (in class stim.TableauSimulator) def measure_observable( self, observable: stim.PauliString, *, flip_probability: float = 0.0, ) -> bool: """Measures an pauli string observable, as if by an MPP instruction. Args: observable: The observable to measure, specified as a stim.PauliString. flip_probability: Probability of the recorded measurement result being flipped. Returns: The result of the measurement. The result is also recorded into the measurement record. Raises: ValueError: The given pauli string isn't Hermitian, or the given probability isn't a valid probability. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.measure_observable(stim.PauliString("XX")) False >>> s.measure_observable(stim.PauliString("YY")) True >>> s.measure_observable(stim.PauliString("-ZZ")) True """ ``` ```python # stim.TableauSimulator.num_qubits # (in class stim.TableauSimulator) @property def num_qubits( self, ) -> int: """Returns the number of qubits currently being tracked by the simulator. Note that the number of qubits being tracked will implicitly increase if qubits beyond the current limit are touched. Untracked qubits are always assumed to be in the |0> state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.num_qubits 0 >>> s.h(2) >>> s.num_qubits 3 """ ``` ```python # stim.TableauSimulator.peek_bloch # (in class stim.TableauSimulator) def peek_bloch( self, target: int, ) -> stim.PauliString: """Returns the state of the qubit as a single-qubit stim.PauliString stabilizer. This is a non-physical operation. It reports information about the qubit without disturbing it. Args: target: The qubit to peek at. Returns: stim.PauliString("I"): The qubit is entangled. Its bloch vector is x=y=z=0. stim.PauliString("+Z"): The qubit is in the |0> state. Its bloch vector is z=+1, x=y=0. stim.PauliString("-Z"): The qubit is in the |1> state. Its bloch vector is z=-1, x=y=0. stim.PauliString("+Y"): The qubit is in the |i> state. Its bloch vector is y=+1, x=z=0. stim.PauliString("-Y"): The qubit is in the |-i> state. Its bloch vector is y=-1, x=z=0. stim.PauliString("+X"): The qubit is in the |+> state. Its bloch vector is x=+1, y=z=0. stim.PauliString("-X"): The qubit is in the |-> state. Its bloch vector is x=-1, y=z=0. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_bloch(0) stim.PauliString("+Z") >>> s.x(0) >>> s.peek_bloch(0) stim.PauliString("-Z") >>> s.h(0) >>> s.peek_bloch(0) stim.PauliString("-X") >>> s.sqrt_x(1) >>> s.peek_bloch(1) stim.PauliString("-Y") >>> s.cz(0, 1) >>> s.peek_bloch(0) stim.PauliString("+_") """ ``` ```python # stim.TableauSimulator.peek_observable_expectation # (in class stim.TableauSimulator) def peek_observable_expectation( self, observable: stim.PauliString, ) -> int: """Determines the expected value of an observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: observable: The observable to determine the expected value of. This observable must have a real sign, not an imaginary sign. Returns: +1: Observable will be deterministically false when measured. -1: Observable will be deterministically true when measured. 0: Observable will be random when measured. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_observable_expectation(stim.PauliString("+Z")) 1 >>> s.peek_observable_expectation(stim.PauliString("+X")) 0 >>> s.peek_observable_expectation(stim.PauliString("-Z")) -1 >>> s.do(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''')) >>> queries = ['XX', 'YY', 'ZZ', '-ZZ', 'ZI', 'II', 'IIZ'] >>> for q in queries: ... print(q, s.peek_observable_expectation(stim.PauliString(q))) XX 1 YY -1 ZZ 1 -ZZ -1 ZI 0 II 1 IIZ 1 """ ``` ```python # stim.TableauSimulator.peek_x # (in class stim.TableauSimulator) def peek_x( self, target: int, ) -> int: """Returns the expected value of a qubit's X observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |+> state. -1: Qubit is in the |-> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_z(0) >>> s.peek_x(0) 0 >>> s.reset_x(0) >>> s.peek_x(0) 1 >>> s.z(0) >>> s.peek_x(0) -1 """ ``` ```python # stim.TableauSimulator.peek_y # (in class stim.TableauSimulator) def peek_y( self, target: int, ) -> int: """Returns the expected value of a qubit's Y observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |i> state. -1: Qubit is in the |-i> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_z(0) >>> s.peek_y(0) 0 >>> s.reset_y(0) >>> s.peek_y(0) 1 >>> s.z(0) >>> s.peek_y(0) -1 """ ``` ```python # stim.TableauSimulator.peek_z # (in class stim.TableauSimulator) def peek_z( self, target: int, ) -> int: """Returns the expected value of a qubit's Z observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |0> state. -1: Qubit is in the |1> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.peek_z(0) 0 >>> s.reset_z(0) >>> s.peek_z(0) 1 >>> s.x(0) >>> s.peek_z(0) -1 """ ``` ```python # stim.TableauSimulator.postselect_observable # (in class stim.TableauSimulator) def postselect_observable( self, observable: stim.PauliString, *, desired_value: bool = False, ) -> None: """Projects into a desired observable, or raises an exception if it was impossible. Postselecting an observable forces it to collapse to a specific eigenstate, as if it was measured and that state was the result of the measurement. Args: observable: The observable to postselect, specified as a pauli string. The pauli string's sign must be -1 or +1 (not -i or +i). desired_value: False (default): Postselect into the +1 eigenstate of the observable. True: Postselect into the -1 eigenstate of the observable. Raises: ValueError: The given observable had an imaginary sign. OR The postselection was impossible. The observable was in the opposite eigenstate, so measuring it would never ever return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.postselect_observable(stim.PauliString("+XX")) >>> s.postselect_observable(stim.PauliString("+ZZ")) >>> s.peek_observable_expectation(stim.PauliString("+YY")) -1 """ ``` ```python # stim.TableauSimulator.postselect_x # (in class stim.TableauSimulator) def postselect_x( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the X basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |+> state. True: postselect targets into the |-> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_x(0) 0 >>> s.postselect_x(0, desired_value=False) >>> s.peek_x(0) 1 >>> s.h(0) >>> s.peek_x(0) 0 >>> s.postselect_x(0, desired_value=True) >>> s.peek_x(0) -1 """ ``` ```python # stim.TableauSimulator.postselect_y # (in class stim.TableauSimulator) def postselect_y( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the Y basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |i> state. True: postselect targets into the |-i> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_y(0) 0 >>> s.postselect_y(0, desired_value=False) >>> s.peek_y(0) 1 >>> s.reset_x(0) >>> s.peek_y(0) 0 >>> s.postselect_y(0, desired_value=True) >>> s.peek_y(0) -1 """ ``` ```python # stim.TableauSimulator.postselect_z # (in class stim.TableauSimulator) def postselect_z( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the Z basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |0> state. True: postselect targets into the |1> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.peek_z(0) 0 >>> s.postselect_z(0, desired_value=False) >>> s.peek_z(0) 1 >>> s.h(0) >>> s.peek_z(0) 0 >>> s.postselect_z(0, desired_value=True) >>> s.peek_z(0) -1 """ ``` ```python # stim.TableauSimulator.reset # (in class stim.TableauSimulator) def reset( self, *targets, ) -> None: """Resets qubits to the |0> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(0) >>> s.reset(0) >>> s.peek_bloch(0) stim.PauliString("+Z") """ ``` ```python # stim.TableauSimulator.reset_x # (in class stim.TableauSimulator) def reset_x( self, *targets, ) -> None: """Resets qubits to the |+> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.peek_bloch(0) stim.PauliString("+X") """ ``` ```python # stim.TableauSimulator.reset_y # (in class stim.TableauSimulator) def reset_y( self, *targets, ) -> None: """Resets qubits to the |i> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_y(0) >>> s.peek_bloch(0) stim.PauliString("+Y") """ ``` ```python # stim.TableauSimulator.reset_z # (in class stim.TableauSimulator) def reset_z( self, *targets, ) -> None: """Resets qubits to the |0> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.reset_z(0) >>> s.peek_bloch(0) stim.PauliString("+Z") """ ``` ```python # stim.TableauSimulator.s # (in class stim.TableauSimulator) def s( self, *targets, ) -> None: """Applies a SQRT_Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.s(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y -X +Z """ ``` ```python # stim.TableauSimulator.s_dag # (in class stim.TableauSimulator) def s_dag( self, *targets, ) -> None: """Applies a SQRT_Z_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.s_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -Y +X +Z """ ``` ```python # stim.TableauSimulator.set_inverse_tableau # (in class stim.TableauSimulator) def set_inverse_tableau( self, new_inverse_tableau: stim.Tableau, ) -> None: """Overwrites the simulator's internal state with the given inverse tableau. The inverse tableau specifies how Pauli product observables of qubits at the current time transform into equivalent Pauli product observables at the beginning of time, when all qubits were in the |0> state. For example, if the Z observable on qubit 5 maps to a product of Z observables at the start of time then a Z basis measurement on qubit 5 will be deterministic and equal to the sign of the product. Whereas if it mapped to a product of observables including an X or a Y then the Z basis measurement would be random. Any qubits not within the length of the tableau are implicitly in the |0> state. Args: new_inverse_tableau: The tableau to overwrite the internal state with. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> t = stim.Tableau.random(4) >>> s.set_inverse_tableau(t) >>> s.current_inverse_tableau() == t True """ ``` ```python # stim.TableauSimulator.set_num_qubits # (in class stim.TableauSimulator) def set_num_qubits( self, new_num_qubits: int, ) -> None: """Resizes the simulator's internal state. This forces the simulator's internal state to track exactly the qubits whose indices are in `range(new_num_qubits)`. Note that untracked qubits are always assumed to be in the |0> state. Therefore, calling this method will effectively force any qubit whose index is outside `range(new_num_qubits)` to be reset to |0>. Note that this method does not prevent future operations from implicitly expanding the size of the tracked state (e.g. setting the number of qubits to 5 will not prevent a Hadamard from then being applied to qubit 100, increasing the number of qubits back to 101). Args: new_num_qubits: The length of the range of qubits the internal simulator should be tracking. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> len(s.current_inverse_tableau()) 0 >>> s.set_num_qubits(5) >>> len(s.current_inverse_tableau()) 5 >>> s.x(0, 1, 2, 3) >>> s.set_num_qubits(2) >>> s.measure_many(0, 1, 2, 3) [True, True, False, False] """ ``` ```python # stim.TableauSimulator.set_state_from_stabilizers # (in class stim.TableauSimulator) def set_state_from_stabilizers( self, stabilizers: Iterable[stim.PauliString], *, allow_redundant: bool = False, allow_underconstrained: bool = False, ) -> None: """Sets the tableau simulator's state to a state satisfying the given stabilizers. The old quantum state is completely overwritten, even if the new state is underconstrained by the given stabilizers. The number of qubits is changed to exactly match the number of qubits in the longest given stabilizer. Args: stabilizers: A list of `stim.PauliString`s specifying the stabilizers that the new state must have. It is permitted for stabilizers to have different lengths. All stabilizers are padded up to the length of the longest stabilizer by appending identity terms. allow_redundant: Defaults to False. If set to False, then the given stabilizers must all be independent. If any one of them is a product of the others (including the empty product), an exception will be raised. If set to True, then redundant stabilizers are simply ignored. allow_underconstrained: Defaults to False. If set to False, then the given stabilizers must form a complete set of generators. They must exactly specify the desired stabilizer state, with no degrees of freedom left over. For an n-qubit state there must be n independent stabilizers. If set to True, then there can be leftover degrees of freedom which can be set arbitrarily. Returns: Nothing. Mutates the states of the simulator to match the desired stabilizers. Guarantees that self.current_inverse_tableau().inverse_z_output(k) will be equal to the k'th independent stabilizer from the `stabilizers` argument. Raises: ValueError: A stabilizer is redundant but allow_redundant=True wasn't set. OR The given stabilizers are contradictory (e.g. "+Z" and "-Z" both specified). OR The given stabilizers anticommute (e.g. "+Z" and "+X" both specified). OR The stabilizers left behind a degree of freedom but allow_underconstrained=True wasn't set. OR A stabilizer has an imaginary sign (i or -i). Examples: >>> import stim >>> tab_sim = stim.TableauSimulator() >>> tab_sim.set_state_from_stabilizers([ ... stim.PauliString("XX"), ... stim.PauliString("ZZ"), ... ]) >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) >>> tab_sim.set_state_from_stabilizers([ ... stim.PauliString("XX_"), ... stim.PauliString("ZZ_"), ... stim.PauliString("-YY_"), ... stim.PauliString(""), ... ], allow_underconstrained=True, allow_redundant=True) >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z__"), stim.PauliString("+_X_"), stim.PauliString("+__X"), ], zs=[ stim.PauliString("+XX_"), stim.PauliString("+ZZ_"), stim.PauliString("+__Z"), ], ) """ ``` ```python # stim.TableauSimulator.set_state_from_state_vector # (in class stim.TableauSimulator) def set_state_from_state_vector( self, state_vector: Iterable[float], *, endian: Literal["little", "big"], ) -> None: """Sets the simulator's state to a superposition specified by an amplitude vector. Args: state_vector: A list of complex amplitudes specifying a superposition. The vector must correspond to a state that is reachable using Clifford operations, and must be normalized (i.e. it must be a unit vector). endian: "little": state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: Nothing. Mutates the states of the simulator to match the desired state. Raises: ValueError: The given state vector isn't a list of complex values specifying a stabilizer state. OR The given endian value isn't 'little' or 'big'. Examples: >>> import stim >>> tab_sim = stim.TableauSimulator() >>> tab_sim.set_state_from_state_vector([ ... 0.5**0.5, ... 0.5**0.5 * 1j, ... ], endian='little') >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+Y"), ], ) >>> tab_sim.set_state_from_state_vector([ ... 0.5**0.5, ... 0, ... 0, ... 0.5**0.5, ... ], endian='little') >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ ``` ```python # stim.TableauSimulator.sqrt_x # (in class stim.TableauSimulator) def sqrt_x( self, *targets, ) -> None: """Applies a SQRT_X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_x(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Z -Y """ ``` ```python # stim.TableauSimulator.sqrt_x_dag # (in class stim.TableauSimulator) def sqrt_x_dag( self, *targets, ) -> None: """Applies a SQRT_X_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_x_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X -Z +Y """ ``` ```python # stim.TableauSimulator.sqrt_y # (in class stim.TableauSimulator) def sqrt_y( self, *targets, ) -> None: """Applies a SQRT_Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_y(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -Z +Y +X """ ``` ```python # stim.TableauSimulator.sqrt_y_dag # (in class stim.TableauSimulator) def sqrt_y_dag( self, *targets, ) -> None: """Applies a SQRT_Y_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_y_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z +Y -X """ ``` ```python # stim.TableauSimulator.state_vector # (in class stim.TableauSimulator) def state_vector( self, *, endian: Literal["little", "big"] = 'little', ) -> np.ndarray[np.complex64]: """Returns a wavefunction for the simulator's current state. This function takes O(n * 2**n) time and O(2**n) space, where n is the number of qubits. The computation is done by initialization a random state vector and iteratively projecting it into the +1 eigenspace of each stabilizer of the state. The state is then canonicalized so that zero values are actually exactly 0, and so that the first non-zero entry is positive. Args: endian: "little" (default): state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A `numpy.ndarray[numpy.complex64]` of computational basis amplitudes. If the result is in little endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_0, the qubit with index 1 is storing the bit b_1, etc. If the result is in big endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_{n-1}, the qubit with index 1 is storing the bit b_{n-2}, etc. Examples: >>> import stim >>> import numpy as np >>> s = stim.TableauSimulator() >>> s.x(2) >>> s.state_vector(endian='little') array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> s.state_vector(endian='big') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> s.sqrt_x(1, 2) >>> s.state_vector() array([0.5+0.j , 0. +0.j , 0. -0.5j, 0. +0.j , 0. +0.5j, 0. +0.j , 0.5+0.j , 0. +0.j ], dtype=complex64) """ ``` ```python # stim.TableauSimulator.swap # (in class stim.TableauSimulator) def swap( self, *targets, ) -> None: """Applies a swap gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.swap(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +Y +X +X +Z """ ``` ```python # stim.TableauSimulator.x # (in class stim.TableauSimulator) def x( self, *targets, ) -> None: """Applies a Pauli X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.x(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X -Y -Z """ ``` ```python # stim.TableauSimulator.x_error # (in class stim.TableauSimulator) def x_error( self, *targets: int, p: float, ): """Probabilistically applies X errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the X error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x_error(0, 1, 2, p=0.01) """ ``` ```python # stim.TableauSimulator.xcx # (in class stim.TableauSimulator) def xcx( self, *targets, ) -> None: """Applies an X-controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ ``` ```python # stim.TableauSimulator.xcy # (in class stim.TableauSimulator) def xcy( self, *targets, ) -> None: """Applies an X-controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ ``` ```python # stim.TableauSimulator.xcz # (in class stim.TableauSimulator) def xcz( self, *targets, ) -> None: """Applies an X-controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ ``` ```python # stim.TableauSimulator.y # (in class stim.TableauSimulator) def y( self, *targets, ) -> None: """Applies a Pauli Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.y(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X +Y -Z """ ``` ```python # stim.TableauSimulator.y_error # (in class stim.TableauSimulator) def y_error( self, *targets: int, p: float, ): """Probabilistically applies Y errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the Y error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.y_error(0, 1, 2, p=0.01) """ ``` ```python # stim.TableauSimulator.ycx # (in class stim.TableauSimulator) def ycx( self, *targets, ) -> None: """Applies a Y-controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ ``` ```python # stim.TableauSimulator.ycy # (in class stim.TableauSimulator) def ycy( self, *targets, ) -> None: """Applies a Y-controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ ``` ```python # stim.TableauSimulator.ycz # (in class stim.TableauSimulator) def ycz( self, *targets, ) -> None: """Applies a Y-controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +_ +_ """ ``` ```python # stim.TableauSimulator.z # (in class stim.TableauSimulator) def z( self, *targets, ) -> None: """Applies a Pauli Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.z(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X -Y +Z """ ``` ```python # stim.TableauSimulator.z_error # (in class stim.TableauSimulator) def z_error( self, *targets: int, p: float, ): """Probabilistically applies Z errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the Z error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.z_error(0, 1, 2, p=0.01) """ ``` ```python # stim.TableauSimulator.zcx # (in class stim.TableauSimulator) def zcx( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ ``` ```python # stim.TableauSimulator.zcy # (in class stim.TableauSimulator) def zcy( self, *targets, ) -> None: """Applies a controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ ``` ```python # stim.TableauSimulator.zcz # (in class stim.TableauSimulator) def zcz( self, *targets, ) -> None: """Applies a controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ ``` ```python # stim.gate_data # (at top-level in the stim module) @overload def gate_data( name: str, ) -> stim.GateData: pass @overload def gate_data( ) -> Dict[str, stim.GateData]: pass def gate_data( name: Optional[str] = None, ) -> Union[str, Dict[str, stim.GateData]]: """Returns gate data for the given named gate, or all gates. Examples: >>> import stim >>> stim.gate_data('cnot').aliases ['CNOT', 'CX', 'ZCX'] >>> stim.gate_data('cnot').is_two_qubit_gate True >>> gate_dict = stim.gate_data() >>> len(gate_dict) > 50 True >>> gate_dict['MX'].produces_measurements True """ ``` ```python # stim.main # (at top-level in the stim module) def main( *, command_line_args: List[str], ) -> int: """Runs the command line tool version of stim on the given arguments. Note that by default any input will be read from stdin, any output will print to stdout (as opposed to being intercepted). For most commands, you can use arguments like `--out` to write to a file instead of stdout and `--in` to read from a file instead of stdin. Returns: An exit code (0 means success, not zero means failure). Raises: A large variety of errors, depending on what you are doing and how it failed! Beware that many errors are caught by the main method itself and printed to stderr, with the only indication that something went wrong being the return code. Example: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f'{d}/tmp.out' ... return_code = stim.main(command_line_args=[ ... "gen", ... "--code=repetition_code", ... "--task=memory", ... "--rounds=1000", ... "--distance=2", ... "--out", ... path, ... ]) ... assert return_code == 0 ... with open(path) as f: ... print(f.read(), end='') # Generated repetition_code circuit. # task: memory # rounds: 1000 # distance: 2 # before_round_data_depolarization: 0 # before_measure_flip_probability: 0 # after_reset_flip_probability: 0 # after_clifford_depolarization: 0 # layout: # L0 Z1 d2 # Legend: # d# = data qubit # L# = data qubit with logical observable crossing # Z# = measurement qubit R 0 1 2 TICK CX 0 1 TICK CX 2 1 TICK MR 1 DETECTOR(1, 0) rec[-1] REPEAT 999 { TICK CX 0 1 TICK CX 2 1 TICK MR 1 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-1] rec[-2] } M 0 2 DETECTOR(1, 1) rec[-1] rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] """ ``` ```python # stim.read_shot_data_file # (at top-level in the stim module) @overload def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, ) -> np.ndarray: pass @overload def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, separate_observables: Literal[True], ) -> Tuple[np.ndarray, np.ndarray]: pass def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, separate_observables: bool = False, ) -> Union[Tuple[np.ndarray, np.ndarray], np.ndarray]: """Reads shot data, such as measurement samples, from a file. Args: path: The path to the file to read the data from. format: The format that the data is stored in, such as 'b8'. See https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md bit_packed: Defaults to false. Determines whether the result is a bool_ numpy array with one bit per byte, or a uint8 numpy array with 8 bits per byte. num_measurements: How many measurements there are per shot. num_detectors: How many detectors there are per shot. num_observables: How many observables there are per shot. Note that this only refers to observables *stored in the file*, not to observables from the original circuit that was sampled. separate_observables: When set to True, the result is a tuple of two arrays, one containing the detection event data and the other containing the observable data, instead of a single array. Returns: If separate_observables=True: A tuple (dets, obs) of numpy arrays containing the loaded data. If bit_packed=False: dets.dtype = np.bool_ dets.shape = (num_shots, num_measurements + num_detectors) det bit b from shot s is at dets[s, b] obs.dtype = np.bool_ obs.shape = (num_shots, num_observables) obs bit b from shot s is at dets[s, b] If bit_packed=True: dets.dtype = np.uint8 dets.shape = (num_shots, math.ceil( (num_measurements + num_detectors) / 8)) obs.dtype = np.uint8 obs.shape = (num_shots, math.ceil(num_observables / 8)) det bit b from shot s is at dets[s, b // 8] & (1 << (b % 8)) obs bit b from shot s is at obs[s, b // 8] & (1 << (b % 8)) If separate_observables=False: A numpy array containing the loaded data. If bit_packed=False: dtype = np.bool_ shape = (num_shots, num_measurements + num_detectors + num_observables) bit b from shot s is at result[s, b] If bit_packed=True: dtype = np.uint8 shape = (num_shots, math.ceil( (num_measurements + num_detectors + num_observables) / 8)) bit b from shot s is at result[s, b // 8] & (1 << (b % 8)) Examples: >>> import stim >>> import pathlib >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = pathlib.Path(d) / 'shots' ... with open(path, 'w') as f: ... print("0000", file=f) ... print("0101", file=f) ... ... read = stim.read_shot_data_file( ... path=str(path), ... format='01', ... num_measurements=4) >>> read array([[False, False, False, False], [False, True, False, True]]) """ ``` ```python # stim.target_combined_paulis # (at top-level in the stim module) def target_combined_paulis( paulis: Union[stim.PauliString, List[stim.GateTarget]], invert: bool = False, ) -> stim.GateTarget: """Returns a list of targets encoding a pauli product for instructions like MPP. Args: paulis: The paulis to encode into the targets. This can be a `stim.PauliString` or a list of pauli targets from `stim.target_x`, `stim.target_pauli`, etc. invert: Defaults to False. If True, the product is inverted (like "!X2*Y3"). Note that this is in addition to any inversions specified by the `paulis` argument. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... *stim.target_combined_paulis(stim.PauliString("-XYZ")), ... *stim.target_combined_paulis([stim.target_x(2), stim.target_y(5)]), ... *stim.target_combined_paulis([stim.target_z(9)], invert=True), ... ]) >>> circuit stim.Circuit(''' MPP !X0*Y1*Z2 X2*Y5 !Z9 ''') """ ``` ```python # stim.target_combiner # (at top-level in the stim module) def target_combiner( ) -> stim.GateTarget: """Returns a target combiner that can be used to build Pauli products. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*Y3*Z5 ''') """ ``` ```python # stim.target_inv # (at top-level in the stim module) def target_inv( qubit_index: Union[int, stim.GateTarget], ) -> stim.GateTarget: """Returns a target flagged as inverted. Inverted targets are used to indicate measurement results should be flipped. Args: qubit_index: The underlying qubit index of the inverted target. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("M", [2, stim.target_inv(3)]) >>> circuit stim.Circuit(''' M 2 !3 ''') For example, the '!1' in 'M 0 !1 2' is qubit 1 flagged as inverted, meaning the measurement result from qubit 1 should be inverted when reported. """ ``` ```python # stim.target_logical_observable_id # (at top-level in the stim module) def target_logical_observable_id( index: int, ) -> stim.DemTarget: """Returns a logical observable id identifying a frame change. Args: index: The index of the observable. Returns: The logical observable target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_logical_observable_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) L13 ''') """ ``` ```python # stim.target_pauli # (at top-level in the stim module) def target_pauli( qubit_index: int, pauli: Union[str, int], invert: bool = False, ) -> stim.GateTarget: """Returns a pauli target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. pauli: The pauli gate to use. This can either be a string identifying the pauli by name ("x", "X", "y", "Y", "z", or "Z") or an integer following the convention (1=X, 2=Y, 3=Z). Setting this argument to "I" or to 0 will return a qubit target instead of a pauli target. invert: Defaults to False. If True, the target is inverted (like "!X10"), indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_pauli(2, "X"), ... stim.target_combiner(), ... stim.target_pauli(3, "y", invert=True), ... stim.target_pauli(5, 3), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3 Z5 ''') >>> circuit.append("M", [ ... stim.target_pauli(7, "I"), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3 Z5 M 7 ''') """ ``` ```python # stim.target_rec # (at top-level in the stim module) def target_rec( lookback_index: int, ) -> stim.GateTarget: """Returns a measurement record target with the given lookback. Measurement record targets are used to refer back to the measurement record; the list of measurements that have been performed so far. Measurement record targets always specify an index relative to the *end* of the measurement record. The latest measurement is `stim.target_rec(-1)`, the next most recent measurement is `stim.target_rec(-2)`, and so forth. Indexing is done this way in order to make it possible to write loops. Args: lookback_index: A negative integer indicating how far to look back, relative to the end of the measurement record. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("M", [5, 7, 11]) >>> circuit.append("CX", [stim.target_rec(-2), 3]) >>> circuit stim.Circuit(''' M 5 7 11 CX rec[-2] 3 ''') """ ``` ```python # stim.target_relative_detector_id # (at top-level in the stim module) def target_relative_detector_id( index: int, ) -> stim.DemTarget: """Returns a relative detector id (e.g. "D5" in a .dem file). Args: index: The index of the detector, relative to the current detector offset. Returns: The relative detector target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_relative_detector_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D13 ''') """ ``` ```python # stim.target_separator # (at top-level in the stim module) def target_separator( ) -> stim.DemTarget: """Returns a target separator (e.g. "^" in a .dem file). Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_relative_detector_id(1), ... stim.target_separator(), ... stim.target_relative_detector_id(2), ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D1 ^ D2 ''') """ ``` ```python # stim.target_sweep_bit # (at top-level in the stim module) def target_sweep_bit( sweep_bit_index: int, ) -> stim.GateTarget: """Returns a sweep bit target that can be passed into `stim.Circuit.append`. Args: sweep_bit_index: The index of the sweep bit to target. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("CX", [stim.target_sweep_bit(2), 5]) >>> circuit stim.Circuit(''' CX sweep[2] 5 ''') """ ``` ```python # stim.target_x # (at top-level in the stim module) def target_x( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli X target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ ``` ```python # stim.target_y # (at top-level in the stim module) def target_y( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli Y target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ ``` ```python # stim.target_z # (at top-level in the stim module) def target_z( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli Z target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ ``` ```python # stim.write_shot_data_file # (at top-level in the stim module) def write_shot_data_file( *, data: np.ndarray, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, ) -> None: """Writes shot data, such as measurement samples, to a file. Args: data: The data to write to the file. This must be a numpy array. The dtype of the array determines whether or not the data is bit packed, and the shape must match the bits per shot. dtype=np.bool_: Not bit packed. Shape must be (num_shots, num_measurements + num_detectors + num_observables). dtype=np.uint8: Yes bit packed. Shape must be (num_shots, math.ceil( (num_measurements + num_detectors + num_observables) / 8)). path: The path to the file to write the data to. format: The format that the data is stored in, such as 'b8'. See https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md num_measurements: How many measurements there are per shot. num_detectors: How many detectors there are per shot. num_observables: How many observables there are per shot. Note that this only refers to observables *in the given shot data*, not to observables from the original circuit that was sampled. Examples: >>> import stim >>> import pathlib >>> import tempfile >>> import numpy as np >>> with tempfile.TemporaryDirectory() as d: ... path = pathlib.Path(d) / 'shots' ... shot_data = np.array([ ... [0, 1, 0], ... [0, 1, 1], ... ], dtype=np.bool_) ... ... stim.write_shot_data_file( ... path=str(path), ... data=shot_data, ... format='01', ... num_measurements=3) ... ... with open(path) as f: ... written = f.read() >>> written '010\n011\n' """ ``` ================================================ FILE: doc/result_formats.md ================================================ # Introduction A *result format* is a way of representing bits from shots sampled from a circuit. It is some way of converting between a list-of-list-of-bits (a list-of-shots) and a flat string of bytes or characters. Generally, the result data formats supported by Stim are extremely minimalist. They do not contain metadata about which circuit was run, how many shots were taken, how many bits are in each shot, or even self-identifying information like a header with magic bytes. They produce *raw* data. Even details about which bits are measurements, which are detection events, and which are observable frame changes must be determined from context. The major driver for having multiple formats is context-dependent preferences for binary-vs-human-readable and dense-vs-sparse. For example, '`01`' is a dense text format and '`r8`' is a sparse binary format. Sometimes you want to be able to eyeball your data, so you want a text format. Other times you want maximum efficiency, so you want a binary format. Sometimes your data is high entropy, with as many 1s as 0s, so you use a dense format. Other times the data is highly biased, with 1s being much rarer and more interesting than 0s, so you use a sparse format. # Index - [The **01** Format](#01) - [The **b8** Format](#b8) - [The **dets** Format](#dets) - [The **hits** Format](#hits) - [The **ptb64** Format](#ptb64) - [The **r8** Format](#r8) # The `01` Format The 01 format is a dense human readable format that stores shots as lines of '0' and '1' characters. The data from each shot is terminated by a newline character '\n'. Each character in the line is a '0' (indicating False) or a '1' (indicating True) corresponding to a measurement result (or a detector result) from a circuit. This is the default format used by Stim, because it's the easiest to understand. *Example of producing 01 format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="01") ... with open(path) as f: ... print(f.read().strip()) 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 *Example 01 parsing code (python)*: ```python from typing import List def parse_01(data: str) -> List[List[bool]]: shots = [] for line in data.split('\n'): if not line: continue shot = [] for c in line: assert c in '01' shot.append(c == '1') shots.append(shot) return shots ``` *Example 01 saving code (python):* ```python from typing import List def save_01(shots: List[List[bool]]) -> str: output = "" for shot in shots: for sample in shot: output += '1' if sample else '0' output += "\n" return output ``` # The `b8` Format The b8 format is a dense binary format that stores shots as bit-packed bytes. Each shot is stored into ceil(n / 8) bytes, where n is the number of bits in the shot. Effectively, each shot is padded up to a multiple of 8 by appending False bits, so that shots always start on a byte boundary. Bits are packed into bytes in significance order (the 1s bit is the first bit, the 2s bit is the second, the 4s bit is the third, and so forth until the 128s bit which is the eighth bit). This format requires the reader to know the number of bits in each shot. *Example of producing b8 format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="b8") ... with open(path, 'rb') as f: ... print(' '.join(hex(e)[2:] for e in f.read())) f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c *Example b8 parsing code (python)*: ```python from typing import List def parse_b8(data: bytes, bits_per_shot: int) -> List[List[bool]]: shots = [] bytes_per_shot = (bits_per_shot + 7) // 8 for offset in range(0, len(data), bytes_per_shot): shot = [] for k in range(bits_per_shot): byte = data[offset + k // 8] bit = (byte >> (k % 8)) % 2 == 1 shot.append(bit) shots.append(shot) return shots ``` *Example b8 saving code (python):* ```python from typing import List def save_b8(shots: List[List[bool]]) -> bytes: output = b"" for shot in shots: bytes_per_shot = (len(shot) + 7) // 8 v = 0 for b in reversed(shot): v <<= 1 v += int(b) output += v.to_bytes(bytes_per_shot, 'little') return output ``` # The `dets` Format The dets format is a sparse human readable format that stores shots as lines starting with the word 'shot' and then containing space-separated prefixed values like 'D5' and 'L2'. Each value's prefix indicates whether it is a measurement (M), a detector (D), or observable frame change (L) and its integer indicates that the corresponding measurement/detection-event/frame-change was True instead of False. The data from each shot is started with the text 'shot' and terminated by a newline character '\n'. The rest of the line is a series of integers, separated by spaces and prefixed by a single letter. The prefix letter indicates the type of value ('M' for measurement, 'D' for detector, and 'L' for logical observable). The integer indicates the index of the value. For example, "D1 D3 L0" indicates detectors 1 and 3 fired, and logical observable 0 was flipped. This format requires the reader to know the number of measurements/detectors/observables in each shot, if the reader wants to produce vectors of bits instead of sets. *Example of producing dets format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 0 1 ... """).compile_sampler().sample_write(shots=3, filepath=path, format="dets") ... with open(path) as f: ... print(f.read().strip()) shot M4 M5 M6 M7 M10 M11 M13 M15 shot M4 M5 M6 M7 M10 M11 M13 M15 shot M4 M5 M6 M7 M10 M11 M13 M15 >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X_ERROR(1) 1 ... M 0 1 2 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... DETECTOR rec[-3] ... OBSERVABLE_INCLUDE(5) rec[-2] ... """).compile_detector_sampler().sample_write(shots=2, filepath=path, format="dets", append_observables=True) ... with open(path) as f: ... print(f.read().strip()) shot D1 L5 shot D1 L5 *Example dets parsing code (python)*: ```python from typing import List def parse_dets(data: str, num_detectors: int, num_observables: int) -> List[List[bool]]: shots = [] for line in data.split('\n'): if not line.strip(): continue assert line.startswith('shot') line = line[4:].strip() shot = [False] * (num_detectors + num_observables) if line: for term in line.split(' '): c = term[0] v = int(term[1:]) if c == 'D': assert 0 <= v < num_detectors shot[v] = True elif c == 'L': assert 0 <= v < num_observables shot[num_detectors + v] = True else: raise NotImplementedError(c) shots.append(shot) return shots ``` *Example dets saving code (python):* ```python from typing import List def save_dets(shots: List[List[bool]], num_detectors: int, num_observables: int): output = "" for shot in shots: assert len(shot) == num_detectors + num_observables detectors = shot[:num_detectors] observables = shot[num_detectors:] output += "shot" for k in range(num_detectors): if shot[k]: output += " D" + str(k) for k in range(num_observables): if shot[num_detectors + k]: output += " L" + str(k) output += "\n" return output ``` # The `hits` Format The hits format is a dense human readable format that stores shots as a comma-separated list of integers. Each integer indicates the position of a bit that was True. Positions that aren't mentioned had bits that were False. The data from each shot is terminated by a newline character '\n'. The line is a series of non-negative integers separated by commas, with each integer indicating a bit from the shot that was true. This format requires the reader to know the number of bits in each shot (if they want to get a list instead of a set). This format requires the reader to know how many trailing newlines, that don't correspond to shots with no hit, are in the text data. This format is useful in contexts where the number of set bits is expected to be low, e.g. when sampling detection events. *Example of producing hits format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="hits") ... with open(path) as f: ... print(f.read().strip()) 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 *Example hits parsing code (python)*: ```python from typing import List def parse_hits(data: str, bits_per_shot: int) -> List[List[bool]]: shots = [] if data.endswith('\n'): data = data[:-1] for line in data.split('\n'): shot = [False] * bits_per_shot if line: for term in line.split(','): shot[int(term)] = True shots.append(shot) return shots ``` *Example hits saving code (python):* ```python from typing import List def save_hits(shots: List[List[bool]]) -> str: output = "" for shot in shots: output += ",".join(str(index) for index, bit in enumerate(shot) if bit) + "\n" return output ``` # The `ptb64` Format The ptb64 format is a dense SIMD-focused binary format that stores shots as partially transposed bit-packed data. Each 64 bit word (8 bytes) of the data contains bits from the same measurement result across 64 separate shots. The next 8 bytes contain bits for the next measurement result from the 64 separate shots. This continues until the 8 bytes containing the bits from the last measurement result, and then starts over again with data from the next 64 shots (if there are more). The shots are stored by byte order then significance order. The first shot's data goes into the least significant bit of the first byte of each 8 byte group. This format requires the number of shots to be a multiple of 64. This format requires the reader to know the number of shots that were taken. This format is generally more tedious to work with, but useful for achieving good performance on data processing tasks where it is possible to parallelize across shots using SIMD instructions. *Example of producing ptb64 format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 1 ... """).compile_sampler().sample_write(shots=64, filepath=path, format="ptb64") ... with open(path, 'rb') as f: ... print(' '.join(hex(e)[2:] for e in f.read())) 0 0 0 0 0 0 0 0 ff ff ff ff ff ff ff ff *Example ptb64 parsing code (python)*: ```python from typing import List def parse_ptb64(data: bytes, bits_per_shot: int) -> List[List[bool]]: num_shot_groups = int(len(data) * 8 / bits_per_shot / 64) if len(data) * 8 != num_shot_groups * 64 * bits_per_shot: raise ValueError("Number of shots must be a multiple of 64.") result = [[False] * bits_per_shot for _ in range(num_shot_groups * 64)] for group_index in range(num_shot_groups): group_bit_offset = 64 * bits_per_shot * group_index for m in range(bits_per_shot): m_bit_offset = m * 64 for shot in range(64): bit_offset = group_bit_offset + m_bit_offset + shot bit = data[bit_offset // 8] & (1 << (bit_offset % 8)) != 0 s = group_index * 64 + shot result[s][m] = bit return result ``` *Example ptb64 saving code (python):* ```python from typing import List def save_ptb64(shots: List[List[bool]]): if len(shots) % 64 != 0: raise ValueError("Number of shots must be a multiple of 64.") output = [] for shot_offset in range(0, len(shots), 64): bits_per_shot = len(shots[0]) for measure_index in range(bits_per_shot): v = 0 for k in range(64)[::-1]: v <<= 1 v += int(shots[shot_offset + k][measure_index]) output.append(v.to_bytes(8, 'little')) return b''.join(output) ``` # The `r8` Format The r8 format is a sparse binary format that stores shots as a series of lengths of runs between 1s. Each byte in the data indicates how many False bits there are before the next True bit. The maximum byte value (255) is special; it indicates to include 255 False bits but not follow them by a True bit. A shot always has a terminating True bit appended to it before encoding. Decoding of the shot ends (and the next shot begin) when this True bit just past the end of the shot data is reached. This format requires the reader to know the number of bits in each shot. This format is useful in contexts where the number of set bits is expected to be low, e.g. when sampling detection events. *Example of producing r8 format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="r8") ... with open(path, 'rb') as f: ... print(' '.join(hex(e)[2:] for e in f.read())) 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="r8") ... with open(path, 'rb') as f: ... print(' '.join(hex(e)[2:] for e in f.read())) 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f *Example r8 parsing code (python)*: ```python from typing import List def parse_r8(data: bytes, bits_per_shot: int) -> List[List[bool]]: shots = [] shot = [] for byte in data: shot += [False] * byte if byte != 255: shot.append(True) if len(shot) > bits_per_shot: assert len(shot) == bits_per_shot + 1 and shot[-1] shot.pop() shots.append(shot) shot = [] assert len(shot) == 0 return shots ``` *Example r8 saving code (python):* ```python from typing import List def save_r8(shots: List[List[bool]]) -> bytes: output = [] for shot in shots: gap = 0 for b in list(shot) + [True]: if b: while gap >= 255: gap -= 255 output.append((255).to_bytes(1, 'big')) output.append(gap.to_bytes(1, 'big')) gap = 0 else: gap += 1 return b''.join(output) ``` ================================================ FILE: doc/sinter_api.md ================================================ # Sinter (Development Version) API Reference *CAUTION*: this API reference is for the in-development version of sinter. Methods and arguments mentioned here may not be accessible in stable versions, yet. API references for stable versions are kept on the [stim github wiki](https://github.com/quantumlib/Stim/wiki) ## Index - [`sinter.AnonTaskStats`](#sinter.AnonTaskStats) - [`sinter.AnonTaskStats.__add__`](#sinter.AnonTaskStats.__add__) - [`sinter.CSV_HEADER`](#sinter.CSV_HEADER) - [`sinter.CollectionOptions`](#sinter.CollectionOptions) - [`sinter.CollectionOptions.combine`](#sinter.CollectionOptions.combine) - [`sinter.CompiledDecoder`](#sinter.CompiledDecoder) - [`sinter.CompiledDecoder.decode_shots_bit_packed`](#sinter.CompiledDecoder.decode_shots_bit_packed) - [`sinter.CompiledSampler`](#sinter.CompiledSampler) - [`sinter.CompiledSampler.handles_throttling`](#sinter.CompiledSampler.handles_throttling) - [`sinter.CompiledSampler.sample`](#sinter.CompiledSampler.sample) - [`sinter.Decoder`](#sinter.Decoder) - [`sinter.Decoder.compile_decoder_for_dem`](#sinter.Decoder.compile_decoder_for_dem) - [`sinter.Decoder.decode_via_files`](#sinter.Decoder.decode_via_files) - [`sinter.Fit`](#sinter.Fit) - [`sinter.Progress`](#sinter.Progress) - [`sinter.Sampler`](#sinter.Sampler) - [`sinter.Sampler.compiled_sampler_for_task`](#sinter.Sampler.compiled_sampler_for_task) - [`sinter.Task`](#sinter.Task) - [`sinter.Task.__init__`](#sinter.Task.__init__) - [`sinter.Task.strong_id`](#sinter.Task.strong_id) - [`sinter.Task.strong_id_bytes`](#sinter.Task.strong_id_bytes) - [`sinter.Task.strong_id_text`](#sinter.Task.strong_id_text) - [`sinter.Task.strong_id_value`](#sinter.Task.strong_id_value) - [`sinter.TaskStats`](#sinter.TaskStats) - [`sinter.TaskStats.to_anon_stats`](#sinter.TaskStats.to_anon_stats) - [`sinter.TaskStats.to_csv_line`](#sinter.TaskStats.to_csv_line) - [`sinter.TaskStats.with_edits`](#sinter.TaskStats.with_edits) - [`sinter.better_sorted_str_terms`](#sinter.better_sorted_str_terms) - [`sinter.collect`](#sinter.collect) - [`sinter.comma_separated_key_values`](#sinter.comma_separated_key_values) - [`sinter.fit_binomial`](#sinter.fit_binomial) - [`sinter.fit_line_slope`](#sinter.fit_line_slope) - [`sinter.fit_line_y_at_x`](#sinter.fit_line_y_at_x) - [`sinter.group_by`](#sinter.group_by) - [`sinter.iter_collect`](#sinter.iter_collect) - [`sinter.log_binomial`](#sinter.log_binomial) - [`sinter.log_factorial`](#sinter.log_factorial) - [`sinter.plot_custom`](#sinter.plot_custom) - [`sinter.plot_discard_rate`](#sinter.plot_discard_rate) - [`sinter.plot_error_rate`](#sinter.plot_error_rate) - [`sinter.post_selection_mask_from_4th_coord`](#sinter.post_selection_mask_from_4th_coord) - [`sinter.predict_discards_bit_packed`](#sinter.predict_discards_bit_packed) - [`sinter.predict_observables`](#sinter.predict_observables) - [`sinter.predict_observables_bit_packed`](#sinter.predict_observables_bit_packed) - [`sinter.predict_on_disk`](#sinter.predict_on_disk) - [`sinter.read_stats_from_csv_files`](#sinter.read_stats_from_csv_files) - [`sinter.shot_error_rate_to_piece_error_rate`](#sinter.shot_error_rate_to_piece_error_rate) - [`sinter.stats_from_csv_files`](#sinter.stats_from_csv_files) ```python # Types used by the method definitions. from typing import overload, TYPE_CHECKING, Any, Counter, Dict, Iterable, List, Optional, Tuple, Union import abc import dataclasses import io import numpy as np import pathlib import stim ``` ```python # sinter.AnonTaskStats # (at top-level in the sinter module) @dataclasses.dataclass(frozen=True) class AnonTaskStats: """Statistics sampled from an unspecified task. Attributes: shots: Number of times the task was sampled. errors: Number of times a sample resulted in an error. discards: Number of times a sample resulted in a discard. Note that discarded a task is not an error. seconds: The amount of CPU core time spent sampling the tasks, in seconds. custom_counts: A counter mapping string keys to integer values. Used for tracking arbitrary values, such as per-observable error counts or the number of times detectors fired. The meaning of the information in the counts is not specified; the only requirement is that it should be correct to add each key's counts when merging statistics. Although this field is an editable object, it's invalid to edit the counter after the stats object is initialized. """ shots: int = 0 errors: int = 0 discards: int = 0 seconds: float = 0 custom_counts: Counter[str] ``` ```python # sinter.AnonTaskStats.__add__ # (in class sinter.AnonTaskStats) def __add__( self, other: sinter.AnonTaskStats, ) -> sinter.AnonTaskStats: """Returns the sum of the statistics from both anonymous stats. Adds the shots, the errors, the discards, and the seconds. Examples: >>> import sinter >>> a = sinter.AnonTaskStats( ... shots=100, ... errors=20, ... ) >>> b = sinter.AnonTaskStats( ... shots=1000, ... errors=200, ... ) >>> a + b sinter.AnonTaskStats(shots=1100, errors=220) """ ``` ```python # sinter.CSV_HEADER # (at top-level in the sinter module) CSV_HEADER: str = ' shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts' ``` ```python # sinter.CollectionOptions # (at top-level in the sinter module) @dataclasses.dataclass(frozen=True) class CollectionOptions: """Describes options for how data is collected for a decoding problem. Attributes: max_shots: Defaults to None (unused). Stops the sampling process after this many samples have been taken from the circuit. max_errors: Defaults to None (unused). Stops the sampling process after this many errors have been seen in samples taken from the circuit. The actual number sampled errors may be larger due to batching. start_batch_size: Defaults to None (collector's choice). The very first shots taken from the circuit will use a batch of this size, and no other batches will be taken in parallel. Once this initial fact finding batch is done, batches can be taken in parallel and the normal batch size limiting processes take over. max_batch_size: Defaults to None (unused). Limits batches from taking more than this many shots at once. For example, this can be used to ensure memory usage stays below some limit. max_batch_seconds: Defaults to None (unused). When set, the recorded data from previous shots is used to estimate how much time is taken per shot. This information is then used to predict the biggest batch size that can finish in under the given number of seconds. Limits each batch to be no larger than that. """ max_shots: Optional[int] = None max_errors: Optional[int] = None start_batch_size: Optional[int] = None max_batch_size: Optional[int] = None max_batch_seconds: Optional[float] = None ``` ```python # sinter.CollectionOptions.combine # (in class sinter.CollectionOptions) def combine( self, other: sinter.CollectionOptions, ) -> sinter.CollectionOptions: """Returns a combination of multiple collection options. All fields are combined by taking the minimum from both collection options objects, with None treated as being infinitely large. Args: other: The collections options to combine with. Returns: The combined collection options. Examples: >>> import sinter >>> a = sinter.CollectionOptions( ... max_shots=1_000_000, ... start_batch_size=100, ... ) >>> b = sinter.CollectionOptions( ... max_shots=100_000, ... max_errors=100, ... ) >>> a.combine(b) sinter.CollectionOptions(max_shots=100000, max_errors=100, start_batch_size=100) """ ``` ```python # sinter.CompiledDecoder # (at top-level in the sinter module) class CompiledDecoder(metaclass=abc.ABCMeta): """Abstract class for decoders preconfigured to a specific decoding task. This is the type returned by `sinter.Decoder.compile_decoder_for_dem`. The idea is that, when many shots of the same decoding task are going to be performed, it is valuable to pay the cost of configuring the decoder only once instead of once per batch of shots. Custom decoders can optionally implement that method, and return this type, to increase sampling efficiency. """ ``` ```python # sinter.CompiledDecoder.decode_shots_bit_packed # (in class sinter.CompiledDecoder) @abc.abstractmethod def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: np.ndarray, ) -> np.ndarray: """Predicts observable flips from the given detection events. All data taken and returned must be bit packed with bitorder='little'. Args: bit_packed_detection_event_data: Detection event data stored as a bit packed numpy array. The numpy array will have the following dtype/shape: dtype: uint8 shape: (num_shots, ceil(dem.num_detectors / 8)) where `num_shots` is the number of shots to decoder and `dem` is the detector error model this instance was compiled to decode. It's guaranteed that the data will be laid out in memory so that detection events within a shot are contiguous in memory (i.e. that bit_packed_detection_event_data.strides[1] == 1). Returns: Bit packed observable flip data stored as a bit packed numpy array. The numpy array must have the following dtype/shape: dtype: uint8 shape: (num_shots, ceil(dem.num_observables / 8)) where `num_shots` is bit_packed_detection_event_data.shape[0] and `dem` is the detector error model this instance was compiled to decode. """ ``` ```python # sinter.CompiledSampler # (at top-level in the sinter module) class CompiledSampler(metaclass=abc.ABCMeta): """A sampler that has been configured for efficiently sampling some task. """ ``` ```python # sinter.CompiledSampler.handles_throttling # (in class sinter.CompiledSampler) def handles_throttling( self, ) -> bool: """Return True to disable sinter wrapping samplers with throttling. By default, sinter will wrap samplers so that they initially only do a small number of shots then slowly ramp up. Sometimes this behavior is not desired (e.g. in unit tests). Override this method to return True to disable it. """ ``` ```python # sinter.CompiledSampler.sample # (in class sinter.CompiledSampler) @abc.abstractmethod def sample( self, suggested_shots: int, ) -> sinter.AnonTaskStats: """Samples shots and returns statistics. Args: suggested_shots: The number of shots being requested. The sampler may perform more shots or fewer shots than this, so technically this argument can just be ignored. If a sampler is optimized for a specific batch size, it can simply return one batch per call regardless of this parameter. However, this parameter is a useful hint about the amount of work being done. The sampler can use this to optimize its behavior. For example, it could adjust its batch size downward if the suggested shots is very small. Whereas if the suggested shots is very high, the sampler should focus entirely on achieving the best possible throughput. Note that, in typical workloads, the sampler will be called repeatedly with the same value of suggested_shots. Therefore it is reasonable to allocate buffers sized to accomodate the current suggested_shots, expecting them to be useful again for the next shot. Returns: A sinter.AnonTaskStats saying how many shots were actually taken, how many errors were seen, etc. The returned stats must have at least one shot. """ ``` ```python # sinter.Decoder # (at top-level in the sinter module) class Decoder: """Abstract base class for custom decoders. Custom decoders can be explained to sinter by inheriting from this class and implementing its methods. Decoder classes MUST be serializable (e.g. via pickling), so that they can be given to worker processes when using python multiprocessing. Child classes should implement `compile_decoder_for_dem`, but (for legacy reasons) can alternatively implement `decode_via_files`. At least one of the two methods must be implemented. """ ``` ```python # sinter.Decoder.compile_decoder_for_dem # (in class sinter.Decoder) def compile_decoder_for_dem( self, *, dem: stim.DetectorErrorModel, ) -> sinter.CompiledDecoder: """Creates a decoder preconfigured for the given detector error model. This method is optional to implement. By default, it will raise a NotImplementedError. When sampling, sinter will attempt to use this method first and otherwise fallback to using `decode_via_files`. The idea is that the preconfigured decoder amortizes the cost of configuration over more calls. This makes smaller batch sizes efficient, reducing the amount of memory used for storing each batch, improving overall efficiency. Args: dem: A detector error model for the samples that will need to be decoded. What to configure the decoder to decode. Returns: An instance of `sinter.CompiledDecoder` that can be used to invoke the preconfigured decoder. Raises: NotImplementedError: This `sinter.Decoder` doesn't support compiling for a dem. """ ``` ```python # sinter.Decoder.decode_via_files # (in class sinter.Decoder) def decode_via_files( self, *, num_shots: int, num_dets: int, num_obs: int, dem_path: pathlib.Path, dets_b8_in_path: pathlib.Path, obs_predictions_b8_out_path: pathlib.Path, tmp_dir: pathlib.Path, ) -> None: """Performs decoding by reading/writing problems and answers from disk. Args: num_shots: The number of times the circuit was sampled. The number of problems to be solved. num_dets: The number of detectors in the circuit. The number of detection event bits in each shot. num_obs: The number of observables in the circuit. The number of predicted bits in each shot. dem_path: The file path where the detector error model should be read from, e.g. using `stim.DetectorErrorModel.from_file`. The error mechanisms specified by the detector error model should be used to configure the decoder. dets_b8_in_path: The file path that detection event data should be read from. Note that the file may be a named pipe instead of a fixed size object. The detection events will be in b8 format (see https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md ). The number of detection events per shot is available via the `num_dets` argument or via the detector error model at `dem_path`. obs_predictions_b8_out_path: The file path that decoder predictions must be written to. The predictions must be written in b8 format (see https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md ). The number of observables per shot is available via the `num_obs` argument or via the detector error model at `dem_path`. tmp_dir: Any temporary files generated by the decoder during its operation MUST be put into this directory. The reason for this requirement is because sinter is allowed to kill the decoding process without warning, without giving it time to clean up any temporary objects. All cleanup should be done via sinter deleting this directory after killing the decoder. """ ``` ```python # sinter.Fit # (at top-level in the sinter module) @dataclasses.dataclass(frozen=True) class Fit: """The result of a fitting process. Attributes: low: The hypothesis with the smallest parameter whose cost or score was still "close to" the cost of the best hypothesis. For example, this could be a hypothesis whose squared error was within some tolerance of the best fit's square error, or whose likelihood was within some maximum Bayes factor of the max likelihood hypothesis. best: The max likelihood hypothesis. The hypothesis that had the lowest squared error, or the best fitting score. high: The hypothesis with the larger parameter whose cost or score was still "close to" the cost of the best hypothesis. For example, this could be a hypothesis whose squared error was within some tolerance of the best fit's square error, or whose likelihood was within some maximum Bayes factor of the max likelihood hypothesis. """ low: Optional[float] best: Optional[float] high: Optional[float] ``` ```python # sinter.Progress # (at top-level in the sinter module) @dataclasses.dataclass(frozen=True) class Progress: """Describes statistics and status messages from ongoing sampling. This is the type yielded by `sinter.iter_collect`, and given to the `progress_callback` argument of `sinter.collect`. Attributes: new_stats: New sampled statistics collected since the last progress update. status_message: A free form human readable string describing the current collection status, such as the number of tasks left and the estimated time to completion for each task. """ new_stats: Tuple[sinter.TaskStats, ...] status_message: str ``` ```python # sinter.Sampler # (at top-level in the sinter module) class Sampler(metaclass=abc.ABCMeta): """A strategy for producing stats from tasks. Call `sampler.compiled_sampler_for_task(task)` to get a compiled sampler for a task, then call `compiled_sampler.sample(shots)` to collect statistics. A sampler differs from a `sinter.Decoder` because the sampler is responsible for the full sampling process (e.g. simulating the circuit), whereas a decoder can do nothing except predict observable flips from detection event data. This prevents the decoders from cheating, but makes them less flexible overall. A sampler can do things like use simulators other than stim, or really anything at all as long as it ends with returning statistics about shot counts, error counts, and etc. """ ``` ```python # sinter.Sampler.compiled_sampler_for_task # (in class sinter.Sampler) @abc.abstractmethod def compiled_sampler_for_task( self, task: sinter.Task, ) -> sinter.CompiledSampler: """Creates, configures, and returns an object for sampling the task. """ ``` ```python # sinter.Task # (at top-level in the sinter module) class Task: """A decoding problem that sinter can sample from. Attributes: circuit: The annotated noisy circuit to sample detection event data and logical observable data form. decoder: The decoder to use to predict the logical observable data from the detection event data. This can be set to None if it will be specified later (e.g. by the call to `collect`). detector_error_model: Specifies the error model to give to the decoder. Defaults to None, indicating that it should be automatically derived using `stim.Circuit.detector_error_model`. postselection_mask: Defaults to None (unused). A bit packed bitmask identifying detectors that must not fire. Shots where the indicated detectors fire are discarded. postselected_observables_mask: Defaults to None (unused). A bit packed bitmask identifying observable indices to postselect on. Anytime the decoder's predicted flip for one of these observables doesn't agree with the actual measured flip value of the observable, the shot is discarded instead of counting as an error. json_metadata: Defaults to None. Custom additional data describing the problem. Must be JSON serializable. For example, this could be a dictionary with "physical_error_rate" and "code_distance" keys. collection_options: Specifies custom options for collecting this single task. These options are merged with the global options to determine what happens. For example, if a task has `collection_options` set to `sinter.CollectionOptions(max_shots=1000, max_errors=100)` and `sinter.collect` was called with `max_shots=500` and `max_errors=200`, then either 500 shots or 100 errors will be collected for the task (whichever comes first). Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... rounds=10, ... distance=10, ... before_round_data_depolarization=1e-3, ... ), ... ) """ ``` ```python # sinter.Task.__init__ # (in class sinter.Task) def __init__( self, *, circuit: Optional[stim.Circuit] = None, decoder: Optional[str] = None, detector_error_model: Optional[stim.DetectorErrorModel] = None, postselection_mask: Optional[np.ndarray] = None, postselected_observables_mask: Optional[np.ndarray] = None, json_metadata: Any = None, collection_options: sinter.CollectionOptions = sinter.CollectionOptions(), skip_validation: bool = False, circuit_path: Union[str, pathlib.Path, NoneType] = None, _unvalidated_strong_id: Optional[str] = None, ) -> None: """ Args: circuit: The annotated noisy circuit to sample detection event data and logical observable data form. decoder: The decoder to use to predict the logical observable data from the detection event data. This can be set to None if it will be specified later (e.g. by the call to `collect`). detector_error_model: Specifies the error model to give to the decoder. Defaults to None, indicating that it should be automatically derived using `stim.Circuit.detector_error_model`. postselection_mask: Defaults to None (unused). A bit packed bitmask identifying detectors that must not fire. Shots where the indicated detectors fire are discarded. postselected_observables_mask: Defaults to None (unused). A bit packed bitmask identifying observable indices to postselect on. Anytime the decoder's predicted flip for one of these observables doesn't agree with the actual measured flip value of the observable, the shot is discarded instead of counting as an error. json_metadata: Defaults to None. Custom additional data describing the problem. Must be JSON serializable. For example, this could be a dictionary with "physical_error_rate" and "code_distance" keys. collection_options: Specifies custom options for collecting this single task. These options are merged with the global options to determine what happens. For example, if a task has `collection_options` set to `sinter.CollectionOptions(max_shots=1000, max_errors=100)` and `sinter.collect` was called with `max_shots=500` and `max_errors=200`, then either 500 shots or 100 errors will be collected for the task (whichever comes first). skip_validation: Defaults to False. Normally the arguments given to this method are checked for consistency (e.g. the detector error model should have the same number of detectors as the circuit). Setting this argument to True will skip doing the consistency checks. Note that this can result in confusing errors later, if the arguments are not actually consistent. circuit_path: Typically set to None. If the circuit isn't specified, this is the filepath to read it from. Not included in the strong id. _unvalidated_strong_id: Must be set to None unless `skip_validation` is set to True. Otherwise, if this is specified then it should be equal to the value returned by self.strong_id(). """ ``` ```python # sinter.Task.strong_id # (in class sinter.Task) def strong_id( self, ) -> str: """Computes a cryptographically unique identifier for this task. This value is affected by: - The exact circuit. - The exact detector error model. - The decoder. - The json metadata. - The postselection mask. Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit(), ... detector_error_model=stim.DetectorErrorModel(), ... decoder='pymatching', ... ) >>> task.strong_id() '7424ea021693d4abc1c31c12e655a48779f61a7c2969e457ae4fe400c852bee5' """ ``` ```python # sinter.Task.strong_id_bytes # (in class sinter.Task) def strong_id_bytes( self, ) -> bytes: """The bytes that are hashed to get the strong id. This value is converted into the actual strong id by: - Hashing these bytes using SHA256. Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit('H 0'), ... detector_error_model=stim.DetectorErrorModel(), ... decoder='pymatching', ... ) >>> task.strong_id_bytes() b'{"circuit": "H 0", "decoder": "pymatching", "decoder_error_model": "", "postselection_mask": null, "json_metadata": null}' """ ``` ```python # sinter.Task.strong_id_text # (in class sinter.Task) def strong_id_text( self, ) -> str: """The text that is serialized and hashed to get the strong id. This value is converted into the actual strong id by: - Serializing into bytes using UTF8. - Hashing the UTF8 bytes using SHA256. Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit('H 0'), ... detector_error_model=stim.DetectorErrorModel(), ... decoder='pymatching', ... ) >>> task.strong_id_text() '{"circuit": "H 0", "decoder": "pymatching", "decoder_error_model": "", "postselection_mask": null, "json_metadata": null}' """ ``` ```python # sinter.Task.strong_id_value # (in class sinter.Task) def strong_id_value( self, ) -> Dict[str, Any]: """Contains all raw values that affect the strong id. This value is converted into the actual strong id by: - Serializing it into text using JSON. - Serializing the JSON text into bytes using UTF8. - Hashing the UTF8 bytes using SHA256. Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit('H 0'), ... detector_error_model=stim.DetectorErrorModel(), ... decoder='pymatching', ... ) >>> task.strong_id_value() {'circuit': 'H 0', 'decoder': 'pymatching', 'decoder_error_model': '', 'postselection_mask': None, 'json_metadata': None} """ ``` ```python # sinter.TaskStats # (at top-level in the sinter module) @dataclasses.dataclass(frozen=True) class TaskStats: """Statistics sampled from a task. The rows in the CSV files produced by sinter correspond to instances of `sinter.TaskStats`. For example, a row can be produced by printing a `sinter.TaskStats`. Attributes: strong_id: The cryptographically unique identifier of the task, from `sinter.Task.strong_id()`. decoder: The name of the decoder that was used to decode the task. Errors are counted when this decoder made a wrong prediction. json_metadata: A JSON-encodable value (such as a dictionary from strings to integers) that were included with the task in order to describe what the task was. This value can be a huge variety of things, but typically it will be a dictionary with fields such as 'd' for the code distance. shots: Number of times the task was sampled. errors: Number of times a sample resulted in an error. discards: Number of times a sample resulted in a discard. Note that discarded a task is not an error. seconds: The amount of CPU core time spent sampling the tasks, in seconds. custom_counts: A counter mapping string keys to integer values. Used for tracking arbitrary values, such as per-observable error counts or the number of times detectors fired. The meaning of the information in the counts is not specified; the only requirement is that it should be correct to add each key's counts when merging statistics. Although this field is an editable object, it's invalid to edit the counter after the stats object is initialized. """ strong_id: str decoder: str json_metadata: Any shots: int = 0 errors: int = 0 discards: int = 0 seconds: float = 0 custom_counts: Counter[str] ``` ```python # sinter.TaskStats.to_anon_stats # (in class sinter.TaskStats) def to_anon_stats( self, ) -> sinter.AnonTaskStats: """Returns a `sinter.AnonTaskStats` with the same statistics. Examples: >>> import sinter >>> stat = sinter.TaskStats( ... strong_id='test', ... json_metadata={'a': [1, 2, 3]}, ... decoder='pymatching', ... shots=22, ... errors=3, ... discards=4, ... seconds=5, ... ) >>> stat.to_anon_stats() sinter.AnonTaskStats(shots=22, errors=3, discards=4, seconds=5) """ ``` ```python # sinter.TaskStats.to_csv_line # (in class sinter.TaskStats) def to_csv_line( self, ) -> str: """Converts into a line that can be printed into a CSV file. Examples: >>> import sinter >>> stat = sinter.TaskStats( ... strong_id='test', ... json_metadata={'a': [1, 2, 3]}, ... decoder='pymatching', ... shots=22, ... errors=3, ... seconds=5, ... ) >>> print(sinter.CSV_HEADER) shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts >>> print(stat.to_csv_line()) 22, 3, 0, 5.00,pymatching,test,"{""a"":[1,2,3]}", """ ``` ```python # sinter.TaskStats.with_edits # (in class sinter.TaskStats) def with_edits( self, *, strong_id: Optional[str] = None, decoder: Optional[str] = None, json_metadata: Optional[Any] = None, shots: Optional[int] = None, errors: Optional[int] = None, discards: Optional[int] = None, seconds: Optional[float] = None, custom_counts: Optional[Counter[str]] = None, ) -> sinter.TaskStats: ``` ```python # sinter.better_sorted_str_terms # (at top-level in the sinter module) def better_sorted_str_terms( val: Any, ) -> Any: """A function that orders "a10000" after "a9", instead of before. Normally, sorting strings sorts them lexicographically, treating numbers so that "1999999" ends up being less than "2". This method splits the string into a tuple of text pairs and parsed number parts, so that sorting by this key puts "2" before "1999999". Because this method is intended for use in plotting, where it's more important to see a bad result than to see nothing, it returns a type that tries to be comparable to everything. Args: val: The value to convert into a value with a better sorting order. Returns: A custom type of object with a better sorting order. Examples: >>> import sinter >>> items = [ ... "distance=199999, rounds=3", ... "distance=2, rounds=3", ... "distance=199999, rounds=199999", ... "distance=2, rounds=199999", ... ] >>> for e in sorted(items, key=sinter.better_sorted_str_terms): ... print(e) distance=2, rounds=3 distance=2, rounds=199999 distance=199999, rounds=3 distance=199999, rounds=199999 """ ``` ```python # sinter.collect # (at top-level in the sinter module) def collect( *, num_workers: int, tasks: Union[Iterator[sinter.Task], Iterable[sinter.Task]], existing_data_filepaths: Iterable[Union[str, pathlib.Path]] = (), save_resume_filepath: Union[NoneType, str, pathlib.Path] = None, progress_callback: Optional[Callable[[sinter.Progress], NoneType]] = None, max_shots: Optional[int] = None, max_errors: Optional[int] = None, count_observable_error_combos: bool = False, count_detection_events: bool = False, decoders: Optional[Iterable[str]] = None, max_batch_seconds: Optional[int] = None, max_batch_size: Optional[int] = None, start_batch_size: Optional[int] = None, print_progress: bool = False, hint_num_tasks: Optional[int] = None, custom_decoders: Optional[Dict[str, Union[sinter.Decoder, sinter.Sampler]]] = None, custom_error_count_key: Optional[str] = None, allowed_cpu_affinity_ids: Optional[Iterable[int]] = None, ) -> List[sinter.TaskStats]: """Collects statistics from the given tasks, using multiprocessing. Args: num_workers: The number of worker processes to use. tasks: Decoding problems to sample. save_resume_filepath: Defaults to None (unused). If set to a filepath, results will be saved to that file while they are collected. If the python interpreter is stopped or killed, calling this method again with the same save_resume_filepath will load the previous results from the file so it can resume where it left off. The stats in this file will be counted in addition to each task's previous_stats field (as opposed to overriding the field). existing_data_filepaths: CSV data saved to these files will be loaded, included in the returned results, and count towards things like max_shots and max_errors. progress_callback: Defaults to None (unused). If specified, then each time new sample statistics are acquired from a worker this method will be invoked with the new `sinter.TaskStats`. hint_num_tasks: If `tasks` is an iterator or a generator, its length can be given here so that progress printouts can say how many cases are left. decoders: Defaults to None (specified by each Task). The names of the decoders to use on each Task. It must either be the case that each Task specifies a decoder and this is set to None, or this is an iterable and each Task has its decoder set to None. count_observable_error_combos: Defaults to False. When set to to True, the returned stats will have a custom counts field with keys like `obs_mistake_mask=E_E__` counting how many times specific combinations of observables were mispredicted by the decoder. count_detection_events: Defaults to False. When set to True, the returned stats will have a custom counts field withs the key `detection_events` counting the number of times a detector fired and also `detectors_checked` counting the number of detectors that were executed. The detection fraction is the ratio of these two numbers. max_shots: Defaults to None (unused). Stops the sampling process after this many samples have been taken from the circuit. max_errors: Defaults to None (unused). Stops the sampling process after this many errors have been seen in samples taken from the circuit. The actual number sampled errors may be larger due to batching. start_batch_size: Defaults to None (collector's choice). The very first shots taken from the circuit will use a batch of this size, and no other batches will be taken in parallel. Once this initial fact finding batch is done, batches can be taken in parallel and the normal batch size limiting processes take over. max_batch_size: Defaults to None (unused). Limits batches from taking more than this many shots at once. For example, this can be used to ensure memory usage stays below some limit. print_progress: When True, progress is printed to stderr while collection runs. max_batch_seconds: Defaults to None (unused). When set, the recorded data from previous shots is used to estimate how much time is taken per shot. This information is then used to predict the biggest batch size that can finish in under the given number of seconds. Limits each batch to be no larger than that. custom_decoders: Named child classes of `sinter.decoder`, that can be used if requested by name by a task or by the decoders list. If not specified, only decoders with support built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. custom_error_count_key: Makes `max_errors` apply to `stat.custom_counts[key]` instead of `stat.errors`. allowed_cpu_affinity_ids: Controls which CPUs the workers can be pinned to. The set of allowed IDs should be at least as large as the number of workers, though this is not strictly required. If not set, defaults to all CPUs being allowed. Returns: A list of sample statistics, one from each problem. The list is not in any specific order. This is the same data that would have been written to a CSV file, but aggregated so that each problem has exactly one sample statistic instead of potentially multiple. Examples: >>> import sinter >>> import stim >>> tasks = [ ... sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... distance=5, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ), ... json_metadata={'d': 5}, ... ), ... sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... distance=7, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ), ... json_metadata={'d': 7}, ... ), ... ] >>> stats = sinter.collect( ... tasks=tasks, ... decoders=['vacuous'], ... num_workers=2, ... max_shots=100, ... ) >>> for stat in sorted(stats, key=lambda e: e.json_metadata['d']): ... print(stat.json_metadata, stat.shots) {'d': 5} 100 {'d': 7} 100 """ ``` ```python # sinter.comma_separated_key_values # (at top-level in the sinter module) def comma_separated_key_values( path: str, ) -> Dict[str, Any]: """Converts paths like 'folder/d=5,r=3.stim' into dicts like {'d':5,'r':3}. On the command line, specifying `--metadata_func auto` results in this method being used to extra metadata from the circuit file paths. Integers and floats will be parsed into their values, instead of being stored as strings. Args: path: A file path where the name of the file has a series of terms like 'a=b' separated by commas and ending in '.stim'. Returns: A dictionary from named keys to parsed values. Examples: >>> import sinter >>> sinter.comma_separated_key_values("folder/d=5,r=3.5,x=abc.stim") {'d': 5, 'r': 3.5, 'x': 'abc'} """ ``` ```python # sinter.fit_binomial # (at top-level in the sinter module) def fit_binomial( *, num_shots: int, num_hits: int, max_likelihood_factor: float, ) -> sinter.Fit: """Determine hypothesis probabilities compatible with the given hit ratio. The result includes the best fit (the max likelihood hypothis) as well as the smallest and largest probabilities whose likelihood is within the given factor of the maximum likelihood hypothesis. Args: num_shots: The number of samples that were taken. num_hits: The number of hits that were seen in the samples. max_likelihood_factor: The maximum Bayes factor between the low/high hypotheses and the best hypothesis (the max likelihood hypothesis). This value should be larger than 1 (as opposed to between 0 and 1). Returns: A `sinter.Fit` with the low, best, and high hypothesis probabilities. Examples: >>> import sinter >>> sinter.fit_binomial( ... num_shots=100_000_000, ... num_hits=2, ... max_likelihood_factor=1000, ... ) sinter.Fit(low=2e-10, best=2e-08, high=1.259e-07) >>> sinter.fit_binomial( ... num_shots=10, ... num_hits=5, ... max_likelihood_factor=9, ... ) sinter.Fit(low=0.202, best=0.5, high=0.798) """ ``` ```python # sinter.fit_line_slope # (at top-level in the sinter module) def fit_line_slope( *, xs: Sequence[float], ys: Sequence[float], max_extra_squared_error: float, ) -> sinter.Fit: """Performs a line fit of the given points, focusing on the line's slope. Finds the slope of the best fit, but also the minimum and maximum slopes for line fits whose squared error cost is within the given `max_extra_squared_error` cost of the best fit. Note that the extra squared error is computed while including a specific offset of some specific line. So the low/high estimates are for specific lines, not for the general class of lines with a given slope, adding together the contributions of all lines in that class. Args: xs: The x coordinates of points to fit. ys: The y coordinates of points to fit. max_extra_squared_error: When computing the low and high fits, this is the maximum additional squared error that can be introduced by varying the slope away from the best fit. Returns: A sinter.Fit containing the best fit, as well as low and high fits that are as far as possible from the best fit while respective the given max_extra_squared_error. Examples: >>> import sinter >>> sinter.fit_line_slope( ... xs=[1, 2, 3], ... ys=[10, 12, 14], ... max_extra_squared_error=1, ... ) sinter.Fit(low=1.2928924560546875, best=2.0, high=2.7071075439453125) """ ``` ```python # sinter.fit_line_y_at_x # (at top-level in the sinter module) def fit_line_y_at_x( *, xs: Sequence[float], ys: Sequence[float], target_x: float, max_extra_squared_error: float, ) -> sinter.Fit: """Performs a line fit, focusing on the line's y coord at a given x coord. Finds the y value at the given x of the best fit, but also the minimum and maximum values for y at the given x amongst all possible line fits whose squared error cost is within the given `max_extra_squared_error` cost of the best fit. Args: xs: The x coordinates of points to fit. ys: The y coordinates of points to fit. target_x: The fit values are the value of y at this x coordinate. max_extra_squared_error: When computing the low and high fits, this is the maximum additional squared error that can be introduced by varying the slope away from the best fit. Returns: A sinter.Fit containing the best fit for y at the given x, as well as low and high fits that are as far as possible from the best fit while respecting the given max_extra_squared_error. Examples: >>> import sinter >>> sinter.fit_line_y_at_x( ... xs=[1, 2, 3], ... ys=[10, 12, 14], ... target_x=4, ... max_extra_squared_error=1, ... ) sinter.Fit(low=14.47247314453125, best=16.0, high=17.52752685546875) """ ``` ```python # sinter.group_by # (at top-level in the sinter module) def group_by( items: Iterable[~TVal], *, key: Callable[[~TVal], ~TKey], ) -> Dict[~TKey, List[~TVal]]: """Groups items based on whether they produce the same key from a function. Args: items: The items to group. key: Items that produce the same value from this function get grouped together. Returns: A dictionary mapping outputs that were produced by the grouping function to the list of items that produced that output. Examples: >>> import sinter >>> sinter.group_by([1, 2, 3], key=lambda i: i == 2) {False: [1, 3], True: [2]} >>> sinter.group_by(range(10), key=lambda i: i % 3) {0: [0, 3, 6, 9], 1: [1, 4, 7], 2: [2, 5, 8]} """ ``` ```python # sinter.iter_collect # (at top-level in the sinter module) def iter_collect( *, num_workers: int, tasks: Union[Iterator[sinter.Task], Iterable[sinter.Task]], hint_num_tasks: Optional[int] = None, additional_existing_data: Union[NoneType, Dict[str, sinter.TaskStats], Iterable[sinter.TaskStats]] = None, max_shots: Optional[int] = None, max_errors: Optional[int] = None, decoders: Optional[Iterable[str]] = None, max_batch_seconds: Optional[int] = None, max_batch_size: Optional[int] = None, start_batch_size: Optional[int] = None, count_observable_error_combos: bool = False, count_detection_events: bool = False, custom_decoders: Optional[Dict[str, Union[sinter.Decoder, sinter.Sampler]]] = None, custom_error_count_key: Optional[str] = None, allowed_cpu_affinity_ids: Optional[Iterable[int]] = None, ) -> Iterator[sinter.Progress]: """Iterates error correction statistics collected from worker processes. It is important to iterate until the sequence ends, or worker processes will be left alive. The values yielded during iteration are progress updates from the workers. Note: if max_batch_size and max_batch_seconds are both not used (or explicitly set to None), a default batch-size-limiting mechanism will be chosen. Args: num_workers: The number of worker processes to use. tasks: Decoding problems to sample. hint_num_tasks: If `tasks` is an iterator or a generator, its length can be given here so that progress printouts can say how many cases are left. additional_existing_data: Defaults to None (no additional data). Statistical data that has already been collected, in addition to anything included in each task's `previous_stats` field. decoders: Defaults to None (specified by each Task). The names of the decoders to use on each Task. It must either be the case that each Task specifies a decoder and this is set to None, or this is an iterable and each Task has its decoder set to None. max_shots: Defaults to None (unused). Stops the sampling process after this many samples have been taken from the circuit. max_errors: Defaults to None (unused). Stops the sampling process after this many errors have been seen in samples taken from the circuit. The actual number sampled errors may be larger due to batching. count_observable_error_combos: Defaults to False. When set to to True, the returned stats will have a custom counts field with keys like `obs_mistake_mask=E_E__` counting how many times specific combinations of observables were mispredicted by the decoder. count_detection_events: Defaults to False. When set to True, the returned stats will have a custom counts field withs the key `detection_events` counting the number of times a detector fired and also `detectors_checked` counting the number of detectors that were executed. The detection fraction is the ratio of these two numbers. start_batch_size: Defaults to None (collector's choice). The very first shots taken from the circuit will use a batch of this size, and no other batches will be taken in parallel. Once this initial fact finding batch is done, batches can be taken in parallel and the normal batch size limiting processes take over. max_batch_size: Defaults to None (unused). Limits batches from taking more than this many shots at once. For example, this can be used to ensure memory usage stays below some limit. max_batch_seconds: Defaults to None (unused). When set, the recorded data from previous shots is used to estimate how much time is taken per shot. This information is then used to predict the biggest batch size that can finish in under the given number of seconds. Limits each batch to be no larger than that. custom_decoders: Custom decoders that can be used if requested by name. If not specified, only decoders built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. custom_error_count_key: Makes `max_errors` apply to `stat.custom_counts[key]` instead of `stat.errors`. allowed_cpu_affinity_ids: Controls which CPUs the workers can be pinned to. The set of allowed IDs should be at least as large as the number of workers, though this is not strictly required. If not set, defaults to all CPUs being allowed. Yields: sinter.Progress instances recording incremental statistical data as it is collected by workers. Examples: >>> import sinter >>> import stim >>> tasks = [ ... sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... distance=5, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ), ... json_metadata={'d': 5}, ... ), ... sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... distance=7, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ), ... json_metadata={'d': 7}, ... ), ... ] >>> iterator = sinter.iter_collect( ... tasks=tasks, ... decoders=['vacuous'], ... num_workers=2, ... max_shots=100, ... ) >>> total_shots = 0 >>> for progress in iterator: ... for stat in progress.new_stats: ... total_shots += stat.shots >>> print(total_shots) 200 """ ``` ```python # sinter.log_binomial # (at top-level in the sinter module) def log_binomial( *, p: Union[float, np.ndarray], n: int, hits: int, ) -> np.ndarray: """Approximates the natural log of a binomial distribution's probability. When working with large binomials, it's often necessary to work in log space to represent the result. For example, suppose that out of two million samples 200_000 are hits. The maximum likelihood estimate is p=0.2. Even if this is the true probability, the chance of seeing *exactly* 20% hits out of a million shots is roughly 10^-217322. Whereas the smallest representable double is roughly 10^-324. But ln(10^-217322) ~= -500402.4 is representable. This method evaluates $\ln(P(hits = B(n, p)))$, with all computations done in log space to ensure intermediate values can be represented as floating point numbers without underflowing to 0 or overflowing to infinity. This method can be broadcast over multiple hypothesis probabilities by giving a numpy array for `p` instead of a single float. Args: p: The hypotehsis probability. The independent probability of a hit occurring for each sample. This can also be an array of probabilities, in which case the function is broadcast over the array. n: The number of samples that were taken. hits: The number of hits that were observed amongst the samples that were taken. Returns: $\ln(P(hits = B(n, p)))$ Examples: >>> import sinter >>> sinter.log_binomial(p=0.5, n=100, hits=50) array(-2.5308762, dtype=float32) >>> sinter.log_binomial(p=0.2, n=1_000_000, hits=1_000) array(-216626.97, dtype=float32) >>> sinter.log_binomial(p=0.1, n=1_000_000, hits=1_000) array(-99654.86, dtype=float32) >>> sinter.log_binomial(p=0.01, n=1_000_000, hits=1_000) array(-6742.573, dtype=float32) >>> sinter.log_binomial(p=[0.01, 0.1, 0.2], n=1_000_000, hits=1_000) array([ -6742.573, -99654.86 , -216626.97 ], dtype=float32) """ ``` ```python # sinter.log_factorial # (at top-level in the sinter module) def log_factorial( n: int, ) -> float: """Approximates $\ln(n!)$; the natural logarithm of a factorial. Args: n: The input to the factorial. Returns: Evaluates $ln(n!)$ using `math.lgamma(n+1)`. Examples: >>> import sinter >>> sinter.log_factorial(0) 0.0 >>> sinter.log_factorial(1) 0.0 >>> sinter.log_factorial(2) 0.693147180559945 >>> sinter.log_factorial(100) 363.73937555556347 """ ``` ```python # sinter.plot_custom # (at top-level in the sinter module) def plot_custom( *, ax: 'plt.Axes', stats: 'Iterable[sinter.TaskStats]', x_func: Callable[[sinter.TaskStats], Any], y_func: Callable[[sinter.TaskStats], Union[sinter.Fit, float, int]], group_func: Callable[[sinter.TaskStats], ~TCurveId] = lambda _: None, point_label_func: Callable[[sinter.TaskStats], Any] = lambda _: None, filter_func: Callable[[sinter.TaskStats], Any] = lambda _: True, plot_args_func: Callable[[int, ~TCurveId, List[sinter.TaskStats]], Dict[str, Any]] = lambda index, group_key, group_stats: dict(), line_fits: Optional[Tuple[Literal['linear', 'log', 'sqrt'], Literal['linear', 'log', 'sqrt']]] = None, ) -> None: """Plots error rates in curves with uncertainty highlights. Args: ax: The plt.Axes to plot onto. For example, the `ax` value from `fig, ax = plt.subplots(1, 1)`. stats: The collected statistics to plot. x_func: The X coordinate to use for each stat's data point. For example, this could be `x_func=lambda stat: stat.json_metadata['physical_error_rate']`. y_func: The Y value to use for each stat's data point. This can be a float or it can be a sinter.Fit value, in which case the curve will follow the fit.best value and a highlighted area will be shown from fit.low to fit.high. group_func: Optional. When specified, multiple curves will be plotted instead of one curve. The statistics are grouped into curves based on whether or not they get the same result out of this function. For example, this could be `group_func=lambda stat: stat.decoder`. If the result of the function is a dictionary, then optional keys in the dictionary will also control the plotting of each curve. Available keys are: 'label': the label added to the legend for the curve 'color': the color used for plotting the curve 'marker': the marker used for the curve 'linestyle': the linestyle used for the curve 'sort': the order in which the curves will be plotted and added to the legend e.g. if two curves (with different resulting dictionaries from group_func) share the same value for key 'marker', they will be plotted with the same marker. Colors, markers and linestyles are assigned in order, sorted by the values for those keys. point_label_func: Optional. Specifies text to draw next to data points. filter_func: Optional. When specified, some curves will not be plotted. The statistics are filtered and only plotted if filter_func(stat) returns True. For example, `filter_func=lambda s: s.json_metadata['basis'] == 'x'` would plot only stats where the saved metadata indicates the basis was 'x'. plot_args_func: Optional. Specifies additional arguments to give the underlying calls to `plot` and `fill_between` used to do the actual plotting. For example, this can be used to specify markers and colors. Takes the index of the curve in sorted order and also a curve_id (these will be 0 and None respectively if group_func is not specified). For example, this could be: plot_args_func=lambda index, group_key, group_stats: { 'color': ( 'red' if group_key == 'decoder=pymatching p=0.001' else 'blue' ), } line_fits: Defaults to None. Set this to a tuple (x_scale, y_scale) to include a dashed line fit to every curve. The scales determine how to transform the coordinates before performing the fit, and can be set to 'linear', 'sqrt', or 'log'. """ ``` ```python # sinter.plot_discard_rate # (at top-level in the sinter module) def plot_discard_rate( *, ax: 'plt.Axes', stats: 'Iterable[sinter.TaskStats]', x_func: Callable[[sinter.TaskStats], Any], failure_units_per_shot_func: Callable[[sinter.TaskStats], Any] = lambda _: 1, group_func: Callable[[sinter.TaskStats], ~TCurveId] = lambda _: None, filter_func: Callable[[sinter.TaskStats], Any] = lambda _: True, plot_args_func: Callable[[int, ~TCurveId, List[sinter.TaskStats]], Dict[str, Any]] = lambda index, group_key, group_stats: dict(), highlight_max_likelihood_factor: Optional[float] = 1000.0, point_label_func: Callable[[sinter.TaskStats], Any] = lambda _: None, ) -> None: """Plots discard rates in curves with uncertainty highlights. Args: ax: The plt.Axes to plot onto. For example, the `ax` value from `fig, ax = plt.subplots(1, 1)`. stats: The collected statistics to plot. x_func: The X coordinate to use for each stat's data point. For example, this could be `x_func=lambda stat: stat.json_metadata['physical_error_rate']`. failure_units_per_shot_func: How many discard chances there are per shot. This rescales what the discard rate means. By default, it is the discard rate per shot, but this allows you to instead make it the discard rate per round. For example, if the metadata associated with a shot has a field 'r' which is the number of rounds, then this can be achieved with `failure_units_per_shot_func=lambda stats: stats.metadata['r']`. group_func: Optional. When specified, multiple curves will be plotted instead of one curve. The statistics are grouped into curves based on whether or not they get the same result out of this function. For example, this could be `group_func=lambda stat: stat.decoder`. If the result of the function is a dictionary, then optional keys in the dictionary will also control the plotting of each curve. Available keys are: 'label': the label added to the legend for the curve 'color': the color used for plotting the curve 'marker': the marker used for the curve 'linestyle': the linestyle used for the curve 'sort': the order in which the curves will be plotted and added to the legend e.g. if two curves (with different resulting dictionaries from group_func) share the same value for key 'marker', they will be plotted with the same marker. Colors, markers and linestyles are assigned in order, sorted by the values for those keys. filter_func: Optional. When specified, some curves will not be plotted. The statistics are filtered and only plotted if filter_func(stat) returns True. For example, `filter_func=lambda s: s.json_metadata['basis'] == 'x'` would plot only stats where the saved metadata indicates the basis was 'x'. plot_args_func: Optional. Specifies additional arguments to give the underlying calls to `plot` and `fill_between` used to do the actual plotting. For example, this can be used to specify markers and colors. Takes the index of the curve in sorted order and also a curve_id (these will be 0 and None respectively if group_func is not specified). For example, this could be: plot_args_func=lambda index, curve_id: {'color': 'red' if curve_id == 'pymatching' else 'blue'} highlight_max_likelihood_factor: Controls how wide the uncertainty highlight region around curves is. Must be 1 or larger. Hypothesis probabilities at most that many times as unlikely as the max likelihood hypothesis will be highlighted. point_label_func: Optional. Specifies text to draw next to data points. """ ``` ```python # sinter.plot_error_rate # (at top-level in the sinter module) def plot_error_rate( *, ax: 'plt.Axes', stats: 'Iterable[sinter.TaskStats]', x_func: Callable[[sinter.TaskStats], Any], failure_units_per_shot_func: Callable[[sinter.TaskStats], Any] = lambda _: 1, failure_values_func: Callable[[sinter.TaskStats], Any] = lambda _: 1, group_func: Callable[[sinter.TaskStats], ~TCurveId] = lambda _: None, filter_func: Callable[[sinter.TaskStats], Any] = lambda _: True, plot_args_func: Callable[[int, ~TCurveId, List[sinter.TaskStats]], Dict[str, Any]] = lambda index, group_key, group_stats: dict(), highlight_max_likelihood_factor: Optional[float] = 1000.0, line_fits: Optional[Tuple[Literal['linear', 'log', 'sqrt'], Literal['linear', 'log', 'sqrt']]] = None, point_label_func: Callable[[sinter.TaskStats], Any] = lambda _: None, ) -> None: """Plots error rates in curves with uncertainty highlights. Args: ax: The plt.Axes to plot onto. For example, the `ax` value from `fig, ax = plt.subplots(1, 1)`. stats: The collected statistics to plot. x_func: The X coordinate to use for each stat's data point. For example, this could be `x_func=lambda stat: stat.json_metadata['physical_error_rate']`. failure_units_per_shot_func: How many error chances there are per shot. This rescales what the logical error rate means. By default, it is the logical error rate per shot, but this allows you to instead make it the logical error rate per round. For example, if the metadata associated with a shot has a field 'r' which is the number of rounds, then this can be achieved with `failure_units_per_shot_func=lambda stats: stats.metadata['r']`. failure_values_func: How many independent ways there are for a shot to fail, such as the number of independent observables in a memory experiment. This affects how the failure units rescaling plays out (e.g. with 1 independent failure the "center" of the conversion is at 50% whereas for 2 independent failures the "center" is at 75%). group_func: Optional. When specified, multiple curves will be plotted instead of one curve. The statistics are grouped into curves based on whether or not they get the same result out of this function. For example, this could be `group_func=lambda stat: stat.decoder`. If the result of the function is a dictionary, then optional keys in the dictionary will also control the plotting of each curve. Available keys are: 'label': the label added to the legend for the curve 'color': the color used for plotting the curve 'marker': the marker used for the curve 'linestyle': the linestyle used for the curve 'sort': the order in which the curves will be plotted and added to the legend e.g. if two curves (with different resulting dictionaries from group_func) share the same value for key 'marker', they will be plotted with the same marker. Colors, markers and linestyles are assigned in order, sorted by the values for those keys. filter_func: Optional. When specified, some curves will not be plotted. The statistics are filtered and only plotted if filter_func(stat) returns True. For example, `filter_func=lambda s: s.json_metadata['basis'] == 'x'` would plot only stats where the saved metadata indicates the basis was 'x'. plot_args_func: Optional. Specifies additional arguments to give the underlying calls to `plot` and `fill_between` used to do the actual plotting. For example, this can be used to specify markers and colors. Takes the index of the curve in sorted order and also a curve_id (these will be 0 and None respectively if group_func is not specified). For example, this could be: plot_args_func=lambda index, curve_id: {'color': 'red' if curve_id == 'pymatching' else 'blue'} highlight_max_likelihood_factor: Controls how wide the uncertainty highlight region around curves is. Must be 1 or larger. Hypothesis probabilities at most that many times as unlikely as the max likelihood hypothesis will be highlighted. line_fits: Defaults to None. Set this to a tuple (x_scale, y_scale) to include a dashed line fit to every curve. The scales determine how to transform the coordinates before performing the fit, and can be set to 'linear', 'sqrt', or 'log'. point_label_func: Optional. Specifies text to draw next to data points. """ ``` ```python # sinter.post_selection_mask_from_4th_coord # (at top-level in the sinter module) def post_selection_mask_from_4th_coord( dem: Union[stim.Circuit, stim.DetectorErrorModel], ) -> np.ndarray: """Returns a mask that postselects detector's with non-zero 4th coordinate. This method is a leftover from before the existence of the command line argument `--postselected_detectors_predicate`, when `--postselect_detectors_with_non_zero_4th_coord` was the only way to do post selection of detectors. Args: dem: The detector error model to pull coordinate data from. Returns: A bit packed numpy array where detectors with non-zero 4th coordinate data have a True bit at their corresponding index. Examples: >>> import sinter >>> import stim >>> dem = stim.DetectorErrorModel(''' ... detector(1, 2, 3) D0 ... detector(1, 1, 1, 1) D1 ... detector(1, 1, 1, 0) D2 ... detector(1, 1, 1, 999) D80 ... ''') >>> sinter.post_selection_mask_from_4th_coord(dem) array([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], dtype=uint8) """ ``` ```python # sinter.predict_discards_bit_packed # (at top-level in the sinter module) def predict_discards_bit_packed( *, dem: stim.DetectorErrorModel, dets_bit_packed: np.ndarray, postselect_detectors_with_non_zero_4th_coord: bool, ) -> np.ndarray: """Determines which shots to discard due to postselected detectors firing. Args: dem: The detector error model the detector data applies to. This is also where coordinate data is read from, in order to determine which detectors to postselect as not having fired. dets_bit_packed: A uint8 numpy array with shape (num_shots, math.ceil(num_dets / 8)). Contains bit packed detection event data. postselect_detectors_with_non_zero_4th_coord: Determines how postselection is done. Currently, this is the only option so it has to be set to True. Any detector from the detector error model that specifies coordinate data with at least four coordinates where the fourth coordinate (coord index 3) is non-zero will be postselected. Returns: A numpy bool_ array with shape (num_shots,) where False means not discarded and True means yes discarded. """ ``` ```python # sinter.predict_observables # (at top-level in the sinter module) def predict_observables( *, dem: stim.DetectorErrorModel, dets: np.ndarray, decoder: str, bit_pack_result: bool = False, custom_decoders: Optional[Dict[str, sinter.Decoder]] = None, ) -> np.ndarray: """Predicts which observables were flipped based on detection event data. Args: dem: The detector error model the detector data applies to. This is also where coordinate data is read from, in order to determine which detectors to postselect as not having fired. dets: The detection event data. Can be bit packed or not bit packed. If dtype=np.bool_ then shape=(num_shots, num_detectors) If dtype=np.uint8 then shape=(num_shots, math.ceil(num_detectors/8)) decoder: The decoder to use for decoding, e.g. "pymatching". bit_pack_result: Defaults to False. Determines if the result is bit packed or not. custom_decoders: Custom decoders that can be used if requested by name. If not specified, only decoders built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. Returns: If bit_packed_result=False (default): dtype=np.bool_ shape=(num_shots, num_observables) If bit_packed_result=True: dtype=np.uint8 shape=(num_shots, math.ceil(num_observables / 8)) Examples: >>> import numpy as np >>> import sinter >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.1) D0 L0 ... error(0.1) D0 D1 ... error(0.1) D1 ... ''') >>> sinter.predict_observables( ... dem=dem, ... dets=np.array([ ... [False, False], ... [True, False], ... [False, True], ... [True, True], ... ], dtype=np.bool_), ... decoder='vacuous', # try replacing with 'pymatching' ... bit_pack_result=False, ... ) array([[False], [False], [False], [False]]) """ ``` ```python # sinter.predict_observables_bit_packed # (at top-level in the sinter module) def predict_observables_bit_packed( *, dem: stim.DetectorErrorModel, dets_bit_packed: np.ndarray, decoder: str, custom_decoders: Optional[Dict[str, sinter.Decoder]] = None, ) -> np.ndarray: """Predicts which observables were flipped based on detection event data. This method predates `sinter.predict_observables` gaining optional bit packing arguments. Args: dem: The detector error model the detector data applies to. This is also where coordinate data is read from, in order to determine which detectors to postselect as not having fired. dets_bit_packed: A uint8 numpy array with shape (num_shots, math.ceil(num_dets / 8)). Contains bit packed detection event data. decoder: The decoder to use for decoding, e.g. "pymatching". custom_decoders: Custom decoders that can be used if requested by name. If not specified, only decoders built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. Returns: A numpy uint8 array with shape (num_shots, math.ceil(num_obs / 8)). Contains bit packed observable prediction data. Examples: >>> import numpy as np >>> import sinter >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.1) D0 L0 ... error(0.1) D0 D1 ... error(0.1) D1 ... ''') >>> sinter.predict_observables_bit_packed( ... dem=dem, ... dets_bit_packed=np.array([ ... [0b00], ... [0b01], ... [0b10], ... [0b11], ... ], dtype=np.uint8), ... decoder='vacuous', # try replacing with 'pymatching' ... ) array([[0], [0], [0], [0]], dtype=uint8) """ ``` ```python # sinter.predict_on_disk # (at top-level in the sinter module) def predict_on_disk( *, decoder: str, dem_path: Union[str, pathlib.Path], dets_path: Union[str, pathlib.Path], dets_format: str, obs_out_path: Union[str, pathlib.Path], obs_out_format: str, postselect_detectors_with_non_zero_4th_coord: bool = False, discards_out_path: Union[str, pathlib.Path, NoneType] = None, discards_out_format: Optional[str] = None, custom_decoders: Dict[str, sinter.Decoder] = None, ) -> None: """Performs decoding and postselection on disk. Args: decoder: The decoder to use for decoding. dem_path: The detector error model to use to configure the decoder. dets_path: Where the detection event data is stored on disk. dets_format: The format the detection event data is stored in (e.g. '01' or 'b8'). obs_out_path: Where to write predicted observable flip data on disk. Note that the predicted observable flip data will not included data from shots discarded by postselection. Use the data in discards_out_path to determine which shots were discarded. obs_out_format: The format to write the observable flip data in (e.g. '01' or 'b8'). postselect_detectors_with_non_zero_4th_coord: Activates postselection. Detectors that have a non-zero 4th coordinate will be postselected. Any shot where a postselected detector fires will be discarded. Requires specifying discards_out_path, for indicating which shots were discarded. discards_out_path: Only used if postselection is being used. Where to write discard data on disk. discards_out_format: The format to write discard data in (e.g. '01' or 'b8'). custom_decoders: Custom decoders that can be used if requested by name. """ ``` ```python # sinter.read_stats_from_csv_files # (at top-level in the sinter module) def read_stats_from_csv_files( *paths_or_files: Any, ) -> List[sinter.TaskStats]: """Reads and aggregates shot statistics from CSV files. Assumes the CSV file was written by printing `sinter.CSV_HEADER` and then a list of `sinter.TaskStats`. When statistics from the same task appear in multiple files (identified by the strong id being the same), the statistics for that task are folded together (so only the total shots, total errors, etc for each task are included in the results). Args: *paths_or_files: Each argument should be either a path (in the form of a string or a pathlib.Path) or a TextIO object (e.g. as returned by `open`). File data is read from each argument. Returns: A list of task stats, where each task appears only once in the list and the stats associated with it are the totals aggregated from all files. Examples: >>> import sinter >>> import io >>> in_memory_file = io.StringIO() >>> _ = in_memory_file.write(''' ... shots,errors,discards,seconds,decoder,strong_id,json_metadata ... 1000,42,0,0.125,pymatching,9c31908e2b,"{""d"":9}" ... 3000,24,0,0.125,pymatching,9c31908e2b,"{""d"":9}" ... 1000,250,0,0.125,pymatching,deadbeef08,"{""d"":7}" ... '''.strip()) >>> _ = in_memory_file.seek(0) >>> stats = sinter.read_stats_from_csv_files(in_memory_file) >>> for stat in stats: ... print(repr(stat)) sinter.TaskStats(strong_id='9c31908e2b', decoder='pymatching', json_metadata={'d': 9}, shots=4000, errors=66, seconds=0.25) sinter.TaskStats(strong_id='deadbeef08', decoder='pymatching', json_metadata={'d': 7}, shots=1000, errors=250, seconds=0.125) """ ``` ```python # sinter.shot_error_rate_to_piece_error_rate # (at top-level in the sinter module) def shot_error_rate_to_piece_error_rate( shot_error_rate: Union[float, sinter.Fit], *, pieces: float, values: float = 1, ) -> Union[float, sinter.Fit]: """Convert from total error rate to per-piece error rate. Args: shot_error_rate: The rate at which shots fail. If this is set to a sinter.Fit, the conversion broadcasts over the low,best,high of the fit. pieces: The number of xor-pieces we want to subdivide each shot into, as if each piece was an independent chance for the shot to fail and the total chance of a shot failing was the xor of each piece failing. values: The number of or-pieces each shot's failure is being formed out of. Returns: Let N = `pieces` (number of rounds) Let V = `values` (number of observables) Let S = `shot_error_rate` Let R = the returned result R satisfies the following property. Let X be the probability of each observable flipping, each round. R will be the probability that any of the observables is flipped after 1 round, given this X. X is chosen to satisfy the following condition. If a Bernoulli distribution with probability X is sampled V*N times, and the results grouped into V groups of N, and each group is reduced to a single value using XOR, and then the reduced group values are reduced to a single final value using OR, then this final value will be True with probability S. Or, in other words, if a shot consists of N rounds which V independent observables must survive, then R is like the per-round failure for any of the observables. Examples: >>> import sinter >>> sinter.shot_error_rate_to_piece_error_rate( ... shot_error_rate=0.1, ... pieces=2, ... ) 0.05278640450004207 >>> sinter.shot_error_rate_to_piece_error_rate( ... shot_error_rate=0.05278640450004207, ... pieces=1 / 2, ... ) 0.10000000000000003 >>> sinter.shot_error_rate_to_piece_error_rate( ... shot_error_rate=1e-9, ... pieces=100, ... ) 1.000000082740371e-11 >>> sinter.shot_error_rate_to_piece_error_rate( ... shot_error_rate=0.6, ... pieces=10, ... values=2, ... ) 0.12052311142021144 """ ``` ```python # sinter.stats_from_csv_files # (at top-level in the sinter module) def stats_from_csv_files( *paths_or_files: Any, ) -> List[sinter.TaskStats]: """Reads and aggregates shot statistics from CSV files. (An old alias of `read_stats_from_csv_files`, kept around for backwards compatibility.) Assumes the CSV file was written by printing `sinter.CSV_HEADER` and then a list of `sinter.TaskStats`. When statistics from the same task appear in multiple files (identified by the strong id being the same), the statistics for that task are folded together (so only the total shots, total errors, etc for each task are included in the results). Args: *paths_or_files: Each argument should be either a path (in the form of a string or a pathlib.Path) or a TextIO object (e.g. as returned by `open`). File data is read from each argument. Returns: A list of task stats, where each task appears only once in the list and the stats associated with it are the totals aggregated from all files. Examples: >>> import sinter >>> import io >>> in_memory_file = io.StringIO() >>> _ = in_memory_file.write(''' ... shots,errors,discards,seconds,decoder,strong_id,json_metadata ... 1000,42,0,0.125,pymatching,9c31908e2b,"{""d"":9}" ... 3000,24,0,0.125,pymatching,9c31908e2b,"{""d"":9}" ... 1000,250,0,0.125,pymatching,deadbeef08,"{""d"":7}" ... '''.strip()) >>> _ = in_memory_file.seek(0) >>> stats = sinter.stats_from_csv_files(in_memory_file) >>> for stat in stats: ... print(repr(stat)) sinter.TaskStats(strong_id='9c31908e2b', decoder='pymatching', json_metadata={'d': 9}, shots=4000, errors=66, seconds=0.25) sinter.TaskStats(strong_id='deadbeef08', decoder='pymatching', json_metadata={'d': 7}, shots=1000, errors=250, seconds=0.125) """ ``` ================================================ FILE: doc/sinter_command_line.md ================================================ # Sinter command line reference ## Index - [sinter collect](#collect) - [sinter combine](#combine) - [sinter plot](#plot) ## Commands ### sinter collect ``` NAME sinter collect SYNOPSIS sinter collect \ --circuits FILEPATH [...] \ --decoders pymatching|fusion_blossom|... [...] \ --processes int|"auto" \ [--max_shots int] \ [--max_errors int] \ [--save_resume_filepath FILEPATH] \ [--metadata_func auto|PYTHON_EXPRESSION] \ \ [--also_print_results_to_stdout] \ [--custom_decoders_module_function PYTHON_EXPRESSION] \ [--existing_data_filepaths FILEPATH [...]] \ [--max_batch_size int] \ [--max_batch_seconds int] \ [--postselected_detectors_predicate PYTHON_EXPRESSION] \ [--postselected_observables_predicate PYTHON_EXPRESSION] \ [--quiet] \ [--count_detection_events] \ [--count_observable_error_combos] \ [--start_batch_size int] \ [--custom_error_count_key NAME] \ [--allowed_cpu_affinity_ids PYTHON_EXPRESSION [ANOTHER_PYTHON_EXPRESSION ...]] DESCRIPTION Uses python multiprocessing to collect shots from the given circuit, decode them using the given decoders, and report CSV statistics on error rates. OPTIONS --circuits FILEPATH [ANOTHER_FILEPATH ...] Circuit files to sample from and decode. This parameter can be given multiple arguments. --decoders NAME [ANOTHER_NAME ...] The decoder to use to predict observables from detection events. --custom_decoders_module_function MODULE_NAME:FUNCTION_NAME Use the syntax "module:function" to "import function from module" and use the result of "function()" as the custom_decoders dictionary. The dictionary must map strings to stim.Decoder instances. --max_shots INT Sampling of a circuit will stop if this many shots have been taken. --max_errors INT Sampling of a circuit will stop if this many errors have been seen. --processes INT Number of processes to use for simultaneous sampling and decoding. --save_resume_filepath FILEPATH Activates MERGE mode. If save_resume_filepath doesn't exist, initializes it with a CSV header. CSV data already at save_resume_filepath counts towards max_shots and max_errors. Collected data is appended to save_resume_filepath. Note that MERGE mode is tolerant to failures: if the process is killed, it can simply be restarted and it will pick up where it left off. Note that MERGE mode is idempotent: if sufficient data has been collected, no additional work is done when run again. --start_batch_size INT Initial number of samples to batch together into one job. Starting small prevents over-sampling of circuits above threshold. The allowed batch size increases exponentially from this starting point. --max_batch_size INT Maximum number of samples to batch together into one job. Bigger values increase the delay between jobs finishing. Smaller values decrease the amount of aggregation of results, increasing the amount of output information. --max_batch_seconds INT Limits number of shots in a batch so that the estimated runtime of the batch is below this amount. --postselect_detectors_with_non_zero_4th_coord Turns on detector postselection. If any detector with a non-zero 4th coordinate fires, the shot is discarded. --postselected_detectors_predicate PYTHON_EXPRESSION Specifies a predicate used to decide which detectors to postselect. When a postselected detector produces a detection event, the shot is discarded instead of being given to the decoder. The number of discarded shots is tracked as a statistic. Available values: index: The unique number identifying the detector, determined by the order of detectors in the circuit file. coords: The coordinate data associated with the detector. An empty tuple, if the circuit file did not specify detector coordinates. metadata: The metadata associated with the task being sampled. Expected expression type: Something that can be given to `bool` to get False (do not postselect) or True (yes postselect). Examples: --postselected_detectors_predicate "coords[2] == 0" --postselected_detectors_predicate "coords[3] < metadata['postselection_level']" --postselected_observables_predicate PYTHON_EXPRESSION Specifies a predicate used to decide which observables to postselect. When a decoder mispredicts a postselected observable, the shot is discarded instead of counting as an error. Available values: index: The index of the observable to postselect or not. metadata: The metadata associated with the task. Expected expression type: Something that can be given to `bool` to get False (do not postselect) or True (yes postselect). Examples: --postselected_observables_predicate "False" --postselected_observables_predicate "metadata['d'] == 5 and index >= 2" --count_observable_error_combos When set, the returned stats will include custom counts like `obs_mistake_mask=E_E__` counting how many times the decoder made each pattern of observable mistakes. --count_detection_events When set, the returned stats will include custom counts `detectors_checked` and `detection_events`. The detection fraction is the ratio of these two numbers. --quiet Disables writing progress to stderr. --also_print_results_to_stdout Even if writing to a file, also write results to stdout. --existing_data_filepaths [FILEPATH ...] CSV data from these files counts towards max_shots and max_errors. This parameter can be given multiple arguments. --metadata_func PYTHON_EXPRESSION A python expression that associates json metadata with a circuit's results. Set to "auto" to use "sinter.comma_separated_key_values(path)". Values available to the expression: path: Relative path to the circuit file, from the command line arguments. circuit: The circuit itself, parsed from the file, as a stim.Circuit. Expected type: A value that can be serialized into JSON, like a Dict[str, int]. Note that the decoder field is already recorded separately, so storing it in the metadata as well would be redundant. But something like decoder version could be usefully added. Examples: --metadata_func "{'path': path}" --metadata_func "auto" --metadata_func "{'n': circuit.num_qubits, 'p': float(path.split('/')[-1].split('.')[0])}" --custom_error_count_key NAME Makes `--max_errors` apply to `stat.custom_counts[key]` instead of to `stat.errors`. --allowed_cpu_affinity_ids PYTHON_EXPRESSION [ANOTHER_PYTHON_EXPRESSION ...], Controls which CPUs workers can be pinned to. By default, any CPU can be pinned to. Specifying this argument makes it so that only the given CPU ids can be pinned. The given arguments will be evaluated as python expressions. The expressions should be integers or iterables of integers. So values like "1" and "[1, 2, 4]" and "range(5, 30)" all work. EXAMPLES Example #1 >>> stim gen --out "d=5,r=5,p=0.01.stim" --code repetition_code --task memory --distance 5 --rounds 5 --before_round_data_depolarization 0.01 >>> stim gen --out "d=7,r=5,p=0.01.stim" --code repetition_code --task memory --distance 7 --rounds 5 --before_round_data_depolarization 0.01 >>> sinter collect \ --processes 4 \ --circuits *.stim \ --metadata_func auto \ --decoders pymatching \ --max_shots 1_000_000 \ --max_errors 1_000 \ --save_resume_filepath stats.csv >>> sinter combine stats.csv shots, errors, discards, seconds,decoder,strong_id,json_metadata 1000000, 12, 0, 0.716,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 1000000, 1, 0, 0.394,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" ``` ### sinter combine ``` NAME sinter combine SYNOPSIS sinter combine \ FILEPATH [...] \ \ [--order preserve|metadata|error] \ [--strip_custom_counts] DESCRIPTION Loads sample statistics from one or more CSV statistics files (produced by `sinter collect`), and aggregates rows corresponding to the same circuit together. OPTIONS filepaths The locations of statistics files with rows aggregate together. --order Decides how to sort the output data. The options are: metadata (default): Orders by the json_metadata column. preserve: Keeps the same order as the input. error: Orders by the error rate. --strip_custom_counts Removes custom_counts data from the output. EXAMPLES Example #1 >>> sinter combine stats.csv shots, errors, discards, seconds,decoder,strong_id,json_metadata 1000000, 12, 0, 0.716,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 1000000, 1, 0, 0.394,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" >>> cat stats.csv shots, errors, discards, seconds,decoder,strong_id,json_metadata 100, 0, 0, 0.174,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 100, 0, 0, 0.000,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 400, 0, 0, 0.000,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 1200, 0, 0, 0.000,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 100, 0, 0, 0.178,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 3600, 0, 0, 0.001,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 3600, 0, 0, 0.001,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 10800, 0, 0, 0.002,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 18000, 0, 0, 0.003,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 39600, 0, 0, 0.007,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 75600, 0, 0, 0.012,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 154800, 2, 0, 0.028,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 306000, 5, 0, 0.051,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 200, 0, 0, 0.000,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 600, 0, 0, 0.001,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 1800, 0, 0, 0.001,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 5400, 0, 0, 0.002,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 16200, 0, 0, 0.004,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 48600, 1, 0, 0.010,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 385800, 5, 0, 0.071,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 145800, 0, 0, 0.030,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 145800, 0, 0, 0.032,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 200, 0, 0, 0.178,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 200, 0, 0, 0.188,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 198100, 0, 0, 0.044,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" 437400, 0, 0, 0.092,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" ``` ### sinter plot ``` NAME sinter plot SYNOPSIS sinter plot \ --in FILEPATH [...] \ [--x_func PYTHON_EXPRESSION] \ [--group_func PYTHON_EXPRESSION] \ [--out FILEPATH] \ [--show] \ \ [--filter_func PYTHON_EXPRESSION] \ [--y_func PYTHON_EXPRESSION] \ [--failure_unit_name string] \ [--failure_units_per_shot_func PYTHON_EXPRESSION] \ [--failure_values_func PYTHON_EXPRESSION] \ [--fig_size float float] \ [--highlight_max_likelihood_factor float] \ [--plot_args_func PYTHON_EXPRESSION] \ [--custom_error_count_keys] \ [--subtitle "{common}"|text] \ [--title text] \ [--type "error_rate"|"discard_rate"|"custom_y" [...] \ [--xaxis "text"|"[log]text"|"[sqrt]text"] \ [--yaxis "text"|"[log]text"|"[sqrt]text"] \ [--xmin float] \ [--xmax float] \ [--ymin float] \ [--line_fits] DESCRIPTION Creates a plot of statistics collected by `sinter collect` by grouping data into curves according to `--group_func`, and laying out points along that curve according to `--x_func`. Plots error rates by default, and also discard rates if there are any discards, but this can be customized by using `--y_func`. OPTIONS --filter_func PYTHON_EXPRESSION A python expression that determines whether a case is kept or not. Values available to the python expression: metadata: The parsed value from the json_metadata for the data point. m: `m.key` is a shorthand for `metadata.get("key", None)`. decoder: The decoder that decoded the data for the data point. strong_id: The cryptographic hash of the case that was sampled for the data point. stat: The sinter.TaskStats object for the data point. Expected expression type: Something that can be given to `bool` to get True or False. Examples: --filter_func "decoder=='pymatching'" --filter_func "0.001 < metadata['p'] < 0.005" --x_func PYTHON_EXPRESSION A python expression that determines where points go on the x axis. Values available to the python expression: metadata: The parsed value from the json_metadata for the data point. m: `m.key` is a shorthand for `metadata.get("key", None)`. decoder: The decoder that decoded the data for the data point. strong_id: The cryptographic hash of the case that was sampled for the data point. stat: The sinter.TaskStats object for the data point. Expected expression type: Something that can be given to `float` to get a float. Examples: --x_func "metadata['p']" --x_func m.p --x_func "metadata['path'].split('/')[-1].split('.')[0]" --y_func PYTHON_EXPRESSION A python expression that determines where points go on the y axis. This argument is not used by error rate or discard rate plots; only by the "custom_y" type plot. Values available to the python expression: metadata: The parsed value from the json_metadata for the data point. m: `m.key` is a shorthand for `metadata.get("key", None)`. decoder: The decoder that decoded the data for the data point. strong_id: The cryptographic hash of the case that was sampled for the data point. stat: The sinter.TaskStats object for the data point. Expected expression type: Something that can be given to `float` to get a float. Examples: --y_func "metadata['p']" --y_func m.p --y_func "metadata['path'].split('/')[-1].split('.')[0]" --fig_size WIDTH HEIGHT Desired figure width and height in pixels. --group_func PYTHON_EXPRESSION A python expression that determines how points are grouped into curves. Values available to the python expression: metadata: The parsed value from the json_metadata for the data point. m: `m.key` is a shorthand for `metadata.get("key", None)`. decoder: The decoder that decoded the data for the data point. strong_id: The cryptographic hash of the case that was sampled for the data point. stat: The sinter.TaskStats object for the data point. Expected expression type: Something that can be given to `str` to get a useful string. Examples: --group_func "(decoder, metadata['d'])" --group_func m.d --group_func "metadata['path'].split('/')[-2]" --failure_unit_name FAILURE_UNIT_NAME The unit of failure, typically either "shot" (the default) or "round". If this argument is specified, --failure_units_per_shot_func must also be specified. --failure_units_per_shot_func PYTHON_EXPRESSION A python expression that evaluates to the number of failure units there are per shot. For example, if the failure unit is rounds, this should be an expression that returns the number of rounds in a shot. Sinter has no way of knowing what you consider a round to be, otherwise. This value is used to rescale the logical error rate plots. For example, if there are 4 failure units per shot then a shot error rate of 10% corresponds to a unit failure rate of 2.7129%. The conversion formula (assuming less than 50% error rates) is: P_unit = 0.5 - 0.5 * (1 - 2 * P_shot)**(1/units_per_shot) Values available to the python expression: metadata: The parsed value from the json_metadata for the data point. m: `m.key` is a shorthand for `metadata.get("key", None)`. decoder: The decoder that decoded the data for the data point. strong_id: The cryptographic hash of the case that was sampled for the data point. stat: The sinter.TaskStats object for the data point. Expected expression type: float. Examples: --failure_units_per_shot_func "metadata['rounds']" --failure_units_per_shot_func m.r --failure_units_per_shot_func "m.distance * 3" --failure_units_per_shot_func "10" --failure_values_func FAILURE_VALUES_FUNC A python expression that evaluates to the number of independent ways a shot can fail. For example, if a shot corresponds to a memory experiment preserving two observables, then the failure unions is 2. This value is necessary to correctly rescale the logical error rate plots when using --failure_values_func. By default it is assumed to be 1. Values available to the python expression: metadata: The parsed value from the json_metadata for the data point. m: `m.key` is a shorthand for `metadata.get("key", None)`. decoder: The decoder that decoded the data for the data point. strong_id: The cryptographic hash of the case that was sampled for the data point. stat: The sinter.TaskStats object for the data point. Expected expression type: float. Examples: --failure_values_func "metadata['num_obs']" --failure_values_func "2" --plot_args_func PLOT_ARGS_FUNC A python expression used to customize the look of curves. Values available to the python expression: index: A unique integer identifying the curve. key: The group key (returned from --group_func) identifying the curve. stats: The list of sinter.TaskStats object in the group. metadata: (From one arbitrary data point in the group.) The parsed value from the json_metadata for the data point. m: `m.key` is a shorthand for `metadata.get("key", None)`. decoder: (From one arbitrary data point in the group.) The decoder that decoded the data for the data point. strong_id: (From one arbitrary data point in the group.) The cryptographic hash of the case that was sampled for the data point. stat: (From one arbitrary data point in the group.) The sinter.TaskStats object for the data point. Expected expression type: A dictionary to give to matplotlib plotting functions as a **kwargs argument. Examples: --plot_args_func "{'label': 'curve #' + str(index), 'linewidth': 5}" --plot_args_func "{'marker': 'ov*sp^<>8PhH+xXDd|'[index % 18]}" --in IN [IN ...] Input files to get data from. --type {error_rate,discard_rate,custom_y} [{error_rate,discard_rate,custom_y} ...] Picks the figures to include. --out OUT Output file to write the plot to. The file extension determines the type of image. Either this or --show must be specified. --xaxis XAXIS Customize the X axis label. Prefix [log] for logarithmic scale. Prefix [sqrt] for square root scale. --yaxis YAXIS Customize the Y axis label. Prefix [log] for logarithmic scale. Prefix [sqrt] for square root scale. --split_custom_counts When a stat has custom counts, this splits it into multiple copies of the stat with each one having exactly one of the custom counts. --show Displays the plot in a window. Either this or --out must be specified. --ymin float Sets the minimum value of the y axis (max always 1). --xmin float Forces the minimum value of the x axis. --xmax float Forces the maximum value of the x axis. --title text Sets the title of the plot. --subtitle SUBTITLE Sets the subtitle of the plot. Note: The pattern "{common}" will expand to text including all json metadata values that are the same across all stats. --highlight_max_likelihood_factor HIGHLIGHT_MAX_LIKELIHOOD_FACTOR The relative likelihood ratio that determines the color highlights around curves. Set this to 1 or larger. Set to 1 to disable highlighting. --line_fits: Adds a least-squared line fit to each curve. Note that the fit is always done according to the scaling of the plot. For example, on a semilog y plot it will be fitting a line to (x, log(y)). Data points are not weighted in any way. On a log plot, data points at infinity are not included in the fit. EXAMPLES Example #1 >>> cat stats.csv shots, errors, discards, seconds,decoder,strong_id,json_metadata 1000000, 12, 0, 0.716,pymatching,41fe89ff6c51e598d51846cb7a2b626fbfcaa76adcd1be9e0f1e2dff1fe87f1c,"{""d"":5,""p"":0.01,""r"":5}" 1000000, 1, 0, 0.394,pymatching,639d47a421d2a7661bb5b19255295767fc7cf0be7592fe4bcbc2639068e21349,"{""d"":7,""p"":0.01,""r"":5}" >>> sinter plot \ --in stats.csv \ --show \ --x_func m.d \ --group_func "f'''rounds={m.r}'''" \ --xaxis "[log]distance" ``` ================================================ FILE: doc/stim.pyi ================================================ """Stim (Development Version): a fast quantum stabilizer circuit library.""" # (This is a stubs file describing the classes and methods in stim.) from __future__ import annotations from typing import overload, TYPE_CHECKING, List, Dict, Tuple, Any, Union, Iterable, Optional, Sequence, Literal if TYPE_CHECKING: import io import pathlib import numpy as np import stim class Circuit: """A mutable stabilizer circuit. The stim.Circuit class is arguably the most important object in the entire library. It is the interface through which you explain a noisy quantum computation to Stim, in order to do fast bulk sampling or fast error analysis. For example, suppose you want to use a matching-based decoder on a new quantum error correction construction. Stim can help you do this but the very first step is to create a circuit implementing the construction. Once you have the circuit you can then use methods like stim.Circuit.detector_error_model() to create an object that can be used to configure the decoder, or like stim.Circuit.compile_detector_sampler() to produce problems for the decoder to solve, or like stim.Circuit.shortest_graphlike_error() to check for mistakes in the implementation of the code. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append("X", 0) >>> c.append("M", 0) >>> c.compile_sampler().sample(shots=1) array([[ True]]) >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''').compile_detector_sampler().sample(shots=1) array([[False]]) """ def __add__( self, second: stim.Circuit, ) -> stim.Circuit: """Creates a circuit by appending two circuits. Examples: >>> import stim >>> c1 = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c2 = stim.Circuit(''' ... M 0 1 2 ... ''') >>> c1 + c2 stim.Circuit(''' X 0 Y 1 2 M 0 1 2 ''') """ def __eq__( self, arg0: stim.Circuit, ) -> bool: """Determines if two circuits have identical contents. """ @overload def __getitem__( self, index_or_slice: int, ) -> Union[stim.CircuitInstruction, stim.CircuitRepeatBlock]: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.Circuit: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns copies of instructions from the circuit. Args: index_or_slice: An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a circuit. Returns: If the index was an integer, then an instruction from the circuit. If the index was a slice, then a circuit made up of the instructions in that slice. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X 0 ... X_ERROR(0.5) 2 ... REPEAT 100 { ... X 0 ... Y 1 2 ... } ... TICK ... M 0 ... DETECTOR rec[-1] ... ''') >>> circuit[1] stim.CircuitInstruction('X_ERROR', [stim.GateTarget(2)], [0.5]) >>> circuit[2] stim.CircuitRepeatBlock(100, stim.Circuit(''' X 0 Y 1 2 ''')) >>> circuit[1::2] stim.Circuit(''' X_ERROR(0.5) 2 TICK DETECTOR rec[-1] ''') """ def __iadd__( self, second: stim.Circuit, ) -> stim.Circuit: """Appends a circuit into the receiving circuit (mutating it). Examples: >>> import stim >>> c1 = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c2 = stim.Circuit(''' ... M 0 1 2 ... ''') >>> c1 += c2 >>> print(repr(c1)) stim.Circuit(''' X 0 Y 1 2 M 0 1 2 ''') """ def __imul__( self, repetitions: int, ) -> stim.Circuit: """Mutates the circuit by putting its contents into a REPEAT block. Special case: if the repetition count is 0, the circuit is cleared. Special case: if the repetition count is 1, nothing happens. Args: repetitions: The number of times the REPEAT block should repeat. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c *= 3 >>> print(repr(c)) stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ def __init__( self, stim_program_text: str = '', ) -> None: """Creates a stim.Circuit. Args: stim_program_text: Defaults to empty. Describes operations to append into the circuit. Examples: >>> import stim >>> empty = stim.Circuit() >>> not_empty = stim.Circuit(''' ... X 0 ... CNOT 0 1 ... M 1 ... ''') """ def __len__( self, ) -> int: """Returns the number of top-level instructions and blocks in the circuit. Instructions inside of blocks are not included in this count. Examples: >>> import stim >>> len(stim.Circuit()) 0 >>> len(stim.Circuit(''' ... X 0 ... X_ERROR(0.5) 1 2 ... TICK ... M 0 ... DETECTOR rec[-1] ... ''')) 5 >>> len(stim.Circuit(''' ... REPEAT 100 { ... X 0 ... Y 1 2 ... } ... ''')) 1 """ def __mul__( self, repetitions: int, ) -> stim.Circuit: """Repeats the circuit using a REPEAT block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the REPEAT block should repeat. Returns: repetitions=0: An empty circuit. repetitions=1: A copy of this circuit. repetitions>=2: A circuit with a single REPEAT block, where the contents of that repeat block are this circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c * 3 stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ def __ne__( self, arg0: stim.Circuit, ) -> bool: """Determines if two circuits have non-identical contents. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.Circuit`. """ def __rmul__( self, repetitions: int, ) -> stim.Circuit: """Repeats the circuit using a REPEAT block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the REPEAT block should repeat. Returns: repetitions=0: An empty circuit. repetitions=1: A copy of this circuit. repetitions>=2: A circuit with a single REPEAT block, where the contents of that repeat block are this circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> 3 * c stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ def __str__( self, ) -> str: """Returns stim instructions (that can be saved to a file and parsed by stim) for the current circuit. """ @overload def append( self, name: str, targets: Union[int, stim.GateTarget, stim.PauliString, Iterable[Union[int, stim.GateTarget, stim.PauliString]]], arg: Union[float, Iterable[float], None] = None, *, tag: str = "", ) -> None: pass @overload def append( self, name: Union[stim.CircuitInstruction, stim.CircuitRepeatBlock, stim.Circuit], ) -> None: pass def append( self, name: object, targets: object = (), arg: object = None, *, tag: str = '', ) -> None: """Appends an operation into the circuit. Note: `stim.Circuit.append_operation` is an alias of `stim.Circuit.append`. Args: name: The name of the operation's gate (e.g. "H" or "M" or "CNOT"). This argument can also be set to a `stim.CircuitInstruction` or `stim.CircuitInstructionBlock`, which results in the instruction or block being appended to the circuit. The other arguments (targets and arg) can't be specified when doing so. (The argument being called `name` is no longer quite right, but is being kept for backwards compatibility.) targets: The objects operated on by the gate. This can be either a single target or an iterable of multiple targets. Each target can be: An int: The index of a targeted qubit. A `stim.GateTarget`: Could be a variety of things. Methods like `stim.target_rec`, `stim.target_sweet`, `stim.target_x`, and `stim.CircuitInstruction.__getitem__` all return this type. A `stim.PauliString`: This will automatically be expanded into a product of pauli targets like `X1*Y2*Z3`. arg: The "parens arguments" for the gate, such as the probability for a noise operation. A double or list of doubles parameterizing the gate. Different gates take different parens arguments. For example, X_ERROR takes a probability, OBSERVABLE_INCLUDE takes an observable index, and PAULI_CHANNEL_1 takes three disjoint probabilities. Note: Defaults to no parens arguments. Except, for backwards compatibility reasons, `cirq.append_operation` (but not `cirq.append`) will default to a single 0.0 argument for gates that take exactly one argument. tag: A customizable string attached to the instruction. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append("X", 0) >>> c.append("H", [0, 1]) >>> c.append("M", [0, stim.target_inv(1)]) >>> c.append("CNOT", [stim.target_rec(-1), 0]) >>> c.append("X_ERROR", [0], 0.125) >>> c.append("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(2)], 0.25) >>> c.append("MPP", [stim.PauliString("X1*Y2"), stim.GateTarget("Z3")]) >>> print(repr(c)) stim.Circuit(''' X 0 H 0 1 M 0 !1 CX rec[-1] 0 X_ERROR(0.125) 0 E(0.25) X0 Y2 MPP X1*Y2 Z3 ''') """ def append_from_stim_program_text( self, stim_program_text: str, ) -> None: """Appends operations described by a STIM format program into the circuit. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append_from_stim_program_text(''' ... H 0 # comment ... CNOT 0 2 ... ... M 2 ... CNOT rec[-1] 1 ... ''') >>> print(c) H 0 CX 0 2 M 2 CX rec[-1] 1 Args: stim_program_text: The STIM program text containing the circuit operations to append. """ def append_operation( self, name: object, targets: object = (), arg: object = None, *, tag: str = '', ) -> None: """[DEPRECATED] use stim.Circuit.append instead """ def approx_equals( self, other: object, *, atol: float, ) -> bool: """Checks if a circuit is approximately equal to another circuit. Two circuits are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example, `X_ERROR(0.100) 0` is approximately equal to `X_ERROR(0.099)` within an absolute tolerance of 0.002. All other details of the circuits (such as the ordering of instructions and targets) must be exactly the same. Args: other: The circuit, or other object, to compare to this one. atol: The absolute error tolerance. The maximum amount each probability may have been perturbed by. Returns: True if the given object is a circuit approximately equal up to the receiving circuit up to the given tolerance, otherwise False. Examples: >>> import stim >>> base = stim.Circuit(''' ... X_ERROR(0.099) 0 1 2 ... M 0 1 2 ... ''') >>> base.approx_equals(base, atol=0) True >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0) False >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0.0001) False >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0.01) True >>> base.approx_equals(stim.Circuit(''' ... DEPOLARIZE1(0.099) 0 1 2 ... MRX 0 1 2 ... '''), atol=9999) False """ def clear( self, ) -> None: """Clears the contents of the circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c.clear() >>> c stim.Circuit() """ def compile_detector_sampler( self, *, seed: object = None, ) -> stim.CompiledDetectorSampler: """Returns an object that can batch sample detection events from the circuit. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[False]]) """ def compile_m2d_converter( self, *, skip_reference_sample: bool = False, ) -> stim.CompiledMeasurementsToDetectionEventsConverter: """Creates a measurement-to-detection-event converter for the given circuit. The converter can efficiently compute detection events and observable flips from raw measurement data. The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is. Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit. Args: skip_reference_sample: Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution). Returns: An initialized stim.CompiledMeasurementsToDetectionEventsConverter. Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> converter.convert( ... measurements=np.array([[0], [1]], dtype=np.bool_), ... append_observables=False, ... ) array([[ True], [False]]) """ def compile_sampler( self, *, skip_reference_sample: bool = False, seed: Optional[int] = None, reference_sample: Optional[np.ndarray] = None, ) -> stim.CompiledMeasurementSampler: """Returns an object that can quickly batch sample measurements from the circuit. Args: skip_reference_sample: Defaults to False. When set to True, the reference sample used by the sampler is initialized to all-zeroes instead of being collected from the circuit. This means that the results returned by the sampler are actually whether or not each measurement was *flipped*, instead of true measurement results. Forcing an all-zero reference sample is useful when you are only interested in error propagation and don't want to have to deal with the fact that some measurements want to be On when no errors occur. It is also useful when you know for sure that the all-zero result is actually a possible result from the circuit (under noiseless execution), meaning it is a valid reference sample as good as any other. Computing the reference sample is the most time consuming and memory intensive part of simulating the circuit, so promising that the simulator can safely skip that step is an effective optimization. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. reference_sample: The data to xor into the measurement flips produced by the frame simulator, in order to produce proper measurement results. This can either be specified as an `np.bool_` array or a bit packed `np.uint8` array (little endian). Under normal conditions, the reference sample should be a valid noiseless sample of the circuit, such as the one returned by `circuit.reference_sample()`. If this argument is not provided, the reference sample will be set to `circuit.reference_sample()`, unless `skip_reference_sample=True` is used, in which case it will be set to all-zeros. Raises: ValueError: skip_reference_sample is True and reference_sample is not None. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 2 ... M 0 1 2 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[False, False, True]]) """ def copy( self, ) -> stim.Circuit: """Returns a copy of the circuit. An independent circuit with the same contents. Examples: >>> import stim >>> c1 = stim.Circuit("H 0") >>> c2 = c1.copy() >>> c2 is c1 False >>> c2 == c1 True """ def count_determined_measurements( self, *, unknown_input: bool = False, ) -> int: """Counts the number of predictable measurements in the circuit. This method ignores any noise in the circuit. This method works by performing a tableau stabilizer simulation of the circuit and, before each measurement is simulated, checking if its expectation is non-zero. A measurement is predictable if its result can be predicted by using other measurements that have already been performed, assuming the circuit is executed without any noise. Note that, when multiple measurements occur at the same time, re-ordering the order they are resolved can change which specific measurements are predictable but won't change how many of them were predictable in total. The number of predictable measurements is a useful quantity because it's related to the number of detectors and observables that a circuit should declare. If circuit.num_detectors + circuit.num_observables is less than circuit.count_determined_measurements(), this is a warning sign that you've missed some detector declarations. The exact relationship between the number of determined measurements and the number of detectors and observables can differ from code to code. For example, the toric code has an extra redundant measurement compared to the surface code because in the toric code the last X stabilizer to be measured is equal to the product of all other X stabilizers even in the first round when initializing in the Z basis. Typically this relationship is not declared as a detector, because it's not local, or as an observable, because it doesn't store a qubit. Args: unknown_input: Defaults to False (inputs assumed to be in the |0> state). When set to True, the inputs are instead treated as being in unknown random states. For example, this means that Z-basis measurements at the very beginning of the circuit will be considered random rather than determined. Returns: The number of measurements that were predictable. Examples: >>> import stim >>> stim.Circuit(''' ... R 0 ... M 0 ... ''').count_determined_measurements() 1 >>> stim.Circuit(''' ... R 0 ... H 0 ... M 0 ... ''').count_determined_measurements() 0 >>> stim.Circuit(''' ... M 0 ... ''').count_determined_measurements() 1 >>> stim.Circuit(''' ... M 0 ... ''').count_determined_measurements(unknown_input=True) 0 >>> stim.Circuit(''' ... M 0 ... M 0 1 ... M 0 1 2 ... M 0 1 2 3 ... ''').count_determined_measurements(unknown_input=True) 6 >>> stim.Circuit(''' ... R 0 1 ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... ''').count_determined_measurements() 2 >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... distance=5, ... rounds=9, ... ) >>> circuit.count_determined_measurements() 217 >>> circuit.num_detectors + circuit.num_observables 217 """ def decomposed( self, ) -> stim.Circuit: """Recreates the circuit using (mostly) the {H,S,CX,M,R} gate set. The intent of this method is to simplify the circuit to use fewer gate types, so it's easier for other tools to consume. Currently, this method performs the following simplifications: - Single qubit cliffords are decomposed into {H,S}. - Multi-qubit cliffords are decomposed into {H,S,CX}. - Single qubit dissipative gates are decomposed into {H,S,M,R}. - Multi-qubit dissipative gates are decomposed into {H,S,CX,M,R}. Currently, the following types of gate *aren't* simplified, but they may be in the future: - Noise instructions (like X_ERROR, DEPOLARIZE2, and E). - Annotations (like TICK, DETECTOR, and SHIFT_COORDS). - The MPAD instruction. - Repeat blocks are not flattened. Returns: A `stim.Circuit` whose function is equivalent to the original circuit, but with most gates decomposed into the {H,S,CX,M,R} gate set. Examples: >>> import stim >>> stim.Circuit(''' ... SWAP 0 1 ... ''').decomposed() stim.Circuit(''' CX 0 1 1 0 0 1 ''') >>> stim.Circuit(''' ... ISWAP 0 1 2 1 ... TICK ... MPP !X1*Y2*Z3 ... ''').decomposed() stim.Circuit(''' H 0 CX 0 1 1 0 H 1 S 1 0 H 2 CX 2 1 1 2 H 1 S 1 2 TICK H 1 2 S 2 H 2 S 2 2 CX 2 1 3 1 M !1 CX 2 1 3 1 H 2 S 2 H 2 S 2 2 H 1 ''') """ def detecting_regions( self, *, targets: Optional[Iterable[stim.DemTarget | str | Iterable[float]]] = None, ticks: Optional[Iterable[int]] = None, ) -> Dict[stim.DemTarget, Dict[int, stim.PauliString]]: """Records where detectors and observables are sensitive to errors over time. The result of this method is a nested dictionary, mapping detectors/observables and ticks to Pauli sensitivities for that detector/observable at that time. For example, if observable 2 has Z-type sensitivity on qubits 5 and 6 during tick 3, then `result[stim.target_logical_observable_id(2)][3]` will be equal to `stim.PauliString("Z5*Z6")`. If you want sensitivities from more places in the circuit, besides just at the TICK instructions, you can work around this by making a version of the circuit with more TICKs. Args: targets: Defaults to everything (None). When specified, this should be an iterable of filters where items matching any one filter are included. A variety of filters are supported: stim.DemTarget: Includes the targeted detector or observable. Iterable[float]: Coordinate prefix match. Includes detectors whose coordinate data begins with the same floats. "D": Includes all detectors. "L": Includes all observables. "D#" (e.g. "D5"): Includes the detector with the specified index. "L#" (e.g. "L5"): Includes the observable with the specified index. ticks: Defaults to everything (None). When specified, this should be a list of integers corresponding to the tick indices to report sensitivities for. ignore_anticommutation_errors: Defaults to False. When set to False, invalid detecting regions that anticommute with a reset will cause the method to raise an exception. When set to True, the offending component will simply be silently dropped. This can result in broken detectors having apparently enormous detecting regions. Returns: Nested dictionaries keyed first by a `stim.DemTarget` identifying the detector or observable, then by the index of the tick, leading to a PauliString with that target's error sensitivity at that tick. Note you can use `stim.PauliString.pauli_indices` to quickly get to the non-identity terms in the sensitivity. Examples: >>> import stim >>> detecting_regions = stim.Circuit(''' ... R 0 ... TICK ... H 0 ... TICK ... CX 0 1 ... TICK ... MX 0 1 ... DETECTOR rec[-1] rec[-2] ... ''').detecting_regions() >>> for target, tick_regions in detecting_regions.items(): ... print("target", target) ... for tick, sensitivity in tick_regions.items(): ... print(" tick", tick, "=", sensitivity) target D0 tick 0 = +Z_ tick 1 = +X_ tick 2 = +XX >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... rounds=5, ... distance=4, ... ) >>> detecting_regions = circuit.detecting_regions( ... targets=["L0", (2, 4), stim.DemTarget.relative_detector_id(5)], ... ticks=range(5, 15), ... ) >>> for target, tick_regions in detecting_regions.items(): ... print("target", target) ... for tick, sensitivity in tick_regions.items(): ... print(" tick", tick, "=", sensitivity) target D1 tick 5 = +____________________X______________________ tick 6 = +____________________Z______________________ target D5 tick 5 = +______X____________________________________ tick 6 = +______Z____________________________________ target D14 tick 5 = +__________X_X______XXX_____________________ tick 6 = +__________X_X______XZX_____________________ tick 7 = +__________X_X______XZX_____________________ tick 8 = +__________X_X______XXX_____________________ tick 9 = +__________XXX_____XXX______________________ tick 10 = +__________XXX_______X______________________ tick 11 = +__________X_________X______________________ tick 12 = +____________________X______________________ tick 13 = +____________________Z______________________ target D29 tick 7 = +____________________Z______________________ tick 8 = +____________________X______________________ tick 9 = +____________________XX_____________________ tick 10 = +___________________XXX_______X_____________ tick 11 = +____________X______XXXX______X_____________ tick 12 = +__________X_X______XXX_____________________ tick 13 = +__________X_X______XZX_____________________ tick 14 = +__________X_X______XZX_____________________ target D44 tick 14 = +____________________Z______________________ target L0 tick 5 = +_X________X________X________X______________ tick 6 = +_X________X________X________X______________ tick 7 = +_X________X________X________X______________ tick 8 = +_X________X________X________X______________ tick 9 = +_X________X_______XX________X______________ tick 10 = +_X________X________X________X______________ tick 11 = +_X________XX_______X________XX_____________ tick 12 = +_X________X________X________X______________ tick 13 = +_X________X________X________X______________ tick 14 = +_X________X________X________X______________ """ def detector_error_model( self, *, decompose_errors: bool = False, flatten_loops: bool = False, allow_gauge_detectors: bool = False, approximate_disjoint_errors: float = False, ignore_decomposition_failures: bool = False, block_decomposition_from_introducing_remnant_edges: bool = False, ) -> stim.DetectorErrorModel: """Returns a stim.DetectorErrorModel describing the error processes in the circuit. Args: decompose_errors: Defaults to false. When set to true, the error analysis attempts to decompose the components of composite error mechanisms (such as depolarization errors) into simpler errors, and suggest this decomposition via `stim.target_separator()` between the components. For example, in an XZ surface code, single qubit depolarization has a Y error term which can be decomposed into simpler X and Z error terms. Decomposition fails (causing this method to throw) if it's not possible to decompose large errors into simple errors that affect at most two detectors. flatten_loops: Defaults to false. When set to True, the output will not contain any `repeat` blocks. When set to False, the error analysis watches for loops in the circuit reaching a periodic steady state with respect to the detectors being introduced, the error mechanisms that affect them, and the locations of the logical observables. When it identifies such a steady state, it outputs a repeat block. This is massively more efficient than flattening for circuits that contain loops, but creates a more complex output. allow_gauge_detectors: Defaults to false. When set to false, the error analysis verifies that detectors in the circuit are actually deterministic under noiseless execution of the circuit. When set to True, these detectors are instead considered to be part of degrees freedom that can be removed from the error model. For example, if detectors D1 and D3 both anti-commute with a reset, then the error model has a gauge `error(0.5) D1 D3`. When gauges are identified, one of the involved detectors is removed from the system using Gaussian elimination. Note that logical observables are still verified to be deterministic, even if this option is set. approximate_disjoint_errors: Defaults to false. When set to false, composite error mechanisms with disjoint components (such as `PAULI_CHANNEL_1(0.1, 0.2, 0.0)`) can cause the error analysis to throw exceptions (because detector error models can only contain independent error mechanisms). When set to true, the probabilities of the disjoint cases are instead assumed to be independent probabilities. For example, a `PAULI_CHANNEL_1(0.1, 0.2, 0.0)` becomes equivalent to an `X_ERROR(0.1)` followed by a `Y_ERROR(0.2)`. This assumption is an approximation, but it is a good approximation for small probabilities. This argument can also be set to a probability between 0 and 1, setting a threshold below which the approximation is acceptable. Any error mechanisms that have a component probability above the threshold will cause an exception to be thrown. ignore_decomposition_failures: Defaults to False. When this is set to True, circuit errors that fail to decompose into graphlike detector error model errors no longer cause the conversion process to abort. Instead, the undecomposed error is inserted into the output. Whatever tool the detector error model is then given to is responsible for dealing with the undecomposed errors (e.g. a tool may choose to simply ignore them). Irrelevant unless decompose_errors=True. block_decomposition_from_introducing_remnant_edges: Defaults to False. Requires that both A B and C D be present elsewhere in the detector error model in order to decompose A B C D into A B ^ C D. Normally, only one of A B or C D needs to appear to allow this decomposition. Remnant edges can be a useful feature for ensuring decomposition succeeds, but they can also reduce the effective code distance by giving the decoder single edges that actually represent multiple errors in the circuit (resulting in the decoder making misinformed choices when decoding). Irrelevant unless decompose_errors=True. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model() stim.DetectorErrorModel(''' error(0.125) D0 error(0.375) D0 D1 error(0.25) D1 ''') """ def diagram( self, type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "interactive", "interactive-html"] = 'timeline-text', *, tick: Union[None, int, range] = None, filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), rows: int | None = None, ) -> 'stim._DiagramHelper': """Returns a diagram of the circuit, from a variety of options. Args: type: The type of diagram. Available types are: "timeline-text" (default): An ASCII diagram of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-svg": An SVG image of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-svg-html": A resizable SVG image viewer of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-3d": A 3d model, in GLTF format, of the operations applied by the circuit over time. "timeline-3d-html": Same 3d model as 'timeline-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. "detslice-text": An ASCII diagram of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. "detslice-svg": An SVG image of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. For example, a detector slice diagram of a CSS surface code circuit during the TICK between a measurement layer and a reset layer will produce the usual diagram of a surface code. Uses the Pauli color convention XYZ=RGB. "detslice-svg-html": Same as detslice-svg but the SVG image is inside a resizable HTML iframe. "matchgraph-svg": An SVG image of the match graph extracted from the circuit by stim.Circuit.detector_error_model. "matchgraph-svg-html": Same as matchgraph-svg but the SVG image is inside a resizable HTML iframe. "matchgraph-3d": An 3D model of the match graph extracted from the circuit by stim.Circuit.detector_error_model. "matchgraph-3d-html": Same 3d model as 'match-graph-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. "timeslice-svg": An SVG image of the operations applied between two TICK instructions in the circuit, with the operations laid out in 2d. "timeslice-svg-html": Same as timeslice-svg but the SVG image is inside a resizable HTML iframe. "detslice-with-ops-svg": A combination of timeslice-svg and detslice-svg, with the operations overlaid over the detector slices taken from the TICK after the operations were applied. "detslice-with-ops-svg-html": Same as detslice-with-ops-svg but the SVG image is inside a resizable HTML iframe. "interactive" or "interactive-html": An HTML web page containing Crumble (an interactive editor for 2D stabilizer circuits) initialized with the given circuit as its default contents. tick: Required for detector and time slice diagrams. Specifies which TICK instruction, or range of TICK instructions, to slice at. Note that the first TICK instruction in the circuit corresponds tick=1. The value tick=0 refers to the very start of the circuit. Passing `range(A, B)` for a detector slice will show the slices for ticks A through B including A but excluding B. Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. rows: In diagrams that have multiple separate pieces, such as timeslice diagrams and detslice diagrams, this controls how many rows of pieces there will be. If not specified, a number of rows that creates a roughly square layout will be chosen. filter_coords: A list of things to include in the diagram. Different effects depending on the diagram. For detslice diagrams, the filter defaults to showing all detectors and no observables. When specified, each list entry can be a collection of floats (detectors whose coordinates start with the same numbers will be included), a stim.DemTarget (specifying a detector or observable to include), a string like "D5" or "L0" specifying a detector or observable to include. Returns: An object whose `__str__` method returns the diagram, so that writing the diagram to a file works correctly. The returned object may also define methods such as `_repr_html_`, so that ipython notebooks recognize it can be shown using a specialized viewer instead of as raw text. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... CNOT 0 1 1 2 ... ''') >>> print(circuit.diagram()) q0: -H-@--- | q1: ---X-@- | q2: -----X- >>> circuit = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... TICK ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.diagram("detslice-text", tick=1)) q0: -Z:D0- | q1: -Z:D0- """ def explain_detector_error_model_errors( self, *, dem_filter: object = None, reduce_to_one_representative_error: bool = False, ) -> List[stim.ExplainedError]: """Explains how detector error model errors are produced by circuit errors. Args: dem_filter: Defaults to None (unused). When used, the output will only contain detector error model errors that appear in the given `stim.DetectorErrorModel`. Any error mechanisms from the detector error model that can't be reproduced using one error from the circuit will also be included in the result, but with an empty list of associated circuit error mechanisms. reduce_to_one_representative_error: Defaults to False. When True, the items in the result will contain at most one circuit error mechanism. Returns: A `List[stim.ExplainedError]` (see `stim.ExplainedError` for more information). Each item in the list describes how a detector error model error can be produced by individual circuit errors. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... # Create Bell pair. ... H 0 ... CNOT 0 1 ... ... # Noise. ... DEPOLARIZE1(0.01) 0 ... ... # Bell basis measurement. ... CNOT 0 1 ... H 0 ... M 0 1 ... ... # Both measurements should be False under noiseless execution. ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... ''') >>> explained_errors = circuit.explain_detector_error_model_errors( ... dem_filter=stim.DetectorErrorModel('error(1) D0 D1'), ... reduce_to_one_representative_error=True, ... ) >>> print(explained_errors[0].circuit_error_locations[0]) CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 0 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.01) 0 } """ def flattened( self, ) -> stim.Circuit: """Creates an equivalent circuit without REPEAT or SHIFT_COORDS. Returns: A `stim.Circuit` with the same instructions in the same order, but with loops flattened into repeated instructions and with all coordinate shifts inlined. Examples: >>> import stim >>> stim.Circuit(''' ... REPEAT 5 { ... MR 0 1 ... DETECTOR(0, 0) rec[-2] ... DETECTOR(1, 0) rec[-1] ... SHIFT_COORDS(0, 1) ... } ... ''').flattened() stim.Circuit(''' MR 0 1 DETECTOR(0, 0) rec[-2] DETECTOR(1, 0) rec[-1] MR 0 1 DETECTOR(0, 1) rec[-2] DETECTOR(1, 1) rec[-1] MR 0 1 DETECTOR(0, 2) rec[-2] DETECTOR(1, 2) rec[-1] MR 0 1 DETECTOR(0, 3) rec[-2] DETECTOR(1, 3) rec[-1] MR 0 1 DETECTOR(0, 4) rec[-2] DETECTOR(1, 4) rec[-1] ''') """ def flattened_operations( self, ) -> list: """[DEPRECATED] Returns a list of tuples encoding the contents of the circuit. Instead of this method, use `for instruction in circuit` or, to avoid REPEAT blocks, `for instruction in circuit.flattened()`. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... X_ERROR(0.125) 1 ... M 0 !1 ... ''').flattened_operations() [('H', [0], 0), ('X_ERROR', [1], 0.125), ('M', [0, ('inv', 1)], 0)] >>> stim.Circuit(''' ... REPEAT 2 { ... H 6 ... } ... ''').flattened_operations() [('H', [6], 0), ('H', [6], 0)] """ def flow_generators( self, ) -> List[stim.Flow]: """Returns a list of flows that generate all of the circuit's flows. Every stabilizer flow that the circuit implements is a product of some subset of the returned generators. Every returned flow will be a flow of the circuit. Returns: A list of flow generators for the circuit. Examples: >>> import stim >>> stim.Circuit("H 0").flow_generators() [stim.Flow("X -> Z"), stim.Flow("Z -> X")] >>> stim.Circuit("M 0").flow_generators() [stim.Flow("1 -> Z xor rec[0]"), stim.Flow("Z -> rec[0]")] >>> stim.Circuit("RX 0").flow_generators() [stim.Flow("1 -> X")] >>> for flow in stim.Circuit("MXX 0 1").flow_generators(): ... print(flow) 1 -> XX xor rec[0] _X -> _X X_ -> _X xor rec[0] ZZ -> ZZ >>> for flow in stim.Circuit.generated( ... "repetition_code:memory", ... rounds=2, ... distance=3, ... after_clifford_depolarization=1e-3, ... ).flow_generators(): ... print(flow) 1 -> rec[0] 1 -> rec[1] 1 -> rec[2] 1 -> rec[3] 1 -> rec[4] 1 -> rec[5] 1 -> rec[6] 1 -> ____Z 1 -> ___Z_ 1 -> __Z__ 1 -> _Z___ 1 -> Z____ """ @staticmethod def from_file( file: Union[io.TextIOBase, str, pathlib.Path], ) -> stim.Circuit: """Reads a stim circuit from a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md Args: file: A file path or open file object to read from. Returns: The circuit parsed from the file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('H 5', file=f) ... circuit = stim.Circuit.from_file(path) >>> circuit stim.Circuit(''' H 5 ''') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('CNOT 4 5', file=f) ... with open(path) as f: ... circuit = stim.Circuit.from_file(f) >>> circuit stim.Circuit(''' CX 4 5 ''') """ @staticmethod def generated( code_task: str, *, distance: int, rounds: int, after_clifford_depolarization: float = 0.0, before_round_data_depolarization: float = 0.0, before_measure_flip_probability: float = 0.0, after_reset_flip_probability: float = 0.0, ) -> stim.Circuit: """Generates common circuits. The generated circuits can include configurable noise. The generated circuits include DETECTOR and OBSERVABLE_INCLUDE annotations so that their detection events and logical observables can be sampled. The generated circuits include TICK annotations to mark the progression of time. (E.g. so that converting them using `stimcirq.stim_circuit_to_cirq_circuit` will produce a `cirq.Circuit` with the intended moment structure.) Args: code_task: A string identifying the type of circuit to generate. Available code tasks are: - "repetition_code:memory" - "surface_code:rotated_memory_x" - "surface_code:rotated_memory_z" - "surface_code:unrotated_memory_x" - "surface_code:unrotated_memory_z" - "color_code:memory_xyz" distance: The desired code distance of the generated circuit. The code distance is the minimum number of physical errors needed to cause a logical error. This parameter indirectly determines how many qubits the generated circuit uses. rounds: How many times the measurement qubits in the generated circuit will be measured. Indirectly determines the duration of the generated circuit. after_clifford_depolarization: Defaults to 0. The probability (p) of `DEPOLARIZE1(p)` operations to add after every single-qubit Clifford operation and `DEPOLARIZE2(p)` operations to add after every two-qubit Clifford operation. The after-Clifford depolarizing operations are only included if this probability is not 0. before_round_data_depolarization: Defaults to 0. The probability (p) of `DEPOLARIZE1(p)` operations to apply to every data qubit at the start of a round of stabilizer measurements. The start-of-round depolarizing operations are only included if this probability is not 0. before_measure_flip_probability: Defaults to 0. The probability (p) of `X_ERROR(p)` operations applied to qubits before each measurement (X basis measurements use `Z_ERROR(p)` instead). The before-measurement flips are only included if this probability is not 0. after_reset_flip_probability: Defaults to 0. The probability (p) of `X_ERROR(p)` operations applied to qubits after each reset (X basis resets use `Z_ERROR(p)` instead). The after-reset flips are only included if this probability is not 0. Returns: The generated circuit. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... distance=4, ... rounds=10000, ... after_clifford_depolarization=0.0125) >>> print(circuit) R 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.0125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.0125) 2 1 4 3 6 5 TICK MR 1 3 5 DETECTOR(1, 0) rec[-3] DETECTOR(3, 0) rec[-2] DETECTOR(5, 0) rec[-1] REPEAT 9999 { TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.0125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.0125) 2 1 4 3 6 5 TICK MR 1 3 5 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-3] rec[-6] DETECTOR(3, 0) rec[-2] rec[-5] DETECTOR(5, 0) rec[-1] rec[-4] } M 0 2 4 6 DETECTOR(1, 1) rec[-3] rec[-4] rec[-7] DETECTOR(3, 1) rec[-2] rec[-3] rec[-6] DETECTOR(5, 1) rec[-1] rec[-2] rec[-5] OBSERVABLE_INCLUDE(0) rec[-1] """ def get_detector_coordinates( self, only: object = None, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of detectors in the circuit. Args: only: Defaults to None (meaning include all detectors). A list of detector indices to include in the result. Detector indices beyond the end of the detector error model of the circuit cause an error. Returns: A dictionary mapping integers (detector indices) to lists of floats (coordinates). Detectors with no specified coordinate data are mapped to an empty tuple. If `only` is specified, then `set(result.keys()) == set(only)`. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... M 0 ... DETECTOR rec[-1] ... DETECTOR(1, 2, 3) rec[-1] ... REPEAT 3 { ... DETECTOR(42) rec[-1] ... SHIFT_COORDS(100) ... } ... ''') >>> circuit.get_detector_coordinates() {0: [], 1: [1.0, 2.0, 3.0], 2: [42.0], 3: [142.0], 4: [242.0]} >>> circuit.get_detector_coordinates(only=[1]) {1: [1.0, 2.0, 3.0]} """ def get_final_qubit_coordinates( self, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of qubits in the circuit. If a qubit's coordinates are specified multiple times, only the last specified coordinates are returned. Returns: A dictionary mapping qubit indices (integers) to coordinates (lists of floats). Qubits that never had their coordinates specified are not included in the result. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... QUBIT_COORDS(1, 2, 3) 1 ... ''') >>> circuit.get_final_qubit_coordinates() {1: [1.0, 2.0, 3.0]} """ def has_all_flows( self, flows: Iterable[stim.Flow], *, unsigned: bool = False, ) -> bool: """Determines if the circuit has all the given stabilizer flow or not. This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster because, behind the scenes, the circuit can be iterated once instead of once per flow. This method ignores any noise in the circuit. Args: flows: An iterable of `stim.Flow` instances representing the flows to check. unsigned: Defaults to False. When False, the flows must be correct including the sign of the Pauli strings. When True, only the Pauli terms need to be correct; the signs are permitted to be inverted. In effect, this requires the circuit to be correct up to Pauli gates. Returns: True if the circuit has the given flow; False otherwise. Examples: >>> import stim >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> Y'), ... stim.Flow('Z -> X'), ... ]) False >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> -Y'), ... stim.Flow('Z -> X'), ... ]) True >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> Y'), ... stim.Flow('Z -> X'), ... ], unsigned=True) True Caveats: Currently, the unsigned=False version of this method is implemented by performing 256 randomized tests. Each test has a 50% chance of a false positive, and a 0% chance of a false negative. So, when the method returns True, there is technically still a 2^-256 chance the circuit doesn't have the flow. This is lower than the chance of a cosmic ray flipping the result. """ def has_flow( self, flow: stim.Flow, *, unsigned: bool = False, ) -> bool: """Determines if the circuit has the given stabilizer flow or not. A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer P at the start of the circuit to the instantaneous stabilizer Q at the end of the circuit. The flow may be mediated by certain measurements. For example, a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and the CNOT flows implemented by the circuit involve these measurements. A flow like P -> Q means the circuit transforms P into Q. A flow like 1 -> P means the circuit prepares P. A flow like P -> 1 means the circuit measures P. A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). This method ignores any noise in the circuit. Args: flow: The flow to check for. unsigned: Defaults to False. When False, the flows must be correct including the sign of the Pauli strings. When True, only the Pauli terms need to be correct; the signs are permitted to be inverted. In effect, this requires the circuit to be correct up to Pauli gates. Returns: True if the circuit has the given flow; False otherwise. Examples: >>> import stim >>> m = stim.Circuit('M 0') >>> m.has_flow(stim.Flow('Z -> Z')) True >>> m.has_flow(stim.Flow('X -> X')) False >>> m.has_flow(stim.Flow('Z -> I')) False >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) True >>> m.has_flow(stim.Flow('Z -> rec[-1]')) True >>> cx58 = stim.Circuit('CX 5 8') >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) True >>> cx58.has_flow(stim.Flow('X_ -> XX')) False >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) True >>> stim.Circuit(''' ... RY 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("Y"), ... )) True >>> stim.Circuit(''' ... RY 0 ... X_ERROR(0.1) 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("Y"), ... )) True >>> stim.Circuit(''' ... RY 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("X"), ... )) False >>> stim.Circuit(''' ... CX 0 1 ... ''').has_flow(stim.Flow( ... input=stim.PauliString("+X_"), ... output=stim.PauliString("+XX"), ... )) True >>> stim.Circuit(''' ... # Lattice surgery CNOT ... R 1 ... MXX 0 1 ... MZZ 1 2 ... MX 1 ... ''').has_flow(stim.Flow( ... input=stim.PauliString("+X_X"), ... output=stim.PauliString("+__X"), ... measurements=[0, 2], ... )) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( ... stim.Flow("Y -> Y"), ... unsigned=True, ... ) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( ... stim.Flow("Y -> Y"), ... unsigned=False, ... ) False Caveats: Currently, the unsigned=False version of this method is implemented by performing 256 randomized tests. Each test has a 50% chance of a false positive, and a 0% chance of a false negative. So, when the method returns True, there is technically still a 2^-256 chance the circuit doesn't have the flow. This is lower than the chance of a cosmic ray flipping the result. """ def insert( self, index: int, operation: Union[stim.CircuitInstruction, stim.Circuit], ) -> None: """Inserts an operation at the given index, pushing existing operations forward. Beware that inserted operations are automatically fused with the preceding and following operations, if possible. This can make it complex to reason about how the indices of operations change in response to insertions. Args: index: The index to insert at. Must satisfy -len(circuit) <= index < len(circuit). Negative indices are made non-negative by adding len(circuit) to them, so they refer to indices relative to the end of the circuit instead of the start. Instructions before the index are not shifted. Instructions that were at or after the index are shifted forwards as needed. operation: The object to insert. This can be a single stim.CircuitInstruction or an entire stim.Circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... S 1 ... X 2 ... ''') >>> c.insert(1, stim.CircuitInstruction("Y", [3, 4, 5])) >>> c stim.Circuit(''' H 0 Y 3 4 5 S 1 X 2 ''') >>> c.insert(-1, stim.Circuit("S 999\nCX 0 1\nCZ 2 3")) >>> c stim.Circuit(''' H 0 Y 3 4 5 S 1 999 CX 0 1 CZ 2 3 X 2 ''') """ def inverse( self, ) -> stim.Circuit: """Returns a circuit that applies the same operations but inverted and in reverse. If circuit starts with QUBIT_COORDS instructions, the returned circuit will still have the same QUBIT_COORDS instructions in the same order at the start. Returns: A `stim.Circuit` that applies inverted operations in the reverse order. Raises: ValueError: The circuit contains operations that don't have an inverse, such as measurements. There are also some unsupported operations such as SHIFT_COORDS. Examples: >>> import stim >>> stim.Circuit(''' ... S 0 1 ... ISWAP 0 1 1 2 ... ''').inverse() stim.Circuit(''' ISWAP_DAG 1 2 0 1 S_DAG 1 0 ''') >>> stim.Circuit(''' ... QUBIT_COORDS(1, 2) 0 ... QUBIT_COORDS(4, 3) 1 ... QUBIT_COORDS(9, 5) 2 ... H 0 1 ... REPEAT 100 { ... CX 0 1 1 2 ... TICK ... S 1 2 ... } ... ''').inverse() stim.Circuit(''' QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(4, 3) 1 QUBIT_COORDS(9, 5) 2 REPEAT 100 { S_DAG 2 1 TICK CX 1 2 0 1 } H 1 0 ''') """ def likeliest_error_sat_problem( self, *, quantization: int = 100, format: str = 'WDIMACS', ) -> str: """Makes a maxSAT problem for the circuit's likeliest undetectable logical error. The output is a string describing the maxSAT problem in WDIMACS format (see https://jix.github.io/varisat/manual/0.2.0/formats/dimacs.html). The optimal solution to the problem is the highest likelihood set of error mechanisms that combine to flip any logical observable while producing no detection events). If there are any errors with probability p > 0.5, they are inverted so that the resulting weight ends up being positive. If there are errors with weight close or equal to 0.5, they can end up with 0 weight meaning that they can be included or not in the solution with no affect on the likelihood. There are many tools that can solve maxSAT problems in WDIMACS format. One quick way to get started is to install pysat by running this BASH terminal command: pip install python-sat Afterwards, you can run the included maxSAT solver "RC2" with this Python code: from pysat.examples.rc2 import RC2 from pysat.formula import WCNF wcnf = WCNF(from_string="p wcnf 1 2 3\n3 -1 0\n3 1 0\n") with RC2(wcnf) as rc2: print(rc2.compute()) print(rc2.cost) Much faster solvers are available online. For example, you can download one of the entries in the 2023 maxSAT competition (see https://maxsat-evaluations.github.io/2023) and run it on your problem by running these BASH terminal commands: wget https://maxsat-evaluations.github.io/2023/mse23-solver-src/exact/CASHWMaxSAT-CorePlus.zip unzip CASHWMaxSAT-CorePlus.zip ./CASHWMaxSAT-CorePlus/bin/cashwmaxsatcoreplus -bm -m your_problem.wcnf Args: format: Defaults to "WDIMACS", corresponding to WDIMACS format which is described here: http://www.maxhs.org/docs/wdimacs.html quantization: Defaults to 10. Error probabilities are converted to log-odds and scaled/rounded to be positive integers at most this large. Setting this argument to a larger number results in more accurate quantization such that the returned error set should have a likelihood closer to the true most likely solution. This comes at the cost of making some maxSAT solvers slower. Returns: A string corresponding to the contents of a maxSAT problem file in the requested format. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X_ERROR(0.1) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... X_ERROR(0.4) 0 ... M 0 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.likeliest_error_sat_problem( ... quantization=1000 ... ), end='') p wcnf 2 4 4001 185 -1 0 1000 -2 0 4001 -1 0 4001 2 0 """ def missing_detectors( self, *, unknown_input: bool = False, ) -> int: """Finds deterministic measurements independent of declared detectors/observables. This method is useful for debugging missing detectors in a circuit, because it identifies generators for uncovered degrees of freedom. It's not recommended to use this method to solve for the detectors of a circuit. The returned detectors are not guaranteed to be stable across versions, and aren't optimized to be "good" (e.g. form a low weight basis or be matchable if possible). It will also identify things that are technically determined but that the user may not want to use as a detector, such as the fact that in the first round after transversal Z basis initialization of a toric code the product of all X stabilizer measurements is deterministic even though the individual measurements are all random. Args: unknown_input: Defaults to False (inputs assumed to be in the |0> state). When set to True, the inputs are instead treated as being in unknown random states. For example, this means that Z-basis measurements at the very beginning of the circuit will be considered random rather than determined. Returns: A circuit containing DETECTOR instructions that specify the uncovered degrees of freedom in the deterministic measurement sets of the input circuit. The returned circuit can be appended to the input circuit to get a circuit with no missing detectors. Examples: >>> import stim >>> stim.Circuit(''' ... R 0 ... M 0 ... ''').missing_detectors() stim.Circuit(''' DETECTOR rec[-1] ''') >>> stim.Circuit(''' ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... DEPOLARIZE1(0.1) 0 1 ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... DETECTOR rec[-1] rec[-4] ... DETECTOR rec[-2] rec[-5] ... DETECTOR rec[-3] rec[-6] ... ''').missing_detectors(unknown_input=True) stim.Circuit(''' DETECTOR rec[-3] rec[-2] rec[-1] ''') """ @property def num_detectors( self, ) -> int: """Counts the number of bits produced when sampling the circuit's detectors. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... DETECTOR rec[-1] ... REPEAT 100 { ... M 0 1 2 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... } ... ''') >>> c.num_detectors 201 """ @property def num_measurements( self, ) -> int: """Counts the number of bits produced when sampling the circuit's measurements. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... REPEAT 100 { ... M 0 1 ... } ... ''') >>> c.num_measurements 201 """ @property def num_observables( self, ) -> int: """Counts the number of logical observables defined by the circuit. This is one more than the largest index that appears as an argument to an OBSERVABLE_INCLUDE instruction. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... OBSERVABLE_INCLUDE(2) rec[-1] ... OBSERVABLE_INCLUDE(5) rec[-1] ... ''') >>> c.num_observables 6 """ @property def num_qubits( self, ) -> int: """Counts the number of qubits used when simulating the circuit. This is always one more than the largest qubit index used by the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... X 0 ... M 0 1 ... ''').num_qubits 2 >>> stim.Circuit(''' ... X 0 ... M 0 1 ... H 100 ... ''').num_qubits 101 """ @property def num_sweep_bits( self, ) -> int: """Returns the number of sweep bits needed to completely configure the circuit. This is always one more than the largest sweep bit index used by the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... CX sweep[2] 0 ... ''').num_sweep_bits 3 >>> stim.Circuit(''' ... CZ sweep[5] 0 ... CX sweep[2] 0 ... ''').num_sweep_bits 6 """ @property def num_ticks( self, ) -> int: """Counts the number of TICK instructions executed when running the circuit. TICKs in loops are counted once per iteration. Returns: The number of ticks executed by the circuit. Examples: >>> import stim >>> stim.Circuit().num_ticks 0 >>> stim.Circuit(''' ... TICK ... ''').num_ticks 1 >>> stim.Circuit(''' ... H 0 ... TICK ... CX 0 1 ... TICK ... ''').num_ticks 2 >>> stim.Circuit(''' ... H 0 ... TICK ... REPEAT 100 { ... CX 0 1 ... TICK ... } ... ''').num_ticks 101 """ def pop( self, index: int = -1, ) -> Union[stim.CircuitInstruction, stim.CircuitRepeatBlock]: """Pops an operation from the end of the circuit, or at the given index. Args: index: Defaults to -1 (end of circuit). The index to pop from. Returns: The popped instruction. Raises: IndexError: The given index is outside the bounds of the circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... S 1 ... X 2 ... Y 3 ... ''') >>> c.pop() stim.CircuitInstruction('Y', [stim.GateTarget(3)], []) >>> c.pop(1) stim.CircuitInstruction('S', [stim.GateTarget(1)], []) >>> c stim.Circuit(''' H 0 X 2 ''') """ def reference_detector_and_observable_signs( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: """Determines noiseless parities of the measurement sets of detectors/observables. BEWARE: the returned values are NOT the "expected value of the detector/observable". Stim consistently defines the value of a detector/observable as whether or not it flipped, so the expected value of a detector/observable is vacuously always 0 (not flipped). This method instead returns the "sign"; the expected parity of the measurement set declared by the detector/observable. The sign is the baseline used to determine if a flip occurred. A detector/observable's value is whether its sign disagrees with the measured parity of its measurement set. Note that this method doesn't account for sweep bits. It will effectively ignore instructions like `CX sweep[0] 0`. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: A (det, obs) tuple with numpy arrays containing the reference parities. if bit_packed: det.shape == (math.ceil(num_detectors / 8),) det.dtype == np.uint8 obs.shape == (math.ceil(num_observables / 8),) obs.dtype == np.uint8 else: det.shape == (num_detectors,) det.dtype == np.bool_ obs.shape == (num_observables,) obs.dtype == np.bool_ Examples: >>> import stim >>> stim.Circuit(''' ... X 1 ... M 0 1 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... OBSERVABLE_INCLUDE(3) rec[-1] rec[-2] ... ''').reference_detector_and_observable_signs() (array([ True, False]), array([False, False, False, True])) """ def reference_sample( self, *, bit_packed: bool = False, ) -> np.ndarray: """Samples the given circuit in a deterministic fashion. Discards all noisy operations, and biases all collapse events towards +Z instead of randomly +Z/-Z. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: A numpy array containing the reference sample. if bit_packed: shape == (math.ceil(num_measurements / 8),) dtype == np.uint8 else: shape == (num_measurements,) dtype == np.bool_ Examples: >>> import stim >>> stim.Circuit(''' ... X 1 ... M 0 1 ... ''').reference_sample() array([False, True]) """ def search_for_undetectable_logical_errors( self, *, dont_explore_detection_event_sets_with_size_above: int, dont_explore_edges_with_degree_above: int, dont_explore_edges_increasing_symptom_degree: bool, canonicalize_circuit_errors: bool = False, ) -> List[stim.ExplainedError]: """Searches for small sets of errors that form an undetectable logical error. THIS IS A HEURISTIC METHOD. It does not guarantee that it will find errors of particular sizes, or with particular properties. The errors it finds are a tangled combination of the truncation parameters you specify, internal optimizations which are correct when not truncating, and minutia of the circuit being considered. If you want a well behaved method that does provide guarantees of finding errors of a particular type, use `stim.Circuit.shortest_graphlike_error`. This method is more thorough than that (assuming you don't truncate so hard you omit graphlike edges), but exactly how thorough is difficult to describe. It's also not guaranteed that the behavior of this method will not be changed in the future in a way that permutes which logical errors are found and which are missed. This search method considers hyper errors, so it has worst case exponential runtime. It is important to carefully consider the arguments you are providing, which truncate the search space and trade cost for quality. The search progresses by starting from each error that crosses a logical observable, noting which detection events each error produces, and then iteratively adding in errors touching those detection events attempting to cancel out the detection event with the lowest index. Beware that the choice of logical observable can interact with the truncation options. Using different observables can change whether or not the search succeeds, even if those observables are equal modulo the stabilizers of the code. This is because the edges crossing logical observables are used as starting points for the search, and starting from different places along a path will result in different numbers of symptoms in intermediate states as the search progresses. For example, if the logical observable is next to a boundary, then the starting edges are likely boundary edges (degree 1) with 'room to grow', whereas if the observable was running through the bulk then the starting edges will have degree at least 2. Args: dont_explore_detection_event_sets_with_size_above: Truncates the search space by refusing to cross an edge (i.e. add an error) when doing so would produce an intermediate state that has more detection events than this limit. dont_explore_edges_with_degree_above: Truncates the search space by refusing to consider errors that cause a lot of detection events. For example, you may only want to consider graphlike errors which have two or fewer detection events. dont_explore_edges_increasing_symptom_degree: Truncates the search space by refusing to cross an edge (i.e. add an error) when doing so would produce an intermediate state that has more detection events that the previous intermediate state. This massively improves the efficiency of the search because instead of, for example, exploring all n^4 possible detection event sets with 4 symptoms, the search will attempt to cancel out symptoms one by one. canonicalize_circuit_errors: Whether or not to use one representative for equal-symptom circuit errors. False (default): Each DEM error lists every possible circuit error that single handedly produces those symptoms as a potential match. This is verbose but gives complete information. True: Each DEM error is matched with one possible circuit error that single handedly produces those symptoms, with a preference towards errors that are simpler (e.g. apply Paulis to fewer qubits). This discards mostly-redundant information about different ways to produce the same symptoms in order to give a succinct result. Returns: A list of error mechanisms that cause an undetected logical error. Each entry in the list is a `stim.ExplainedError` detailing the location and effects of a single physical error. The effects of the entire list combine to produce a logical frame change without any detection events. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... rounds=5, ... distance=5, ... after_clifford_depolarization=0.001) >>> print(len(circuit.search_for_undetectable_logical_errors( ... dont_explore_detection_event_sets_with_size_above=4, ... dont_explore_edges_with_degree_above=4, ... dont_explore_edges_increasing_symptom_degree=True, ... ))) 5 """ def shortest_error_sat_problem( self, *, format: str = 'WDIMACS', ) -> str: """Makes a maxSAT problem of the circuit's distance, that other tools can solve. The output is a string describing the maxSAT problem in WDIMACS format (see https://jix.github.io/varisat/manual/0.2.0/formats/dimacs.html). The optimal solution to the problem is the fault distance of the circuit (the minimum number of error mechanisms that combine to flip any logical observable while producing no detection events). This method ignores the probabilities of the error mechanisms since it only cares about minimizing the number of errors triggered. There are many tools that can solve maxSAT problems in WDIMACS format. One quick way to get started is to install pysat by running this BASH terminal command: pip install python-sat Afterwards, you can run the included maxSAT solver "RC2" with this Python code: from pysat.examples.rc2 import RC2 from pysat.formula import WCNF wcnf = WCNF(from_string="p wcnf 1 2 3\n3 -1 0\n3 1 0\n") with RC2(wcnf) as rc2: print(rc2.compute()) print(rc2.cost) Much faster solvers are available online. For example, you can download one of the entries in the 2023 maxSAT competition (see https://maxsat-evaluations.github.io/2023) and run it on your problem by running these BASH terminal commands: wget https://maxsat-evaluations.github.io/2023/mse23-solver-src/exact/CASHWMaxSAT-CorePlus.zip unzip CASHWMaxSAT-CorePlus.zip ./CASHWMaxSAT-CorePlus/bin/cashwmaxsatcoreplus -bm -m your_problem.wcnf Args: format: Defaults to "WDIMACS", corresponding to WDIMACS format which is described here: http://www.maxhs.org/docs/wdimacs.html Returns: A string corresponding to the contents of a maxSAT problem file in the requested format. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X_ERROR(0.1) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... X_ERROR(0.4) 0 ... M 0 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.shortest_error_sat_problem(), end='') p wcnf 2 4 5 1 -1 0 1 -2 0 5 -1 0 5 2 0 """ def shortest_graphlike_error( self, *, ignore_ungraphlike_errors: bool = True, canonicalize_circuit_errors: bool = False, ) -> List[stim.ExplainedError]: """Finds a minimum set of graphlike errors to produce an undetected logical error. A "graphlike error" is an error that creates at most two detection events (causes a change in the parity of the measurement sets of at most two DETECTOR annotations). Note that this method does not pay attention to error probabilities (other than ignoring errors with probability 0). It searches for a logical error with the minimum *number* of physical errors, not the maximum probability of those physical errors all occurring. This method works by converting the circuit into a `stim.DetectorErrorModel` using `circuit.detector_error_model(...)`, computing the shortest graphlike error of the error model, and then converting the physical errors making up that logical error back into representative circuit errors. Args: ignore_ungraphlike_errors: False: Attempt to decompose any ungraphlike errors in the circuit into graphlike parts. If this fails, raise an exception instead of continuing. Note: in some cases, graphlike errors only appear as parts of decomposed ungraphlike errors. This can produce a result that lists DEM errors with zero matching circuit errors, because the only way to achieve those errors is by combining a decomposed error with a graphlike error. As a result, when using this option it is NOT guaranteed that the length of the result is an upper bound on the true code distance. That is only the case if every item in the result lists at least one matching circuit error. True (default): Ungraphlike errors are simply skipped as if they weren't present, even if they could become graphlike if decomposed. This guarantees the length of the result is an upper bound on the true code distance. canonicalize_circuit_errors: Whether or not to use one representative for equal-symptom circuit errors. False (default): Each DEM error lists every possible circuit error that single handedly produces those symptoms as a potential match. This is verbose but gives complete information. True: Each DEM error is matched with one possible circuit error that single handedly produces those symptoms, with a preference towards errors that are simpler (e.g. apply Paulis to fewer qubits). This discards mostly-redundant information about different ways to produce the same symptoms in order to give a succinct result. Returns: A list of error mechanisms that cause an undetected logical error. Each entry in the list is a `stim.ExplainedError` detailing the location and effects of a single physical error. The effects of the entire list combine to produce a logical frame change without any detection events. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... before_round_data_depolarization=0.01) >>> len(circuit.shortest_graphlike_error()) 7 """ def solve_flow_measurements( self, flows: List[stim.Flow], ) -> List[Optional[List[int]]]: """Finds measurements to explain the starts/ends of the given flows, ignoring sign. CAUTION: it's not guaranteed that the solutions returned by this method are minimal. It may use 20 measurements when only 2 are needed. The method applies some simple heuristics that attempt to reduce the size, but these heuristics aren't perfect and don't make any strong guarantees. The recommended way to use this method is on small parts of a circuit, such as a single surface code round. The ideal use case is when there is exactly one solution for each flow, because then the method behaves predictably and consistently. When there are multiple solutions, the method has no real way to pick out a "good" solution rather than a "cataclysmic trash fire of a" solution. For example, if you have a multi-round surface code circuit with open time boundaries and solve the flow 1 -> Z1*Z2*Z3*Z4, then there's a good solution (the Z1*Z2*Z3*Z4 measurement from the last round), various mediocre solutions (a Z1*Z2*Z3*Z4 measurement from a different round), and lots of terrible solutions (a combination of multiple Z1*Z2*Z3*Z4 measurements from an odd number of rounds, times a random combination of unrelated detectors). The method is permitted to return any of those solutions. Args: flows: A list of flows, each of which to be solved. Measurements and signs are entirely ignored. An error is raised if one of the given flows has an identity pauli string as its input and as its output, despite the fact that this case has a vacuous solution (no measurements). This error is only present as a safety check that catches some possible bugs in the calling code, such as accidentally applying this method to detector flows. This error may be removed in the future, so that the vacuous case succeeds vacuously. Returns: A list of solutions for each given flow. If no solution exists for flows[k], then solutions[k] is None. Otherwise, solutions[k] is a list of measurement indices for flows[k]. When solutions[k] is not None, it's guaranteed that circuit.has_flow(stim.Flow( input=flows[k].input, output=flows[k].output, measurements=solutions[k], ), unsigned=True) Raises: ValueError: A flow had an empty input and output. Examples: >>> import stim >>> stim.Circuit(''' ... M 2 ... ''').solve_flow_measurements([ ... stim.Flow("Z2 -> 1"), ... ]) [[0]] >>> stim.Circuit(''' ... M 2 ... ''').solve_flow_measurements([ ... stim.Flow("X2 -> X2"), ... ]) [None] >>> stim.Circuit(''' ... MXX 0 1 ... ''').solve_flow_measurements([ ... stim.Flow("YY -> ZZ"), ... ]) [[0]] >>> # Rep code cycle >>> stim.Circuit(''' ... R 1 3 ... CX 0 1 2 3 ... CX 4 3 2 1 ... M 1 3 ... ''').solve_flow_measurements([ ... stim.Flow("1 -> Z0*Z4"), ... stim.Flow("Z0 -> Z2"), ... stim.Flow("X0*X2*X4 -> X0*X2*X4"), ... stim.Flow("Y0 -> Y0"), ... ]) [[0, 1], [0], [], None] """ def time_reversed_for_flows( self, flows: Iterable[stim.Flow], *, dont_turn_measurements_into_resets: bool = False, ) -> Tuple[stim.Circuit, List[stim.Flow]]: """Time-reverses the circuit while preserving error correction structure. This method returns a circuit that has the same internal detecting regions as the given circuit, as well as the same internal-to-external flows given in the `flows` argument, except they are all time-reversed. For example, if you pass a fault tolerant preparation circuit into this method (1 -> Z), the result will be a fault tolerant *measurement* circuit (Z -> 1). Or, if you pass a fault tolerant C_XYZ circuit into this method (X->Y, Y->Z, and Z->X), the result will be a fault tolerant C_ZYX circuit (X->Z, Y->X, and Z->Y). Note that this method doesn't guarantee that it will preserve the *sign* of the detecting regions or stabilizer flows. For example, inverting a memory circuit that preserves a logical observable (X->X and Z->Z) may produce a memory circuit that always bit flips the logical observable (X->X and Z->-Z) or that dynamically adjusts the logical observable in response to measurements (like "X -> X xor rec[-1]" and "Z -> Z xor rec[-2]"). This method will turn time-reversed resets into measurements, and attempts to turn time-reversed measurements into resets. A measurement will time-reverse into a reset if some annotated detectors, annotated observables, or given flows have detecting regions with sensitivity just before the measurement but none have detecting regions with sensitivity after the measurement. In some cases this method will have to introduce new operations. In particular, when a measurement-reset operation has a noisy result, time-reversing this measurement noise produces reset noise. But the measure-reset operations don't have built-in reset noise, so the reset noise is specified by adding an X_ERROR or Z_ERROR noise instruction after the time-reversed measure-reset operation. Args: flows: Flows you care about, that reach past the start/end of the given circuit. The result will contain an inverted flow for each of these given flows. You need this information because it reveals the measurements needed to produce the inverted flows that you care about. An exception will be raised if the circuit doesn't have all these flows. The inverted circuit will have the inverses of these flows (ignoring sign). dont_turn_measurements_into_resets: Defaults to False. When set to True, measurements will time-reverse into measurements even if nothing is sensitive to the measured qubit after the measurement completes. This guarantees the output circuit has *all* flows that the input circuit has (up to sign and feedback), even ones that aren't annotated. Returns: An (inverted_circuit, inverted_flows) tuple. inverted_circuit is the qec inverse of the given circuit. inverted_flows is a list of flows, matching up by index with the flows given as arguments to the method. The input, output, and sign fields of these flows are boring. The useful field is measurement_indices, because it's difficult to predict which measurements are needed for the inverted flow due to effects such as implicitly-included resets inverting into explicitly-included measurements. Caveats: Currently, this method doesn't compute the sign of the inverted flows. It unconditionally sets the sign to False. Examples: >>> import stim >>> inv_circuit, inv_flows = stim.Circuit(''' ... R 0 ... H 0 ... S 0 ... MY 0 ... DETECTOR rec[-1] ... ''').time_reversed_for_flows([]) >>> inv_circuit stim.Circuit(''' RY 0 S_DAG 0 H 0 M 0 DETECTOR rec[-1] ''') >>> inv_flows [] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows([ ... stim.Flow("Z -> rec[-1]"), ... ]) >>> inv_circuit stim.Circuit(''' R 0 ''') >>> inv_flows [stim.Flow("1 -> Z")] >>> inv_circuit.has_all_flows(inv_flows, unsigned=True) True >>> inv_circuit, inv_flows = stim.Circuit(''' ... R 0 ... ''').time_reversed_for_flows([ ... stim.Flow("1 -> Z"), ... ]) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("Z -> rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows([ ... stim.Flow("1 -> Z xor rec[-1]"), ... ]) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("Z -> rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows( ... flows=[stim.Flow("Z -> rec[-1]")], ... dont_turn_measurements_into_resets=True, ... ) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("1 -> Z xor rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... MR(0.125) 0 ... ''').time_reversed_for_flows([]) >>> inv_circuit stim.Circuit(''' MR 0 X_ERROR(0.125) 0 ''') >>> inv_flows [] >>> inv_circuit, inv_flows = stim.Circuit(''' ... MXX 0 1 ... H 0 ... ''').time_reversed_for_flows([ ... stim.Flow("ZZ -> YY xor rec[-1]"), ... stim.Flow("ZZ -> XZ"), ... ]) >>> inv_circuit stim.Circuit(''' H 0 MXX 0 1 ''') >>> inv_flows [stim.Flow("YY -> ZZ xor rec[-1]"), stim.Flow("XZ -> ZZ")] >>> stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... distance=2, ... rounds=1, ... ).time_reversed_for_flows([])[0] stim.Circuit(''' QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 1) 3 QUBIT_COORDS(1, 3) 6 QUBIT_COORDS(2, 2) 7 QUBIT_COORDS(3, 3) 8 QUBIT_COORDS(2, 4) 12 RX 8 6 3 1 MR 12 7 2 TICK H 12 2 TICK CX 1 7 12 6 TICK CX 6 7 12 8 TICK CX 3 7 2 1 TICK CX 8 7 2 3 TICK H 12 2 TICK M 12 7 2 DETECTOR(2, 0, 1) rec[-1] DETECTOR(2, 4, 1) rec[-3] MX 8 6 3 1 DETECTOR(2, 0, 0) rec[-5] rec[-2] rec[-1] DETECTOR(2, 4, 0) rec[-7] rec[-4] rec[-3] OBSERVABLE_INCLUDE(0) rec[-3] rec[-1] ''') """ def to_crumble_url( self, *, skip_detectors: bool = False, mark: Optional[Dict[int, List[stim.ExplainedError]]] = None, ) -> str: """Returns a URL that opens up crumble and loads this circuit into it. Crumble is a tool for editing stabilizer circuits, and visualizing their stabilizer flows. Its source code is in the `glue/crumble` directory of the stim code repository on github. A prebuilt version is made available at https://algassert.com/crumble, which is what the URL returned by this method will point to. Args: skip_detectors: Defaults to False. If set to True, detectors from the circuit aren't included in the crumble URL. This can reduce visual clutter in crumble, and improve its performance, since it doesn't need to indicate or track the sensitivity regions of detectors. mark: Defaults to None (no marks). If set to a dictionary from int to errors, such as `mark={1: circuit.shortest_graphlike_error()}`, then the errors will be highlighted and tracked forward by crumble. Returns: A URL that can be opened in a web browser. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... S 1 ... ''').to_crumble_url() 'https://algassert.com/crumble#circuit=H_0;CX_0_1;S_1_' >>> circuit = stim.Circuit(''' ... M(0.25) 0 1 2 ... DETECTOR rec[-1] rec[-2] ... DETECTOR rec[-2] rec[-3] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''') >>> err = circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) >>> circuit.to_crumble_url(skip_detectors=True, mark={1: err}) 'https://algassert.com/crumble#circuit=;TICK;MARKX(1)1;MARKX(1)2;MARKX(1)0;TICK;M(0.25)0_1_2;OI(0)rec[-1]_' """ def to_file( self, file: Union[io.TextIOBase, str, pathlib.Path], ) -> None: """Writes the stim circuit to a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md Args: file: A file path or an open file to write to. Examples: >>> import stim >>> import tempfile >>> c = stim.Circuit('H 5\nX 0') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... c.to_file(f) ... with open(path) as f: ... contents = f.read() >>> contents 'H 5\nX 0\n' >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... c.to_file(path) ... with open(path) as f: ... contents = f.read() >>> contents 'H 5\nX 0\n' """ def to_qasm( self, *, open_qasm_version: int, skip_dets_and_obs: bool = False, ) -> str: """Creates an equivalent OpenQASM implementation of the circuit. Args: open_qasm_version: The version of OpenQASM to target. This should be set to 2 or to 3. Differences between the versions are: - Support for operations on classical bits (only version 3). This means DETECTOR and OBSERVABLE_INCLUDE only work with version 3. - Support for feedback operations (only version 3). - Support for subroutines (only version 3). Without subroutines, non-standard dissipative gates like MR and RX need to decompose inline every single time they're used. - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc). skip_dets_and_obs: Defaults to False. When set to False, the output will include a `dets` register and an `obs` register (assuming the circuit has detectors and observables). These registers will be computed as part of running the circuit. This requires performing a simulation of the circuit, in order to correctly account for the expected value of measurements. When set to True, the `dets` and `obs` registers are not included in the output, and no simulation of the circuit is performed. Returns: The OpenQASM code as a string. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... R 0 1 ... X 1 ... H 0 ... CX 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... '''); >>> qasm = circuit.to_qasm(open_qasm_version=3); >>> print(qasm.strip().replace('\n\n', '\n')) OPENQASM 3.0; include "stdgates.inc"; qreg q[2]; creg rec[2]; creg dets[1]; reset q[0]; reset q[1]; x q[1]; h q[0]; cx q[0], q[1]; measure q[0] -> rec[0]; measure q[1] -> rec[1]; dets[0] = rec[1] ^ rec[0] ^ 1; """ def to_quirk_url( self, ) -> str: """Returns a URL that opens up quirk and loads this circuit into it. Quirk is an open source drag and drop circuit editor with support for up to 16 qubits. Its source code is available at https://github.com/strilanc/quirk and a prebuilt version is available at https://algassert.com/quirk, which is what the URL returned by this method will point to. Quirk doesn't support features like noise, feedback, or detectors. This method will simply drop any unsupported operations from the circuit when producing the URL. Returns: A URL that can be opened in a web browser. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... S 1 ... ''').to_quirk_url() 'https://algassert.com/quirk#circuit={"cols":[["H"],["•","X"],[1,"Z^½"]]}' """ def to_tableau( self, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False, ) -> stim.Tableau: """Converts the circuit into an equivalent stabilizer tableau. Args: ignore_noise: Defaults to False. When False, any noise operations in the circuit will cause the conversion to fail with an exception. When True, noise operations are skipped over as if they weren't even present in the circuit. ignore_measurement: Defaults to False. When False, any measurement operations in the circuit will cause the conversion to fail with an exception. When True, measurement operations are skipped over as if they weren't even present in the circuit. ignore_reset: Defaults to False. When False, any reset operations in the circuit will cause the conversion to fail with an exception. When True, reset operations are skipped over as if they weren't even present in the circuit. Returns: A tableau equivalent to the circuit (up to global phase). Raises: ValueError: The circuit contains noise operations but ignore_noise=False. OR The circuit contains measurement operations but ignore_measurement=False. OR The circuit contains reset operations but ignore_reset=False. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''').to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ def with_inlined_feedback( self, ) -> stim.Circuit: """Returns a circuit without feedback with rewritten detectors/observables. When a feedback operation affects the expected parity of a detector or observable, the measurement controlling that feedback operation is implicitly part of the measurement set that defines the detector or observable. This method removes all feedback, but avoids changing the meaning of detectors or observables by turning these implicit measurement dependencies into explicit measurement dependencies added to the observable or detector. This method guarantees that the detector error model derived from the original circuit, and the transformed circuit, will be equivalent (modulo floating point rounding errors and variations in where loops are placed). Specifically, the following should be true for any circuit: dem1 = circuit.flattened().detector_error_model() dem2 = circuit.with_inlined_feedback().flattened().detector_error_model() assert dem1.approx_equals(dem2, 1e-5) Returns: A `stim.Circuit` with feedback operations removed, with rewritten DETECTOR instructions (as needed to avoid changing the meaning of each detector), and with additional OBSERVABLE_INCLUDE instructions (as needed to avoid changing the meaning of each observable). The circuit's function is permitted to differ from the original in that any feedback operation can be pushed to the end of the circuit and discarded. All non-feedback operations must stay where they are, preserving the structure of the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... CX 0 1 # copy to measure qubit ... M 1 # measure first time ... CX rec[-1] 1 # use feedback to reset measurement qubit ... CX 0 1 # copy to measure qubit ... M 1 # measure second time ... DETECTOR rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').with_inlined_feedback() stim.Circuit(''' CX 0 1 M 1 OBSERVABLE_INCLUDE(0) rec[-1] CX 0 1 M 1 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''') """ def without_noise( self, ) -> stim.Circuit: """Returns a copy of the circuit with all noise processes removed. Pure noise instructions, such as X_ERROR and DEPOLARIZE2, are not included in the result. Noisy measurement instructions, like `M(0.001)`, have their noise parameter removed. Returns: A `stim.Circuit` with the same instructions except all noise processes have been removed. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.25) 0 ... CNOT 0 1 ... M(0.125) 0 ... ''').without_noise() stim.Circuit(''' CX 0 1 M 0 ''') """ def without_tags( self, ) -> stim.Circuit: """Returns a copy of the circuit with all tags removed. Returns: A `stim.Circuit` with the same instructions except all tags have been removed. Examples: >>> import stim >>> stim.Circuit(''' ... X[test-tag] 0 ... M[test-tag-2](0.125) 0 ... ''').without_tags() stim.Circuit(''' X 0 M(0.125) 0 ''') """ class CircuitErrorLocation: """Describes the location of an error mechanism from a stim circuit. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... distance=5, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ) >>> logical_error = circuit.shortest_graphlike_error() >>> error_location = logical_error[0].circuit_error_locations[0] >>> print(error_location) CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } """ def __init__( self, *, tick_offset: int, flipped_pauli_product: List[stim.GateTargetWithCoords], flipped_measurement: object, instruction_targets: stim.CircuitTargetsInsideInstruction, stack_frames: List[stim.CircuitErrorLocationStackFrame], noise_tag: str = '', ) -> None: """Creates a stim.CircuitErrorLocation. Examples: >>> import stim >>> err = stim.CircuitErrorLocation( ... tick_offset=1, ... flipped_pauli_product=( ... stim.GateTargetWithCoords( ... gate_target=stim.target_x(0), ... coords=[], ... ), ... ), ... flipped_measurement=stim.FlippedMeasurement( ... record_index=None, ... observable=(), ... ), ... instruction_targets=stim.CircuitTargetsInsideInstruction( ... gate='DEPOLARIZE1', ... args=[0.001], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=(stim.GateTargetWithCoords( ... gate_target=0, ... coords=[], ... ),) ... ), ... stack_frames=( ... stim.CircuitErrorLocationStackFrame( ... instruction_offset=2, ... iteration_index=0, ... instruction_repetitions_arg=0, ... ), ... ), ... noise_tag='test-tag', ... ) >>> print(err) CircuitErrorLocation { noise_tag: test-tag flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } """ @property def flipped_measurement( self, ) -> Optional[stim.FlippedMeasurement]: """The measurement that was flipped by the error mechanism. If the error isn't a measurement error, this will be None. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... M(0.125) 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement stim.FlippedMeasurement( record_index=0, observable=(stim.GateTargetWithCoords(stim.target_z(0), []),), ) """ @property def flipped_pauli_product( self, ) -> List[stim.GateTargetWithCoords]: """The Pauli errors that the error mechanism applied to qubits. When the error is a measurement error, this will be an empty list. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_pauli_product [stim.GateTargetWithCoords(stim.target_y(0), [])] """ @property def instruction_targets( self, ) -> stim.CircuitTargetsInsideInstruction: """Within the error instruction, which may have hundreds of targets, which specific targets were being executed to produce the error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> targets = err[0].circuit_error_locations[0].instruction_targets >>> targets == stim.CircuitTargetsInsideInstruction( ... gate='Y_ERROR', ... args=[0.125], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=(stim.GateTargetWithCoords(0, []),), ... ) True """ @property def noise_tag( self, ) -> str: """The tag on the noise instruction that caused the error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... Y_ERROR[test-tag](0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].noise_tag 'test-tag' """ @property def stack_frames( self, ) -> List[stim.CircuitErrorLocationStackFrame]: """Describes where in the circuit's execution the error happened. Multiple frames are needed because the error may occur within a loop, or a loop nested inside a loop, or etc. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames [stim.CircuitErrorLocationStackFrame( instruction_offset=2, iteration_index=0, instruction_repetitions_arg=0, )] """ @property def tick_offset( self, ) -> int: """The number of TICKs that executed before the error happened. This counts TICKs occurring multiple times during loops. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... TICK ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].tick_offset 3 """ class CircuitErrorLocationStackFrame: """Describes the location of an instruction being executed within a circuit or loop, distinguishing between separate loop iterations. The full location of an instruction is a list of these frames, drilling down from the top level circuit to the inner-most loop that the instruction is within. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames[0] stim.CircuitErrorLocationStackFrame( instruction_offset=0, iteration_index=0, instruction_repetitions_arg=5, ) >>> err[0].circuit_error_locations[0].stack_frames[1] stim.CircuitErrorLocationStackFrame( instruction_offset=1, iteration_index=4, instruction_repetitions_arg=0, ) """ def __init__( self, *, instruction_offset: int, iteration_index: int, instruction_repetitions_arg: int, ) -> None: """Creates a stim.CircuitErrorLocationStackFrame. Examples: >>> import stim >>> frame = stim.CircuitErrorLocationStackFrame( ... instruction_offset=1, ... iteration_index=2, ... instruction_repetitions_arg=3, ... ) """ @property def instruction_offset( self, ) -> int: """The index of the instruction within the circuit, or within the instruction's parent REPEAT block. This is slightly different from the line number, because blank lines and commented lines don't count and also because the offset of the first instruction is 0 instead of 1. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames[0].instruction_offset 2 """ @property def instruction_repetitions_arg( self, ) -> int: """If the instruction being referred to is a REPEAT block, this is the repetition count of that REPEAT block. Otherwise this field defaults to 0. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> full = err[0].circuit_error_locations[0].stack_frames[0] >>> loop = err[0].circuit_error_locations[0].stack_frames[1] >>> full.instruction_repetitions_arg 5 >>> loop.instruction_repetitions_arg 0 """ @property def iteration_index( self, ) -> int: """Disambiguates which iteration of the loop containing this instruction is being referred to. If the instruction isn't in a REPEAT block, this field defaults to 0. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> full = err[0].circuit_error_locations[0].stack_frames[0] >>> loop = err[0].circuit_error_locations[0].stack_frames[1] >>> full.iteration_index 0 >>> loop.iteration_index 4 """ class CircuitInstruction: """An instruction, like `H 0 1` or `CNOT rec[-1] 5`, from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... M 0 1 ... X_ERROR(0.125) 5 ... ''') >>> circuit[0] stim.CircuitInstruction('H', [stim.GateTarget(0)], []) >>> circuit[1] stim.CircuitInstruction('M', [stim.GateTarget(0), stim.GateTarget(1)], []) >>> circuit[2] stim.CircuitInstruction('X_ERROR', [stim.GateTarget(5)], [0.125]) """ def __eq__( self, arg0: stim.CircuitInstruction, ) -> bool: """Determines if two `stim.CircuitInstruction`s are identical. """ def __init__( self, name: str, targets: Optional[Iterable[Union[int, stim.GateTarget]]] = None, gate_args: Optional[Iterable[float]] = None, *, tag: str = "", ) -> None: """Creates or parses a `stim.CircuitInstruction`. Args: name: The name of the instruction being applied. If `targets` and `gate_args` aren't specified, this can be a full instruction line from a stim Circuit file, like "CX 0 1". targets: The targets the instruction is being applied to. These can be raw values like `0` and `stim.target_rec(-1)`, or instances of `stim.GateTarget`. gate_args: The sequence of numeric arguments parameterizing a gate. For noise gates this is their probabilities. For `OBSERVABLE_INCLUDE` instructions it's the index of the logical observable to affect. tag: Defaults to "". A custom string attached to the instruction. For example, for a TICK instruction, this could a string specifying an amount of time which is used by custom code for adding noise to a circuit. In general, stim will attempt to propagate tags across circuit transformations but will otherwise completely ignore them. Examples: >>> import stim >>> print(stim.CircuitInstruction('DEPOLARIZE1', [5], [0.25])) DEPOLARIZE1(0.25) 5 >>> stim.CircuitInstruction('CX rec[-1] 5 # comment') stim.CircuitInstruction('CX', [stim.target_rec(-1), stim.GateTarget(5)], []) >>> print(stim.CircuitInstruction('I', [2], tag='100ns')) I[100ns] 2 """ def __ne__( self, arg0: stim.CircuitInstruction, ) -> bool: """Determines if two `stim.CircuitInstruction`s are different. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CircuitInstruction`. """ def __str__( self, ) -> str: """Returns a text description of the instruction as a stim circuit file line. """ def gate_args_copy( self, ) -> List[float]: """Returns the gate's arguments (numbers parameterizing the instruction). For noisy gates this typically a list of probabilities. For OBSERVABLE_INCLUDE it's a singleton list containing the logical observable index. Examples: >>> import stim >>> instruction = stim.CircuitInstruction('X_ERROR', [2, 3], [0.125]) >>> instruction.gate_args_copy() [0.125] >>> instruction.gate_args_copy() == instruction.gate_args_copy() True >>> instruction.gate_args_copy() is instruction.gate_args_copy() False """ @property def name( self, ) -> str: """The name of the instruction (e.g. `H` or `X_ERROR` or `DETECTOR`). """ @property def num_measurements( self, ) -> int: """Returns the number of bits produced when running this instruction. Examples: >>> import stim >>> stim.CircuitInstruction('H', [0]).num_measurements 0 >>> stim.CircuitInstruction('M', [0]).num_measurements 1 >>> stim.CircuitInstruction('M', [2, 3, 5, 7, 11]).num_measurements 5 >>> stim.CircuitInstruction('MXX', [0, 1, 4, 5, 11, 13]).num_measurements 3 >>> stim.Circuit('MPP X0*X1 X0*Z1*Y2')[0].num_measurements 2 >>> stim.CircuitInstruction('HERALDED_ERASE', [0], [0.25]).num_measurements 1 """ @property def tag( self, ) -> str: """The custom tag attached to the instruction. The tag is an arbitrary string. The default tag, when none is specified, is the empty string. Examples: >>> import stim >>> stim.Circuit("H[test] 0")[0].tag 'test' >>> stim.Circuit("H 0")[0].tag '' """ def target_groups( self, ) -> List[List[stim.GateTarget]]: """Splits the instruction's targets into groups depending on the type of gate. Single qubit gates like H get one group per target. Two qubit gates like CX get one group per pair of targets. Pauli product gates like MPP get one group per combined product. Returns: A list of groups of targets. Examples: >>> import stim >>> for g in stim.Circuit('H 0 1 2')[0].target_groups(): ... print(repr(g)) [stim.GateTarget(0)] [stim.GateTarget(1)] [stim.GateTarget(2)] >>> for g in stim.Circuit('CX 0 1 2 3')[0].target_groups(): ... print(repr(g)) [stim.GateTarget(0), stim.GateTarget(1)] [stim.GateTarget(2), stim.GateTarget(3)] >>> for g in stim.Circuit('MPP X0*Y1*Z2 X5*X6')[0].target_groups(): ... print(repr(g)) [stim.target_x(0), stim.target_y(1), stim.target_z(2)] [stim.target_x(5), stim.target_x(6)] >>> for g in stim.Circuit('DETECTOR rec[-1] rec[-2]')[0].target_groups(): ... print(repr(g)) [stim.target_rec(-1)] [stim.target_rec(-2)] >>> for g in stim.Circuit('CORRELATED_ERROR(0.1) X0 Y1')[0].target_groups(): ... print(repr(g)) [stim.target_x(0), stim.target_y(1)] """ def targets_copy( self, ) -> List[stim.GateTarget]: """Returns a copy of the targets of the instruction. Examples: >>> import stim >>> instruction = stim.CircuitInstruction('X_ERROR', [2, 3], [0.125]) >>> instruction.targets_copy() [stim.GateTarget(2), stim.GateTarget(3)] >>> instruction.targets_copy() == instruction.targets_copy() True >>> instruction.targets_copy() is instruction.targets_copy() False """ class CircuitRepeatBlock: """A REPEAT block from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 >>> repeat_block.body_copy() stim.Circuit(''' CX 0 1 CZ 1 2 ''') """ def __eq__( self, arg0: stim.CircuitRepeatBlock, ) -> bool: """Determines if two `stim.CircuitRepeatBlock`s are identical. """ def __init__( self, repeat_count: int, body: stim.Circuit, *, tag: str = '', ) -> None: """Initializes a `stim.CircuitRepeatBlock`. Args: repeat_count: The number of times to repeat the block. body: The body of the block, as a circuit. tag: Defaults to empty. A custom string attached to the REPEAT instruction. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append(stim.CircuitRepeatBlock(100, stim.Circuit("M 0"))) >>> c stim.Circuit(''' REPEAT 100 { M 0 } ''') """ def __ne__( self, arg0: stim.CircuitRepeatBlock, ) -> bool: """Determines if two `stim.CircuitRepeatBlock`s are different. """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.CircuitRepeatBlock`. """ def body_copy( self, ) -> stim.Circuit: """Returns a copy of the body of the repeat block. (Making a copy is enforced to make it clear that editing the result won't change the block's body.) Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.body_copy() stim.Circuit(''' CX 0 1 CZ 1 2 ''') """ @property def name( self, ) -> str: """Returns the name "REPEAT". This is a duck-typing convenience method. It exists so that code that doesn't know whether it has a `stim.CircuitInstruction` or a `stim.CircuitRepeatBlock` can check the object's name without having to do an `instanceof` check first. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 1 2 ... } ... S 1 ... ''') >>> [instruction.name for instruction in circuit] ['H', 'REPEAT', 'S'] """ @property def num_measurements( self, ) -> int: """Returns the number of bits produced when running this loop. Examples: >>> import stim >>> stim.CircuitRepeatBlock( ... body=stim.Circuit("M 0 1"), ... repeat_count=25, ... ).num_measurements 50 """ @property def repeat_count( self, ) -> int: """The repetition count of the repeat block. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 """ @property def tag( self, ) -> str: """The custom tag attached to the REPEAT instruction. The tag is an arbitrary string. The default tag, when none is specified, is the empty string. Examples: >>> import stim >>> stim.Circuit(''' ... REPEAT[test] 5 { ... H 0 ... } ... ''')[0].tag 'test' >>> stim.Circuit(''' ... REPEAT 5 { ... H 0 ... } ... ''')[0].tag '' """ class CircuitTargetsInsideInstruction: """Describes a range of targets within a circuit instruction. """ def __init__( self, *, gate: str, tag: str = '', args: List[float], target_range_start: int, target_range_end: int, targets_in_range: List[stim.GateTargetWithCoords], ) -> None: """Creates a stim.CircuitTargetsInsideInstruction. Examples: >>> import stim >>> val = stim.CircuitTargetsInsideInstruction( ... gate='X_ERROR', ... tag='', ... args=[0.25], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=[stim.GateTargetWithCoords(0, [])], ... ) """ @property def args( self, ) -> List[float]: """Returns parens arguments of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.args [0.25] """ @property def gate( self, ) -> Optional[str]: """Returns the name of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.gate 'X_ERROR' """ @property def tag( self, ) -> str: """Returns the tag of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR[look-at-me-imma-tag](0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.tag 'look-at-me-imma-tag' """ @property def target_range_end( self, ) -> int: """Returns the exclusive end of the range of targets that were executing within the gate / instruction. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.target_range_start 0 >>> loc.instruction_targets.target_range_end 1 """ @property def target_range_start( self, ) -> int: """Returns the inclusive start of the range of targets that were executing within the gate / instruction. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.target_range_start 0 >>> loc.instruction_targets.target_range_end 1 """ @property def targets_in_range( self, ) -> List[stim.GateTargetWithCoords]: """Returns the subset of targets of the gate/instruction that were being executed. Includes coordinate data with the targets. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.targets_in_range [stim.GateTargetWithCoords(0, [])] """ class CliffordString: """A tensor product of single qubit Clifford gates (e.g. "H \u2297 X \u2297 S"). Represents a collection of Clifford operations applied pairwise to a collection of qubits. Ignores global phase. Examples: >>> import stim >>> stim.CliffordString("H,S,C_XYZ") * stim.CliffordString("H,H,H") stim.CliffordString("I,C_ZYX,SQRT_X_DAG") """ def __add__( self, rhs: stim.CliffordString, ) -> stim.CliffordString: """Concatenates two CliffordStrings. Args: rhs: The suffix of the concatenation. Returns: The concatenated Clifford string. Examples: >>> import stim >>> stim.CliffordString("I,X,H") + stim.CliffordString("Y,S") stim.CliffordString("I,X,H,Y,S") """ def __eq__( self, arg0: stim.CliffordString, ) -> bool: """Determines if two Clifford strings have identical contents. """ @overload def __getitem__( self, index_or_slice: int, ) -> stim.GateData: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.CliffordString: pass def __getitem__( self, index_or_slice: Union[int, slice], ) -> Union[stim.GateData, stim.CliffordString]: """Returns a Clifford or substring from the CliffordString. Args: index_or_slice: The index of the Clifford to return, or the slice corresponding to the sub CliffordString to return. Returns: The indexed Clifford (as a stim.GateData instance) or the sliced CliffordString. Examples: >>> import stim >>> s = stim.CliffordString("I,X,Y,Z,H") >>> s[2] stim.gate_data('Y') >>> s[-1] stim.gate_data('H') >>> s[:-1] stim.CliffordString("I,X,Y,Z") >>> s[::2] stim.CliffordString("I,Y,H") """ def __iadd__( self, rhs: stim.CliffordString, ) -> stim.CliffordString: """Mutates the CliffordString by concatenating onto it. Args: rhs: The suffix to concatenate onto the target CliffordString. Returns: The mutated Clifford string. Examples: >>> import stim >>> c = stim.CliffordString("I,X,H") >>> alias = c >>> alias += stim.CliffordString("Y,S") >>> c stim.CliffordString("I,X,H,Y,S") """ def __imul__( self, rhs: Union[stim.CliffordString, int], ) -> stim.CliffordString: """Inplace CliffordString multiplication. Mutates the CliffordString into itself multiplied by another CliffordString (via pairwise Clifford multipliation) or by an integer (via repeating the contents). Args: rhs: Either a stim.CliffordString or an int. If rhs is a stim.CliffordString, then the Cliffords from each string are multiplied pairwise. If rhs is an int, it is the number of times to repeat the Clifford string's contents. Returns: The mutated Clifford string. Examples: >>> import stim >>> c = stim.CliffordString("S,X,X") >>> alias = c >>> alias *= stim.CliffordString("S,Z,H,Z") >>> c stim.CliffordString("Z,Y,SQRT_Y,Z") >>> c = stim.CliffordString("I,X,H") >>> alias = c >>> alias *= 2 >>> c stim.CliffordString("I,X,H,I,X,H") """ def __init__( self, arg: Union[int, str, stim.CliffordString, stim.PauliString, stim.Circuit], /, ) -> None: """Initializes a stim.CliffordString from the given argument. Args: arg [position-only]: This can be a variety of types, including: int: initializes an identity Clifford string of the given length. str: initializes by parsing a comma-separated list of gate names. stim.CliffordString: initializes by copying the given Clifford string. stim.PauliString: initializes by copying from the given Pauli string (ignores the sign of the Pauli string). stim.Circuit: initializes a CliffordString equivalent to the action of the circuit (as long as the circuit only contains single qubit unitary operations and annotations). Iterable: initializes by interpreting each item as a Clifford. Each item can be a single-qubit Clifford gate name (like "SQRT_X") or stim.GateData instance. Examples: >>> import stim >>> stim.CliffordString(5) stim.CliffordString("I,I,I,I,I") >>> stim.CliffordString("X,Y,Z,SQRT_X") stim.CliffordString("X,Y,Z,SQRT_X") >>> stim.CliffordString(["H", stim.gate_data("S")]) stim.CliffordString("H,S") >>> stim.CliffordString(stim.PauliString("XYZ")) stim.CliffordString("X,Y,Z") >>> stim.CliffordString(stim.CliffordString("X,Y,Z")) stim.CliffordString("X,Y,Z") >>> stim.CliffordString(stim.Circuit(''' ... H 0 1 2 ... S 2 3 ... TICK ... S 3 ... I 6 ... ''')) stim.CliffordString("H,H,C_ZYX,Z,I,I,I") """ def __ipow__( self, num_qubits: int, ) -> object: """Mutates the CliffordString into itself raised to a power. Args: power: The power to raise the CliffordString's Cliffords to. This value can be negative (e.g. -1 inverts the string). Returns: The mutated Clifford string. Examples: >>> import stim >>> p = stim.CliffordString("I,X,H,S,C_XYZ") >>> p **= 3 >>> p stim.CliffordString("I,X,H,S_DAG,I") >>> p **= 2 >>> p stim.CliffordString("I,I,I,Z,I") >>> alias = p >>> alias **= 2 >>> p stim.CliffordString("I,I,I,I,I") """ def __len__( self, ) -> int: """Returns the number of Clifford operations in the string. Examples: >>> import stim >>> len(stim.CliffordString("I,X,Y,Z,H")) 5 """ def __mul__( self, rhs: Union[stim.CliffordString, int], ) -> stim.CliffordString: """CliffordString multiplication. Args: rhs: Either a stim.CliffordString or an int. If rhs is a stim.CliffordString, then the Cliffords from each string are multiplied pairwise. If rhs is an int, it is the number of times to repeat the Clifford string's contents. Examples: >>> import stim >>> stim.CliffordString("S,X,X") * stim.CliffordString("S,Z,H,Z") stim.CliffordString("Z,Y,SQRT_Y,Z") >>> stim.CliffordString("I,X,H") * 3 stim.CliffordString("I,X,H,I,X,H,I,X,H") """ def __ne__( self, arg0: stim.CliffordString, ) -> bool: """Determines if two Clifford strings have non-identical contents. """ def __pow__( self, power: int, ) -> stim.CliffordString: """Returns the CliffordString raised to a power. Args: power: The power to raise the CliffordString's Cliffords to. This value can be negative (e.g. -1 returns the inverse string). Returns: The Clifford string raised to the power. Examples: >>> import stim >>> p = stim.CliffordString("I,X,H,S,C_XYZ") >>> p**0 stim.CliffordString("I,I,I,I,I") >>> p**1 stim.CliffordString("I,X,H,S,C_XYZ") >>> p**12000001 stim.CliffordString("I,X,H,S,C_XYZ") >>> p**2 stim.CliffordString("I,I,I,Z,C_ZYX") >>> p**3 stim.CliffordString("I,X,H,S_DAG,I") >>> p**-1 stim.CliffordString("I,X,H,S_DAG,C_ZYX") """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CliffordString`. """ def __rmul__( self, lhs: int, ) -> stim.CliffordString: """CliffordString left-multiplication. Args: lhs: The number of times to repeat the Clifford string's contents. Returns: The repeated Clifford string. Examples: >>> import stim >>> 2 * stim.CliffordString("I,X,H") stim.CliffordString("I,X,H,I,X,H") >>> 0 * stim.CliffordString("I,X,H") stim.CliffordString("") >>> 5 * stim.CliffordString("I") stim.CliffordString("I,I,I,I,I") """ def __setitem__( self, index_or_slice: Union[int, slice], new_value: Union[str, stim.GateData, stim.CliffordString, stim.PauliString, stim.Tableau], ) -> None: """Overwrites an indexed Clifford, or slice of Cliffords, with the given value. Args: index_or_slice: The index of the Clifford to overwrite, or the slice of Cliffords to overwrite. new_value: Specifies the value to write into the Clifford string. This can be set to a few different types of values: - str: Name of the single qubit Clifford gate to write to the index or broadcast over the slice. - stim.GateData: The single qubit Clifford gate to write to the index or broadcast over the slice. - stim.Tableau: Must be a single qubit tableau. Specifies the single qubit Clifford gate to write to the index or broadcast over the slice. - stim.CliffordString: String of Cliffords to write into the slice. Examples: >>> import stim >>> s = stim.CliffordString("I,I,I,I,I") >>> s[1] = 'H' >>> s stim.CliffordString("I,H,I,I,I") >>> s[2:] = 'SQRT_X' >>> s stim.CliffordString("I,H,SQRT_X,SQRT_X,SQRT_X") >>> s[0] = stim.gate_data('S_DAG').inverse >>> s stim.CliffordString("S,H,SQRT_X,SQRT_X,SQRT_X") >>> s[:] = 'I' >>> s stim.CliffordString("I,I,I,I,I") >>> s[::2] = stim.CliffordString("X,Y,Z") >>> s stim.CliffordString("X,I,Y,I,Z") >>> s[0] = stim.Tableau.from_named_gate("H") >>> s stim.CliffordString("H,I,Y,I,Z") >>> s[:] = stim.Tableau.from_named_gate("S") >>> s stim.CliffordString("S,S,S,S,S") >>> s[:4] = stim.PauliString("IXYZ") >>> s stim.CliffordString("I,X,Y,Z,S") """ def __str__( self, ) -> str: """Returns a string representation of the CliffordString's operations. """ @staticmethod def all_cliffords_string( ) -> stim.CliffordString: """Returns a stim.CliffordString containing each single qubit Clifford once. Useful for things like testing that a method works on every single Clifford. Examples: >>> import stim >>> cliffords = stim.CliffordString.all_cliffords_string() >>> len(cliffords) 24 >>> print(cliffords[:8]) I,X,Y,Z,H_XY,S,S_DAG,H_NXY >>> print(cliffords[8:16]) H,SQRT_Y_DAG,H_NXZ,SQRT_Y,H_YZ,H_NYZ,SQRT_X,SQRT_X_DAG >>> print(cliffords[16:]) C_XYZ,C_XYNZ,C_NXYZ,C_XNYZ,C_ZYX,C_ZNYX,C_NZYX,C_ZYNX """ def copy( self, ) -> stim.CliffordString: """Returns a copy of the CliffordString. Returns: The copy. Examples: >>> import stim >>> c = stim.CliffordString("H,X") >>> alias = c >>> copy = c.copy() >>> c *= 5 >>> alias stim.CliffordString("H,X,H,X,H,X,H,X,H,X") >>> copy stim.CliffordString("H,X") """ @staticmethod def random( num_qubits: int, ) -> stim.CliffordString: """Samples a uniformly random CliffordString. Args: num_qubits: The number of qubits the CliffordString should act upon. Examples: >>> import stim >>> p = stim.CliffordString.random(5) >>> len(p) 5 Returns: The sampled Clifford string. """ def x_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates an X input into. For example, H conjugates X into +Z and S_DAG conjugates X into -Y. Combined with `z_outputs`, the results of this method completely specify the single qubit Clifford applied to each qubit. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> x_paulis, x_signs = stim.CliffordString("I,Y,H,S").x_outputs() >>> x_paulis stim.PauliString("+XXZY") >>> x_signs array([False, True, False, False]) >>> stim.CliffordString("I,Y,H,S").x_outputs(bit_packed_signs=True)[1] array([2], dtype=uint8) """ def y_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates a Y input into. For example, H conjugates Y into -Y and S_DAG conjugates Y into +X. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> y_paulis, y_signs = stim.CliffordString("I,X,H,S").y_outputs() >>> y_paulis stim.PauliString("+YYYX") >>> y_signs array([False, True, True, True]) >>> stim.CliffordString("I,X,H,S").y_outputs(bit_packed_signs=True)[1] array([14], dtype=uint8) """ def z_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates a Z input into. For example, H conjugates Z into +X and SQRT_X conjugates Z into -Y. Combined with `x_outputs`, the results of this method completely specify the single qubit Clifford applied to each qubit. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> z_paulis, z_signs = stim.CliffordString("I,Y,H,S").z_outputs() >>> z_paulis stim.PauliString("+ZZXZ") >>> z_signs array([False, True, False, False]) >>> stim.CliffordString("I,Y,H,S").z_outputs(bit_packed_signs=True)[1] array([2], dtype=uint8) """ class CompiledDemSampler: """A helper class for efficiently sampler from a detector error model. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) """ def sample( self, shots: int, *, bit_packed: bool = False, return_errors: bool = False, recorded_errors_to_replay: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray]]: """Samples the detector error model's error mechanisms to produce sample data. Args: shots: The number of times to sample from the model. bit_packed: Defaults to false. False: the returned numpy arrays have dtype=np.bool_. True: the returned numpy arrays have dtype=np.uint8 and pack 8 bits into each byte. Setting this to True is equivalent to running `np.packbits(data, bitorder='little', axis=1)` on each output value, but has the performance benefit of the data never being expanded into an unpacked form. return_errors: Defaults to False. False: the third entry of the returned tuple is None. True: the third entry of the returned tuple is a numpy array recording which errors were sampled. recorded_errors_to_replay: Defaults to None, meaning sample errors randomly. If not None, this is expected to be a 2d numpy array specifying which errors to apply (e.g. one returned from a previous call to the sample method). The array must have dtype=np.bool_ and shape=(num_shots, num_errors) or dtype=np.uint8 and shape=(num_shots, math.ceil(num_errors / 8)). Returns: A tuple (detector_data, obs_data, error_data). Assuming bit_packed is False and return_errors is True: - If error_data[s, k] is True, then the error with index k fired in the shot with index s. - If detector_data[s, k] is True, then the detector with index k ended up flipped in the shot with index s. - If obs_data[s, k] is True, then the observable with index k ended up flipped in the shot with index s. The dtype and shape of the data depends on the arguments: if bit_packed: detector_data.shape == (num_shots, math.ceil(num_detectors / 8)) detector_data.dtype == np.uint8 obs_data.shape == (num_shots, math.ceil(num_observables / 8)) obs_data.dtype == np.uint8 if return_errors: error_data.shape = (num_shots, math.ceil(num_errors / 8)) error_data.dtype = np.uint8 else: error_data is None else: detector_data.shape == (num_shots, num_detectors) detector_data.dtype == np.bool_ obs_data.shape == (num_shots, num_observables) obs_data.dtype == np.bool_ if return_errors: error_data.shape = (num_shots, num_errors) error_data.dtype = np.bool_ else: error_data is None Note that bit packing is done using little endian order on the last axis (i.e. like `np.packbits(data, bitorder='little', axis=1)`). Examples: >>> import stim >>> import numpy as np >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> # Taking samples. >>> det_data, obs_data, err_data_not_requested = sampler.sample(shots=4) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data_not_requested is None True >>> # Recording errors. >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) >>> # Bit packing. >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True, ... bit_packed=True) >>> det_data array([[6], [6], [6], [6]], dtype=uint8) >>> obs_data array([[1], [1], [1], [1]], dtype=uint8) >>> err_data array([[2], [2], [2], [2]], dtype=uint8) >>> # Recording and replaying errors. >>> noisy_dem = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.25) D1 ... ''') >>> noisy_sampler = noisy_dem.compile_sampler() >>> det_data, obs_data, err_data = noisy_sampler.sample( ... shots=100, ... return_errors=True) >>> replay_det_data, replay_obs_data, _ = noisy_sampler.sample( ... shots=100, ... recorded_errors_to_replay=err_data) >>> np.array_equal(det_data, replay_det_data) True >>> np.array_equal(obs_data, replay_obs_data) True """ def sample_write( self, shots: int, *, det_out_file: Union[None, str, pathlib.Path], det_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', obs_out_file: Union[None, str, pathlib.Path], obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', err_out_file: Union[None, str, pathlib.Path] = None, err_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', replay_err_in_file: Union[None, str, pathlib.Path] = None, replay_err_in_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Samples the detector error model and writes the results to disk. Args: shots: The number of times to sample from the model. det_out_file: Where to write detection event data. If None: detection event data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase det_out_format: The format to write the detection event data in (e.g. "01" or "b8"). obs_out_file: Where to write observable flip data. If None: observable flip data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase obs_out_format: The format to write the observable flip data in (e.g. "01" or "b8"). err_out_file: Where to write errors-that-occurred data. If None: errors-that-occurred data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase err_out_format: The format to write the errors-that-occurred data in (e.g. "01" or "b8"). replay_err_in_file: If this is specified, errors are replayed from data instead of generated randomly. The following types are supported: - None: errors are generated randomly according to the probabilities in the detector error model. - str or pathlib.Path: the file at the given path is opened and errors-to-apply data is read from there. - io.IOBase: NOT IMPLEMENTED replay_err_in_format: The format to write the errors-that-occurred data in (e.g. "01" or "b8"). Returns: Nothing. Results are written to disk. Examples: >>> import stim >>> import tempfile >>> import pathlib >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(0) D1 ... error(0) D0 ... error(1) D1 D2 L0 ... error(0) D0 ... ''') >>> sampler = dem.compile_sampler() >>> with tempfile.TemporaryDirectory() as d: ... d = pathlib.Path(d) ... sampler.sample_write( ... shots=1, ... det_out_file=d / 'dets.01', ... det_out_format='01', ... obs_out_file=d / 'obs.01', ... obs_out_format='01', ... err_out_file=d / 'err.hits', ... err_out_format='hits', ... ) ... with open(d / 'dets.01') as f: ... assert f.read() == "011\n" ... with open(d / 'obs.01') as f: ... assert f.read() == "1\n" ... with open(d / 'err.hits') as f: ... assert f.read() == "3\n" """ class CompiledDetectorSampler: """An analyzed stabilizer circuit whose detection events can be sampled quickly. """ def __init__( self, circuit: stim.Circuit, *, seed: object = None, ) -> None: """Creates an object that can sample the detection events from a circuit. Args: circuit: The circuit to sample from. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Returns: An initialized stim.CompiledDetectorSampler. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(1.0) 0 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[ True]]) """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.CompiledDetectorSampler`. """ def sample( self, shots: int, *, prepend_observables: bool = False, append_observables: bool = False, separate_observables: bool = False, bit_packed: bool = False, dets_out: Optional[np.ndarray] = None, obs_out: Optional[np.ndarray] = None, ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: """Returns a numpy array containing a batch of detector samples from the circuit. The circuit must define the detectors using DETECTOR instructions. Observables defined by OBSERVABLE_INCLUDE instructions can also be included in the results as honorary detectors. Args: shots: The number of times to sample every detector in the circuit. separate_observables: Defaults to False. When set to True, the return value is a (detection_events, observable_flips) tuple instead of a flat detection_events array. prepend_observables: Defaults to false. When set, observables are included with the detectors and are placed at the start of the results. append_observables: Defaults to false. When set, observables are included with the detectors and are placed at the end of the results. bit_packed: Returns a uint8 numpy array with 8 bits per byte, instead of a bool_ numpy array with 1 bit per byte. Uses little endian packing. dets_out: Defaults to None. Specifies a pre-allocated numpy array to write the detection event data into. This array must have the correct shape and dtype. obs_out: Defaults to None. Specifies a pre-allocated numpy array to write the observable flip data into. This array must have the correct shape and dtype. Returns: A numpy array or tuple of numpy arrays containing the samples. if separate_observables=False and bit_packed=False: A single numpy array. dtype=bool_ shape=( shots, num_detectors + num_observables * ( append_observables + prepend_observables), ) The bit for detection event `m` in shot `s` is at result[s, m] if separate_observables=False and bit_packed=True: A single numpy array. dtype=uint8 shape=( shots, math.ceil((num_detectors + num_observables * ( append_observables + prepend_observables)) / 8), ) The bit for detection event `m` in shot `s` is at (result[s, m // 8] >> (m % 8)) & 1 if separate_observables=True and bit_packed=False: A (dets, obs) tuple. dets.dtype=bool_ dets.shape=(shots, num_detectors) obs.dtype=bool_ obs.shape=(shots, num_observables) The bit for detection event `m` in shot `s` is at dets[s, m] The bit for observable `m` in shot `s` is at obs[s, m] if separate_observables=True and bit_packed=True: A (dets, obs) tuple. dets.dtype=uint8 dets.shape=(shots, math.ceil(num_detectors / 8)) obs.dtype=uint8 obs.shape=(shots, math.ceil(num_observables / 8)) The bit for detection event `m` in shot `s` is at (dets[s, m // 8] >> (m % 8)) & 1 The bit for observable `m` in shot `s` is at (obs[s, m // 8] >> (m % 8)) & 1 Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(1.0) 0 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[ True]]) """ def sample_bit_packed( self, shots: int, *, prepend_observables: bool = False, append_observables: bool = False, ) -> object: """[DEPRECATED] Use sampler.sample(..., bit_packed=True) instead. Returns a numpy array containing bit packed detector samples from the circuit. The circuit must define the detectors using DETECTOR instructions. Observables defined by OBSERVABLE_INCLUDE instructions can also be included in the results as honorary detectors. Args: shots: The number of times to sample every detector in the circuit. prepend_observables: Defaults to false. When set, observables are included with the detectors and are placed at the start of the results. append_observables: Defaults to false. When set, observables are included with the detectors and are placed at the end of the results. Returns: A numpy array with `dtype=uint8` and `shape=(shots, n)` where `n` is `num_detectors + num_observables*(append_observables+prepend_observables)`. The bit for detection event `m` in shot `s` is at `result[s, (m // 8)] & 2**(m % 8)`. """ def sample_write( self, shots: int, *, filepath: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', obs_out_filepath: Optional[Union[str, pathlib.Path]] = None, obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', prepend_observables: bool = False, append_observables: bool = False, ) -> None: """Samples detection events from the circuit and writes them to a file. Args: shots: The number of times to sample every measurement in the circuit. filepath: The file to write the results to. format: The output format to write the results with. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". obs_out_filepath: Sample observables as part of each shot, and write them to this file. This keeps the observable data separate from the detector data. obs_out_format: If writing the observables to a file, this is the format to write them in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". prepend_observables: Sample observables as part of each shot, and put them at the start of the detector data. append_observables: Sample observables as part of each shot, and put them at the end of the detector data. Returns: None. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f"{d}/tmp.dat" ... c = stim.Circuit(''' ... X_ERROR(1) 0 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''') ... c.compile_detector_sampler().sample_write( ... shots=3, ... filepath=path, ... format="dets") ... with open(path) as f: ... print(f.read(), end='') shot D0 shot D0 shot D0 """ class CompiledMeasurementSampler: """An analyzed stabilizer circuit whose measurements can be sampled quickly. """ def __init__( self, circuit: stim.Circuit, *, skip_reference_sample: bool = False, seed: object = None, reference_sample: object = None, ) -> None: """Creates a measurement sampler for the given circuit. The sampler uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the sampler, as a baseline for deriving more samples using an error propagation simulator. Args: circuit: The stim circuit to sample from. skip_reference_sample: Defaults to False. When set to True, the reference sample used by the sampler is initialized to all-zeroes instead of being collected from the circuit. This means that the results returned by the sampler are actually whether or not each measurement was *flipped*, instead of true measurement results. Forcing an all-zero reference sample is useful when you are only interested in error propagation and don't want to have to deal with the fact that some measurements want to be On when no errors occur. It is also useful when you know for sure that the all-zero result is actually a possible result from the circuit (under noiseless execution), meaning it is a valid reference sample as good as any other. Computing the reference sample is the most time consuming and memory intensive part of simulating the circuit, so promising that the simulator can safely skip that step is an effective optimization. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. reference_sample: The data to xor into the measurement flips produced by the frame simulator, in order to produce proper measurement results. This can either be specified as an `np.bool_` array or a bit packed `np.uint8` array (little endian). Under normal conditions, the reference sample should be a valid noiseless sample of the circuit, such as the one returned by `circuit.reference_sample()`. If this argument is not provided, the reference sample will be set to `circuit.reference_sample()`, unless `skip_reference_sample=True` is used, in which case it will be set to all-zeros. Returns: An initialized stim.CompiledMeasurementSampler. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[ True, False, True, True]]) """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CompiledMeasurementSampler`. """ def sample( self, shots: int, *, bit_packed: bool = False, ) -> np.ndarray: """Samples a batch of measurement samples from the circuit. Args: shots: The number of times to sample every measurement in the circuit. bit_packed: Returns a uint8 numpy array with 8 bits per byte, instead of a bool_ numpy array with 1 bit per byte. Uses little endian packing. Returns: A numpy array containing the samples. If bit_packed=False: dtype=bool_ shape=(shots, circuit.num_measurements) The bit for measurement `m` in shot `s` is at result[s, m] If bit_packed=True: dtype=uint8 shape=(shots, math.ceil(circuit.num_measurements / 8)) The bit for measurement `m` in shot `s` is at (result[s, m // 8] >> (m % 8)) & 1 Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[ True, False, True, True]]) """ def sample_bit_packed( self, shots: int, ) -> np.ndarray: """[DEPRECATED] Use sampler.sample(..., bit_packed=True) instead. Samples a bit packed batch of measurement samples from the circuit. Args: shots: The number of times to sample every measurement in the circuit. Returns: A numpy array with `dtype=uint8` and `shape=(shots, (num_measurements + 7) // 8)`. The bit for measurement `m` in shot `s` is at `result[s, (m // 8)] & 2**(m % 8)`. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 1 2 3 4 5 6 7 10 ... M 0 1 2 3 4 5 6 7 8 9 10 ... ''') >>> s = c.compile_sampler() >>> s.sample_bit_packed(shots=1) array([[255, 4]], dtype=uint8) """ def sample_write( self, shots: int, *, filepath: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Samples measurements from the circuit and writes them to a file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f"{d}/tmp.dat" ... c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') ... c.compile_sampler().sample_write(5, filepath=path, format="01") ... with open(path) as f: ... print(f.read(), end='') 1011 1011 1011 1011 1011 Args: shots: The number of times to sample every measurement in the circuit. filepath: The file to write the results to. format: The output format to write the results with. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". Returns: None. """ class CompiledMeasurementsToDetectionEventsConverter: """A tool for quickly converting measurements from an analyzed stabilizer circuit into detection events. """ def __init__( self, circuit: stim.Circuit, *, skip_reference_sample: bool = False, ) -> None: """Creates a measurement-to-detection-events converter for the given circuit. The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is. Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit. Args: circuit: The stim circuit to use for conversions. skip_reference_sample: Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution). Returns: An initialized stim.CompiledMeasurementsToDetectionEventsConverter. Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> converter.convert( ... measurements=np.array([[0], [1]], dtype=np.bool_), ... append_observables=False, ... ) array([[ True], [False]]) """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CompiledMeasurementsToDetectionEventsConverter`. """ @overload def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, append_observables: bool = False, bit_packed: bool = False, ) -> np.ndarray: pass @overload def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, separate_observables: Literal[True], append_observables: bool = False, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: pass def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, separate_observables: bool = False, append_observables: bool = False, bit_packed: bool = False, ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: """Converts measurement data into detection event data. Args: measurements: A numpy array containing measurement data. The dtype of the array is used to determine if it is bit packed or not. dtype=np.bool_ (unpacked data): shape=(num_shots, circuit.num_measurements) dtype=np.uint8 (bit packed data): shape=(num_shots, math.ceil(circuit.num_measurements / 8)) sweep_bits: Optional. A numpy array containing sweep data for the `sweep[k]` controls in the circuit. The dtype of the array is used to determine if it is bit packed or not. dtype=np.bool_ (unpacked data): shape=(num_shots, circuit.num_sweep_bits) dtype=np.uint8 (bit packed data): shape=(num_shots, math.ceil(circuit.num_sweep_bits / 8)) separate_observables: Defaults to False. When set to True, two numpy arrays are returned instead of one, with the second array containing the observable flip data. append_observables: Defaults to False. When set to True, the observables in the circuit are treated as if they were additional detectors. Their results are appended to the end of the detection event data. bit_packed: Defaults to False. When set to True, the returned numpy array contains bit packed data (dtype=np.uint8 with 8 bits per item) instead of unpacked data (dtype=np.bool_). Returns: The detection event data and (optionally) observable data. The result is a single numpy array if separate_observables is false, otherwise it's a tuple of two numpy arrays. When returning two numpy arrays, the first array is the detection event data and the second is the observable flip data. The dtype of the returned arrays is np.bool_ if bit_packed is false, otherwise they're np.uint8 arrays. shape[0] of the array(s) is the number of shots. shape[1] of the array(s) is the number of bits per shot (divided by 8 if bit packed) (e.g. for just detection event data it would be circuit.num_detectors). Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 1 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-2] ... ''').compile_m2d_converter() >>> dets, obs = converter.convert( ... measurements=np.array([[1, 0], ... [1, 0], ... [1, 0], ... [0, 0], ... [1, 0]], dtype=np.bool_), ... separate_observables=True, ... ) >>> dets array([[False, False], [False, False], [False, False], [False, True], [False, False]]) >>> obs array([[False], [False], [False], [ True], [False]]) """ def convert_file( self, *, measurements_filepath: Union[str, pathlib.Path], measurements_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', sweep_bits_filepath: Optional[Union[str, pathlib.Path]] = None, sweep_bits_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', detection_events_filepath: Union[str, pathlib.Path], detection_events_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', append_observables: bool = False, obs_out_filepath: Optional[Union[str, pathlib.Path]] = None, obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Reads measurement data from a file and writes detection events to another file. Args: measurements_filepath: A file containing measurement data to be converted. measurements_format: The format the measurement data is stored in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". detection_events_filepath: Where to save detection event data to. detection_events_format: The format to save the detection event data in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". sweep_bits_filepath: Defaults to None. A file containing sweep data, or None. When specified, sweep data (used for `sweep[k]` controls in the circuit, which can vary from shot to shot) will be read from the given file. When not specified, all sweep bits default to False and no sweep-controlled operations occur. sweep_bits_format: The format the sweep data is stored in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". obs_out_filepath: Sample observables as part of each shot, and write them to this file. This keeps the observable data separate from the detector data. obs_out_format: If writing the observables to a file, this is the format to write them in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". append_observables: When True, the observables in the circuit are included as part of the detection event data. Specifically, they are treated as if they were additional detectors at the end of the circuit. When False, observable data is not output. Examples: >>> import stim >>> import tempfile >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> with tempfile.TemporaryDirectory() as d: ... with open(f"{d}/measurements.01", "w") as f: ... print("0", file=f) ... print("1", file=f) ... converter.convert_file( ... measurements_filepath=f"{d}/measurements.01", ... detection_events_filepath=f"{d}/detections.01", ... append_observables=False, ... ) ... with open(f"{d}/detections.01") as f: ... print(f.read(), end="") 1 0 """ class DemInstruction: """An instruction from a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 L0 ... error(0.125) D1 D2 ... error(0.125) D2 D3 ... error(0.125) D3 ... ''') >>> instruction = model[0] >>> instruction stim.DemInstruction('error', [0.125], [stim.target_relative_detector_id(0)]) """ def __eq__( self, arg0: stim.DemInstruction, ) -> bool: """Determines if two instructions have identical contents. """ def __init__( self, type: str, args: Optional[Iterable[float]] = None, targets: Optional[Iterable[stim.DemTarget]] = None, *, tag: str = "", ) -> None: """Creates or parses a stim.DemInstruction. Args: type: The name of the instruction type (e.g. "error" or "shift_detectors"). If `args` and `targets` aren't specified, this can also be set to a full line of text from a dem file, like "error(0.25) D0". args: Numeric values parameterizing the instruction (e.g. the 0.1 in "error(0.1)"). targets: The objects the instruction involves (e.g. the "D0" and "L1" in "error(0.1) D0 L1"). tag: An arbitrary piece of text attached to the instruction. Examples: >>> import stim >>> instruction = stim.DemInstruction( ... 'error', ... [0.125], ... [stim.target_relative_detector_id(5)], ... tag='test-tag', ... ) >>> print(instruction) error[test-tag](0.125) D5 >>> print(stim.DemInstruction('error(0.125) D5 L6 ^ D4 # comment')) error(0.125) D5 L6 ^ D4 """ def __ne__( self, arg0: stim.DemInstruction, ) -> bool: """Determines if two instructions have non-identical contents. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.DetectorErrorModel`. """ def __str__( self, ) -> str: """Returns detector error model (.dem) instructions (that can be parsed by stim) for the model. """ def args_copy( self, ) -> List[float]: """Returns a copy of the list of numbers parameterizing the instruction. For example, this would be coordinates of a detector instruction or the probability of an error instruction. The result is a copy, meaning that editing it won't change the instruction's targets or future copies. Examples: >>> import stim >>> instruction = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''')[0] >>> instruction.args_copy() [0.125] >>> instruction.args_copy() == instruction.args_copy() True >>> instruction.args_copy() is instruction.args_copy() False """ @property def tag( self, ) -> str: """Returns the arbitrary text tag attached to the instruction. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error[test-tag](0.125) D0 ... error(0.125) D0 ... ''') >>> dem[0].tag 'test-tag' >>> dem[1].tag '' """ def target_groups( self, ) -> List[List[stim.DemTarget]]: """Returns a copy of the instruction's targets, split by target separators. When a detector error model instruction contains a suggested decomposition, its targets contain separators (`stim.DemTarget("^")`). This method splits the targets into groups based the separators, similar to how `str.split` works. Returns: A list of groups of targets. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.01) D0 D1 ^ D2 ... error(0.01) D0 L0 ... error(0.01) ... ''') >>> dem[0].target_groups() [[stim.DemTarget('D0'), stim.DemTarget('D1')], [stim.DemTarget('D2')]] >>> dem[1].target_groups() [[stim.DemTarget('D0'), stim.DemTarget('L0')]] >>> dem[2].target_groups() [[]] """ def targets_copy( self, ) -> List[Union[int, stim.DemTarget]]: """Returns a copy of the instruction's targets. The result is a copy, meaning that editing it won't change the instruction's targets or future copies. Examples: >>> import stim >>> instruction = stim.DetectorErrorModel(''' ... error(0.125) D0 L2 ... ''')[0] >>> instruction.targets_copy() [stim.DemTarget('D0'), stim.DemTarget('L2')] >>> instruction.targets_copy() == instruction.targets_copy() True >>> instruction.targets_copy() is instruction.targets_copy() False """ @property def type( self, ) -> str: """The name of the instruction type (e.g. "error" or "shift_detectors"). """ class DemRepeatBlock: """A repeat block from a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... repeat 100 { ... error(0.125) D0 D1 ... shift_detectors 1 ... } ... ''') >>> model[0] stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' error(0.125) D0 D1 shift_detectors 1 ''')) """ def __eq__( self, arg0: stim.DemRepeatBlock, ) -> bool: """Determines if two repeat blocks are identical. """ def __init__( self, repeat_count: int, block: stim.DetectorErrorModel, ) -> None: """Creates a stim.DemRepeatBlock. Args: repeat_count: The number of times the repeat block's body is supposed to execute. block: The body of the repeat block as a DetectorErrorModel containing the instructions to repeat. Examples: >>> import stim >>> repeat_block = stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' ... error(0.125) D0 D1 ... shift_detectors 1 ... ''')) """ def __ne__( self, arg0: stim.DemRepeatBlock, ) -> bool: """Determines if two repeat blocks are different. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.DemRepeatBlock`. """ def body_copy( self, ) -> stim.DetectorErrorModel: """Returns a copy of the block's body, as a stim.DetectorErrorModel. Examples: >>> import stim >>> body = stim.DetectorErrorModel(''' ... error(0.125) D0 D1 ... shift_detectors 1 ... ''') >>> repeat_block = stim.DemRepeatBlock(100, body) >>> repeat_block.body_copy() == body True >>> repeat_block.body_copy() is repeat_block.body_copy() False """ @property def repeat_count( self, ) -> int: """The number of times the repeat block's body is supposed to execute. """ @property def type( self, ) -> object: """Returns the type name "repeat". This is a duck-typing convenience method. It exists so that code that doesn't know whether it has a `stim.DemInstruction` or a `stim.DemRepeatBlock` can check the type field without having to do an `instanceof` check first. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.1) D0 L0 ... repeat 5 { ... error(0.1) D0 D1 ... shift_detectors 1 ... } ... logical_observable L0 ... ''') >>> [instruction.type for instruction in dem] ['error', 'repeat', 'logical_observable'] """ class DemTarget: """An instruction target from a detector error model (.dem) file. """ def __eq__( self, arg0: stim.DemTarget, ) -> bool: """Determines if two `stim.DemTarget`s are identical. """ def __init__( self, arg: object, /, ) -> None: """Creates a stim.DemTarget from the given object. Args: arg: A string to parse as a stim.DemTarget, or some other object to convert into a stim.DemTarget. Examples: >>> import stim >>> stim.DemTarget("D5") == stim.target_relative_detector_id(5) True >>> stim.DemTarget("L2") == stim.target_logical_observable_id(2) True >>> stim.DemTarget("^") == stim.target_separator() True """ def __ne__( self, arg0: stim.DemTarget, ) -> bool: """Determines if two `stim.DemTarget`s are different. """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.DemTarget`. """ def __str__( self, ) -> str: """Returns a text description of the detector error model target. """ def is_logical_observable_id( self, ) -> bool: """Determines if the detector error model target is a logical observable id target. In a detector error model file, observable targets are prefixed by `L`. For example, in `error(0.25) D0 L1` the `L1` is an observable target. Examples: >>> import stim >>> stim.DemTarget("L2").is_logical_observable_id() True >>> stim.DemTarget("D3").is_logical_observable_id() False >>> stim.DemTarget("^").is_logical_observable_id() False """ def is_relative_detector_id( self, ) -> bool: """Determines if the detector error model target is a relative detector id target. In a detector error model file, detectors are prefixed by `D`. For example, in `error(0.25) D0 L1` the `D0` is a relative detector target. Examples: >>> import stim >>> stim.DemTarget("L2").is_relative_detector_id() False >>> stim.DemTarget("D3").is_relative_detector_id() True >>> stim.DemTarget("^").is_relative_detector_id() False """ def is_separator( self, ) -> bool: """Determines if the detector error model target is a separator. Separates separate the components of a suggested decompositions within an error. For example, the `^` in `error(0.25) D1 D2 ^ D3 D4` is the separator. Examples: >>> import stim >>> stim.DemTarget("L2").is_separator() False >>> stim.DemTarget("D3").is_separator() False >>> stim.DemTarget("^").is_separator() True """ @staticmethod def logical_observable_id( index: int, ) -> stim.DemTarget: """Returns a logical observable id identifying a frame change. Args: index: The index of the observable. Returns: The logical observable target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.logical_observable_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) L13 ''') """ @staticmethod def relative_detector_id( index: int, ) -> stim.DemTarget: """Returns a relative detector id (e.g. "D5" in a .dem file). Args: index: The index of the detector, relative to the current detector offset. Returns: The relative detector target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D13 ''') """ @staticmethod def separator( ) -> stim.DemTarget: """Returns a target separator (e.g. "^" in a .dem file). Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(1), ... stim.DemTarget.separator(), ... stim.DemTarget.relative_detector_id(2), ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D1 ^ D2 ''') """ @property def val( self, ) -> int: """Returns the target's integer value. Example: >>> import stim >>> stim.DemTarget("D5").val 5 >>> stim.DemTarget("L6").val 6 """ class DemTargetWithCoords: """A detector error model instruction target with associated coords. It is also guaranteed that, if the type of the DEM target is a relative detector id, it is actually absolute (i.e. relative to 0). For example, if the DEM target is a detector from a circuit with coordinate arguments given to detectors, the coords field will contain the coordinate data for the detector. This is helpful information to have available when debugging a problem in a circuit, instead of having to constantly manually look up the coordinates of a detector index in order to understand what is happening. Examples: >>> import stim >>> t = stim.DemTargetWithCoords(stim.DemTarget("D1"), [1.5, 2.0]) >>> t.dem_target stim.DemTarget('D1') >>> t.coords [1.5, 2.0] """ def __init__( self, dem_target: stim.DemTarget, coords: List[float], ) -> None: """Creates a stim.DemTargetWithCoords. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0] stim.DemTargetWithCoords(dem_target=stim.DemTarget('D0'), coords=[2, 3]) """ @property def coords( self, ) -> List[float]: """Returns the associated coordinate information as a list of floats. If there is no coordinate information, returns an empty list. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0].coords [2.0, 3.0] """ @property def dem_target( self, ) -> stim.DemTarget: """Returns the actual DEM target as a `stim.DemTarget`. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0].dem_target stim.DemTarget('D0') """ class DetectorErrorModel: """An error model built out of independent error mechanics. This class is one of the most important classes in Stim, because it is the mechanism used to explain circuits to decoders. A typical workflow would look something like: 1. Create a quantum error correction circuit annotated with detectors and observables. 2. Fail at configuring your favorite decoder using the circuit, because it's a pain to convert circuit error mechanisms into a format understood by the decoder. 2a. Call circuit.detector_error_model(), with decompose_errors=True if working with a matching-based code. This converts the circuit errors into a straightforward list of independent "with probability p these detectors and observables get flipped" terms. 3. Write tedious but straightforward glue code to create whatever graph-like object the decoder needs from the detector error model. 3a. Actually, ideally, someone has already done that for you. For example, pymatching can take detector error models directly and sinter knows how to explain a detector error model to fusion_blossom. 4. Get samples using circuit.compile_detector_sampler(), feed them to the decoder, and compare its observable flip predictions to the actual flips recorded in the samples. 4a. Actually, sinter will basically handle steps 2 through 4 for you. So you should probably have just generated your circuits, called `sinter collect` on them, then `sinter plot` on the results. 5. Write the paper. Error mechanisms are described in terms of the visible detection events and the hidden observable frame changes that they causes. Error mechanisms can also suggest decompositions of their effects into components, which can be helpful for decoders that want to work with a simpler decomposed error model instead of the full error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 L0 ... error(0.125) D1 D2 ... error(0.125) D2 D3 ... error(0.125) D3 ... ''') >>> len(model) 5 >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model() stim.DetectorErrorModel(''' error(0.125) D0 error(0.375) D0 D1 error(0.25) D1 ''') """ def __add__( self, second: stim.DetectorErrorModel, ) -> stim.DetectorErrorModel: """Creates a detector error model by appending two models. Examples: >>> import stim >>> m1 = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''') >>> m2 = stim.DetectorErrorModel(''' ... error(0.25) D1 ... ''') >>> m1 + m2 stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D1 ''') """ def __eq__( self, arg0: stim.DetectorErrorModel, ) -> bool: """Determines if two detector error models have identical contents. """ @overload def __getitem__( self, index_or_slice: int, ) -> Union[stim.DemInstruction, stim.DemRepeatBlock]: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.DetectorErrorModel: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns copies of instructions from the detector error model. Args: index_or_slice: An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D1 L1 ... repeat 100 { ... error(0.125) D1 D2 ... shift_detectors 1 ... } ... error(0.125) D2 ... logical_observable L0 ... detector D5 ... ''') >>> model[0] stim.DemInstruction('error', [0.125], [stim.target_relative_detector_id(0)]) >>> model[2] stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' error(0.125) D1 D2 shift_detectors 1 ''')) >>> model[1::2] stim.DetectorErrorModel(''' error(0.125) D1 L1 error(0.125) D2 detector D5 ''') """ def __iadd__( self, second: stim.DetectorErrorModel, ) -> stim.DetectorErrorModel: """Appends a detector error model into the receiving model (mutating it). Examples: >>> import stim >>> m1 = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''') >>> m2 = stim.DetectorErrorModel(''' ... error(0.25) D1 ... ''') >>> m1 += m2 >>> print(repr(m1)) stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D1 ''') """ def __imul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Mutates the detector error model by putting its contents into a repeat block. Special case: if the repetition count is 0, the model is cleared. Special case: if the repetition count is 1, nothing happens. Args: repetitions: The number of times the repeat block should repeat. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> m *= 3 >>> print(m) repeat 3 { error(0.25) D0 shift_detectors 1 } """ def __init__( self, detector_error_model_text: str = '', ) -> None: """Creates a stim.DetectorErrorModel. Args: detector_error_model_text: Defaults to empty. Describes instructions to append into the circuit in the detector error model (.dem) format. Examples: >>> import stim >>> empty = stim.DetectorErrorModel() >>> not_empty = stim.DetectorErrorModel(''' ... error(0.125) D0 L0 ... ''') """ def __len__( self, ) -> int: """Returns the number of top-level instructions/blocks in the detector error model. Instructions inside of blocks are not included in this count. Examples: >>> import stim >>> len(stim.DetectorErrorModel()) 0 >>> len(stim.DetectorErrorModel(''' ... error(0.1) D0 D1 ... shift_detectors 100 ... logical_observable L5 ... ''')) 3 >>> len(stim.DetectorErrorModel(''' ... repeat 100 { ... error(0.1) D0 D1 ... error(0.1) D1 D2 ... } ... ''')) 1 """ def __mul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Repeats the detector error model using a repeat block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the repeat block should repeat. Returns: repetitions=0: An empty detector error model. repetitions=1: A copy of this detector error model. repetitions>=2: A detector error model with a single repeat block, where the contents of that repeat block are this detector error model. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> m * 3 stim.DetectorErrorModel(''' repeat 3 { error(0.25) D0 shift_detectors 1 } ''') """ def __ne__( self, arg0: stim.DetectorErrorModel, ) -> bool: """Determines if two detector error models have non-identical contents. """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.DetectorErrorModel`. """ def __rmul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Repeats the detector error model using a repeat block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the repeat block should repeat. Returns: repetitions=0: An empty detector error model. repetitions=1: A copy of this detector error model. repetitions>=2: A detector error model with a single repeat block, where the contents of that repeat block are this detector error model. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> 3 * m stim.DetectorErrorModel(''' repeat 3 { error(0.25) D0 shift_detectors 1 } ''') """ def __str__( self, ) -> str: """Returns the contents of a detector error model file (.dem) encoding the model. """ def append( self, instruction: object, parens_arguments: object = None, targets: List[object] = (), *, tag: str = '', ) -> None: """Appends an instruction to the detector error model. Args: instruction: Either the name of an instruction, a stim.DemInstruction, a stim.DemRepeatBlock. or a stim.DetectorErrorModel. The `parens_arguments`, `targets`, and 'tag' arguments should be given iff the instruction is a name. parens_arguments: Numeric values parameterizing the instruction. The numbers inside parentheses in a detector error model file (eg. the `0.25` in `error(0.25) D0`). This argument can be given either a list of doubles, or a single double (which will be implicitly wrapped into a list). targets: The instruction targets, such as the `D0` in `error(0.25) D0`. tag: An arbitrary piece of text attached to the repeat instruction. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.125, [ ... stim.DemTarget.relative_detector_id(1), ... ]) >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(1), ... stim.DemTarget.separator(), ... stim.DemTarget.relative_detector_id(2), ... stim.DemTarget.logical_observable_id(3), ... ], tag='test-tag') >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 ''') >>> m.append("shift_detectors", (1, 2, 3), [5]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 ''') >>> m += m * 3 >>> m.append(m[0]) >>> m.append(m[-2]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 repeat 3 { error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } error(0.125) D1 repeat 3 { error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } ''') """ def approx_equals( self, other: object, *, atol: float, ) -> bool: """Checks if detector error models are approximately equal. Two detector error model are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example `error(0.100) D0` is approximately equal to `error(0.099) D0` within an absolute tolerance of 0.002. All other details of the models (such as the ordering of errors and their targets) must be exactly the same. Args: other: The detector error model, or other object, to compare to this one. atol: The absolute error tolerance. The maximum amount each probability may have been perturbed by. Returns: True if the given object is a detector error model approximately equal up to the receiving circuit up to the given tolerance, otherwise False. Examples: >>> import stim >>> base = stim.DetectorErrorModel(''' ... error(0.099) D0 D1 ... ''') >>> base.approx_equals(base, atol=0) True >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0) False >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0.0001) False >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0.01) True >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.099) D0 D1 L0 L1 L2 L3 L4 ... '''), atol=9999) False """ def clear( self, ) -> None: """Clears the contents of the detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.1) D0 D1 ... ''') >>> model.clear() >>> model stim.DetectorErrorModel() """ def compile_sampler( self, *, seed: object = None, ) -> stim.CompiledDemSampler: """Returns a CompiledDemSampler that can batch sample from detector error models. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Returns: A seeded stim.CompiledDemSampler for the given detector error model. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) """ def copy( self, ) -> stim.DetectorErrorModel: """Returns a copy of the detector error model. The copy is an independent detector error model with the same contents. Examples: >>> import stim >>> c1 = stim.DetectorErrorModel("error(0.1) D0 D1") >>> c2 = c1.copy() >>> c2 is c1 False >>> c2 == c1 True """ def diagram( self, type: Literal["matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html"] = 'matchgraph-svg', ) -> Any: """Returns a diagram of the circuit, from a variety of options. Args: type: The type of diagram. Available types are: "matchgraph-svg": An image of the decoding graph of the detector error model. Red lines are errors crossing a logical observable. Blue lines are undecomposed hyper errors. "matchgraph-svg-html": Same as matchgraph-svg but with the SVG wrapped in a resizable HTML iframe. "matchgraph-3d": A 3d model of the decoding graph of the detector error model. Red lines are errors crossing a logical observable. Blue lines are undecomposed hyper errors. GLTF files can be opened with a variety of programs, or opened online in viewers such as https://gltf-viewer.donmccurdy.com/ . Red lines are errors crossing a logical observable. "matchgraph-3d-html": Same 3d model as 'match-graph-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. Returns: An object whose `__str__` method returns the diagram, so that writing the diagram to a file works correctly. The returned object also defines a `_repr_html_` method, so that ipython notebooks recognize it can be shown using a specialized viewer instead of as raw text. Examples: >>> import stim >>> import tempfile >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... after_clifford_depolarization=0.01) >>> dem = circuit.detector_error_model(decompose_errors=True) >>> with tempfile.TemporaryDirectory() as d: ... diagram = circuit.diagram("match-graph-svg") ... with open(f"{d}/dem_image.svg", "w") as f: ... print(diagram, file=f) >>> with tempfile.TemporaryDirectory() as d: ... diagram = circuit.diagram("match-graph-3d") ... with open(f"{d}/dem_3d_model.gltf", "w") as f: ... print(diagram, file=f) """ def flattened( self, ) -> stim.DetectorErrorModel: """Returns the detector error model without repeat or detector_shift instructions. Returns: A `stim.DetectorErrorModel` with the same errors in the same order, but with repeat loops flattened into actually repeated instructions and with all coordinate/index shifts inlined. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... REPEAT 5 { ... error(0.25) D0 D1 ... shift_detectors 1 ... } ... error(0.125) D0 L0 ... ''').flattened() stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D0 D1 error(0.25) D1 D2 error(0.25) D2 D3 error(0.25) D3 D4 error(0.25) D4 D5 error(0.125) D5 L0 ''') """ @staticmethod def from_file( file: Union[io.TextIOBase, str, pathlib.Path], ) -> stim.DetectorErrorModel: """Reads a detector error model from a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md Args: file: A file path or open file object to read from. Returns: The circuit parsed from the file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('error(0.25) D2 D3', file=f) ... circuit = stim.DetectorErrorModel.from_file(path) >>> circuit stim.DetectorErrorModel(''' error(0.25) D2 D3 ''') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('error(0.25) D2 D3', file=f) ... with open(path) as f: ... circuit = stim.DetectorErrorModel.from_file(f) >>> circuit stim.DetectorErrorModel(''' error(0.25) D2 D3 ''') """ def get_detector_coordinates( self, only: object = None, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of detectors in the detector error model. Args: only: Defaults to None (meaning include all detectors). A list of detector indices to include in the result. Detector indices beyond the end of the detector error model cause an error. Returns: A dictionary mapping integers (detector indices) to lists of floats (coordinates). Detectors with no specified coordinate data are mapped to an empty tuple. If `only` is specified, then `set(result.keys()) == set(only)`. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.25) D0 D1 ... detector(1, 2, 3) D1 ... shift_detectors(5) 1 ... detector(1, 2) D2 ... ''') >>> dem.get_detector_coordinates() {0: [], 1: [1.0, 2.0, 3.0], 2: [], 3: [6.0, 2.0]} >>> dem.get_detector_coordinates(only=[1]) {1: [1.0, 2.0, 3.0]} """ @property def num_detectors( self, ) -> int: """Counts the number of detectors (e.g. `D2`) in the error model. Detector indices are assumed to be contiguous from 0 up to whatever the maximum detector id is. If the largest detector's absolute id is n-1, then the number of detectors is n. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model().num_detectors 2 >>> stim.DetectorErrorModel(''' ... error(0.1) D0 D199 ... ''').num_detectors 200 >>> stim.DetectorErrorModel(''' ... shift_detectors 1000 ... error(0.1) D0 D199 ... ''').num_detectors 1200 """ @property def num_errors( self, ) -> int: """Counts the number of errors (e.g. `error(0.1) D0`) in the error model. Error instructions inside repeat blocks count once per repetition. Redundant errors with the same targets count as separate errors. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... repeat 100 { ... repeat 5 { ... error(0.25) D1 ... } ... } ... ''').num_errors 501 """ @property def num_observables( self, ) -> int: """Counts the number of frame changes (e.g. `L2`) in the error model. Observable indices are assumed to be contiguous from 0 up to whatever the maximum observable id is. If the largest observable's id is n-1, then the number of observables is n. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(99) rec[-1] ... ''').detector_error_model().num_observables 100 >>> stim.DetectorErrorModel(''' ... error(0.1) L399 ... ''').num_observables 400 """ def rounded( self, arg0: int, ) -> stim.DetectorErrorModel: """Creates an equivalent detector error model but with rounded error probabilities. Args: digits: The number of digits to round to. Returns: A `stim.DetectorErrorModel` with the same instructions in the same order, but with the parens arguments of error instructions rounded to the given precision. Instructions whose error probability was rounded to zero are still included in the output. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.019499) D0 ... error(0.000001) D0 D1 ... ''') >>> dem.rounded(2) stim.DetectorErrorModel(''' error(0.02) D0 error(0) D0 D1 ''') >>> dem.rounded(3) stim.DetectorErrorModel(''' error(0.019) D0 error(0) D0 D1 ''') """ def shortest_graphlike_error( self, ignore_ungraphlike_errors: bool = True, ) -> stim.DetectorErrorModel: """Finds a minimum set of graphlike errors to produce an undetected logical error. Note that this method does not pay attention to error probabilities (other than ignoring errors with probability 0). It searches for a logical error with the minimum *number* of physical errors, not the maximum probability of those physical errors all occurring. This method works by looking for errors that have frame changes (eg. "error(0.1) D0 D1 L5" flips the frame of observable 5). These errors are converted into one or two symptoms and a net frame change. The symptoms can then be moved around by following errors touching that symptom. Each symptom is moved until it disappears into a boundary or cancels against another remaining symptom, while leaving the other symptoms alone (ensuring only one symptom is allowed to move significantly reduces waste in the search space). Eventually a path or cycle of errors is found that cancels out the symptoms, and if there is still a frame change at that point then that path or cycle is a logical error (otherwise all that was found was a stabilizer of the system; a dead end). The search process advances like a breadth first search, seeded from all the frame-change errors and branching them outward in tandem, until one of them wins the race to find a solution. Args: ignore_ungraphlike_errors: Defaults to True. When False, an exception is raised if there are any errors in the model that are not graphlike. When True, those errors are skipped as if they weren't present. A graphlike error is an error with less than two symptoms. For the purposes of this method, errors are also considered graphlike if they are decomposed into graphlike components: graphlike: error(0.1) D0 error(0.1) D0 D1 error(0.1) D0 D1 L0 not graphlike but decomposed into graphlike components: error(0.1) D0 D1 ^ D2 not graphlike, not decomposed into graphlike components: error(0.1) D0 D1 D2 error(0.1) D0 D1 D2 ^ D3 Returns: A detector error model containing just the error instructions corresponding to an undetectable logical error. There will be no other kinds of instructions (no `repeat`s, no `shift_detectors`, etc). The error probabilities will all be set to 1. The `len` of the returned model is the graphlike code distance of the circuit. But beware that in general the true code distance may be smaller. For example, in the XZ surface code with twists, the true minimum sized logical error is likely to use Y errors. But each Y error decomposes into two graphlike components (the X part and the Z part). As a result, the graphlike code distance in that context is likely to be nearly twice as large as the true code distance. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 ... error(0.125) D1 L55 ... error(0.125) D1 ... ''').shortest_graphlike_error() stim.DetectorErrorModel(''' error(1) D1 error(1) D1 L55 ''') >>> stim.DetectorErrorModel(''' ... error(0.125) D0 D1 D2 ... error(0.125) L0 ... ''').shortest_graphlike_error(ignore_ungraphlike_errors=True) stim.DetectorErrorModel(''' error(1) L0 ''') >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... before_round_data_depolarization=0.01) >>> model = circuit.detector_error_model(decompose_errors=True) >>> len(model.shortest_graphlike_error()) 7 """ def to_file( self, file: Union[io.TextIOBase, str, pathlib.Path], ) -> None: """Writes the detector error model to a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md Args: file: A file path or an open file to write to. Examples: >>> import stim >>> import tempfile >>> c = stim.DetectorErrorModel('error(0.25) D2 D3') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... c.to_file(f) ... with open(path) as f: ... contents = f.read() >>> contents 'error(0.25) D2 D3\n' >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... c.to_file(path) ... with open(path) as f: ... contents = f.read() >>> contents 'error(0.25) D2 D3\n' """ def without_tags( self, ) -> stim.DetectorErrorModel: """Returns a copy of the detector error model with all tags removed. Returns: A `stim.DetectorErrorModel` with the same instructions except all tags have been removed. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error[test-tag](0.25) D0 ... ''').without_tags() stim.DetectorErrorModel(''' error(0.25) D0 ''') """ class ExplainedError: """Describes the location of an error mechanism from a stim circuit. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0]) ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } } """ def __init__( self, *, dem_error_terms: List[stim.DemTargetWithCoords], circuit_error_locations: List[stim.CircuitErrorLocation], ) -> None: """Creates a stim.ExplainedError. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0]) ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } } """ @property def circuit_error_locations( self, ) -> List[stim.CircuitErrorLocation]: """The locations of circuit errors that produce the symptoms in dem_error_terms. Note: if this list contains a single entry, it may be because a result with a single representative error was requested (as opposed to all possible errors). Note: if this list is empty, it may be because there was a DEM error decomposed into parts where one of the parts is impossible to make on its own from a single circuit error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0].circuit_error_locations[0]) CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } """ @property def dem_error_terms( self, ) -> List[stim.DemTargetWithCoords]: """The detectors and observables flipped by this error mechanism. """ class FlipSimulator: """A simulator that tracks whether things are flipped, instead of what they are. Tracking flips is significantly cheaper than tracking actual values, requiring O(1) work per gate (compared to O(n) for unitary operations and O(n^2) for collapsing operations in the tableau simulator, where n is the qubit count). Supports interactive usage, where gates and measurements are applied on demand. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) """ def __init__( self, *, batch_size: int, disable_stabilizer_randomization: bool = False, num_qubits: int = 0, seed: Optional[int] = None, ) -> None: """Initializes a stim.FlipSimulator. Args: batch_size: For speed, the flip simulator simulates many instances in parallel. This argument determines the number of parallel instances. It's recommended to use a multiple of 256, because internally the state of the instances is striped across SSE (128 bit) or AVX (256 bit) words with one bit in the word belonging to each instance. The result is that, even if you only ask for 1 instance, probably the same amount of work is being done as if you'd asked for 256 instances. The extra results just aren't being used, creating waste. disable_stabilizer_randomization: Determines whether or not the flip simulator uses stabilizer randomization. Defaults to False (stabilizer randomization used). Set to True to disable stabilizer randomization. Stabilizer randomization means that, when a qubit is initialized or measured in the Z basis, a Z error is added to the qubit with 50% probability. More generally, anytime a stabilizer is introduced into the system by any means, an error equal to that stabilizer is applied with 50% probability. This ensures that observables anticommuting with stabilizers of the system must be maximally uncertain. In other words, this feature enforces Heisenberg's uncertainty principle. This is a safety feature that you should not turn off unless you have a reason to do so. Stabilizer randomization is turned on by default because it catches mistakes. For example, suppose you are trying to create a stabilizer code but you accidentally have the code measure two anticommuting stabilizers. With stabilizer randomization turned off, it will look like this code works. With stabilizer randomization turned on, the two measurements will correctly randomize each other revealing that the code doesn't work. In some use cases, stabilizer randomization is a hindrance instead of helpful. For example, if you are using the flip simulator to understand how an error propagates through the system, the stabilizer randomization will be introducing error terms that you don't want. num_qubits: Sets the initial number of qubits tracked by the simulation. The simulator will still automatically resize as needed when qubits beyond this limit are touched. This parameter exists as a way to hint at the desired size of the simulator's state for performance, and to ensure methods that peek at the size have the expected size from the start instead of only after the relevant qubits have been touched. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: An initialized stim.FlipSimulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) """ def append_measurement_flips( self, measurement_flip_data: np.ndarray, ) -> None: """Appends measurement flip data to the simulator's measurement record. Args: measurement_flip_data: The flip data to append. The following shape/dtype combinations are supported. Single measurement without bit packing: shape=(self.batch_size,) dtype=np.bool_ Single measurement with bit packing: shape=(math.ceil(self.batch_size / 8),) dtype=np.uint8 Multiple measurements without bit packing: shape=(num_measurements, self.batch_size) dtype=np.bool_ Multiple measurements with bit packing: shape=(num_measurements, math.ceil(self.batch_size / 8)) dtype=np.uint8 Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.append_measurement_flips(np.array( ... [0, 1, 0, 0, 1, 0, 0, 1, 1], ... dtype=np.bool_, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True]]) >>> sim.append_measurement_flips(np.array( ... [0b11001001, 0], ... dtype=np.uint8, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False]]) >>> sim.append_measurement_flips(np.array( ... [[0b11111111, 0b1], [0b00000000, 0b0], [0b11111111, 0b1]], ... dtype=np.uint8, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False], [ True, True, True, True, True, True, True, True, True], [False, False, False, False, False, False, False, False, False], [ True, True, True, True, True, True, True, True, True]]) >>> sim.append_measurement_flips(np.array( ... [[1, 0, 1, 0, 1, 0, 1, 0, 1], [0, 1, 0, 1, 0, 1, 0, 1, 0]], ... dtype=np.bool_, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False], [ True, True, True, True, True, True, True, True, True], [False, False, False, False, False, False, False, False, False], [ True, True, True, True, True, True, True, True, True], [ True, False, True, False, True, False, True, False, True], [False, True, False, True, False, True, False, True, False]]) """ @property def batch_size( self, ) -> int: """Returns the number of instances being simulated by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.batch_size 256 >>> sim = stim.FlipSimulator(batch_size=42) >>> sim.batch_size 42 """ def broadcast_pauli_errors( self, *, pauli: Union[str, int], mask: np.ndarray, p: float = 1, ) -> None: """Applies a pauli error to all qubits in all instances, filtered by a mask. Args: pauli: The pauli, specified as an integer or string. Uses the convention 0=I, 1=X, 2=Y, 3=Z. Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. mask: A 2d numpy array specifying where to apply errors. The first axis is qubits, the second axis is simulation instances. The first axis can have a length less than the current number of qubits (or more, which adds qubits to the simulation). The length of the second axis must match the simulator's `batch_size`. The array must satisfy mask.dtype == np.bool_ len(mask.shape) == 2 mask.shape[1] == flip_sim.batch_size The error is only applied to qubit q in instance k when mask[q, k] == True. p: Defaults to 1 (no effect). When specified, the error is applied probabilistically instead of deterministically to each (instance, qubit) pair matching the mask. This argument specifies the probability. Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator( ... batch_size=2, ... num_qubits=3, ... disable_stabilizer_randomization=True, ... ) >>> sim.broadcast_pauli_errors( ... pauli='X', ... mask=np.asarray([[True, False],[False, False],[True, True]]), ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+X_X"), stim.PauliString("+__X")] >>> sim.broadcast_pauli_errors( ... pauli='Z', ... mask=np.asarray([[False, True],[False, False],[True, True]]), ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] """ def clear( self, ) -> None: """Clears the simulator's state, so it can be reused for another simulation. This clears the measurement flip history, clears the detector flip history, and zeroes the observable flip state. It also resets all qubits to |0>. If stabilizer randomization is disabled, this zeros all pauli flip data. Otherwise it randomizes all pauli flips to be I or Z with equal probability. Behind the scenes, this doesn't free memory or resize the simulator. So, repeating the same simulation with calls to `clear` in between will be faster than allocating a new simulator each time (by avoiding re-allocations). Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.do(stim.Circuit("M(0.1) 9")) >>> sim.num_qubits 10 >>> sim.get_measurement_flips().shape (1, 256) >>> sim.clear() >>> sim.num_qubits 10 >>> sim.get_measurement_flips().shape (0, 256) """ def copy( self, *, copy_rng: bool = False, seed: Optional[int] = None, ) -> stim.FlipSimulator: """Returns a simulator with the same internal state, except perhaps its prng. Args: copy_rng: Defaults to False. When False, the copy's pseudo random number generator is reinitialized with a random seed instead of being a copy of the original simulator's pseudo random number generator. This causes the copy and the original to sample independent randomness, instead of identical randomness, for future random operations. When set to true, the copy will have the exact same pseudo random number generator state as the original, and so will produce identical results if told to do the same noisy operations. This argument is incompatible with the `seed` argument. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng state is either copied from the original simulator or reseeded from system entropy, depending on the copy_rng argument. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: The copy of the simulator. Examples: >>> import stim >>> import numpy as np >>> s1 = stim.FlipSimulator(batch_size=256) >>> s1.set_pauli_flip('X', qubit_index=2, instance_index=3) >>> s2 = s1.copy() >>> s2 is s1 False >>> s2.peek_pauli_flips() == s1.peek_pauli_flips() True >>> s1 = stim.FlipSimulator(batch_size=256) >>> s2 = s1.copy(copy_rng=True) >>> s1.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) >>> s2.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) >>> np.array_equal(s1.get_measurement_flips(), s2.get_measurement_flips()) True """ def do( self, obj: Union[stim.Circuit, stim.CircuitInstruction, stim.CircuitRepeatBlock], ) -> None: """Applies a circuit or circuit instruction to the simulator's state. The results of any measurements performed can be retrieved using the `get_measurement_flips` method. Args: obj: The circuit or instruction to apply to the simulator's state. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=1, ... disable_stabilizer_randomization=True, ... ) >>> circuit = stim.Circuit(''' ... X_ERROR(1) 0 1 3 ... REPEAT 5 { ... H 0 ... C_XYZ 1 ... } ... ''') >>> sim.do(circuit) >>> sim.peek_pauli_flips() [stim.PauliString("+ZZ_X")] >>> sim.do(circuit[0]) >>> sim.peek_pauli_flips() [stim.PauliString("+YY__")] >>> sim.do(circuit[1]) >>> sim.peek_pauli_flips() [stim.PauliString("+YX__")] """ def generate_bernoulli_samples( self, num_samples: int, *, p: float, bit_packed: bool = False, out: Optional[np.ndarray] = None, ) -> np.ndarray: """Uses the simulator's random number generator to produce biased coin flips. This method has best performance when specifying `bit_packed=True` and when specifying an `out=` parameter pointing to a numpy array that has contiguous data aligned to a 64 bit boundary. (If `out` isn't specified, the returned numpy array will have this property.) Args: num_samples: The number of samples to produce. p: The probability of each sample being True instead of False. bit_packed: Defaults to False (no bit packing). When True, the result has type np.uint8 instead of np.bool_ and 8 samples are packed into each byte as if by np.packbits(bitorder='little'). (The bit order is relevant when producing a number of samples that isn't a multiple of 8.) out: Defaults to None (allocate new). A numpy array to write the samples into. Must have the correct size and dtype. Returns: A numpy array containing the samples. The shape and dtype depends on the bit_packed argument: if not bit_packed: shape = (num_samples,) dtype = np.bool_ elif not transpose and bit_packed: shape = (math.ceil(num_samples / 8),) dtype = np.uint8 Raises: ValueError: The given `out` argument had a shape or dtype inconsistent with the requested data. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> r = sim.generate_bernoulli_samples(1001, p=0.25) >>> r.dtype dtype('bool') >>> r.shape (1001,) >>> r = sim.generate_bernoulli_samples(53, p=0.1, bit_packed=True) >>> r.dtype dtype('uint8') >>> r.shape (7,) >>> r[6] & 0b1110_0000 # zero'd padding bits np.uint8(0) >>> r2 = sim.generate_bernoulli_samples(53, p=0.2, bit_packed=True, out=r) >>> r is r2 # Check request to reuse r worked. True """ def get_detector_flips( self, *, detector_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves detector flip data from the simulator's detection event record. Args: record_index: Identifies a detector to read results from. Setting this to None (default) returns results from all detectors. Otherwise this should be an integer in range(0, self.num_detectors). instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_detectors, self.batch_size), where the first index is the detector_index and the second index is the instance_index and the dtype is np.bool_. Specifying detector_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a detector_index (or a 0d array, a boolean, if detector_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit(''' ... M 0 0 0 ... DETECTOR rec[-2] rec[-3] ... DETECTOR rec[-1] rec[-2] ... ''')) >>> sim.get_detector_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_detector_flips(bit_packed=True) array([[0, 0], [0, 0]], dtype=uint8) >>> sim.get_detector_flips(instance_index=2) array([False, False]) >>> sim.get_detector_flips(detector_index=1) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_detector_flips(instance_index=2, detector_index=1) array(False) """ def get_measurement_flips( self, *, record_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves measurement flip data from the simulator's measurement record. Args: record_index: Identifies a measurement to read results from. Setting this to None (default) returns results from all measurements. Setting this to a non-negative integer indexes measurements by the order they occurred. For example, record index 0 is the first measurement. Setting this to a negative integer indexes measurements by recency. For example, recording index -1 is the most recent measurement. instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be set to an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_measurements, self.batch_size), where the first index is the measurement_index and the second index is the instance_index and the dtype is np.bool_. Specifying record_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a measurement_index (or a 0d array, a boolean, if record_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit('M 0 1 2')) >>> sim.get_measurement_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_measurement_flips(bit_packed=True) array([[0, 0], [0, 0], [0, 0]], dtype=uint8) >>> sim.get_measurement_flips(instance_index=1) array([False, False, False]) >>> sim.get_measurement_flips(record_index=2) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_measurement_flips(instance_index=1, record_index=2) array(False) """ def get_observable_flips( self, *, observable_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves observable flip data from the simulator's detection event record. Args: record_index: Identifies a observable to read results from. Setting this to None (default) returns results from all observables. Otherwise this should be an integer in range(0, self.num_observables). instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_observables, self.batch_size), where the first index is the observable_index and the second index is the instance_index and the dtype is np.bool_. Specifying observable_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a observable_index (or a 0d array, a boolean, if observable_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit(''' ... M 0 0 0 ... OBSERVABLE_INCLUDE(0) rec[-2] ... OBSERVABLE_INCLUDE(1) rec[-1] ... ''')) >>> sim.get_observable_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_observable_flips(bit_packed=True) array([[0, 0], [0, 0]], dtype=uint8) >>> sim.get_observable_flips(instance_index=2) array([False, False]) >>> sim.get_observable_flips(observable_index=1) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_observable_flips(instance_index=2, observable_index=1) array(False) """ @property def num_detectors( self, ) -> int: """Returns the number of detectors that have been simulated and stored. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_detectors 0 >>> sim.do(stim.Circuit(''' ... M 0 0 ... DETECTOR rec[-1] rec[-2] ... ''')) >>> sim.num_detectors 1 """ @property def num_measurements( self, ) -> int: """Returns the number of measurements that have been simulated and stored. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_measurements 0 >>> sim.do(stim.Circuit('M 3 5')) >>> sim.num_measurements 2 """ @property def num_observables( self, ) -> int: """Returns the number of observables currently tracked by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_observables 0 >>> sim.do(stim.Circuit(''' ... M 0 ... OBSERVABLE_INCLUDE(4) rec[-1] ... ''')) >>> sim.num_observables 5 """ @property def num_qubits( self, ) -> int: """Returns the number of qubits currently tracked by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_qubits 0 >>> sim = stim.FlipSimulator(batch_size=256, num_qubits=4) >>> sim.num_qubits 4 >>> sim.do(stim.Circuit('H 5')) >>> sim.num_qubits 6 """ @overload def peek_pauli_flips( self, ) -> List[stim.PauliString]: pass @overload def peek_pauli_flips( self, *, instance_index: int, ) -> stim.PauliString: pass def peek_pauli_flips( self, *, instance_index: Optional[int] = None, ) -> Union[stim.PauliString, List[stim.PauliString]]: """Returns the current pauli errors packed into stim.PauliString instances. Args: instance_index: Defaults to None. When set to None, the pauli errors from all instances are returned as a list of `stim.PauliString`. When set to an integer, a single `stim.PauliString` is returned containing the errors for the indexed instance. Returns: if instance_index is None: A list of stim.PauliString, with the k'th entry being the errors from the k'th simulation instance. else: A stim.PauliString with the errors from the k'th simulation instance. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=2, ... disable_stabilizer_randomization=True, ... num_qubits=10, ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+__________"), stim.PauliString("+__________")] >>> sim.peek_pauli_flips(instance_index=0) stim.PauliString("+__________") >>> sim.do(stim.Circuit(''' ... X_ERROR(1) 0 3 5 ... Z_ERROR(1) 3 6 ... ''')) >>> sim.peek_pauli_flips() [stim.PauliString("+X__Y_XZ___"), stim.PauliString("+X__Y_XZ___")] >>> sim = stim.FlipSimulator( ... batch_size=1, ... num_qubits=100, ... ) >>> flips: stim.PauliString = sim.peek_pauli_flips(instance_index=0) >>> sorted(set(str(flips))) # Should have Zs from stabilizer randomization ['+', 'Z', '_'] """ def set_pauli_flip( self, pauli: Union[str, int], *, qubit_index: int, instance_index: int, ) -> None: """Sets the pauli flip on a given qubit in a given simulation instance. Args: pauli: The pauli, specified as an integer or string. Uses the convention 0=I, 1=X, 2=Y, 3=Z. Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. qubit_index: The qubit to put the error on. Must be non-negative. The state will automatically expand as needed to store the error. instance_index: The simulation index to put the error inside. Use negative indices to index from the end of the list. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=2, ... num_qubits=3, ... disable_stabilizer_randomization=True, ... ) >>> sim.set_pauli_flip('X', qubit_index=2, instance_index=1) >>> sim.peek_pauli_flips() [stim.PauliString("+___"), stim.PauliString("+__X")] """ def to_numpy( self, *, bit_packed: bool = False, transpose: bool = False, output_xs: Union[bool, np.ndarray] = False, output_zs: Union[bool, np.ndarray] = False, output_measure_flips: Union[bool, np.ndarray] = False, output_detector_flips: Union[bool, np.ndarray] = False, output_observable_flips: Union[bool, np.ndarray] = False, ) -> Optional[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]]: """Writes the simulator state into numpy arrays. Args: bit_packed: Whether or not the result is bit packed, storing 8 bits per byte instead of 1 bit per byte. Bit packing always applies to the second index of the result. Bits are packed in little endian order (as if by `np.packbits(X, axis=1, order='little')`). transpose: Defaults to False. When set to False, the second index of the returned array (the index affected by bit packing) is the shot index (meaning the first index is the qubit index or measurement index or etc). When set to True, results are transposed so that the first index is the shot index. output_xs: Defaults to False. When set to False, the X flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the X flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_zs: Defaults to False. When set to False, the Z flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the Z flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_measure_flips: Defaults to False. When set to False, the measure flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the measure flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_detector_flips: Defaults to False. When set to False, the detector flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the detector flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_observable_flips: Defaults to False. When set to False, the obs flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the obs flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). Returns: A tuple (xs, zs, ms, ds, os) of numpy arrays. The xs and zs arrays are the pauli flip data specified using XZ encoding (00=I, 10=X, 11=Y, 01=Z). The ms array is the measure flip data, the ds array is the detector flip data, and the os array is the obs flip data. The arrays default to `None` when the corresponding `output_*` argument was left False. The shape and dtype of the data depends on arguments given to the function. The following specifies each array's shape and dtype for each case: if not transpose and not bit_packed: xs.shape = (sim.batch_size, sim.num_qubits) zs.shape = (sim.batch_size, sim.num_qubits) ms.shape = (sim.batch_size, sim.num_measurements) ds.shape = (sim.batch_size, sim.num_detectors) os.shape = (sim.batch_size, sim.num_observables) xs.dtype = np.bool_ zs.dtype = np.bool_ ms.dtype = np.bool_ ds.dtype = np.bool_ os.dtype = np.bool_ elif not transpose and bit_packed: xs.shape = (sim.batch_size, math.ceil(sim.num_qubits / 8)) zs.shape = (sim.batch_size, math.ceil(sim.num_qubits / 8)) ms.shape = (sim.batch_size, math.ceil(sim.num_measurements / 8)) ds.shape = (sim.batch_size, math.ceil(sim.num_detectors / 8)) os.shape = (sim.batch_size, math.ceil(sim.num_observables / 8)) xs.dtype = np.uint8 zs.dtype = np.uint8 ms.dtype = np.uint8 ds.dtype = np.uint8 os.dtype = np.uint8 elif transpose and not bit_packed: xs.shape = (sim.num_qubits, sim.batch_size) zs.shape = (sim.num_qubits, sim.batch_size) ms.shape = (sim.num_measurements, sim.batch_size) ds.shape = (sim.num_detectors, sim.batch_size) os.shape = (sim.num_observables, sim.batch_size) xs.dtype = np.bool_ zs.dtype = np.bool_ ms.dtype = np.bool_ ds.dtype = np.bool_ os.dtype = np.bool_ elif transpose and bit_packed: xs.shape = (sim.num_qubits, math.ceil(sim.batch_size / 8)) zs.shape = (sim.num_qubits, math.ceil(sim.batch_size / 8)) ms.shape = (sim.num_measurements, math.ceil(sim.batch_size / 8)) ds.shape = (sim.num_detectors, math.ceil(sim.batch_size / 8)) os.shape = (sim.num_observables, math.ceil(sim.batch_size / 8)) xs.dtype = np.uint8 zs.dtype = np.uint8 ms.dtype = np.uint8 ds.dtype = np.uint8 os.dtype = np.uint8 Raises: ValueError: All the `output_*` arguments were False, or an `output_*` argument had a shape or dtype inconsistent with the requested data. Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit('M(1) 0 1 2')) >>> ms_buf = np.empty(shape=(9, 1), dtype=np.uint8) >>> xs, zs, ms, ds, os = sim.to_numpy( ... transpose=True, ... bit_packed=True, ... output_xs=True, ... output_measure_flips=ms_buf, ... ) >>> assert ms is ms_buf >>> xs array([[0], [0], [0], [0], [0], [0], [0], [0], [0]], dtype=uint8) >>> zs >>> ms array([[7], [7], [7], [7], [7], [7], [7], [7], [7]], dtype=uint8) >>> ds >>> os """ class FlippedMeasurement: """Describes a measurement that was flipped. Gives the measurement's index in the measurement record, and also the observable of the measurement. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement stim.FlippedMeasurement( record_index=1, observable=(stim.GateTargetWithCoords(stim.target_z(10), []),), ) """ def __init__( self, measurement_record_index: Optional[int], measured_observable: Iterable[stim.GateTargetWithCoords], ): """Creates a stim.FlippedMeasurement. Examples: >>> import stim >>> print(stim.FlippedMeasurement( ... record_index=5, ... observable=[], ... )) stim.FlippedMeasurement( record_index=5, observable=(), ) """ @property def observable( self, ) -> List[stim.GateTargetWithCoords]: """Returns the observable of the flipped measurement. For example, an `MX 5` measurement will have the observable X5. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement.observable [stim.GateTargetWithCoords(stim.target_z(10), [])] """ @property def record_index( self, ) -> int: """The measurement record index of the flipped measurement. For example, the fifth measurement in a circuit has a measurement record index of 4. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement.record_index 1 """ class Flow: """A stabilizer flow (e.g. "XI -> XX xor rec[-1]"). Stabilizer circuits implement, and can be defined by, how they turn input stabilizers into output stabilizers mediated by measurements. These relationships are called stabilizer flows, and `stim.Flow` is a representation of such a flow. For example, a `stim.Flow` can be given to `stim.Circuit.has_flow` to verify that a circuit implements the flow. A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer P at the start of the circuit to the instantaneous stabilizer Q at the end of the circuit. The flow may be mediated by certain measurements. For example, a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and the CNOT flows implemented by the circuit involve these measurements. A flow like P -> Q means the circuit transforms P into Q. A flow like 1 -> P means the circuit prepares P. A flow like P -> 1 means the circuit measures P. A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). References: Stim's gate documentation includes the stabilizer flows of each gate. Appendix A of https://arxiv.org/abs/2302.02192 describes how flows are defined and provides a circuit construction for experimentally verifying their presence. Examples: >>> import stim >>> c = stim.Circuit("CNOT 2 4") >>> c.has_flow(stim.Flow("__X__ -> __X_X")) True >>> c.has_flow(stim.Flow("X2*X4 -> X2")) True >>> c.has_flow(stim.Flow("Z4 -> Z4")) False """ def __eq__( self, arg0: stim.Flow, ) -> bool: """Determines if two flows have identical contents. """ def __init__( self, arg: Union[None, str, stim.Flow] = None, /, *, input: Optional[stim.PauliString] = None, output: Optional[stim.PauliString] = None, measurements: Optional[Iterable[Union[int, GateTarget]]] = None, included_observables: Optional[Iterable[int]] = None, ) -> None: """Initializes a stim.Flow. When given a string, the string is parsed as flow shorthand. For example, the string "X_ -> ZZ xor rec[-1]" will result in a flow with input pauli string "X_", output pauli string "ZZ", and measurement indices [-1]. Args: arg [position-only]: Defaults to None. Must be specified by itself if used. str: Initializes a flow by parsing the given shorthand text. stim.Flow: Initializes a copy of the given flow. None (default): Initializes an empty flow. input: Defaults to None. Can be set to a stim.PauliString to directly specify the flow's input stabilizer. output: Defaults to None. Can be set to a stim.PauliString to directly specify the flow's output stabilizer. measurements: Defaults to None. Can be set to a list of integers or gate targets like `stim.target_rec(-1)`, to specify the measurements that mediate the flow. Negative and positive measurement indices are allowed. Indexes follow the python convention where -1 is the last measurement in a circuit and 0 is the first measurement in a circuit. included_observables: Defaults to None. `OBSERVABLE_INCLUDE` instructions that target an observable index from this list will be implicitly included in the flow. This allows flows to refer to observables. For example, the flow "X5 -> obs[3]" says "At the start of the circuit, observable 3 should be an X term on qubit 5. By the end of the circuit it will be measured. The `OBSERVABLE_INCLUDE(3)` instructions in the circuit should explain how this happened.". Examples: >>> import stim >>> stim.Flow("X2 -> -Y2*Z4 xor rec[-1]") stim.Flow("__X -> -__Y_Z xor rec[-1]") >>> stim.Flow("Z -> 1 xor rec[-1]") stim.Flow("Z -> rec[-1]") >>> stim.Flow( ... input=stim.PauliString("XX"), ... output=stim.PauliString("_X"), ... measurements=[], ... ) stim.Flow("XX -> _X") >>> # Identical terms cancel. >>> stim.Flow("X2 -> Y2*Y2 xor rec[-2] xor rec[-2]") stim.Flow("__X -> ___") >>> stim.Flow("X -> Y xor obs[3] xor obs[3] xor obs[3]") stim.Flow("X -> Y xor obs[3]") """ def __mul__( self, rhs: stim.Flow, ) -> stim.Flow: """Computes the product of two flows. Args: rhs: The right hand side of the multiplication. Returns: The product of the two flows. Raises: ValueError: The inputs anti-commute (their product would be anti-Hermitian). For example, 1 -> X times 1 -> Y fails because it would give 1 -> iZ. Examples: >>> import stim >>> stim.Flow("X -> X") * stim.Flow("Z -> Z") stim.Flow("Y -> Y") >>> stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ") stim.Flow("1 -> -YY") >>> stim.Flow("X -> rec[-1]") * stim.Flow("X -> rec[-2]") stim.Flow("_ -> rec[-2] xor rec[-1]") """ def __ne__( self, arg0: stim.Flow, ) -> bool: """Determines if two flows have non-identical contents. """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.Flow`. """ def __str__( self, ) -> str: """Returns a shorthand description of the flow. """ def included_observables_copy( self, ) -> List[int]: """Returns a copy of the flow's included observable indices. When an observable is included in a flow, the flow implicitly includes all measurements and pauli terms from `OBSERVABLE_INCLUDE` instructions targeting that observable index. Examples: >>> import stim >>> f = stim.Flow(included_observables=[3, 2]) >>> f.included_observables_copy() [2, 3] >>> f.included_observables_copy() is f.included_observables_copy() False >>> f = stim.Flow("X2 -> obs[3]") >>> f.included_observables_copy() [3] >>> stim.Circuit("OBSERVABLE_INCLUDE(3) X2").has_flow(f) True """ def input_copy( self, ) -> stim.PauliString: """Returns a copy of the flow's input stabilizer. Examples: >>> import stim >>> f = stim.Flow(input=stim.PauliString('XX')) >>> f.input_copy() stim.PauliString("+XX") >>> f.input_copy() is f.input_copy() False """ def measurements_copy( self, ) -> List[int]: """Returns a copy of the flow's measurement indices. Examples: >>> import stim >>> f = stim.Flow(measurements=[-1, 2]) >>> f.measurements_copy() [-1, 2] >>> f.measurements_copy() is f.measurements_copy() False """ def output_copy( self, ) -> stim.PauliString: """Returns a copy of the flow's output stabilizer. Examples: >>> import stim >>> f = stim.Flow(output=stim.PauliString('XX')) >>> f.output_copy() stim.PauliString("+XX") >>> f.output_copy() is f.output_copy() False """ class GateData: """Details about a gate supported by stim. Examples: >>> import stim >>> stim.gate_data('h').name 'H' >>> stim.gate_data('h').is_unitary True >>> stim.gate_data('h').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) """ def __eq__( self, arg0: stim.GateData, ) -> bool: """Determines if two GateData instances are identical. """ def __init__( self, name: str, ) -> None: """Finds gate data for the named gate. Examples: >>> import stim >>> stim.GateData('H').is_unitary True """ def __ne__( self, arg0: stim.GateData, ) -> bool: """Determines if two GateData instances are not identical. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.GateData`. """ def __str__( self, ) -> str: """Returns text describing the gate data. """ @property def aliases( self, ) -> List[str]: """Returns all aliases that can be used to name the gate. Although gates can be referred to by lower case and mixed case named, the result only includes upper cased aliases. Examples: >>> import stim >>> stim.gate_data('H').aliases ['H', 'H_XZ'] >>> stim.gate_data('cnot').aliases ['CNOT', 'CX', 'ZCX'] """ @property def flows( self, ) -> Optional[List[stim.Flow]]: """Returns stabilizer flow generators for the gate, or else None. A stabilizer flow describes an input-output relationship that the gate satisfies, where an input pauli string is transformed into an output pauli string mediated by certain measurement results. Caution: this method returns None for variable-target-count gates like MPP. Not because MPP has no stabilizer flows, but because its stabilizer flows depend on how many qubits it targets and what basis it targets them in. Returns: A list of stim.Flow instances representing the generators. Examples: >>> import stim >>> stim.gate_data('H').flows [stim.Flow("X -> Z"), stim.Flow("Z -> X")] >>> for e in stim.gate_data('ISWAP').flows: ... print(e) X_ -> ZY Z_ -> _Z _X -> YZ _Z -> Z_ >>> for e in stim.gate_data('MXX').flows: ... print(e) X_ -> X_ _X -> _X ZZ -> ZZ XX -> rec[-1] """ @property def generalized_inverse( self, ) -> stim.GateData: """The closest-thing-to-an-inverse for the gate, if forced to pick something. The generalized inverse of a unitary gate U is its actual inverse U^-1. The generalized inverse of a reset or measurement gate U is a gate V such that, for every stabilizer flow that U has, V has the time reverse of that flow (up to Pauli feedback, with potentially more flows). For example, the time-reverse of R is MR because R has the single flow 1 -> Z and MR has the time reversed flow Z -> rec[-1]. The generalized inverse of noise like X_ERROR is just the same noise. The generalized inverse of an annotation like TICK is just the same annotation. Examples: >>> import stim >>> stim.gate_data('H').generalized_inverse stim.gate_data('H') >>> stim.gate_data('CXSWAP').generalized_inverse stim.gate_data('SWAPCX') >>> stim.gate_data('X_ERROR').generalized_inverse stim.gate_data('X_ERROR') >>> stim.gate_data('MX').generalized_inverse stim.gate_data('MX') >>> stim.gate_data('MRY').generalized_inverse stim.gate_data('MRY') >>> stim.gate_data('R').generalized_inverse stim.gate_data('M') >>> stim.gate_data('DETECTOR').generalized_inverse stim.gate_data('DETECTOR') >>> stim.gate_data('TICK').generalized_inverse stim.gate_data('TICK') """ def hadamard_conjugated( self, *, unsigned: bool = False, ) -> Optional[stim.GateData]: """Returns a stim gate equivalent to this gate conjugated by Hadamard gates. The Hadamard conjugate can be thought of as the XZ dual of the gate; the gate you get by exchanging the X and Z bases. For example, a SQRT_X will become a SQRT_Z and a CX gate will switch directions into an XCZ. If stim doesn't define a gate equivalent to conjugating this gate by Hadamards, the value `None` is returned. Args: unsigned: Defaults to False. When False, the returned gate must be *exactly* the Hadamard conjugation of this gate. When True, the returned gate must have the same flows but the sign of the flows can be different (i.e. the returned gate must be the Hadamard conjugate up to Pauli gate differences). Returns: A stim.GateData instance of the Hadamard conjugate, if it exists in stim. None, if stim doesn't define a gate equal to the Hadamard conjugate. Examples: >>> import stim >>> stim.gate_data('X').hadamard_conjugated() stim.gate_data('Z') >>> stim.gate_data('CX').hadamard_conjugated() stim.gate_data('XCZ') >>> stim.gate_data('RY').hadamard_conjugated() is None True >>> stim.gate_data('RY').hadamard_conjugated(unsigned=True) stim.gate_data('RY') >>> stim.gate_data('ISWAP').hadamard_conjugated(unsigned=True) is None True >>> stim.gate_data('SWAP').hadamard_conjugated() stim.gate_data('SWAP') >>> stim.gate_data('CXSWAP').hadamard_conjugated() stim.gate_data('SWAPCX') >>> stim.gate_data('MXX').hadamard_conjugated() stim.gate_data('MZZ') >>> stim.gate_data('DEPOLARIZE1').hadamard_conjugated() stim.gate_data('DEPOLARIZE1') >>> stim.gate_data('X_ERROR').hadamard_conjugated() stim.gate_data('Z_ERROR') >>> stim.gate_data('H_XY').hadamard_conjugated() stim.gate_data('H_NYZ') >>> stim.gate_data('DETECTOR').hadamard_conjugated(unsigned=True) stim.gate_data('DETECTOR') """ @property def inverse( self, ) -> Optional[stim.GateData]: """The inverse of the gate, or None if it has no inverse. The inverse V of a gate U must have the property that V undoes the effects of U and that U undoes the effects of V. In particular, the circuit U 0 1 V 0 1 should be equivalent to doing nothing at all. Examples: >>> import stim >>> stim.gate_data('H').inverse stim.gate_data('H') >>> stim.gate_data('CX').inverse stim.gate_data('CX') >>> stim.gate_data('S').inverse stim.gate_data('S_DAG') >>> stim.gate_data('CXSWAP').inverse stim.gate_data('SWAPCX') >>> stim.gate_data('X_ERROR').inverse is None True >>> stim.gate_data('M').inverse is None True >>> stim.gate_data('R').inverse is None True >>> stim.gate_data('DETECTOR').inverse is None True >>> stim.gate_data('TICK').inverse is None True """ @property def is_noisy_gate( self, ) -> bool: """Returns whether or not the gate can produce noise. Note that measurement operations are considered noisy, because for example `M(0.001) 2 3 5` will include noise that flips its result 0.1% of the time. Examples: >>> import stim >>> stim.gate_data('M').is_noisy_gate True >>> stim.gate_data('MXX').is_noisy_gate True >>> stim.gate_data('X_ERROR').is_noisy_gate True >>> stim.gate_data('CORRELATED_ERROR').is_noisy_gate True >>> stim.gate_data('MPP').is_noisy_gate True >>> stim.gate_data('H').is_noisy_gate False >>> stim.gate_data('CX').is_noisy_gate False >>> stim.gate_data('R').is_noisy_gate False >>> stim.gate_data('DETECTOR').is_noisy_gate False """ @property def is_reset( self, ) -> bool: """Returns whether or not the gate resets qubits in any basis. Examples: >>> import stim >>> stim.gate_data('R').is_reset True >>> stim.gate_data('RX').is_reset True >>> stim.gate_data('MR').is_reset True >>> stim.gate_data('M').is_reset False >>> stim.gate_data('MXX').is_reset False >>> stim.gate_data('MPP').is_reset False >>> stim.gate_data('H').is_reset False >>> stim.gate_data('CX').is_reset False >>> stim.gate_data('HERALDED_ERASE').is_reset False >>> stim.gate_data('DEPOLARIZE2').is_reset False >>> stim.gate_data('X_ERROR').is_reset False >>> stim.gate_data('CORRELATED_ERROR').is_reset False >>> stim.gate_data('DETECTOR').is_reset False """ @property def is_single_qubit_gate( self, ) -> bool: """Returns whether or not the gate is a single qubit gate. Single qubit gates apply separately to each of their targets. Variable-qubit gates like CORRELATED_ERROR and MPP are not considered single qubit gates. Examples: >>> import stim >>> stim.gate_data('H').is_single_qubit_gate True >>> stim.gate_data('R').is_single_qubit_gate True >>> stim.gate_data('M').is_single_qubit_gate True >>> stim.gate_data('X_ERROR').is_single_qubit_gate True >>> stim.gate_data('CX').is_single_qubit_gate False >>> stim.gate_data('MXX').is_single_qubit_gate False >>> stim.gate_data('CORRELATED_ERROR').is_single_qubit_gate False >>> stim.gate_data('MPP').is_single_qubit_gate False >>> stim.gate_data('DETECTOR').is_single_qubit_gate False >>> stim.gate_data('TICK').is_single_qubit_gate False >>> stim.gate_data('REPEAT').is_single_qubit_gate False """ @property def is_symmetric_gate( self, ) -> bool: """Returns whether or not the gate is the same when its targets are swapped. A two qubit gate is symmetric if it doesn't matter if you swap its targets. It is unaffected when conjugated by the SWAP gate. Single qubit gates are vacuously symmetric. A multi-qubit gate is symmetric if swapping any two of its targets has no effect. Note that this method is for symmetry *without broadcasting*. For example, SWAP is symmetric even though SWAP 1 2 3 4 isn't equal to SWAP 1 3 2 4. Returns: True if the gate is symmetric. False if the gate isn't symmetric. Examples: >>> import stim >>> stim.gate_data('CX').is_symmetric_gate False >>> stim.gate_data('CZ').is_symmetric_gate True >>> stim.gate_data('ISWAP').is_symmetric_gate True >>> stim.gate_data('CXSWAP').is_symmetric_gate False >>> stim.gate_data('MXX').is_symmetric_gate True >>> stim.gate_data('DEPOLARIZE2').is_symmetric_gate True >>> stim.gate_data('PAULI_CHANNEL_2').is_symmetric_gate False >>> stim.gate_data('H').is_symmetric_gate True >>> stim.gate_data('R').is_symmetric_gate True >>> stim.gate_data('X_ERROR').is_symmetric_gate True >>> stim.gate_data('CORRELATED_ERROR').is_symmetric_gate False >>> stim.gate_data('MPP').is_symmetric_gate False >>> stim.gate_data('DETECTOR').is_symmetric_gate False """ @property def is_two_qubit_gate( self, ) -> bool: """Returns whether or not the gate is a two qubit gate. Two qubit gates must be given an even number of targets. Variable-qubit gates like CORRELATED_ERROR and MPP are not considered two qubit gates. Returns: True if the gate is a two qubit gate. False if the gate isn't a two qubit gate. Examples: >>> import stim >>> stim.gate_data('CX').is_two_qubit_gate True >>> stim.gate_data('MXX').is_two_qubit_gate True >>> stim.gate_data('H').is_two_qubit_gate False >>> stim.gate_data('R').is_two_qubit_gate False >>> stim.gate_data('M').is_two_qubit_gate False >>> stim.gate_data('X_ERROR').is_two_qubit_gate False >>> stim.gate_data('CORRELATED_ERROR').is_two_qubit_gate False >>> stim.gate_data('MPP').is_two_qubit_gate False >>> stim.gate_data('DETECTOR').is_two_qubit_gate False """ @property def is_unitary( self, ) -> bool: """Returns whether or not the gate is a unitary gate. Examples: >>> import stim >>> stim.gate_data('H').is_unitary True >>> stim.gate_data('CX').is_unitary True >>> stim.gate_data('R').is_unitary False >>> stim.gate_data('M').is_unitary False >>> stim.gate_data('MXX').is_unitary False >>> stim.gate_data('X_ERROR').is_unitary False >>> stim.gate_data('CORRELATED_ERROR').is_unitary False >>> stim.gate_data('MPP').is_unitary False >>> stim.gate_data('DETECTOR').is_unitary False """ @property def name( self, ) -> str: """Returns the canonical name of the gate. Examples: >>> import stim >>> stim.gate_data('H').name 'H' >>> stim.gate_data('cnot').name 'CX' """ @property def num_parens_arguments_range( self, ) -> range: """Returns the min/max parens arguments taken by the gate, as a python range. Examples: >>> import stim >>> stim.gate_data('M').num_parens_arguments_range range(0, 2) >>> list(stim.gate_data('M').num_parens_arguments_range) [0, 1] >>> list(stim.gate_data('R').num_parens_arguments_range) [0] >>> list(stim.gate_data('H').num_parens_arguments_range) [0] >>> list(stim.gate_data('X_ERROR').num_parens_arguments_range) [1] >>> list(stim.gate_data('PAULI_CHANNEL_1').num_parens_arguments_range) [3] >>> list(stim.gate_data('PAULI_CHANNEL_2').num_parens_arguments_range) [15] >>> stim.gate_data('DETECTOR').num_parens_arguments_range range(0, 256) >>> list(stim.gate_data('OBSERVABLE_INCLUDE').num_parens_arguments_range) [1] """ @property def produces_measurements( self, ) -> bool: """Returns whether or not the gate produces measurement results. Examples: >>> import stim >>> stim.gate_data('M').produces_measurements True >>> stim.gate_data('MRY').produces_measurements True >>> stim.gate_data('MXX').produces_measurements True >>> stim.gate_data('MPP').produces_measurements True >>> stim.gate_data('HERALDED_ERASE').produces_measurements True >>> stim.gate_data('H').produces_measurements False >>> stim.gate_data('CX').produces_measurements False >>> stim.gate_data('R').produces_measurements False >>> stim.gate_data('X_ERROR').produces_measurements False >>> stim.gate_data('CORRELATED_ERROR').produces_measurements False >>> stim.gate_data('DETECTOR').produces_measurements False """ @property def tableau( self, ) -> Optional[stim.Tableau]: """Returns the gate's tableau, or None if the gate has no tableau. Examples: >>> import stim >>> print(stim.gate_data('M').tableau) None >>> stim.gate_data('H').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> stim.gate_data('ISWAP').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZY"), stim.PauliString("+YZ"), ], zs=[ stim.PauliString("+_Z"), stim.PauliString("+Z_"), ], ) """ @property def takes_measurement_record_targets( self, ) -> bool: """Returns whether or not the gate can accept rec targets. For example, `CX` can take a measurement record target like `CX rec[-1] 1`. Examples: >>> import stim >>> stim.gate_data('CX').takes_measurement_record_targets True >>> stim.gate_data('DETECTOR').takes_measurement_record_targets True >>> stim.gate_data('H').takes_measurement_record_targets False >>> stim.gate_data('SWAP').takes_measurement_record_targets False >>> stim.gate_data('R').takes_measurement_record_targets False >>> stim.gate_data('M').takes_measurement_record_targets False >>> stim.gate_data('MRY').takes_measurement_record_targets False >>> stim.gate_data('MXX').takes_measurement_record_targets False >>> stim.gate_data('X_ERROR').takes_measurement_record_targets False >>> stim.gate_data('CORRELATED_ERROR').takes_measurement_record_targets False >>> stim.gate_data('MPP').takes_measurement_record_targets False """ @property def takes_pauli_targets( self, ) -> bool: """Returns whether or not the gate expects pauli targets. For example, `CORRELATED_ERROR` takes targets like `X0` and `Y1` instead of `0` or `1`. Examples: >>> import stim >>> stim.gate_data('CORRELATED_ERROR').takes_pauli_targets True >>> stim.gate_data('MPP').takes_pauli_targets True >>> stim.gate_data('H').takes_pauli_targets False >>> stim.gate_data('CX').takes_pauli_targets False >>> stim.gate_data('R').takes_pauli_targets False >>> stim.gate_data('M').takes_pauli_targets False >>> stim.gate_data('MRY').takes_pauli_targets False >>> stim.gate_data('MXX').takes_pauli_targets False >>> stim.gate_data('X_ERROR').takes_pauli_targets False >>> stim.gate_data('DETECTOR').takes_pauli_targets False """ @property def unitary_matrix( self, ) -> Optional[np.ndarray]: """Returns the gate's unitary matrix, or None if the gate isn't unitary. Examples: >>> import stim >>> print(stim.gate_data('M').unitary_matrix) None >>> stim.gate_data('X').unitary_matrix array([[0.+0.j, 1.+0.j], [1.+0.j, 0.+0.j]], dtype=complex64) >>> stim.gate_data('ISWAP').unitary_matrix array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j], [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]], dtype=complex64) """ class GateTarget: """Represents a gate target, like `0` or `rec[-1]`, from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... M 0 !1 ... ''') >>> circuit[0].targets_copy()[0] stim.GateTarget(0) >>> circuit[0].targets_copy()[1] stim.target_inv(1) """ def __eq__( self, arg0: stim.GateTarget, ) -> bool: """Determines if two `stim.GateTarget`s are identical. """ def __init__( self, value: object, ) -> None: """Initializes a `stim.GateTarget`. Args: value: A value to convert into a gate target, like an integer to interpret as a qubit target or a string to parse. Examples: >>> import stim >>> stim.GateTarget(stim.GateTarget(5)) stim.GateTarget(5) >>> stim.GateTarget("X7") stim.target_x(7) >>> stim.GateTarget("rec[-3]") stim.target_rec(-3) >>> stim.GateTarget("!Z7") stim.target_z(7, invert=True) >>> stim.GateTarget("*") stim.GateTarget.combiner() """ def __ne__( self, arg0: stim.GateTarget, ) -> bool: """Determines if two `stim.GateTarget`s are different. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.GateTarget`. """ @property def is_combiner( self, ) -> bool: """Returns whether or not this is a combiner target like `*`. Examples: >>> import stim >>> stim.GateTarget(6).is_combiner False >>> stim.target_inv(7).is_combiner False >>> stim.target_x(8).is_combiner False >>> stim.target_y(2).is_combiner False >>> stim.target_z(3).is_combiner False >>> stim.target_sweep_bit(9).is_combiner False >>> stim.target_rec(-5).is_combiner False >>> stim.target_combiner().is_combiner True """ @property def is_inverted_result_target( self, ) -> bool: """Returns whether or not this is an inverted target like `!5` or `!X4`. Examples: >>> import stim >>> stim.GateTarget(6).is_inverted_result_target False >>> stim.target_inv(7).is_inverted_result_target True >>> stim.target_x(8).is_inverted_result_target False >>> stim.target_x(8, invert=True).is_inverted_result_target True >>> stim.target_y(2).is_inverted_result_target False >>> stim.target_z(3).is_inverted_result_target False >>> stim.target_sweep_bit(9).is_inverted_result_target False >>> stim.target_rec(-5).is_inverted_result_target False """ @property def is_measurement_record_target( self, ) -> bool: """Returns whether or not this is a measurement record target like `rec[-5]`. Examples: >>> import stim >>> stim.GateTarget(6).is_measurement_record_target False >>> stim.target_inv(7).is_measurement_record_target False >>> stim.target_x(8).is_measurement_record_target False >>> stim.target_y(2).is_measurement_record_target False >>> stim.target_z(3).is_measurement_record_target False >>> stim.target_sweep_bit(9).is_measurement_record_target False >>> stim.target_rec(-5).is_measurement_record_target True """ @property def is_qubit_target( self, ) -> bool: """Returns whether or not this is a qubit target like `5` or `!6`. Examples: >>> import stim >>> stim.GateTarget(6).is_qubit_target True >>> stim.target_inv(7).is_qubit_target True >>> stim.target_x(8).is_qubit_target False >>> stim.target_y(2).is_qubit_target False >>> stim.target_z(3).is_qubit_target False >>> stim.target_sweep_bit(9).is_qubit_target False >>> stim.target_rec(-5).is_qubit_target False """ @property def is_sweep_bit_target( self, ) -> bool: """Returns whether or not this is a sweep bit target like `sweep[4]`. Examples: >>> import stim >>> stim.GateTarget(6).is_sweep_bit_target False >>> stim.target_inv(7).is_sweep_bit_target False >>> stim.target_x(8).is_sweep_bit_target False >>> stim.target_y(2).is_sweep_bit_target False >>> stim.target_z(3).is_sweep_bit_target False >>> stim.target_sweep_bit(9).is_sweep_bit_target True >>> stim.target_rec(-5).is_sweep_bit_target False """ @property def is_x_target( self, ) -> bool: """Returns whether or not this is an X pauli target like `X2` or `!X7`. Examples: >>> import stim >>> stim.GateTarget(6).is_x_target False >>> stim.target_inv(7).is_x_target False >>> stim.target_x(8).is_x_target True >>> stim.target_y(2).is_x_target False >>> stim.target_z(3).is_x_target False >>> stim.target_sweep_bit(9).is_x_target False >>> stim.target_rec(-5).is_x_target False """ @property def is_y_target( self, ) -> bool: """Returns whether or not this is a Y pauli target like `Y2` or `!Y7`. Examples: >>> import stim >>> stim.GateTarget(6).is_y_target False >>> stim.target_inv(7).is_y_target False >>> stim.target_x(8).is_y_target False >>> stim.target_y(2).is_y_target True >>> stim.target_z(3).is_y_target False >>> stim.target_sweep_bit(9).is_y_target False >>> stim.target_rec(-5).is_y_target False """ @property def is_z_target( self, ) -> bool: """Returns whether or not this is a Z pauli target like `Z2` or `!Z7`. Examples: >>> import stim >>> stim.GateTarget(6).is_z_target False >>> stim.target_inv(7).is_z_target False >>> stim.target_x(8).is_z_target False >>> stim.target_y(2).is_z_target False >>> stim.target_z(3).is_z_target True >>> stim.target_sweep_bit(9).is_z_target False >>> stim.target_rec(-5).is_z_target False """ @property def pauli_type( self, ) -> str: """Returns whether this is an 'X', 'Y', or 'Z' target. For non-pauli targets, this property evaluates to 'I'. Examples: >>> import stim >>> stim.GateTarget(6).pauli_type 'I' >>> stim.target_inv(7).pauli_type 'I' >>> stim.target_x(8).pauli_type 'X' >>> stim.target_y(2).pauli_type 'Y' >>> stim.target_z(3).pauli_type 'Z' >>> stim.target_sweep_bit(9).pauli_type 'I' >>> stim.target_rec(-5).pauli_type 'I' """ @property def qubit_value( self, ) -> Optional[int]: """Returns the integer value of the targeted qubit, or else None. Examples: >>> import stim >>> stim.GateTarget(6).qubit_value 6 >>> stim.target_inv(7).qubit_value 7 >>> stim.target_x(8).qubit_value 8 >>> stim.target_y(2).qubit_value 2 >>> stim.target_z(3).qubit_value 3 >>> print(stim.target_sweep_bit(9).qubit_value) None >>> print(stim.target_rec(-5).qubit_value) None """ @property def value( self, ) -> int: """The numeric part of the target. This is non-negative integer for qubit targets, and a negative integer for measurement record targets. Examples: >>> import stim >>> stim.GateTarget(6).value 6 >>> stim.target_inv(7).value 7 >>> stim.target_x(8).value 8 >>> stim.target_y(2).value 2 >>> stim.target_z(3).value 3 >>> stim.target_sweep_bit(9).value 9 >>> stim.target_rec(-5).value -5 """ class GateTargetWithCoords: """A gate target with associated coordinate information. For example, if the gate target is a qubit from a circuit with QUBIT_COORDS instructions, the coords field will contain the coordinate data from the QUBIT_COORDS instruction for the qubit. This is helpful information to have available when debugging a problem in a circuit, instead of having to constantly manually look up the coordinates of a qubit index in order to understand what is happening. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) >>> t.coords [1.5, 2.0] """ def __init__( self, gate_target: object, coords: List[float], ) -> None: """Creates a stim.GateTargetWithCoords. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) >>> t.coords [1.5, 2.0] """ @property def coords( self, ) -> List[float]: """Returns the associated coordinate information as a list of floats. If there is no coordinate information, returns an empty list. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.coords [1.5, 2.0] """ @property def gate_target( self, ) -> stim.GateTarget: """Returns the actual gate target as a `stim.GateTarget`. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) """ class PauliString: """A signed Pauli tensor product (e.g. "+X \u2297 X \u2297 X" or "-Y \u2297 Z". Represents a collection of Pauli operations (I, X, Y, Z) applied pairwise to a collection of qubits. Examples: >>> import stim >>> stim.PauliString("XX") * stim.PauliString("YY") stim.PauliString("-ZZ") >>> print(stim.PauliString(5)) +_____ """ def __add__( self, rhs: stim.PauliString, ) -> stim.PauliString: """Returns the tensor product of two Pauli strings. Concatenates the Pauli strings and multiplies their signs. Args: rhs: A second stim.PauliString. Examples: >>> import stim >>> stim.PauliString("X") + stim.PauliString("YZ") stim.PauliString("+XYZ") >>> stim.PauliString("iX") + stim.PauliString("-X") stim.PauliString("-iXX") Returns: The tensor product. """ def __eq__( self, arg0: stim.PauliString, ) -> bool: """Determines if two Pauli strings have identical contents. """ @overload def __getitem__( self, index_or_slice: int, ) -> int: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.PauliString: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns an individual Pauli or Pauli string slice from the pauli string. Individual Paulis are returned as an int using the encoding 0=I, 1=X, 2=Y, 3=Z. Slices are returned as a stim.PauliString (always with positive sign). Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p[2] 2 >>> p[-1] 3 >>> p[:2] stim.PauliString("+_X") >>> p[::-1] stim.PauliString("+ZYX_") Args: index_or_slice: The index of the pauli to return, or the slice of paulis to return. Returns: 0: Identity. 1: Pauli X. 2: Pauli Y. 3: Pauli Z. """ def __iadd__( self, rhs: stim.PauliString, ) -> stim.PauliString: """Performs an inplace tensor product. Concatenates the given Pauli string onto the receiving string and multiplies their signs. Args: rhs: A second stim.PauliString. Examples: >>> import stim >>> p = stim.PauliString("iX") >>> alias = p >>> p += stim.PauliString("-YY") >>> p stim.PauliString("-iXYY") >>> alias is p True Returns: The mutated pauli string. """ def __imul__( self, rhs: object, ) -> stim.PauliString: """Inplace right-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: rhs: The right hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term into the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply into the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> p = stim.PauliString("X") >>> p *= 1j >>> p stim.PauliString("+iX") >>> p = stim.PauliString("iXY_") >>> p *= 3 >>> p stim.PauliString("-iXY_XY_XY_") >>> p = stim.PauliString("X") >>> alias = p >>> p *= stim.PauliString("Y") >>> alias stim.PauliString("+iZ") >>> p = stim.PauliString("X") >>> p *= stim.PauliString("_YY") >>> p stim.PauliString("+XYY") Returns: The mutated Pauli string. """ def __init__( self, arg: Union[None, int, str, stim.PauliString, Iterable[Union[int, Literal["_", "I", "X", "Y", "Z"]]]] = None, /, ) -> None: """Initializes a stim.PauliString from the given argument. When given a string, the string is parsed as a pauli string. The string can optionally start with a sign ('+', '-', 'i', '+i', or '-i'). The rest of the string should be either a dense pauli string or a sparse pauli string. A dense pauli string is made up of characters from '_IXYZ' where '_' and 'I' mean identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z. A sparse pauli string is a series of integers seperated by '*' and prefixed by 'I', 'X', 'Y', or 'Z'. Args: arg [position-only]: This can be a variety of types, including: None (default): initializes an empty Pauli string. int: initializes an identity Pauli string of the given length. str: initializes by parsing the given text. stim.PauliString: initializes a copy of the given Pauli string. Iterable: initializes by interpreting each item as a Pauli. Each item can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Dict[int, Union[int, str]]: initializes by interpreting keys as the qubit index and values as the Pauli for that index. Each value can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Dict[Union[int, str], Iterable[int]]: initializes by interpreting keys as Pauli operators and values as the qubit indices for that Pauli. Each key can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Examples: >>> import stim >>> stim.PauliString("-XYZ") stim.PauliString("-XYZ") >>> stim.PauliString() stim.PauliString("+") >>> stim.PauliString(5) stim.PauliString("+_____") >>> stim.PauliString(stim.PauliString("XX")) stim.PauliString("+XX") >>> stim.PauliString([0, 1, 3, 2]) stim.PauliString("+_XZY") >>> stim.PauliString("X" for _ in range(4)) stim.PauliString("+XXXX") >>> stim.PauliString("-X2*Y6") stim.PauliString("-__X___Y") >>> stim.PauliString("X6*Y6") stim.PauliString("+i______Z") >>> stim.PauliString({0: "X", 2: "Y", 3: "X"}) stim.PauliString("+X_YX") >>> stim.PauliString({0: "X", 2: 2, 3: 1}) stim.PauliString("+X_YX") >>> stim.PauliString({"X": [1], 2: [4], "Z": [0, 3]}) stim.PauliString("+ZX_ZY") """ def __itruediv__( self, rhs: complex, ) -> stim.PauliString: """Inplace divides the Pauli string by a complex unit. Args: rhs: The divisor. Can be 1, -1, 1j, or -1j. Examples: >>> import stim >>> p = stim.PauliString("X") >>> p /= 1j >>> p stim.PauliString("-iX") Returns: The mutated Pauli string. Raises: ValueError: The divisor isn't 1, -1, 1j, or -1j. """ def __len__( self, ) -> int: """Returns the length the pauli string; the number of qubits it operates on. Examples: >>> import stim >>> len(stim.PauliString("XY_ZZ")) 5 >>> len(stim.PauliString("X0*Z99")) 100 """ def __mul__( self, rhs: object, ) -> stim.PauliString: """Right-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: rhs: The right hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term with the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply with the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> stim.PauliString("X") * 1 stim.PauliString("+X") >>> stim.PauliString("X") * -1 stim.PauliString("-X") >>> stim.PauliString("X") * 1j stim.PauliString("+iX") >>> stim.PauliString("X") * 2 stim.PauliString("+XX") >>> stim.PauliString("-X") * 2 stim.PauliString("+XX") >>> stim.PauliString("iX") * 2 stim.PauliString("-XX") >>> stim.PauliString("X") * 3 stim.PauliString("+XXX") >>> stim.PauliString("iX") * 3 stim.PauliString("-iXXX") >>> stim.PauliString("X") * stim.PauliString("Y") stim.PauliString("+iZ") >>> stim.PauliString("X") * stim.PauliString("XX_") stim.PauliString("+_X_") >>> stim.PauliString("XXXX") * stim.PauliString("_XYZ") stim.PauliString("+X_ZY") Returns: The product or tensor power. Raises: TypeError: The right hand side isn't a stim.PauliString, a non-negative integer, or a complex unit (1, -1, 1j, or -1j). """ def __ne__( self, arg0: stim.PauliString, ) -> bool: """Determines if two Pauli strings have non-identical contents. """ def __neg__( self, ) -> stim.PauliString: """Returns the negation of the pauli string. Examples: >>> import stim >>> -stim.PauliString("X") stim.PauliString("-X") >>> -stim.PauliString("-Y") stim.PauliString("+Y") >>> -stim.PauliString("iZZZ") stim.PauliString("-iZZZ") """ def __pos__( self, ) -> stim.PauliString: """Returns a pauli string with the same contents. Examples: >>> import stim >>> +stim.PauliString("+X") stim.PauliString("+X") >>> +stim.PauliString("-YY") stim.PauliString("-YY") >>> +stim.PauliString("iZZZ") stim.PauliString("+iZZZ") """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.PauliString`. """ def __rmul__( self, lhs: object, ) -> stim.PauliString: """Left-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: lhs: The left hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term with the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply with the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> 1 * stim.PauliString("X") stim.PauliString("+X") >>> -1 * stim.PauliString("X") stim.PauliString("-X") >>> 1j * stim.PauliString("X") stim.PauliString("+iX") >>> 2 * stim.PauliString("X") stim.PauliString("+XX") >>> 2 * stim.PauliString("-X") stim.PauliString("+XX") >>> 2 * stim.PauliString("iX") stim.PauliString("-XX") >>> 3 * stim.PauliString("X") stim.PauliString("+XXX") >>> 3 * stim.PauliString("iX") stim.PauliString("-iXXX") >>> stim.PauliString("X") * stim.PauliString("Y") stim.PauliString("+iZ") >>> stim.PauliString("X") * stim.PauliString("XX_") stim.PauliString("+_X_") >>> stim.PauliString("XXXX") * stim.PauliString("_XYZ") stim.PauliString("+X_ZY") Returns: The product. Raises: ValueError: The scalar phase factor isn't 1, -1, 1j, or -1j. """ def __setitem__( self, index: int, new_pauli: object, ) -> None: """Mutates an entry in the pauli string using the encoding 0=I, 1=X, 2=Y, 3=Z. Args: index: The index of the pauli to overwrite. new_pauli: Either a character from '_IXYZ' or an integer from range(4). Examples: >>> import stim >>> p = stim.PauliString(4) >>> p[2] = 1 >>> print(p) +__X_ >>> p[0] = 3 >>> p[1] = 2 >>> p[3] = 0 >>> print(p) +ZYX_ >>> p[0] = 'I' >>> p[1] = 'X' >>> p[2] = 'Y' >>> p[3] = 'Z' >>> print(p) +_XYZ >>> p[-1] = 'Y' >>> print(p) +_XYY """ def __str__( self, ) -> str: """Returns a text description. """ def __truediv__( self, rhs: complex, ) -> stim.PauliString: """Divides the Pauli string by a complex unit. Args: rhs: The divisor. Can be 1, -1, 1j, or -1j. Examples: >>> import stim >>> stim.PauliString("X") / 1j stim.PauliString("-iX") Returns: The quotient. Raises: ValueError: The divisor isn't 1, -1, 1j, or -1j. """ @overload def after( self, operation: Union[stim.Circuit, stim.CircuitInstruction], ) -> stim.PauliString: pass @overload def after( self, operation: stim.Tableau, targets: Iterable[int], ) -> stim.PauliString: pass def after( self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None, ) -> stim.PauliString: """Returns the result of conjugating the Pauli string by an operation. Args: operation: A circuit, tableau, or circuit instruction to conjugate the Pauli string by. Must be Clifford (e.g. if it's a circuit, the circuit can't have noise or measurements). targets: Required if and only if the operation is a tableau. Specifies which qubits to target. Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p.after(stim.CircuitInstruction("H", [1])) stim.PauliString("+_ZYZ") >>> p.after(stim.Circuit(''' ... C_XYZ 1 2 3 ... ''')) stim.PauliString("+_YZX") >>> p.after(stim.Tableau.from_named_gate('CZ'), targets=[0, 1]) stim.PauliString("+ZXYZ") Returns: The conjugated Pauli string. The Pauli string after the operation that is exactly equivalent to the given Pauli string before the operation. """ @overload def before( self, operation: Union[stim.Circuit, stim.CircuitInstruction], ) -> stim.PauliString: pass @overload def before( self, operation: stim.Tableau, targets: Iterable[int], ) -> stim.PauliString: pass def before( self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None, ) -> stim.PauliString: """Returns the result of conjugating the Pauli string by an operation. Args: operation: A circuit, tableau, or circuit instruction to anti-conjugate the Pauli string by. Must be Clifford (e.g. if it's a circuit, the circuit can't have noise or measurements). targets: Required if and only if the operation is a tableau. Specifies which qubits to target. Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p.before(stim.CircuitInstruction("H", [1])) stim.PauliString("+_ZYZ") >>> p.before(stim.Circuit(''' ... C_XYZ 1 2 3 ... ''')) stim.PauliString("+_ZXY") >>> p.before(stim.Tableau.from_named_gate('CZ'), targets=[0, 1]) stim.PauliString("+ZXYZ") Returns: The conjugated Pauli string. The Pauli string before the operation that is exactly equivalent to the given Pauli string after the operation. """ def commutes( self, other: stim.PauliString, ) -> bool: """Determines if two Pauli strings commute or not. Two Pauli strings commute if they have an even number of matched non-equal non-identity Pauli terms. Otherwise they anticommute. Args: other: The other Pauli string. Examples: >>> import stim >>> xx = stim.PauliString("XX") >>> xx.commutes(stim.PauliString("X_")) True >>> xx.commutes(stim.PauliString("XX")) True >>> xx.commutes(stim.PauliString("XY")) False >>> xx.commutes(stim.PauliString("XZ")) False >>> xx.commutes(stim.PauliString("ZZ")) True >>> xx.commutes(stim.PauliString("X_Y__")) True >>> xx.commutes(stim.PauliString("")) True Returns: True if the Pauli strings commute, False if they anti-commute. """ def copy( self, ) -> stim.PauliString: """Returns a copy of the pauli string. The copy is an independent pauli string with the same contents. Examples: >>> import stim >>> p1 = stim.PauliString.random(2) >>> p2 = p1.copy() >>> p2 is p1 False >>> p2 == p1 True """ def extended_product( self, other: stim.PauliString, ) -> Tuple[complex, stim.PauliString]: """[DEPRECATED] Use multiplication (__mul__ or *) instead. """ @staticmethod def from_numpy( *, xs: np.ndarray, zs: np.ndarray, sign: Union[int, float, complex] = +1, num_qubits: Optional[int] = None, ) -> stim.PauliString: """Creates a pauli string from X bit and Z bit numpy arrays, using the encoding: x=0 and z=0 -> P=I x=1 and z=0 -> P=X x=1 and z=1 -> P=Y x=0 and z=1 -> P=Z Args: xs: The X bits of the pauli string. This array can either be a 1-dimensional numpy array with dtype=np.bool_, or a bit packed 1-dimensional numpy array with dtype=np.uint8. If the dtype is np.uint8 then the array is assumed to be bit packed in little endian order and the "num_qubits" argument must be specified. When bit packed, the x bit with offset k is stored at (xs[k // 8] >> (k % 8)) & 1. zs: The Z bits of the pauli string. This array can either be a 1-dimensional numpy array with dtype=np.bool_, or a bit packed 1-dimensional numpy array with dtype=np.uint8. If the dtype is np.uint8 then the array is assumed to be bit packed in little endian order and the "num_qubits" argument must be specified. When bit packed, the x bit with offset k is stored at (xs[k // 8] >> (k % 8)) & 1. sign: Defaults to +1. Set to +1, -1, 1j, or -1j to control the sign of the returned Pauli string. num_qubits: Must be specified if xs or zs is a bit packed array. Specifies the expected length of the Pauli string. Returns: The created pauli string. Examples: >>> import stim >>> import numpy as np >>> xs = np.array([1, 1, 1, 1, 1, 1, 1, 0, 0], dtype=np.bool_) >>> zs = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1], dtype=np.bool_) >>> stim.PauliString.from_numpy(xs=xs, zs=zs, sign=-1) stim.PauliString("-XXXXYYYZZ") >>> xs = np.array([127, 0], dtype=np.uint8) >>> zs = np.array([240, 1], dtype=np.uint8) >>> stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=9) stim.PauliString("+XXXXYYYZZ") """ @staticmethod def from_unitary_matrix( matrix: Iterable[Iterable[Union[int, float, complex]]], *, endian: Literal["little", "big"] = 'little', unsigned: bool = False, ) -> stim.PauliString: """Creates a stim.PauliString from the unitary matrix of a Pauli group member. Args: matrix: A unitary matrix specified as an iterable of rows, with each row is an iterable of amplitudes. The unitary matrix must correspond to a Pauli string, including global phase. endian: "little": matrix entries are in little endian order, where higher index qubits correspond to larger changes in row/col indices. "big": matrix entries are in big endian order, where higher index qubits correspond to smaller changes in row/col indices. unsigned: When False, the input must only contain the values [0, 1, -1, 1j, -1j] and the output will have the correct global phase. When True, the input is permitted to be scaled by an arbitrary unit complex value and the output will always have positive sign. False is stricter but provides more information, while True is more flexible but provides less information. Returns: The pauli string equal to the given unitary matrix. Raises: ValueError: The given matrix isn't the unitary matrix of a Pauli string. Examples: >>> import stim >>> stim.PauliString.from_unitary_matrix([ ... [1j, 0], ... [0, -1j], ... ], endian='little') stim.PauliString("+iZ") >>> stim.PauliString.from_unitary_matrix([ ... [1j**0.1, 0], ... [0, -(1j**0.1)], ... ], endian='little', unsigned=True) stim.PauliString("+Z") >>> stim.PauliString.from_unitary_matrix([ ... [0, 1, 0, 0], ... [1, 0, 0, 0], ... [0, 0, 0, -1], ... [0, 0, -1, 0], ... ], endian='little') stim.PauliString("+XZ") """ @staticmethod def iter_all( num_qubits: int, *, min_weight: int = 0, max_weight: object = None, allowed_paulis: str = 'XYZ', ) -> stim.PauliStringIterator: """Returns an iterator that iterates over all matching pauli strings. Args: num_qubits: The desired number of qubits in the pauli strings. min_weight: Defaults to 0. The minimum number of non-identity terms that must be present in each yielded pauli string. max_weight: Defaults to None (unused). The maximum number of non-identity terms that must be present in each yielded pauli string. allowed_paulis: Defaults to "XYZ". Set this to a string containing the non-identity paulis that are allowed to appear in each yielded pauli string. This argument must be a string made up of only "X", "Y", and "Z" characters. A non-identity Pauli is allowed if it appears in the string, and not allowed if it doesn't. Identity Paulis are always allowed. Returns: An Iterable[stim.PauliString] that yields the requested pauli strings. Examples: >>> import stim >>> pauli_string_iterator = stim.PauliString.iter_all( ... num_qubits=3, ... min_weight=1, ... max_weight=2, ... allowed_paulis="XZ", ... ) >>> for p in pauli_string_iterator: ... print(p) +X__ +Z__ +_X_ +_Z_ +__X +__Z +XX_ +XZ_ +ZX_ +ZZ_ +X_X +X_Z +Z_X +Z_Z +_XX +_XZ +_ZX +_ZZ """ def pauli_indices( self, included_paulis: str = "XYZ", ) -> List[int]: """Returns the indices of non-identity Paulis, or of specified Paulis. Args: include: A string containing the Pauli types to include. X type Pauli indices are included if "X" or "x" is in the string. Y type Pauli indices are included if "Y" or "y" is in the string. Z type Pauli indices are included if "Z" or "z" is in the string. I type Pauli indices are included if "I" or "_" is in the string. An exception is thrown if other characters are in the string. Returns: A list containing the ascending indices of matching Pauli terms. Examples: >>> import stim >>> stim.PauliString("_____X___Y____Z___").pauli_indices() [5, 9, 14] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("XZ") [5, 14] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("X") [5] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("Y") [9] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("IY") [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17] >>> stim.PauliString("-X103*Y100").pauli_indices() [100, 103] """ @staticmethod def random( num_qubits: int, *, allow_imaginary: bool = False, ) -> stim.PauliString: """Samples a uniformly random Hermitian Pauli string. Args: num_qubits: The number of qubits the Pauli string should act on. allow_imaginary: Defaults to False. If True, the sign of the result may be 1j or -1j in addition to +1 or -1. In other words, setting this to True allows the result to be non-Hermitian. Examples: >>> import stim >>> p = stim.PauliString.random(5) >>> len(p) 5 >>> p.sign in [-1, +1] True >>> p2 = stim.PauliString.random(3, allow_imaginary=True) >>> len(p2) 3 >>> p2.sign in [-1, +1, 1j, -1j] True Returns: The sampled Pauli string. """ @property def sign( self, ) -> complex: """The sign of the Pauli string. Can be +1, -1, 1j, or -1j. Examples: >>> import stim >>> stim.PauliString("X").sign (1+0j) >>> stim.PauliString("-X").sign (-1+0j) >>> stim.PauliString("iX").sign 1j >>> stim.PauliString("-iX").sign (-0-1j) """ @sign.setter def sign(self, value: complex): pass def to_numpy( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: """Decomposes the contents of the pauli string into X bit and Z bit numpy arrays. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: An (xs, zs) tuple encoding the paulis from the string. The k'th Pauli from the string is encoded into k'th bit of xs and the k'th bit of zs using the "xz" encoding: P=I -> x=0 and z=0 P=X -> x=1 and z=0 P=Y -> x=1 and z=1 P=Z -> x=0 and z=1 The dtype and shape of the result depends on the bit_packed argument. If bit_packed=False: Each bit gets its own byte. xs.dtype = zs.dtype = np.bool_ xs.shape = zs.shape = len(self) xs_k = xs[k] zs_k = zs[k] If bit_packed=True: Equivalent to applying np.packbits(bitorder='little') to the result. xs.dtype = zs.dtype = np.uint8 xs.shape = zs.shape = math.ceil(len(self) / 8) xs_k = (xs[k // 8] >> (k % 8)) & 1 zs_k = (zs[k // 8] >> (k % 8)) & 1 Examples: >>> import stim >>> xs, zs = stim.PauliString("XXXXYYYZZ").to_numpy() >>> xs array([ True, True, True, True, True, True, True, False, False]) >>> zs array([False, False, False, False, True, True, True, True, True]) >>> xs, zs = stim.PauliString("XXXXYYYZZ").to_numpy(bit_packed=True) >>> xs array([127, 0], dtype=uint8) >>> zs array([240, 1], dtype=uint8) """ def to_tableau( self, ) -> stim.Tableau: """Creates a Tableau equivalent to this Pauli string. The tableau represents a Clifford operation that multiplies qubits by the corresponding Pauli operations from this Pauli string. The global phase of the pauli operation is lost in the conversion. Returns: The created tableau. Examples: >>> import stim >>> p = stim.PauliString("ZZ") >>> p.to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-X_"), stim.PauliString("-_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+_Z"), ], ) >>> q = stim.PauliString("YX_Z") >>> q.to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-X___"), stim.PauliString("+_X__"), stim.PauliString("+__X_"), stim.PauliString("-___X"), ], zs=[ stim.PauliString("-Z___"), stim.PauliString("-_Z__"), stim.PauliString("+__Z_"), stim.PauliString("+___Z"), ], ) """ def to_unitary_matrix( self, *, endian: Literal["little", "big"], ) -> np.ndarray[np.complex64]: """Converts the pauli string into a unitary matrix. Args: endian: "little": The first qubit is the least significant (corresponds to an offset of 1 in the matrix). "big": The first qubit is the most significant (corresponds to an offset of 2**(n - 1) in the matrix). Returns: A numpy array with dtype=np.complex64 and shape=(1 << len(pauli_string), 1 << len(pauli_string)). Example: >>> import stim >>> stim.PauliString("-YZ").to_unitary_matrix(endian="little") array([[0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.-1.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 0.-1.j], [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j]], dtype=complex64) """ @property def weight( self, ) -> int: """Returns the number of non-identity pauli terms in the pauli string. Examples: >>> import stim >>> stim.PauliString("+___").weight 0 >>> stim.PauliString("+__X").weight 1 >>> stim.PauliString("+XYZ").weight 3 >>> stim.PauliString("-XXX___XXYZ").weight 7 """ class PauliStringIterator: """Iterates over all pauli strings matching specified patterns. Examples: >>> import stim >>> pauli_string_iterator = stim.PauliString.iter_all( ... 2, ... min_weight=1, ... max_weight=1, ... allowed_paulis="XZ", ... ) >>> for p in pauli_string_iterator: ... print(p) +X_ +Z_ +_X +_Z """ def __iter__( self, ) -> stim.PauliStringIterator: """Returns an independent copy of the pauli string iterator. Since for-loops and loop-comprehensions call `iter` on things they iterate, this effectively allows the iterator to be iterated multiple times. """ def __next__( self, ) -> stim.PauliString: """Returns the next iterated pauli string. """ class Tableau: """A stabilizer tableau. Represents a Clifford operation by explicitly storing how that operation conjugates a list of Pauli group generators into composite Pauli products. Examples: >>> import stim >>> stim.Tableau.from_named_gate("H") stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> t = stim.Tableau.random(5) >>> t_inv = t**-1 >>> print(t * t_inv) +-xz-xz-xz-xz-xz- | ++ ++ ++ ++ ++ | XZ __ __ __ __ | __ XZ __ __ __ | __ __ XZ __ __ | __ __ __ XZ __ | __ __ __ __ XZ >>> x2z3 = t.x_output(2) * t.z_output(3) >>> t_inv(x2z3) stim.PauliString("+__XZ_") """ def __add__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Returns the direct sum (diagonal concatenation) of two Tableaus. Args: rhs: A second stim.Tableau. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> cz = stim.Tableau.from_named_gate("CZ") >>> print(s + cz) +-xz-xz-xz- | ++ ++ ++ | YZ __ __ | __ XZ Z_ | __ Z_ XZ Returns: The direct sum. """ def __call__( self, pauli_string: stim.PauliString, ) -> stim.PauliString: """Returns the equivalent PauliString after the Tableau's Clifford operation. If P is a Pauli product before a Clifford operation C, then this method returns Q = C * P * C**-1 (the conjugation of P by C). Q is the equivalent Pauli product after C. This works because: C*P = C*P * I = C*P * (C**-1 * C) = (C*P*C**-1) * C = Q*C (Keep in mind that A*B means first B is applied, then A is applied.) Args: pauli_string: The pauli string to conjugate. Returns: The new conjugated pauli string. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> p = stim.PauliString("XX") >>> result = t(p) >>> print(result) +X_ """ def __eq__( self, arg0: stim.Tableau, ) -> bool: """Determines if two tableaus have identical contents. """ def __iadd__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Performs an inplace direct sum (diagonal concatenation). Args: rhs: A second stim.Tableau. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> cz = stim.Tableau.from_named_gate("CZ") >>> alias = s >>> s += cz >>> alias is s True >>> print(s) +-xz-xz-xz- | ++ ++ ++ | YZ __ __ | __ XZ Z_ | __ Z_ XZ Returns: The mutated tableau. """ def __init__( self, num_qubits: int, ) -> None: """Creates an identity tableau over the given number of qubits. Examples: >>> import stim >>> t = stim.Tableau(3) >>> print(t) +-xz-xz-xz- | ++ ++ ++ | XZ __ __ | __ XZ __ | __ __ XZ Args: num_qubits: The number of qubits the tableau's operation acts on. """ def __len__( self, ) -> int: """Returns the number of qubits operated on by the tableau. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> len(t) 2 """ def __mul__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Returns the product of two tableaus. If the tableau T1 represents the Clifford operation with unitary C1, and the tableau T2 represents the Clifford operation with unitary C2, then the tableau T1*T2 represents the Clifford operation with unitary C1*C2. Args: rhs: The tableau on the right hand side of the multiplication. Examples: >>> import stim >>> t1 = stim.Tableau.random(4) >>> t2 = stim.Tableau.random(4) >>> t3 = t2 * t1 >>> p = stim.PauliString.random(4) >>> t3(p) == t2(t1(p)) True """ def __ne__( self, arg0: stim.Tableau, ) -> bool: """Determines if two tableaus have non-identical contents. """ def __pow__( self, exponent: int, ) -> stim.Tableau: """Raises the tableau to an integer power. Large powers are reached efficiently using repeated squaring. Negative powers are reached by inverting the tableau. Args: exponent: The power to raise to. Can be negative, zero, or positive. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> s**0 == stim.Tableau(1) True >>> s**1 == s True >>> s**2 == stim.Tableau.from_named_gate("Z") True >>> s**-1 == s**3 == stim.Tableau.from_named_gate("S_DAG") True >>> s**5 == s True >>> s**(400000000 + 1) == s True >>> s**(-400000000 + 1) == s True """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equal `stim.Tableau`. """ def __str__( self, ) -> str: """Returns a text description. """ def append( self, gate: stim.Tableau, targets: Sequence[int], ) -> None: """Appends an operation's effect into this tableau, mutating this tableau. Time cost is O(n*m*m) where n=len(self) and m=len(gate). Args: gate: The tableau of the operation being appended into this tableau. targets: The qubits being targeted by the gate. Examples: >>> import stim >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> t = stim.Tableau(2) >>> t.append(cnot, [0, 1]) >>> t.append(cnot, [1, 0]) >>> t.append(cnot, [0, 1]) >>> t == stim.Tableau.from_named_gate("SWAP") True """ def copy( self, ) -> stim.Tableau: """Returns a copy of the tableau. An independent tableau with the same contents. Examples: >>> import stim >>> t1 = stim.Tableau.random(2) >>> t2 = t1.copy() >>> t2 is t1 False >>> t2 == t1 True """ @staticmethod def from_circuit( circuit: stim.Circuit, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False, ) -> stim.Tableau: """Converts a circuit into an equivalent stabilizer tableau. Args: circuit: The circuit to compile into a tableau. ignore_noise: Defaults to False. When False, any noise operations in the circuit will cause the conversion to fail with an exception. When True, noise operations are skipped over as if they weren't even present in the circuit. ignore_measurement: Defaults to False. When False, any measurement operations in the circuit will cause the conversion to fail with an exception. When True, measurement operations are skipped over as if they weren't even present in the circuit. ignore_reset: Defaults to False. When False, any reset operations in the circuit will cause the conversion to fail with an exception. When True, reset operations are skipped over as if they weren't even present in the circuit. Returns: The tableau equivalent to the given circuit (up to global phase). Raises: ValueError: The circuit contains noise operations but ignore_noise=False. OR The circuit contains measurement operations but ignore_measurement=False. OR The circuit contains reset operations but ignore_reset=False. Examples: >>> import stim >>> stim.Tableau.from_circuit(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''')) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ @staticmethod def from_conjugated_generators( *, xs: List[stim.PauliString], zs: List[stim.PauliString], ) -> stim.Tableau: """Creates a tableau from the given outputs for each generator. Verifies that the tableau is well formed. Args: xs: A List[stim.PauliString] with the results of conjugating X0, X1, etc. zs: A List[stim.PauliString] with the results of conjugating Z0, Z1, etc. Returns: The created tableau. Raises: ValueError: The given outputs are malformed. Their lengths are inconsistent, or they don't satisfy the required commutation relationships. Examples: >>> import stim >>> identity3 = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("X__"), ... stim.PauliString("_X_"), ... stim.PauliString("__X"), ... ], ... zs=[ ... stim.PauliString("Z__"), ... stim.PauliString("_Z_"), ... stim.PauliString("__Z"), ... ], ... ) >>> identity3 == stim.Tableau(3) True """ @staticmethod def from_named_gate( name: str, ) -> stim.Tableau: """Returns the tableau of a named Clifford gate. Args: name: The name of the Clifford gate. Returns: The gate's tableau. Examples: >>> import stim >>> print(stim.Tableau.from_named_gate("H")) +-xz- | ++ | ZX >>> print(stim.Tableau.from_named_gate("CNOT")) +-xz-xz- | ++ ++ | XZ _Z | X_ XZ >>> print(stim.Tableau.from_named_gate("S")) +-xz- | ++ | YZ """ def from_numpy( self, *, x2x: np.ndarray, x2z: np.ndarray, z2x: np.ndarray, z2z: np.ndarray, x_signs: Optional[np.ndarray] = None, z_signs: Optional[np.ndarray] = None, ) -> stim.Tableau: """Creates a tableau from numpy arrays x2x, x2z, z2x, z2z, x_signs, and z_signs. The x2x, x2z, z2x, z2z arrays are the four quadrants of the table defined in Aaronson and Gottesman's "Improved Simulation of Stabilizer Circuits" ( https://arxiv.org/abs/quant-ph/0406196 ). Args: x2x: A 2d numpy array containing the x-to-x coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.x_output_pauli(i, j) in [1, 2] == x2x[i, j]. Bit packing must be in little endian order and only applies to the second axis. x2z: A 2d numpy array containing the x-to-z coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.x_output_pauli(i, j) in [2, 3] == x2z[i, j]. Bit packing must be in little endian order and only applies to the second axis. z2x: A 2d numpy array containing the z-to-x coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.z_output_pauli(i, j) in [1, 2] == z2x[i, j]. Bit packing must be in little endian order and only applies to the second axis. z2z: A 2d numpy array containing the z-to-z coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.z_output_pauli(i, j) in [2, 3] == z2z[i, j]. Bit packing must be in little endian order and only applies to the second axis. x_signs: Defaults to all-positive if not specified. A 1d numpy array containing the sign bits for the X generator outputs. False means positive and True means negative. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). Bit packing must be in little endian order. z_signs: Defaults to all-positive if not specified. A 1d numpy array containing the sign bits for the Z generator outputs. False means positive and True means negative. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). Bit packing must be in little endian order. Returns: The tableau created from the numpy data. Examples: >>> import stim >>> import numpy as np >>> tableau = stim.Tableau.from_numpy( ... x2x=np.array([[1, 1], [0, 1]], dtype=np.bool_), ... z2x=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... x2z=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... z2z=np.array([[1, 0], [1, 1]], dtype=np.bool_), ... ) >>> tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+ZZ"), ], ) >>> tableau == stim.Tableau.from_named_gate("CNOT") True >>> stim.Tableau.from_numpy( ... x2x=np.array([[9], [5], [7], [6]], dtype=np.uint8), ... x2z=np.array([[13], [13], [0], [3]], dtype=np.uint8), ... z2x=np.array([[8], [5], [9], [15]], dtype=np.uint8), ... z2z=np.array([[6], [11], [2], [3]], dtype=np.uint8), ... x_signs=np.array([7], dtype=np.uint8), ... z_signs=np.array([9], dtype=np.uint8), ... ) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-Y_ZY"), stim.PauliString("-Y_YZ"), stim.PauliString("-XXX_"), stim.PauliString("+ZYX_"), ], zs=[ stim.PauliString("-_ZZX"), stim.PauliString("+YZXZ"), stim.PauliString("+XZ_X"), stim.PauliString("-YYXX"), ], ) """ @staticmethod def from_stabilizers( stabilizers: Iterable[stim.PauliString], *, allow_redundant: bool = False, allow_underconstrained: bool = False, ) -> stim.Tableau: """Creates a tableau representing a state with the given stabilizers. Args: stabilizers: A list of `stim.PauliString`s specifying the stabilizers that the state must have. It is permitted for stabilizers to have different lengths. All stabilizers are padded up to the length of the longest stabilizer by appending identity terms. allow_redundant: Defaults to False. If set to False, then the given stabilizers must all be independent. If any one of them is a product of the others (including the empty product), an exception will be raised. If set to True, then redundant stabilizers are simply ignored. allow_underconstrained: Defaults to False. If set to False, then the given stabilizers must form a complete set of generators. They must exactly specify the desired stabilizer state, with no degrees of freedom left over. For an n-qubit state there must be n independent stabilizers. If set to True, then there can be leftover degrees of freedom which can be set arbitrarily. Returns: A tableau which, when applied to the all-zeroes state, produces a state with the given stabilizers. Guarantees that result.z_output(k) will be equal to the k'th independent stabilizer from the `stabilizers` argument. Raises: ValueError: A stabilizer is redundant but allow_redundant=True wasn't set. OR The given stabilizers are contradictory (e.g. "+Z" and "-Z" both specified). OR The given stabilizers anticommute (e.g. "+Z" and "+X" both specified). OR The stabilizers left behind a degree of freedom but allow_underconstrained=True wasn't set. OR A stabilizer has an imaginary sign (i or -i). Examples: >>> import stim >>> stim.Tableau.from_stabilizers([ ... stim.PauliString("XX"), ... stim.PauliString("ZZ"), ... ]) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) >>> stim.Tableau.from_stabilizers([ ... stim.PauliString("XX_"), ... stim.PauliString("ZZ_"), ... stim.PauliString("-YY_"), ... stim.PauliString(""), ... ], allow_underconstrained=True, allow_redundant=True) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z__"), stim.PauliString("+_X_"), stim.PauliString("+__X"), ], zs=[ stim.PauliString("+XX_"), stim.PauliString("+ZZ_"), stim.PauliString("+__Z"), ], ) """ @staticmethod def from_state_vector( state_vector: Iterable[float], *, endian: Literal["little", "big"], ) -> stim.Tableau: """Creates a tableau representing the stabilizer state of the given state vector. Args: state_vector: A list of complex amplitudes specifying a superposition. The vector must correspond to a state that is reachable using Clifford operations, and can be unnormalized. endian: "little": state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A tableau which, when applied to the all-zeroes state, produces a state with the given state vector. Raises: ValueError: The given state vector isn't a list of complex values specifying a stabilizer state. OR The given endian value isn't 'little' or 'big'. Examples: >>> import stim >>> stim.Tableau.from_state_vector([ ... 0.5**0.5, ... 0.5**0.5 * 1j, ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+Y"), ], ) >>> stim.Tableau.from_state_vector([ ... 0.5**0.5, ... 0, ... 0, ... 0.5**0.5, ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ @staticmethod def from_unitary_matrix( matrix: Iterable[Iterable[float]], *, endian: Literal["little", "big"] = 'little', ) -> stim.Tableau: """Creates a tableau from the unitary matrix of a Clifford operation. Args: matrix: A unitary matrix specified as an iterable of rows, with each row is an iterable of amplitudes. The unitary matrix must correspond to a Clifford operation. endian: "little": matrix entries are in little endian order, where higher index qubits correspond to larger changes in row/col indices. "big": matrix entries are in big endian order, where higher index qubits correspond to smaller changes in row/col indices. Returns: The tableau equivalent to the given unitary matrix (up to global phase). Raises: ValueError: The given matrix isn't the unitary matrix of a Clifford operation. Examples: >>> import stim >>> stim.Tableau.from_unitary_matrix([ ... [1, 0], ... [0, 1j], ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Y"), ], zs=[ stim.PauliString("+Z"), ], ) >>> stim.Tableau.from_unitary_matrix([ ... [1, 0, 0, 0], ... [0, 1, 0, 0], ... [0, 0, 0, -1j], ... [0, 0, 1j, 0], ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XZ"), stim.PauliString("+YX"), ], zs=[ stim.PauliString("+ZZ"), stim.PauliString("+_Z"), ], ) """ def inverse( self, *, unsigned: bool = False, ) -> stim.Tableau: """Computes the inverse of the tableau. The inverse T^-1 of a tableau T is the unique tableau with the property that T * T^-1 = T^-1 * T = I where I is the identity tableau. Args: unsigned: Defaults to false. When set to true, skips computing the signs of the output observables and instead just set them all to be positive. This is beneficial because computing the signs takes O(n^3) time and the rest of the inverse computation is O(n^2) where n is the number of qubits in the tableau. So, if you only need the Pauli terms (not the signs), it is significantly cheaper. Returns: The inverse tableau. Examples: >>> import stim >>> # Check that the inverse agrees with hard-coded tableaus. >>> s = stim.Tableau.from_named_gate("S") >>> s_dag = stim.Tableau.from_named_gate("S_DAG") >>> s.inverse() == s_dag True >>> z = stim.Tableau.from_named_gate("Z") >>> z.inverse() == z True >>> # Check that multiplying by the inverse produces the identity. >>> t = stim.Tableau.random(10) >>> t_inv = t.inverse() >>> identity = stim.Tableau(10) >>> t * t_inv == t_inv * t == identity True >>> # Check a manual case. >>> t = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("-__Z"), ... stim.PauliString("+XZ_"), ... stim.PauliString("+_ZZ"), ... ], ... zs=[ ... stim.PauliString("-YYY"), ... stim.PauliString("+Z_Z"), ... stim.PauliString("-ZYZ") ... ], ... ) >>> print(t.inverse()) +-xz-xz-xz- | -- +- -- | XX XX YX | XZ Z_ X_ | X_ YX Y_ >>> print(t.inverse(unsigned=True)) +-xz-xz-xz- | ++ ++ ++ | XX XX YX | XZ Z_ X_ | X_ YX Y_ """ def inverse_x_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit X Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).x_output(input_index)`. Args: input_index: Identifies the column (the qubit of the X generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating an X generator by the inverse of the tableau. Examples: >>> import stim # Check equivalence with the inverse's x_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().x_output(0) >>> t.inverse_x_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_x_output(0, unsigned=True) == expected True """ def inverse_x_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().x_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input X generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_x_output_pauli(0, 0) 2 >>> t_inv.inverse_x_output_pauli(0, 1) 0 >>> t_inv.inverse_x_output_pauli(1, 0) 2 >>> t_inv.inverse_x_output_pauli(1, 1) 3 """ def inverse_y_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit Y Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).y_output(input_index)`. Args: input_index: Identifies the column (the qubit of the Y generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating a Y generator by the inverse of the tableau. Examples: >>> import stim # Check equivalence with the inverse's y_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().y_output(0) >>> t.inverse_y_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_y_output(0, unsigned=True) == expected True """ def inverse_y_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().y_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input Y generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_y_output_pauli(0, 0) 1 >>> t_inv.inverse_y_output_pauli(0, 1) 2 >>> t_inv.inverse_y_output_pauli(1, 0) 0 >>> t_inv.inverse_y_output_pauli(1, 1) 2 """ def inverse_z_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit Z Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).z_output(input_index)`. Args: input_index: Identifies the column (the qubit of the Z generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating a Z generator by the inverse of the tableau. Examples: >>> import stim >>> import stim # Check equivalence with the inverse's z_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().z_output(0) >>> t.inverse_z_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_z_output(0, unsigned=True) == expected True """ def inverse_z_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().z_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input Z generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_z_output_pauli(0, 0) 3 >>> t_inv.inverse_z_output_pauli(0, 1) 2 >>> t_inv.inverse_z_output_pauli(1, 0) 2 >>> t_inv.inverse_z_output_pauli(1, 1) 1 """ @staticmethod def iter_all( num_qubits: int, *, unsigned: bool = False, ) -> stim.TableauIterator: """Returns an iterator that iterates over all Tableaus of a given size. Args: num_qubits: The size of tableau to iterate over. unsigned: Defaults to False. If set to True, only tableaus where all columns have positive sign are yielded by the iterator. This substantially reduces the total number of tableaus to iterate over. Returns: An Iterable[stim.Tableau] that yields the requested tableaus. Examples: >>> import stim >>> single_qubit_gate_reprs = set() >>> for t in stim.Tableau.iter_all(1): ... single_qubit_gate_reprs.add(repr(t)) >>> len(single_qubit_gate_reprs) 24 >>> num_2q_gates_mod_paulis = 0 >>> for _ in stim.Tableau.iter_all(2, unsigned=True): ... num_2q_gates_mod_paulis += 1 >>> num_2q_gates_mod_paulis 720 """ def prepend( self, gate: stim.Tableau, targets: Sequence[int], ) -> None: """Prepends an operation's effect into this tableau, mutating this tableau. Time cost is O(n*m*m) where n=len(self) and m=len(gate). Args: gate: The tableau of the operation being prepended into this tableau. targets: The qubits being targeted by the gate. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("H") >>> t.prepend(stim.Tableau.from_named_gate("X"), [0]) >>> t == stim.Tableau.from_named_gate("SQRT_Y_DAG") True """ @staticmethod def random( num_qubits: int, ) -> stim.Tableau: """Samples a uniformly random Clifford operation and returns its tableau. Args: num_qubits: The number of qubits the tableau should act on. Returns: The sampled tableau. Examples: >>> import stim >>> t = stim.Tableau.random(42) References: "Hadamard-free circuits expose the structure of the Clifford group" Sergey Bravyi, Dmitri Maslov https://arxiv.org/abs/2003.09412 """ def then( self, second: stim.Tableau, ) -> stim.Tableau: """Returns the result of composing two tableaus. If the tableau T1 represents the Clifford operation with unitary C1, and the tableau T2 represents the Clifford operation with unitary C2, then the tableau T1.then(T2) represents the Clifford operation with unitary C2*C1. Args: second: The result is equivalent to applying the second tableau after the receiving tableau. Examples: >>> import stim >>> t1 = stim.Tableau.random(4) >>> t2 = stim.Tableau.random(4) >>> t3 = t1.then(t2) >>> p = stim.PauliString.random(4) >>> t3(p) == t2(t1(p)) True """ def to_circuit( self, method: Literal["elimination", "graph_state"] = 'elimination', ) -> stim.Circuit: """Synthesizes a circuit that implements the tableau's Clifford operation. The circuits returned by this method are not guaranteed to be stable from version to version, and may be produced using randomization. Args: method: The method to use when synthesizing the circuit. Available values: "elimination": Cancels off-diagonal terms using Gaussian elimination. Gate set: H, S, CX Circuit qubit count: n Circuit operation count: O(n^2) Circuit depth: O(n^2) "graph_state": Prepares the tableau's state using a graph state circuit. Gate set: RX, CZ, H, S, X, Y, Z Circuit qubit count: n Circuit operation count: O(n^2) The circuit will be made up of three layers: 1. An RX layer initializing all qubits. 2. A CZ layer coupling the qubits. (Each CZ is an edge in the graph state.) 3. A single qubit rotation layer. Note: "graph_state" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. "mpp_state": Prepares the tableau's state using MPP and feedback. Gate set: MPP, CX rec, CY rec, CZ rec Circuit qubit count: n Circuit operation count: O(n^2) The circuit will be made up of two layers: 1. An MPP layer measuring each of the tableau's stabilizers. 2. A feedback layer using the measurement results to control whether or not to apply each of the tableau's destabilizers in order to get the correct sign for each stabilizer. Note: "mpp_state" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. "mpp_state_unsigned": Prepares the tableau's state up to sign using MPP. Gate set: MPP Circuit qubit count: n Circuit operation count: O(n^2) The circuit will contain a series of MPP measurements measuring each of the tableau's stabilizers. The stabilizers are measured in the order used by the tableau (i.e. tableau.z_output(k) is the k'th stabilizer measured). Note: "mpp_state_unsigned" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. Returns: The synthesized circuit. Example: >>> import stim >>> tableau = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("+YZ__"), ... stim.PauliString("-Y_XY"), ... stim.PauliString("+___Y"), ... stim.PauliString("+YZX_"), ... ], ... zs=[ ... stim.PauliString("+XZYY"), ... stim.PauliString("-XYX_"), ... stim.PauliString("-ZXXZ"), ... stim.PauliString("+XXZ_"), ... ], ... ) >>> tableau.to_circuit() stim.Circuit(''' S 0 H 0 1 3 CX 0 1 0 2 0 3 S 1 3 H 1 3 CX 1 0 3 0 3 1 1 3 3 1 H 1 S 1 CX 1 3 H 2 3 CX 2 1 3 1 3 2 2 3 3 2 H 3 CX 2 3 S 3 H 3 0 1 2 S 0 0 1 1 2 2 H 0 1 2 S 3 3 ''') >>> tableau.to_circuit("graph_state") stim.Circuit(''' RX 0 1 2 3 TICK CZ 0 3 1 2 1 3 TICK X 0 1 Z 2 S 2 3 H 3 S 3 ''') >>> tableau.to_circuit("mpp_state_unsigned") stim.Circuit(''' MPP X0*Z1*Y2*Y3 !X0*Y1*X2 !Z0*X1*X2*Z3 X0*X1*Z2 ''') >>> tableau.to_circuit("mpp_state") stim.Circuit(''' MPP X0*Z1*Y2*Y3 !X0*Y1*X2 !Z0*X1*X2*Z3 X0*X1*Z2 CX rec[-3] 2 rec[-1] 2 CY rec[-4] 0 rec[-3] 0 rec[-3] 3 rec[-2] 3 rec[-1] 0 CZ rec[-4] 1 rec[-1] 1 ''') """ def to_numpy( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Decomposes the contents of the tableau into six numpy arrays. The first four numpy arrays correspond to the four quadrants of the table defined in Aaronson and Gottesman's "Improved Simulation of Stabilizer Circuits" ( https://arxiv.org/abs/quant-ph/0406196 ). The last two numpy arrays are the X and Z sign bit vectors of the tableau. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: An (x2x, x2z, z2x, z2z, x_signs, z_signs) tuple encoding the tableau. x2x: A 2d table of whether tableau(X_i)_j is X or Y (instead of I or Z). x2z: A 2d table of whether tableau(X_i)_j is Z or Y (instead of I or X). z2x: A 2d table of whether tableau(Z_i)_j is X or Y (instead of I or Z). z2z: A 2d table of whether tableau(Z_i)_j is Z or Y (instead of I or X). x_signs: A vector of whether tableau(X_i) is negative. z_signs: A vector of whether tableau(Z_i) is negative. If bit_packed=False then: x2x.dtype = np.bool_ x2z.dtype = np.bool_ z2x.dtype = np.bool_ z2z.dtype = np.bool_ x_signs.dtype = np.bool_ z_signs.dtype = np.bool_ x2x.shape = (len(tableau), len(tableau)) x2z.shape = (len(tableau), len(tableau)) z2x.shape = (len(tableau), len(tableau)) z2z.shape = (len(tableau), len(tableau)) x_signs.shape = len(tableau) z_signs.shape = len(tableau) x2x[i, j] = tableau.x_output_pauli(i, j) in [1, 2] x2z[i, j] = tableau.x_output_pauli(i, j) in [2, 3] z2x[i, j] = tableau.z_output_pauli(i, j) in [1, 2] z2z[i, j] = tableau.z_output_pauli(i, j) in [2, 3] If bit_packed=True then: x2x.dtype = np.uint8 x2z.dtype = np.uint8 z2x.dtype = np.uint8 z2z.dtype = np.uint8 x_signs.dtype = np.uint8 z_signs.dtype = np.uint8 x2x.shape = (len(tableau), math.ceil(len(tableau) / 8)) x2z.shape = (len(tableau), math.ceil(len(tableau) / 8)) z2x.shape = (len(tableau), math.ceil(len(tableau) / 8)) z2z.shape = (len(tableau), math.ceil(len(tableau) / 8)) x_signs.shape = math.ceil(len(tableau) / 8) z_signs.shape = math.ceil(len(tableau) / 8) (x2x[i, j // 8] >> (j % 8)) & 1 = tableau.x_output_pauli(i, j) in [1, 2] (x2z[i, j // 8] >> (j % 8)) & 1 = tableau.x_output_pauli(i, j) in [2, 3] (z2x[i, j // 8] >> (j % 8)) & 1 = tableau.z_output_pauli(i, j) in [1, 2] (z2z[i, j // 8] >> (j % 8)) & 1 = tableau.z_output_pauli(i, j) in [2, 3] Examples: >>> import stim >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> print(repr(cnot)) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+ZZ"), ], ) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = cnot.to_numpy() >>> x2x array([[ True, True], [False, True]]) >>> x2z array([[False, False], [False, False]]) >>> z2x array([[False, False], [False, False]]) >>> z2z array([[ True, False], [ True, True]]) >>> x_signs array([False, False]) >>> z_signs array([False, False]) >>> t = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("-Y_ZY"), ... stim.PauliString("-Y_YZ"), ... stim.PauliString("-XXX_"), ... stim.PauliString("+ZYX_"), ... ], ... zs=[ ... stim.PauliString("-_ZZX"), ... stim.PauliString("+YZXZ"), ... stim.PauliString("+XZ_X"), ... stim.PauliString("-YYXX"), ... ], ... ) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy() >>> x2x array([[ True, False, False, True], [ True, False, True, False], [ True, True, True, False], [False, True, True, False]]) >>> x2z array([[ True, False, True, True], [ True, False, True, True], [False, False, False, False], [ True, True, False, False]]) >>> z2x array([[False, False, False, True], [ True, False, True, False], [ True, False, False, True], [ True, True, True, True]]) >>> z2z array([[False, True, True, False], [ True, True, False, True], [False, True, False, False], [ True, True, False, False]]) >>> x_signs array([ True, True, True, False]) >>> z_signs array([ True, False, False, True]) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy(bit_packed=True) >>> x2x array([[9], [5], [7], [6]], dtype=uint8) >>> x2z array([[13], [13], [ 0], [ 3]], dtype=uint8) >>> z2x array([[ 8], [ 5], [ 9], [15]], dtype=uint8) >>> z2z array([[ 6], [11], [ 2], [ 3]], dtype=uint8) >>> x_signs array([7], dtype=uint8) >>> z_signs array([9], dtype=uint8) """ def to_pauli_string( self, ) -> stim.PauliString: """Return a Pauli string equivalent to the tableau. If the tableau is equivalent to a pauli product, creates an equivalent pauli string. If not, then an error is raised. Returns: The created pauli string Raises: ValueError: The Tableau isn't equivalent to a Pauli product. Example: >>> import stim >>> t = (stim.Tableau.from_named_gate("Z") + ... stim.Tableau.from_named_gate("Y") + ... stim.Tableau.from_named_gate("I") + ... stim.Tableau.from_named_gate("X")) >>> print(t) +-xz-xz-xz-xz- | -+ -- ++ +- | XZ __ __ __ | __ XZ __ __ | __ __ XZ __ | __ __ __ XZ >>> print(t.to_pauli_string()) +ZY_X """ def to_stabilizers( self, *, canonicalize: bool = False, ) -> List[stim.PauliString]: """Returns the stabilizer generators of the tableau, optionally canonicalized. The stabilizer generators of the tableau are its Z outputs. Canonicalizing standardizes the generators, so that states that are equal will produce the same generators. For example, [ZI, IZ], [ZI, ZZ], amd [ZZ, ZI] describe equal states and all canonicalize to [ZI, IZ]. The canonical form is computed as follows: 1. Get a list of stabilizers using `tableau.z_output(k)` for each k. 2. Perform Gaussian elimination. pivoting on standard generators. 2a) Pivot on g=X0 first, then Z0, X1, Z1, X2, Z2, etc. 2b) Find a stabilizer that uses the generator g. If there are none, go to the next g. 2c) Multiply that stabilizer into all other stabilizers that use the generator g. 2d) Swap that stabilizer with the stabilizer at position `r` then increment `r`. `r` starts at 0. Args: canonicalize: Defaults to False. When False, the tableau's Z outputs are returned unchanged. When True, the Z outputs are rewritten into a standard form. Two stabilizer states have the same standard form if and only if they describe equivalent quantum states. Returns: A List[stim.PauliString] of the tableau's stabilizer generators. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> raw_stabilizers = t.to_stabilizers() >>> for e in raw_stabilizers: ... print(repr(e)) stim.PauliString("+Z_") stim.PauliString("+ZZ") >>> canonical_stabilizers = t.to_stabilizers(canonicalize=True) >>> for e in canonical_stabilizers: ... print(repr(e)) stim.PauliString("+Z_") stim.PauliString("+_Z") """ def to_state_vector( self, *, endian: Literal["little", "big"] = 'little', ) -> np.ndarray[np.complex64]: """Returns the state vector produced by applying the tableau to the |0..0> state. This function takes O(n * 2**n) time and O(2**n) space, where n is the number of qubits. The computation is done by initialization a random state vector and iteratively projecting it into the +1 eigenspace of each stabilizer of the state. The state is then canonicalized so that zero values are actually exactly 0, and so that the first non-zero entry is positive. Args: endian: "little" (default): state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A `numpy.ndarray[numpy.complex64]` of computational basis amplitudes. If the result is in little endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_0, the qubit with index 1 is storing the bit b_1, etc. If the result is in big endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_{n-1}, the qubit with index 1 is storing the bit b_{n-2}, etc. Examples: >>> import stim >>> import numpy as np >>> i2 = stim.Tableau.from_named_gate('I') >>> x = stim.Tableau.from_named_gate('X') >>> h = stim.Tableau.from_named_gate('H') >>> (x + i2).to_state_vector(endian='little') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> (i2 + x).to_state_vector(endian='little') array([0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], dtype=complex64) >>> (i2 + x).to_state_vector(endian='big') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> (h + h).to_state_vector(endian='little') array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], dtype=complex64) """ def to_unitary_matrix( self, *, endian: Literal["little", "big"], ) -> np.ndarray[np.complex64]: """Converts the tableau into a unitary matrix. For an n-qubit tableau, this method performs O(n 4^n) work. It uses the state channel duality to transform the tableau into a list of stabilizers, then generates a random state vector and projects it into the +1 eigenspace of each stabilizer. Note that tableaus don't have a defined global phase, so the result's global phase may be different from what you expect. For example, the square of SQRT_X's unitary might equal -X instead of +X. Args: endian: "little": The first qubit is the least significant (corresponds to an offset of 1 in the state vector). "big": The first qubit is the most significant (corresponds to an offset of 2**(n - 1) in the state vector). Returns: A numpy array with dtype=np.complex64 and shape=(1 << len(tableau), 1 << len(tableau)). Example: >>> import stim >>> cnot = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("XX"), ... stim.PauliString("_X"), ... ], ... zs=[ ... stim.PauliString("Z_"), ... stim.PauliString("ZZ"), ... ], ... ) >>> cnot.to_unitary_matrix(endian='big') array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64) """ def x_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli X by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli X operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.x_output(0) stim.PauliString("+Z") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.x_output(0) stim.PauliString("+XX") >>> cnot.x_output(1) stim.PauliString("+_X") """ def x_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.x_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input X generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.x_output_pauli(0, 0) 2 >>> t.x_output_pauli(0, 1) 0 >>> t.x_output_pauli(1, 0) 2 >>> t.x_output_pauli(1, 1) 3 """ def x_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating an X generator. This operation runs in constant time. Args: target: The qubit the X generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("S_DAG").x_sign(0) -1 >>> stim.Tableau.from_named_gate("S").x_sign(0) 1 """ def y_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli Y by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli Y operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.y_output(0) stim.PauliString("-Y") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.y_output(0) stim.PauliString("+YX") >>> cnot.y_output(1) stim.PauliString("+ZY") """ def y_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.y_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input Y generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.y_output_pauli(0, 0) 1 >>> t.y_output_pauli(0, 1) 2 >>> t.y_output_pauli(1, 0) 0 >>> t.y_output_pauli(1, 1) 2 """ def y_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating a Y generator. Unlike x_sign and z_sign, this operation runs in linear time. The Y generator has to be computed by multiplying the X and Z outputs and the sign depends on all terms. Args: target: The qubit the Y generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("S_DAG").y_sign(0) 1 >>> stim.Tableau.from_named_gate("S").y_sign(0) -1 """ def z_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli Z by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli Z operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.z_output(0) stim.PauliString("+X") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.z_output(0) stim.PauliString("+Z_") >>> cnot.z_output(1) stim.PauliString("+ZZ") """ def z_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.z_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input Z generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.z_output_pauli(0, 0) 3 >>> t.z_output_pauli(0, 1) 2 >>> t.z_output_pauli(1, 0) 2 >>> t.z_output_pauli(1, 1) 1 """ def z_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating a Z generator. This operation runs in constant time. Args: target: The qubit the Z generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("SQRT_X_DAG").z_sign(0) 1 >>> stim.Tableau.from_named_gate("SQRT_X").z_sign(0) -1 """ class TableauIterator: """Iterates over all stabilizer tableaus of a specified size. Examples: >>> import stim >>> tableau_iterator = stim.Tableau.iter_all(1) >>> n = 0 >>> for single_qubit_clifford in tableau_iterator: ... n += 1 >>> n 24 """ def __iter__( self, ) -> stim.TableauIterator: """Returns an independent copy of the tableau iterator. Since for-loops and loop-comprehensions call `iter` on things they iterate, this effectively allows the iterator to be iterated multiple times. """ def __next__( self, ) -> stim.Tableau: """Returns the next iterated tableau. """ class TableauSimulator: """A stabilizer circuit simulator that tracks an inverse stabilizer tableau. Supports interactive usage, where gates and measurements are applied on demand. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> if s.measure(0): ... s.h(1) ... s.cnot(1, 2) >>> s.measure(1) == s.measure(2) True >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+X_"), stim.PauliString("+XZ"), ], ) """ def __init__( self, *, seed: Optional[int] = None, ) -> None: """Initializes a stim.TableauSimulator. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: An initialized stim.TableauSimulator. Examples: >>> import stim >>> s = stim.TableauSimulator(seed=0) >>> s2 = stim.TableauSimulator(seed=0) >>> s.h(0) >>> s2.h(0) >>> s.measure(0) == s2.measure(0) True """ def c_xyz( self, *targets, ) -> None: """Applies a C_XYZ gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.c_xyz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y +Z +X """ def c_zyx( self, *targets, ) -> None: """Applies a C_ZYX gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.c_zyx(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z +X +Y """ def canonical_stabilizers( self, ) -> List[stim.PauliString]: """Returns a standardized list of the simulator's current stabilizer generators. Two simulators have the same canonical stabilizers if and only if their current quantum state is equal (and tracking the same number of qubits). The canonical form is computed as follows: 1. Get a list of stabilizers using the `z_output`s of `simulator.current_inverse_tableau()**-1`. 2. Perform Gaussian elimination on each generator g. 2a) The generators are considered in order X0, Z0, X1, Z1, X2, Z2, etc. 2b) Pick any stabilizer that uses the generator g. If there are none, go to the next g. 2c) Multiply that stabilizer into all other stabilizers that use the generator g. 2d) Swap that stabilizer with the stabilizer at position `next_output` then increment `next_output`. Returns: A List[stim.PauliString] of the simulator's state's stabilizers. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.x(2) >>> for e in s.canonical_stabilizers(): ... print(repr(e)) stim.PauliString("+XX_") stim.PauliString("+ZZ_") stim.PauliString("-__Z") >>> # Scramble the stabilizers then check the canonical form is unchanged. >>> s.set_inverse_tableau(s.current_inverse_tableau()**-1) >>> s.cnot(0, 1) >>> s.cz(0, 2) >>> s.s(0, 2) >>> s.cy(2, 1) >>> s.set_inverse_tableau(s.current_inverse_tableau()**-1) >>> for e in s.canonical_stabilizers(): ... print(repr(e)) stim.PauliString("+XX_") stim.PauliString("+ZZ_") stim.PauliString("-__Z") """ def cnot( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cnot(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def copy( self, *, copy_rng: bool = False, seed: Optional[int] = None, ) -> stim.TableauSimulator: """Returns a simulator with the same internal state, except perhaps its prng. Args: copy_rng: By default, new simulator's prng is reinitialized with a random seed. However, one can set this argument to True in order to have the prng state copied together with the rest of the original simulator's state. Consequently, in this case the two simulators will produce the same measurement outcomes for the same quantum circuits. If both seed and copy_rng are set, an exception is raised. Defaults to False. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng state is either copied from the original simulator or reseeded from system entropy, depending on the copy_rng argument. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Examples: >>> import stim >>> s1 = stim.TableauSimulator() >>> s1.set_inverse_tableau(stim.Tableau.random(1)) >>> s2 = s1.copy() >>> s2 is s1 False >>> s2.current_inverse_tableau() == s1.current_inverse_tableau() True >>> s1 = stim.TableauSimulator() >>> s2 = s1.copy(copy_rng=True) >>> s1.h(0) >>> s2.h(0) >>> assert s1.measure(0) == s2.measure(0) >>> s = stim.TableauSimulator() >>> def brute_force_post_select(qubit, desired_result): ... global s ... while True: ... s2 = s.copy() ... if s2.measure(qubit) == desired_result: ... s = s2 ... break >>> s.h(0) >>> brute_force_post_select(qubit=0, desired_result=True) >>> s.measure(0) True """ def current_inverse_tableau( self, ) -> stim.Tableau: """Returns a copy of the internal state of the simulator as a stim.Tableau. Returns: A stim.Tableau copy of the simulator's state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> s.cnot(0, 1) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+X_"), stim.PauliString("+XZ"), ], ) """ def current_measurement_record( self, ) -> List[bool]: """Returns a copy of the record of all measurements performed by the simulator. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.current_measurement_record() [] >>> s.measure(0) False >>> s.x(0) >>> s.measure(0) True >>> s.current_measurement_record() [False, True] >>> s.do(stim.Circuit("M 0")) >>> s.current_measurement_record() [False, True, True] Returns: A list of booleans containing the result of every measurement performed by the simulator so far. """ def cx( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def cy( self, *targets, ) -> None: """Applies a controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ def cz( self, *targets, ) -> None: """Applies a controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def depolarize1( self, *targets: int, p: float, ): """Probabilistically applies single-qubit depolarization to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.depolarize1(0, 1, 2, p=0.01) """ def depolarize2( self, *targets: int, p: float, ): """Probabilistically applies two-qubit depolarization to targets. Args: *targets: The indices of the qubits to target with the noise. The pairs of qubits are formed by zip(targets[::1], targets[1::2]). p: The chance of the error being applied, independently, to each qubit pair. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.depolarize1(0, 1, 4, 5, p=0.01) """ def do( self, circuit_or_pauli_string: Union[stim.Circuit, stim.PauliString, stim.CircuitInstruction, stim.CircuitRepeatBlock], ) -> None: """Applies a circuit or pauli string to the simulator's state. Args: circuit_or_pauli_string: A stim.Circuit, stim.PauliString, stim.CircuitInstruction, or stim.CircuitRepeatBlock with operations to apply to the simulator's state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do(stim.Circuit(''' ... X 0 ... M 0 ... ''')) >>> s.current_measurement_record() [True] >>> s = stim.TableauSimulator() >>> s.do(stim.PauliString("IXYZ")) >>> s.measure_many(0, 1, 2, 3) [False, True, True, False] """ def do_circuit( self, circuit: stim.Circuit, ) -> None: """Applies a circuit to the simulator's state. Args: circuit: A stim.Circuit containing operations to apply. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do_circuit(stim.Circuit(''' ... X 0 ... M 0 ... ''')) >>> s.current_measurement_record() [True] """ def do_pauli_string( self, pauli_string: stim.PauliString, ) -> None: """Applies the paulis from a pauli string to the simulator's state. Args: pauli_string: A stim.PauliString containing Paulis to apply. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do_pauli_string(stim.PauliString("IXYZ")) >>> s.measure_many(0, 1, 2, 3) [False, True, True, False] """ def do_tableau( self, tableau: stim.Tableau, targets: List[int], ) -> None: """Applies a custom tableau operation to qubits in the simulator. Note that this method has to compute the inverse of the tableau, because the simulator's internal state is an inverse tableau. Args: tableau: A stim.Tableau representing the Clifford operation to apply. targets: The indices of the qubits to operate on. Examples: >>> import stim >>> sim = stim.TableauSimulator() >>> sim.h(1) >>> sim.h_yz(2) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+X', '+Y', '+Z'] >>> rot3 = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("_X_"), ... stim.PauliString("__X"), ... stim.PauliString("X__"), ... ], ... zs=[ ... stim.PauliString("_Z_"), ... stim.PauliString("__Z"), ... stim.PauliString("Z__"), ... ], ... ) >>> sim.do_tableau(rot3, [1, 2, 3]) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+Z', '+X', '+Y'] >>> sim.do_tableau(rot3, [1, 2, 3]) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+Y', '+Z', '+X'] """ def h( self, *targets, ) -> None: """Applies a Hadamard gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z -Y +X """ def h_xy( self, *targets, ) -> None: """Applies an operation that swaps the X and Y axes to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_xy(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y +X -Z """ def h_xz( self, *targets, ) -> None: """Applies a Hadamard gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_xz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z -Y +X """ def h_yz( self, *targets, ) -> None: """Applies an operation that swaps the Y and Z axes to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_yz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X +Z +Y """ def iswap( self, *targets, ) -> None: """Applies an ISWAP gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.iswap(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Y +Z """ def iswap_dag( self, *targets, ) -> None: """Applies an ISWAP_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.iswap_dag(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ -Y +Z """ def measure( self, target: int, ) -> bool: """Measures a single qubit. Unlike the other methods on TableauSimulator, this one does not broadcast over multiple targets. This is to avoid returning a list, which would create a pitfall where typing `if sim.measure(qubit)` would be a bug. To measure multiple qubits, use `TableauSimulator.measure_many`. Args: target: The index of the qubit to measure. Returns: The measurement result as a bool. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(1) >>> s.measure(0) False >>> s.measure(1) True """ def measure_kickback( self, target: int, ) -> tuple: """Measures a qubit and returns the result as well as its Pauli kickback (if any). The "Pauli kickback" of a stabilizer circuit measurement is a set of Pauli operations that flip the post-measurement system state between the two possible post-measurement states. For example, consider measuring one of the qubits in the state |00>+|11> in the Z basis. If the measurement result is False, then the system projects into the state |00>. If the measurement result is True, then the system projects into the state |11>. Applying a Pauli X operation to both qubits flips between |00> and |11>. Therefore the Pauli kickback of the measurement is `stim.PauliString("XX")`. Note that there are often many possible equivalent Pauli kickbacks. For example, if in the previous example there was a third qubit in the |0> state, then both `stim.PauliString("XX_")` and `stim.PauliString("XXZ")` are valid kickbacks. Measurements with deterministic results don't have a Pauli kickback. Args: target: The index of the qubit to measure. Returns: A (result, kickback) tuple. The result is a bool containing the measurement's output. The kickback is either None (meaning the measurement was deterministic) or a stim.PauliString (meaning the measurement was random, and the operations in the Pauli string flip between the two possible post-measurement states). Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.measure_kickback(0) (False, None) >>> s.h(0) >>> s.measure_kickback(0)[1] stim.PauliString("+X") >>> def pseudo_post_select(qubit, desired_result): ... m, kick = s.measure_kickback(qubit) ... if m != desired_result: ... if kick is None: ... raise ValueError("Post-selected the impossible!") ... s.do(kick) >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.cnot(0, 2) >>> pseudo_post_select(qubit=2, desired_result=True) >>> s.measure_many(0, 1, 2) [True, True, True] """ def measure_many( self, *targets, ) -> List[bool]: """Measures multiple qubits. Args: *targets: The indices of the qubits to measure. Returns: The measurement results as a list of bools. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(1) >>> s.measure_many(0, 1) [False, True] """ def measure_observable( self, observable: stim.PauliString, *, flip_probability: float = 0.0, ) -> bool: """Measures an pauli string observable, as if by an MPP instruction. Args: observable: The observable to measure, specified as a stim.PauliString. flip_probability: Probability of the recorded measurement result being flipped. Returns: The result of the measurement. The result is also recorded into the measurement record. Raises: ValueError: The given pauli string isn't Hermitian, or the given probability isn't a valid probability. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.measure_observable(stim.PauliString("XX")) False >>> s.measure_observable(stim.PauliString("YY")) True >>> s.measure_observable(stim.PauliString("-ZZ")) True """ @property def num_qubits( self, ) -> int: """Returns the number of qubits currently being tracked by the simulator. Note that the number of qubits being tracked will implicitly increase if qubits beyond the current limit are touched. Untracked qubits are always assumed to be in the |0> state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.num_qubits 0 >>> s.h(2) >>> s.num_qubits 3 """ def peek_bloch( self, target: int, ) -> stim.PauliString: """Returns the state of the qubit as a single-qubit stim.PauliString stabilizer. This is a non-physical operation. It reports information about the qubit without disturbing it. Args: target: The qubit to peek at. Returns: stim.PauliString("I"): The qubit is entangled. Its bloch vector is x=y=z=0. stim.PauliString("+Z"): The qubit is in the |0> state. Its bloch vector is z=+1, x=y=0. stim.PauliString("-Z"): The qubit is in the |1> state. Its bloch vector is z=-1, x=y=0. stim.PauliString("+Y"): The qubit is in the |i> state. Its bloch vector is y=+1, x=z=0. stim.PauliString("-Y"): The qubit is in the |-i> state. Its bloch vector is y=-1, x=z=0. stim.PauliString("+X"): The qubit is in the |+> state. Its bloch vector is x=+1, y=z=0. stim.PauliString("-X"): The qubit is in the |-> state. Its bloch vector is x=-1, y=z=0. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_bloch(0) stim.PauliString("+Z") >>> s.x(0) >>> s.peek_bloch(0) stim.PauliString("-Z") >>> s.h(0) >>> s.peek_bloch(0) stim.PauliString("-X") >>> s.sqrt_x(1) >>> s.peek_bloch(1) stim.PauliString("-Y") >>> s.cz(0, 1) >>> s.peek_bloch(0) stim.PauliString("+_") """ def peek_observable_expectation( self, observable: stim.PauliString, ) -> int: """Determines the expected value of an observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: observable: The observable to determine the expected value of. This observable must have a real sign, not an imaginary sign. Returns: +1: Observable will be deterministically false when measured. -1: Observable will be deterministically true when measured. 0: Observable will be random when measured. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_observable_expectation(stim.PauliString("+Z")) 1 >>> s.peek_observable_expectation(stim.PauliString("+X")) 0 >>> s.peek_observable_expectation(stim.PauliString("-Z")) -1 >>> s.do(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''')) >>> queries = ['XX', 'YY', 'ZZ', '-ZZ', 'ZI', 'II', 'IIZ'] >>> for q in queries: ... print(q, s.peek_observable_expectation(stim.PauliString(q))) XX 1 YY -1 ZZ 1 -ZZ -1 ZI 0 II 1 IIZ 1 """ def peek_x( self, target: int, ) -> int: """Returns the expected value of a qubit's X observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |+> state. -1: Qubit is in the |-> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_z(0) >>> s.peek_x(0) 0 >>> s.reset_x(0) >>> s.peek_x(0) 1 >>> s.z(0) >>> s.peek_x(0) -1 """ def peek_y( self, target: int, ) -> int: """Returns the expected value of a qubit's Y observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |i> state. -1: Qubit is in the |-i> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_z(0) >>> s.peek_y(0) 0 >>> s.reset_y(0) >>> s.peek_y(0) 1 >>> s.z(0) >>> s.peek_y(0) -1 """ def peek_z( self, target: int, ) -> int: """Returns the expected value of a qubit's Z observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |0> state. -1: Qubit is in the |1> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.peek_z(0) 0 >>> s.reset_z(0) >>> s.peek_z(0) 1 >>> s.x(0) >>> s.peek_z(0) -1 """ def postselect_observable( self, observable: stim.PauliString, *, desired_value: bool = False, ) -> None: """Projects into a desired observable, or raises an exception if it was impossible. Postselecting an observable forces it to collapse to a specific eigenstate, as if it was measured and that state was the result of the measurement. Args: observable: The observable to postselect, specified as a pauli string. The pauli string's sign must be -1 or +1 (not -i or +i). desired_value: False (default): Postselect into the +1 eigenstate of the observable. True: Postselect into the -1 eigenstate of the observable. Raises: ValueError: The given observable had an imaginary sign. OR The postselection was impossible. The observable was in the opposite eigenstate, so measuring it would never ever return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.postselect_observable(stim.PauliString("+XX")) >>> s.postselect_observable(stim.PauliString("+ZZ")) >>> s.peek_observable_expectation(stim.PauliString("+YY")) -1 """ def postselect_x( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the X basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |+> state. True: postselect targets into the |-> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_x(0) 0 >>> s.postselect_x(0, desired_value=False) >>> s.peek_x(0) 1 >>> s.h(0) >>> s.peek_x(0) 0 >>> s.postselect_x(0, desired_value=True) >>> s.peek_x(0) -1 """ def postselect_y( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the Y basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |i> state. True: postselect targets into the |-i> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_y(0) 0 >>> s.postselect_y(0, desired_value=False) >>> s.peek_y(0) 1 >>> s.reset_x(0) >>> s.peek_y(0) 0 >>> s.postselect_y(0, desired_value=True) >>> s.peek_y(0) -1 """ def postselect_z( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the Z basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |0> state. True: postselect targets into the |1> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.peek_z(0) 0 >>> s.postselect_z(0, desired_value=False) >>> s.peek_z(0) 1 >>> s.h(0) >>> s.peek_z(0) 0 >>> s.postselect_z(0, desired_value=True) >>> s.peek_z(0) -1 """ def reset( self, *targets, ) -> None: """Resets qubits to the |0> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(0) >>> s.reset(0) >>> s.peek_bloch(0) stim.PauliString("+Z") """ def reset_x( self, *targets, ) -> None: """Resets qubits to the |+> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.peek_bloch(0) stim.PauliString("+X") """ def reset_y( self, *targets, ) -> None: """Resets qubits to the |i> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_y(0) >>> s.peek_bloch(0) stim.PauliString("+Y") """ def reset_z( self, *targets, ) -> None: """Resets qubits to the |0> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.reset_z(0) >>> s.peek_bloch(0) stim.PauliString("+Z") """ def s( self, *targets, ) -> None: """Applies a SQRT_Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.s(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y -X +Z """ def s_dag( self, *targets, ) -> None: """Applies a SQRT_Z_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.s_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -Y +X +Z """ def set_inverse_tableau( self, new_inverse_tableau: stim.Tableau, ) -> None: """Overwrites the simulator's internal state with the given inverse tableau. The inverse tableau specifies how Pauli product observables of qubits at the current time transform into equivalent Pauli product observables at the beginning of time, when all qubits were in the |0> state. For example, if the Z observable on qubit 5 maps to a product of Z observables at the start of time then a Z basis measurement on qubit 5 will be deterministic and equal to the sign of the product. Whereas if it mapped to a product of observables including an X or a Y then the Z basis measurement would be random. Any qubits not within the length of the tableau are implicitly in the |0> state. Args: new_inverse_tableau: The tableau to overwrite the internal state with. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> t = stim.Tableau.random(4) >>> s.set_inverse_tableau(t) >>> s.current_inverse_tableau() == t True """ def set_num_qubits( self, new_num_qubits: int, ) -> None: """Resizes the simulator's internal state. This forces the simulator's internal state to track exactly the qubits whose indices are in `range(new_num_qubits)`. Note that untracked qubits are always assumed to be in the |0> state. Therefore, calling this method will effectively force any qubit whose index is outside `range(new_num_qubits)` to be reset to |0>. Note that this method does not prevent future operations from implicitly expanding the size of the tracked state (e.g. setting the number of qubits to 5 will not prevent a Hadamard from then being applied to qubit 100, increasing the number of qubits back to 101). Args: new_num_qubits: The length of the range of qubits the internal simulator should be tracking. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> len(s.current_inverse_tableau()) 0 >>> s.set_num_qubits(5) >>> len(s.current_inverse_tableau()) 5 >>> s.x(0, 1, 2, 3) >>> s.set_num_qubits(2) >>> s.measure_many(0, 1, 2, 3) [True, True, False, False] """ def set_state_from_stabilizers( self, stabilizers: Iterable[stim.PauliString], *, allow_redundant: bool = False, allow_underconstrained: bool = False, ) -> None: """Sets the tableau simulator's state to a state satisfying the given stabilizers. The old quantum state is completely overwritten, even if the new state is underconstrained by the given stabilizers. The number of qubits is changed to exactly match the number of qubits in the longest given stabilizer. Args: stabilizers: A list of `stim.PauliString`s specifying the stabilizers that the new state must have. It is permitted for stabilizers to have different lengths. All stabilizers are padded up to the length of the longest stabilizer by appending identity terms. allow_redundant: Defaults to False. If set to False, then the given stabilizers must all be independent. If any one of them is a product of the others (including the empty product), an exception will be raised. If set to True, then redundant stabilizers are simply ignored. allow_underconstrained: Defaults to False. If set to False, then the given stabilizers must form a complete set of generators. They must exactly specify the desired stabilizer state, with no degrees of freedom left over. For an n-qubit state there must be n independent stabilizers. If set to True, then there can be leftover degrees of freedom which can be set arbitrarily. Returns: Nothing. Mutates the states of the simulator to match the desired stabilizers. Guarantees that self.current_inverse_tableau().inverse_z_output(k) will be equal to the k'th independent stabilizer from the `stabilizers` argument. Raises: ValueError: A stabilizer is redundant but allow_redundant=True wasn't set. OR The given stabilizers are contradictory (e.g. "+Z" and "-Z" both specified). OR The given stabilizers anticommute (e.g. "+Z" and "+X" both specified). OR The stabilizers left behind a degree of freedom but allow_underconstrained=True wasn't set. OR A stabilizer has an imaginary sign (i or -i). Examples: >>> import stim >>> tab_sim = stim.TableauSimulator() >>> tab_sim.set_state_from_stabilizers([ ... stim.PauliString("XX"), ... stim.PauliString("ZZ"), ... ]) >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) >>> tab_sim.set_state_from_stabilizers([ ... stim.PauliString("XX_"), ... stim.PauliString("ZZ_"), ... stim.PauliString("-YY_"), ... stim.PauliString(""), ... ], allow_underconstrained=True, allow_redundant=True) >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z__"), stim.PauliString("+_X_"), stim.PauliString("+__X"), ], zs=[ stim.PauliString("+XX_"), stim.PauliString("+ZZ_"), stim.PauliString("+__Z"), ], ) """ def set_state_from_state_vector( self, state_vector: Iterable[float], *, endian: Literal["little", "big"], ) -> None: """Sets the simulator's state to a superposition specified by an amplitude vector. Args: state_vector: A list of complex amplitudes specifying a superposition. The vector must correspond to a state that is reachable using Clifford operations, and must be normalized (i.e. it must be a unit vector). endian: "little": state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: Nothing. Mutates the states of the simulator to match the desired state. Raises: ValueError: The given state vector isn't a list of complex values specifying a stabilizer state. OR The given endian value isn't 'little' or 'big'. Examples: >>> import stim >>> tab_sim = stim.TableauSimulator() >>> tab_sim.set_state_from_state_vector([ ... 0.5**0.5, ... 0.5**0.5 * 1j, ... ], endian='little') >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+Y"), ], ) >>> tab_sim.set_state_from_state_vector([ ... 0.5**0.5, ... 0, ... 0, ... 0.5**0.5, ... ], endian='little') >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ def sqrt_x( self, *targets, ) -> None: """Applies a SQRT_X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_x(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Z -Y """ def sqrt_x_dag( self, *targets, ) -> None: """Applies a SQRT_X_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_x_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X -Z +Y """ def sqrt_y( self, *targets, ) -> None: """Applies a SQRT_Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_y(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -Z +Y +X """ def sqrt_y_dag( self, *targets, ) -> None: """Applies a SQRT_Y_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_y_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z +Y -X """ def state_vector( self, *, endian: Literal["little", "big"] = 'little', ) -> np.ndarray[np.complex64]: """Returns a wavefunction for the simulator's current state. This function takes O(n * 2**n) time and O(2**n) space, where n is the number of qubits. The computation is done by initialization a random state vector and iteratively projecting it into the +1 eigenspace of each stabilizer of the state. The state is then canonicalized so that zero values are actually exactly 0, and so that the first non-zero entry is positive. Args: endian: "little" (default): state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A `numpy.ndarray[numpy.complex64]` of computational basis amplitudes. If the result is in little endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_0, the qubit with index 1 is storing the bit b_1, etc. If the result is in big endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_{n-1}, the qubit with index 1 is storing the bit b_{n-2}, etc. Examples: >>> import stim >>> import numpy as np >>> s = stim.TableauSimulator() >>> s.x(2) >>> s.state_vector(endian='little') array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> s.state_vector(endian='big') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> s.sqrt_x(1, 2) >>> s.state_vector() array([0.5+0.j , 0. +0.j , 0. -0.5j, 0. +0.j , 0. +0.5j, 0. +0.j , 0.5+0.j , 0. +0.j ], dtype=complex64) """ def swap( self, *targets, ) -> None: """Applies a swap gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.swap(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +Y +X +X +Z """ def x( self, *targets, ) -> None: """Applies a Pauli X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.x(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X -Y -Z """ def x_error( self, *targets: int, p: float, ): """Probabilistically applies X errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the X error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x_error(0, 1, 2, p=0.01) """ def xcx( self, *targets, ) -> None: """Applies an X-controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ def xcy( self, *targets, ) -> None: """Applies an X-controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ def xcz( self, *targets, ) -> None: """Applies an X-controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ def y( self, *targets, ) -> None: """Applies a Pauli Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.y(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X +Y -Z """ def y_error( self, *targets: int, p: float, ): """Probabilistically applies Y errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the Y error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.y_error(0, 1, 2, p=0.01) """ def ycx( self, *targets, ) -> None: """Applies a Y-controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def ycy( self, *targets, ) -> None: """Applies a Y-controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ def ycz( self, *targets, ) -> None: """Applies a Y-controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +_ +_ """ def z( self, *targets, ) -> None: """Applies a Pauli Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.z(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X -Y +Z """ def z_error( self, *targets: int, p: float, ): """Probabilistically applies Z errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the Z error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.z_error(0, 1, 2, p=0.01) """ def zcx( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def zcy( self, *targets, ) -> None: """Applies a controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ def zcz( self, *targets, ) -> None: """Applies a controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ @overload def gate_data( name: str, ) -> stim.GateData: pass @overload def gate_data( ) -> Dict[str, stim.GateData]: pass def gate_data( name: Optional[str] = None, ) -> Union[str, Dict[str, stim.GateData]]: """Returns gate data for the given named gate, or all gates. Examples: >>> import stim >>> stim.gate_data('cnot').aliases ['CNOT', 'CX', 'ZCX'] >>> stim.gate_data('cnot').is_two_qubit_gate True >>> gate_dict = stim.gate_data() >>> len(gate_dict) > 50 True >>> gate_dict['MX'].produces_measurements True """ def main( *, command_line_args: List[str], ) -> int: """Runs the command line tool version of stim on the given arguments. Note that by default any input will be read from stdin, any output will print to stdout (as opposed to being intercepted). For most commands, you can use arguments like `--out` to write to a file instead of stdout and `--in` to read from a file instead of stdin. Returns: An exit code (0 means success, not zero means failure). Raises: A large variety of errors, depending on what you are doing and how it failed! Beware that many errors are caught by the main method itself and printed to stderr, with the only indication that something went wrong being the return code. Example: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f'{d}/tmp.out' ... return_code = stim.main(command_line_args=[ ... "gen", ... "--code=repetition_code", ... "--task=memory", ... "--rounds=1000", ... "--distance=2", ... "--out", ... path, ... ]) ... assert return_code == 0 ... with open(path) as f: ... print(f.read(), end='') # Generated repetition_code circuit. # task: memory # rounds: 1000 # distance: 2 # before_round_data_depolarization: 0 # before_measure_flip_probability: 0 # after_reset_flip_probability: 0 # after_clifford_depolarization: 0 # layout: # L0 Z1 d2 # Legend: # d# = data qubit # L# = data qubit with logical observable crossing # Z# = measurement qubit R 0 1 2 TICK CX 0 1 TICK CX 2 1 TICK MR 1 DETECTOR(1, 0) rec[-1] REPEAT 999 { TICK CX 0 1 TICK CX 2 1 TICK MR 1 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-1] rec[-2] } M 0 2 DETECTOR(1, 1) rec[-1] rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] """ @overload def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, ) -> np.ndarray: pass @overload def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, separate_observables: Literal[True], ) -> Tuple[np.ndarray, np.ndarray]: pass def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, separate_observables: bool = False, ) -> Union[Tuple[np.ndarray, np.ndarray], np.ndarray]: """Reads shot data, such as measurement samples, from a file. Args: path: The path to the file to read the data from. format: The format that the data is stored in, such as 'b8'. See https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md bit_packed: Defaults to false. Determines whether the result is a bool_ numpy array with one bit per byte, or a uint8 numpy array with 8 bits per byte. num_measurements: How many measurements there are per shot. num_detectors: How many detectors there are per shot. num_observables: How many observables there are per shot. Note that this only refers to observables *stored in the file*, not to observables from the original circuit that was sampled. separate_observables: When set to True, the result is a tuple of two arrays, one containing the detection event data and the other containing the observable data, instead of a single array. Returns: If separate_observables=True: A tuple (dets, obs) of numpy arrays containing the loaded data. If bit_packed=False: dets.dtype = np.bool_ dets.shape = (num_shots, num_measurements + num_detectors) det bit b from shot s is at dets[s, b] obs.dtype = np.bool_ obs.shape = (num_shots, num_observables) obs bit b from shot s is at dets[s, b] If bit_packed=True: dets.dtype = np.uint8 dets.shape = (num_shots, math.ceil( (num_measurements + num_detectors) / 8)) obs.dtype = np.uint8 obs.shape = (num_shots, math.ceil(num_observables / 8)) det bit b from shot s is at dets[s, b // 8] & (1 << (b % 8)) obs bit b from shot s is at obs[s, b // 8] & (1 << (b % 8)) If separate_observables=False: A numpy array containing the loaded data. If bit_packed=False: dtype = np.bool_ shape = (num_shots, num_measurements + num_detectors + num_observables) bit b from shot s is at result[s, b] If bit_packed=True: dtype = np.uint8 shape = (num_shots, math.ceil( (num_measurements + num_detectors + num_observables) / 8)) bit b from shot s is at result[s, b // 8] & (1 << (b % 8)) Examples: >>> import stim >>> import pathlib >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = pathlib.Path(d) / 'shots' ... with open(path, 'w') as f: ... print("0000", file=f) ... print("0101", file=f) ... ... read = stim.read_shot_data_file( ... path=str(path), ... format='01', ... num_measurements=4) >>> read array([[False, False, False, False], [False, True, False, True]]) """ def target_combined_paulis( paulis: Union[stim.PauliString, List[stim.GateTarget]], invert: bool = False, ) -> stim.GateTarget: """Returns a list of targets encoding a pauli product for instructions like MPP. Args: paulis: The paulis to encode into the targets. This can be a `stim.PauliString` or a list of pauli targets from `stim.target_x`, `stim.target_pauli`, etc. invert: Defaults to False. If True, the product is inverted (like "!X2*Y3"). Note that this is in addition to any inversions specified by the `paulis` argument. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... *stim.target_combined_paulis(stim.PauliString("-XYZ")), ... *stim.target_combined_paulis([stim.target_x(2), stim.target_y(5)]), ... *stim.target_combined_paulis([stim.target_z(9)], invert=True), ... ]) >>> circuit stim.Circuit(''' MPP !X0*Y1*Z2 X2*Y5 !Z9 ''') """ def target_combiner( ) -> stim.GateTarget: """Returns a target combiner that can be used to build Pauli products. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*Y3*Z5 ''') """ def target_inv( qubit_index: Union[int, stim.GateTarget], ) -> stim.GateTarget: """Returns a target flagged as inverted. Inverted targets are used to indicate measurement results should be flipped. Args: qubit_index: The underlying qubit index of the inverted target. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("M", [2, stim.target_inv(3)]) >>> circuit stim.Circuit(''' M 2 !3 ''') For example, the '!1' in 'M 0 !1 2' is qubit 1 flagged as inverted, meaning the measurement result from qubit 1 should be inverted when reported. """ def target_logical_observable_id( index: int, ) -> stim.DemTarget: """Returns a logical observable id identifying a frame change. Args: index: The index of the observable. Returns: The logical observable target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_logical_observable_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) L13 ''') """ def target_pauli( qubit_index: int, pauli: Union[str, int], invert: bool = False, ) -> stim.GateTarget: """Returns a pauli target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. pauli: The pauli gate to use. This can either be a string identifying the pauli by name ("x", "X", "y", "Y", "z", or "Z") or an integer following the convention (1=X, 2=Y, 3=Z). Setting this argument to "I" or to 0 will return a qubit target instead of a pauli target. invert: Defaults to False. If True, the target is inverted (like "!X10"), indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_pauli(2, "X"), ... stim.target_combiner(), ... stim.target_pauli(3, "y", invert=True), ... stim.target_pauli(5, 3), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3 Z5 ''') >>> circuit.append("M", [ ... stim.target_pauli(7, "I"), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3 Z5 M 7 ''') """ def target_rec( lookback_index: int, ) -> stim.GateTarget: """Returns a measurement record target with the given lookback. Measurement record targets are used to refer back to the measurement record; the list of measurements that have been performed so far. Measurement record targets always specify an index relative to the *end* of the measurement record. The latest measurement is `stim.target_rec(-1)`, the next most recent measurement is `stim.target_rec(-2)`, and so forth. Indexing is done this way in order to make it possible to write loops. Args: lookback_index: A negative integer indicating how far to look back, relative to the end of the measurement record. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("M", [5, 7, 11]) >>> circuit.append("CX", [stim.target_rec(-2), 3]) >>> circuit stim.Circuit(''' M 5 7 11 CX rec[-2] 3 ''') """ def target_relative_detector_id( index: int, ) -> stim.DemTarget: """Returns a relative detector id (e.g. "D5" in a .dem file). Args: index: The index of the detector, relative to the current detector offset. Returns: The relative detector target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_relative_detector_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D13 ''') """ def target_separator( ) -> stim.DemTarget: """Returns a target separator (e.g. "^" in a .dem file). Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_relative_detector_id(1), ... stim.target_separator(), ... stim.target_relative_detector_id(2), ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D1 ^ D2 ''') """ def target_sweep_bit( sweep_bit_index: int, ) -> stim.GateTarget: """Returns a sweep bit target that can be passed into `stim.Circuit.append`. Args: sweep_bit_index: The index of the sweep bit to target. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("CX", [stim.target_sweep_bit(2), 5]) >>> circuit stim.Circuit(''' CX sweep[2] 5 ''') """ def target_x( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli X target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ def target_y( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli Y target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ def target_z( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli Z target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ def write_shot_data_file( *, data: np.ndarray, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, ) -> None: """Writes shot data, such as measurement samples, to a file. Args: data: The data to write to the file. This must be a numpy array. The dtype of the array determines whether or not the data is bit packed, and the shape must match the bits per shot. dtype=np.bool_: Not bit packed. Shape must be (num_shots, num_measurements + num_detectors + num_observables). dtype=np.uint8: Yes bit packed. Shape must be (num_shots, math.ceil( (num_measurements + num_detectors + num_observables) / 8)). path: The path to the file to write the data to. format: The format that the data is stored in, such as 'b8'. See https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md num_measurements: How many measurements there are per shot. num_detectors: How many detectors there are per shot. num_observables: How many observables there are per shot. Note that this only refers to observables *in the given shot data*, not to observables from the original circuit that was sampled. Examples: >>> import stim >>> import pathlib >>> import tempfile >>> import numpy as np >>> with tempfile.TemporaryDirectory() as d: ... path = pathlib.Path(d) / 'shots' ... shot_data = np.array([ ... [0, 1, 0], ... [0, 1, 1], ... ], dtype=np.bool_) ... ... stim.write_shot_data_file( ... path=str(path), ... data=shot_data, ... format='01', ... num_measurements=3) ... ... with open(path) as f: ... written = f.read() >>> written '010\n011\n' """ ================================================ FILE: doc/usage_command_line.md ================================================ # Stim command line reference ## Index - [stim analyze_errors](#analyze_errors) - [stim convert](#convert) - [stim detect](#detect) - [stim diagram](#diagram) - [stim explain_errors](#explain_errors) - [stim gen](#gen) - [stim help](#help) - [stim m2d](#m2d) - [stim repl](#repl) - [stim sample](#sample) - [stim sample_dem](#sample_dem) ## Commands ### stim analyze_errors ``` NAME stim analyze_errors SYNOPSIS stim analyze_errors \ [--allow_gauge_detectors] \ [--approximate_disjoint_errors [probability]] \ [--block_decompose_from_introducing_remnant_edges] \ [--decompose_errors] \ [--fold_loops] \ [--ignore_decomposition_failures] \ [--in filepath] \ [--out filepath] DESCRIPTION Converts a circuit into a detector error model. OPTIONS --allow_gauge_detectors Allows non-deterministic detectors to appear in the circuit. Normally (without `--allow_gauge_detectors`), when a detector's detecting region anti-commutes with a reset or measurement, stim will raise an exception when analyzing the circuit. When `--allow_gauge_detectors` is set, stim will instead append an error mechanism into the detector error model that has a probability of 50% and flips all the detectors that anticommute with the operation. This is potentially useful in situations where the layout of detectors is supposed to stay fixed despite variations in the circuit structure. Decoders can interpret the existence of the 50% error as a weight 0 edge saying that the detectors should be fused together. For example, in the following stim circuit, the two detectors each anticommute with the reset operation: R 0 H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] Without `--allow_gauge_detectors`, stim will raise an exception when analyzing this circuit. With `--allow_gauge_detectors`, stim will add `error(0.5) D1 D2` to the output detector error model. BEWARE that gauge detectors are very tricky to work with, and not necessarily supported by all tools (even within stim itself). For example, when converting from measurements to detection events, there isn't a single choice for whether or not each individual gauge detector produced a detection event. This means that it is valid behavior for one conversion from measurements to detection events to give different results from another, as long as the gauge detectors that anticommute with the same operations flip together in a consistent fashion that respects the structure of the circuit. --approximate_disjoint_errors Allows disjoint errors to be approximated during the conversion. Detector error models require that all error mechanisms be specified as independent mechanisms. But some of the circuit error mechanisms that Stim allows can express errors that don't correspond to independent mechanisms. For example, the custom error channel `PAULI_CHANNEL_1(0.1, 0.2, 0.0)` can't be expressed exactly as a set of independent error mechanisms. But it can be approximated as an `X_ERROR(0.1)` followed by a `Y_ERROR(0.2)`. This flag can be set to any probability between 0 (the default when not specified) and 1 (the default when specified without a value). When set to a value strictly between 0 and 1, this determines the maximum disjoint probability that is allowed to be approximated as an independent probability. Without `--approximate_disjoint_errors`, attempting to convert a circuit containing `PAULI_CHANNEL_1(0.1, 0.2, 0.0)` will fail with an error stating an approximation is needed. With `--approximate_disjoint_errors`, the conversion will succeed by approximating the error into an `X_ERROR(0.1)` followed by an independent `Y_ERROR(0.2)`. Note that, although `DEPOLARIZE1` and `DEPOLARIZE2` are often defined in terms of disjoint errors, they can be exactly converted into a set of independent errors (unless the probability of the depolarizing error occurring exceeds maximum mixing, which is 75% for `DEPOLARIZE1` and 93.75% for `DEPOLARIZE2`). So the `--approximate_disjoint_errors` flag isn't needed for depolarizing errors that appear in practice. The error mechanisms that require approximations are: - PAULI_CHANNEL_1 - PAULI_CHANNEL_2 - ELSE_CORRELATED_ERROR In principle some custom Pauli channels can be converted exactly, but Stim does not currently contain logic that attempts to do this. --block_decompose_from_introducing_remnant_edges Prevents A*B from being decomposed unless A,B BOTH appear elsewhere. Irrelevant unless `--decompose_errors` is specified. When `--decompose_errors` is specified, any circuit error that causes more than two detection events must be decomposed into a set of errors with at most two detection events. The main constraint on this process is that it must not use errors that couldn't otherwise occur, since introducing such errors could violate important properties that are used for decoding. For example, in the normal surface code, it is very important that the decoding graphs for X errors and Z errors are disjoint in the bulk, and decomposing an error into a set of errors that violated this property would be disastrous. However, a corner case in this logic occurs if an error E1 that produces detection events A*B needs to be decomposed when an error E2 that produces detection events A appears elsewhere but no error producing detection events B appears elsewhere. The detection events B can be produced by both E1 and E2 occurring, but this a combination of two errors and so treating it as one error can cause problems. For example, it can result in the code distance appearing to be smaller than it actually is. Introducing B is referred to as introducing a "remnant edge" because B *only* appears in the detector error model as a remnant of removing A from A*B. By default, Stim does allow remnant edges to be introduced. Stim will only do this if it is absolutely necessary, but it *will* do it. And there are in fact QEC circuits where the decomposition requires these edges to succeed. But sometimes the presence of a remnant edge is a hint that the DETECTOR declarations in the circuit are subtly wrong. To cause the decomposition process to fail in this case, the `--block_decompose_from_introducing_remnant_edges` can be specified. --decompose_errors Decomposes errors with many detection events into "graphlike" parts. When `--decompose_errors` is specified, Stim will suggest how errors that cause more than 2 detection events (non-graphlike errors) can be decomposed into errors with at most 2 detection events (graphlike errors). For example, an error like `error(0.1) D0 D1 D2 D3` may be instead output as `error(0.1) D0 D1 ^ D2 D3` or as `error(0.1) D0 D3 ^ D1 ^ D2`. The purpose of this feature is to make matching apply to more cases. A common decoding strategy is "matching", where detection events are paired up in order to determine which errors occurred. Matching only works when the Tanner graph of the problem is a graph, not a hypergraph. In other words, it requires all errors to produce at most two detection events. This is a problem, because in practice there are essentially always circuit error mechanisms that produce more than two detection events. For example, in a CSS surface code, Y type errors on the data qubits will produce four detection events. For matching to work in these cases, non-graphlike errors (errors with more than two detection events) need to be approximated as a combination of graphlike errors. When Stim is decomposing errors, the main guarantee that it provides is that it will not introduce error mechanisms with symptoms that are otherwise impossible or that would require a combination of non-local errors to actually achieve. Informally, Stim guarantees it will preserve the "structure" of the detector error model when suggesting decompositions. It's also worth noting that the suggested decompositions are information preserving: the undecomposed model can always be recovered by simply filtering out all `^` characters splitting the errors into suggested components. Stim uses two strategies for decomposing errors: intra-channel and inter-channel. The *intra-channel* strategy is always applied first, and works by looking at the various detector/observable sets produced by each case of a single noise channel. If some cases are products of other cases, that product is *always* decomposed. For example, suppose that a single qubit depolarizing channel has a `Y5` case that produces four detection events `D0 D1 D2 D3`, an `X5` case that produces two detection events `D0 D1`, and a `Z5` case that produces two detection events `D2 D3`. Because `D0 D1 D2 D3` is the combination of `D0 D1` and `D2 D3`, the `Y5` case will be decomposed into `D0 D1 ^ D2 D3`. An important corner case here is the corner of the CSS surface code, where a Y error has two symptoms which is graphlike but because the intra-channel strategy is aggressive the Y error will still be decomposed into X and Z pieces. This can keep the X and Z decoding graphs disjoint. The *inter-channel* strategy is used when an error component is still not graphlike after the intra-channel strategy was applied. This strategy searches over all other error mechanisms looking for a combination that explains the error. If `--block_decompose_from_introducing_remnant_edges` is specified then this must be an exact match, otherwise the match can omit up to two of the symptoms in the error (resulting in the producing of a "remnant edge"). Note that the code implementing these strategies does not special case any Pauli basis. For example, it does not prefer to decompose Y into X*Z as opposed to X into Y*Z. It also does not prefer to decompose YY into IY*YI as opposed to IY into YY*YI. The code operates purely in terms of the sets of symptoms produced by the various cases, with little regard for how those sets were produced. If these strategies fail to decompose error into graphlike pieces, Stim will throw an error saying it failed to find a satisfying decomposition. --fold_loops Allows the output to contain `repeat` blocks. This flag substantially improves performance on circuits with `REPEAT` blocks with large repetition counts. The analysis will take less time and the output will be more compact. This option is only OFF by default to maintain strict backwards compatibility of the output. When a circuit contains a `REPEAT` block, the structure of the detectors often settles into a form that is identical from iteration to iteration. Specifying the `--fold_loops` option tells Stim to watch for periodicity in the structure of detectors by using a "tortoise and hare" algorithm (see https://en.wikipedia.org/wiki/Cycle_detection ). This improves the asymptotic complexity of analyzing the loop from O(total_repetitions) to O(cycle_period). Note that, although logical observables can "cross" from the end of the loop to the start of the loop without preventing loop folding, detectors CANNOT. If there is any detector introduced after the loop, whose sensitivity region extends to before the loop, loop folding will fail and the code will fall back to flattening the loop. This is disastrous for loops with huge repetition counts (e.g. in the billions) because in that case loop folding is the difference between the error analysis finishing in seconds instead of in days. --ignore_decomposition_failures Allows non-graphlike errors into the output when decomposing errors. Irrelevant unless `--decompose_errors` is specified. Without `--ignore_decomposition_failures`, circuit errors that fail to decompose into graphlike detector error model errors will cause an error and abort the conversion process. When `--ignore_decomposition_failures` is specified, circuit errors that fail to decompose into graphlike detector error model errors produce non-graphlike detector error models. Whatever processes the detector error model is then responsible for dealing with the undecomposed errors (e.g. a tool may choose to simply ignore them). --in Chooses the stim circuit file to read the circuit to convert from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md --out Chooses where to write the output detector error model. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is a stim detector error model. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md EXAMPLES Example #1 >>> cat example_circuit.stim R 0 1 X_ERROR(0.125) 0 1 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] >>> stim analyze_errors --in example_circuit.stim error(0.125) D0 error(0.125) D0 D1 Example #2 >>> stim gen \ --code repetition_code \ --task memory \ --distance 3 \ --rounds 1000 \ --after_reset_flip_probability 0.125 \ > rep_code.stim >>> stim analyze_errors --fold_loops --in rep_code.stim error(0.125) D0 error(0.125) D0 D1 error(0.125) D0 D2 error(0.125) D1 D3 error(0.125) D1 L0 error(0.125) D2 D4 error(0.125) D3 D5 detector(1, 0) D0 detector(3, 0) D1 repeat 998 { error(0.125) D4 D6 error(0.125) D5 D7 shift_detectors(0, 1) 0 detector(1, 0) D2 detector(3, 0) D3 shift_detectors 2 } shift_detectors(0, 1) 0 detector(1, 0) D2 detector(3, 0) D3 detector(1, 1) D4 detector(3, 1) D5 ``` ### stim convert ``` NAME stim convert SYNOPSIS stim convert \ --bits_per_shot int \ [--circuit filepath] \ [--in filepath] \ [--in_format 01|b8|r8|ptb64|hits|dets] \ --num_detectors int \ --num_measurements int \ --num_observables int \ [--obs_out filepath] \ [--obs_out_format 01|b8|r8|ptb64|hits|dets] \ [--out filepath] \ [--out_format 01|b8|r8|ptb64|hits|dets] \ --types M|D|L DESCRIPTION Convert data between result formats. See the various formats here: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md To read and write data, the size of the records must be known. If writing to a dets file, then the number of measurements, detectors and observables per record must also be known. Both of these pieces of information can either be given directly, or inferred from various data sources, such as circuit or dem files. OPTIONS --bits_per_shot Specifies the number of bits per shot in the input/output files. This argument is required if the circuit, dem or num_* flags are not given, and not supported when writing to a dets file. In this case we just treat the bits aas arbitrary data. It is up to the user to interpert it correctly. --circuit Specifies where the circuit that generated the data is. This argument is optional, but can be used to infer the number of measurements, detectors and observables to use per record. The circuit file should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md --in Chooses the file to read data from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input's format is specified by `--in_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --in_format Specifies the data format to use when reading data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --num_detectors Specifies the number of detectors in the input/output files. This argument is required if writing to a dets file and the circuit or dem is not given. --num_measurements Specifies the number of measurements in the input/output files. This argument is required if writing to a dets file and the circuit is not given. --num_observables Specifies the number of observables in the input/output files. This argument is required if writing to a dets file and the circuit or dem is not given. --obs_out Specifies the file to write observable flip data to. When producing detection event data, the goal is typically to predict whether or not the logical observables were flipped by using the detection events. This argument specifies where to write that observable flip data. If this argument isn't specified, the observable flip data isn't written to a file. The output is in a format specified by `--obs_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --obs_out_format Specifies the data format to use when writing observable flip data. Irrelevant unless `--obs_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out Chooses where to write the data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output's format is specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out_format Specifies the data format to use when writing output data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --types Specifies the types of events in the files. This argument is required if a circuit is given as the circuit can give the number of each type of event, but not which events are contained within an input file. Note that in most cases, a file will have either measurements only, detections only, or detections and observables. The type values (M, D, L) correspond to the value prefix letters in dets files. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md#dets EXAMPLES Example #1 >>> cat example.01 10000 11001 00000 01001 >>> stim convert \ --in example.01 \ --in_format 01 \ --out_format dets --num_measurements 5 shot M0 shot M0 M1 M4 shot shot M1 M4 Example #2 >>> cat example.dem detector D0 detector D1 logical_observable L2 >>> cat example.dets shot D0 shot D0 D1 L2 shot shot D1 L2 >>> stim convert \ --in example.dets \ --in_format dets \ --out_format 01 --dem example.dem 10000 11001 00000 01001 Example #3 >>> cat example_circuit.stim X 0 M 0 1 DETECTOR rec[-2] DETECTOR rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] >>> cat example_measure_data.01 00 01 10 11 >>> stim convert \ --in example_measure_data.01 \ --in_format 01 \ --out_format dets --circuit example_circuit.stim \ --types M shot shot M1 shot M0 shot M0 M1 Example #4 >>> cat example.01 0010 0111 1000 1110 >>> stim convert \ --in example.01 \ --in_format 01 \ --out_format hits --bits_per_shot 4 2 1,2,3 0 0,1,2 ``` ### stim detect ``` NAME stim detect SYNOPSIS stim detect \ [--append_observables] \ [--in filepath] \ [--obs_out filepath] \ [--obs_out_format 01|b8|r8|ptb64|hits|dets] \ [--out filepath] \ [--out_format 01|b8|r8|ptb64|hits|dets] \ [--seed int] \ [--shots int] DESCRIPTION Sample detection events and observable flips from a circuit. OPTIONS --append_observables Appends observable flips to the end of samples as extra detectors. PREFER --obs_out OVER THIS FLAG. Mixing the observable flip data into detection event data tends to require simply separating them again immediately, creating unnecessary work. For example, when testing a decoder, you do not want to give the observable flips to the decoder because that is the information the decoder is supposed to be predicting from the detection events. This flag causes observable flip data to be appended to each sample, as if the observables were extra detectors at the end of the circuit. For example, if there are 100 detectors and 10 observables in the circuit, then the output will contain 110 detectors and the last 10 are the observables. Note that, when using `--out_format dets`, this option is implicitly activated but observables are not appended as if they were detectors (because `dets` has type hinting information). For example, in the example from the last paragraph, the observables would be named `L0` through `L9` instead of `D100` through `D109`. --in Chooses the stim circuit file to read the circuit to sample from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md --obs_out Specifies the file to write observable flip data to. When sampling detection event data, the goal is typically to predict whether or not the logical observables were flipped by using the detection events. This argument specifies where to write that observable flip data. If this argument isn't specified, the observable flip data isn't written to a file. The output is in a format specified by `--obs_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --obs_out_format Specifies the data format to use when writing observable flip data. Irrelevant unless `--obs_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out Chooses where to write the sampled data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is in a format specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out_format Specifies the data format to use when writing output data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --seed Makes simulation results PARTIALLY deterministic. The seed integer must be a non-negative 64 bit signed integer. When `--seed` isn't specified, the random number generator is seeded using fresh entropy requested from the operating system. When `--seed #` is set, the exact same simulation results will be produced every time ASSUMING: - the exact same other flags are specified - the exact same version of Stim is being used - the exact same machine architecture is being used (for example, you're not switching from a machine that has AVX2 instructions to one that doesn't). CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary other flags and modes. For example, `--skip_reference_sample` may result in fewer calls the to the random number generator before reported sampling begins. More generally, using the same seed for `stim sample` and `stim detect` will not result in detection events corresponding to the measurement results. --shots Specifies the number of samples to take from the circuit. Defaults to 1. Must be an integer between 0 and a quintillion (10^18). EXAMPLES Example #1 >>> cat example.stim H 0 CNOT 0 1 X_ERROR(0.1) 0 1 M 0 1 DETECTOR rec[-1] rec[-2] >>> stim detect --shots 5 --in example.stim 0 1 0 0 0 Example #2 >>> cat example.stim # Single-shot X-basis rep code circuit. RX 0 1 2 3 4 5 6 MPP X0*X1 X1*X2 X2*X3 X3*X4 X4*X5 X5*X6 Z_ERROR(0.1) 0 1 2 3 4 5 6 MPP X0 X1 X2 X3 X4 X5 X6 DETECTOR rec[-1] rec[-2] rec[-8] # X6 X5 now = X5*X6 before DETECTOR rec[-2] rec[-3] rec[-9] # X5 X4 now = X4*X5 before DETECTOR rec[-3] rec[-4] rec[-10] # X4 X3 now = X3*X4 before DETECTOR rec[-4] rec[-5] rec[-11] # X3 X2 now = X2*X3 before DETECTOR rec[-5] rec[-6] rec[-12] # X2 X1 now = X1*X2 before DETECTOR rec[-6] rec[-7] rec[-13] # X1 X0 now = X0*X1 before OBSERVABLE_INCLUDE(0) rec[-1] >>> stim detect \ --in example.stim \ --out_format dets \ --shots 10 shot shot shot L0 D0 D5 shot D1 D2 shot shot L0 D0 shot D5 shot shot D3 D4 shot D0 D1 ``` ### stim diagram ``` NAME stim diagram SYNOPSIS stim diagram \ [--filter_coords (float.seperatedby(',') | L# | D#).seperatedby(':')] \ [--in filepath] \ [--out filepath] \ [--remove_noise] \ [--tick int | int:int] \ --type name DESCRIPTION Produces various kinds of diagrams. OPTIONS --filter_coords Specifies coordinate filters that determine what appears in the diagram. A coordinate is a double precision floating point number. A point is a tuple of coordinates. The coordinates of a point are separate by commas (','). A filter is a set of points. Points are separated by colons (':'). Filters can also be set to specific detector or observable indices, like D0 or L0. Example: --filter-coords 2,3:4,5,6 In a detector slice diagram this means that only detectors whose first two coordinates are (2,3), or whose first three coordinate are (4,5,6), should be included in the diagram. --filter-coords L0 In a detector slice diagram this means that logical observable 0 should be included. Logical observables are only included if explicitly filtered in. --in Where to read the object to diagram from. By default, the object is read from stdin. When `--in $FILEPATH` is specified, the object is instead read from the file at $FILEPATH. The expected type of object depends on the type of diagram. --out Chooses where to write the diagram to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The type of output produced depends on the type of diagram. --remove_noise Removes noise from the input before turning it into a diagram. For example, if the input is a noisy circuit and you aren't interested in the details of the noise but rather in the structure of the circuit, you can specify this flag in order to filter out the noise. --tick Specifies that the diagram should apply to a specific TICK or range of TICKS from the input circuit. To specify a single tick, pass an integer like `--tick=5`. To specify a range, pass two integers separated by a colon like `--tick=start:end`. Note that the range is half open. In detector and time slice diagrams, `--tick` identifies which ticks to include in the diagram. Note that `--tick=0` is the very beginning of the circuit and `--tick=1` is the instant of the first TICK instruction. --type The type of diagram to make. The available diagram types are: "timeline-text": Produces an ASCII text diagram of the operations performed by a circuit over time. The qubits are laid out into a line top to bottom, and time advances left to right. The input object should be a stim circuit. INPUT MUST BE A CIRCUIT. "timeline-svg": Produces an SVG image diagram of the operations performed by a circuit over time. The qubits are laid out into a line top to bottom, and time advances left to right. The input object should be a stim circuit. INPUT MUST BE A CIRCUIT. "timeline-3d": Produces a 3d model, in GLTF format, of the operations applied by a stim circuit over time. GLTF files can be opened with a variety of programs, or opened online in viewers such as https://gltf-viewer.donmccurdy.com/ . INPUT MUST BE A CIRCUIT. "timeline-3d-html": A web page containing a 3d model viewer of the operations applied by a stim circuit over time. INPUT MUST BE A CIRCUIT. "matchgraph-svg": An image of the decoding graph of a detector error model. Red lines are errors crossing a logical observable. INPUT MUST BE A DETECTOR ERROR MODEL OR A CIRCUIT. "matchgraph-3d": A 3d model, in GLTF format, of the decoding graph of a detector error model. Red lines are errors crossing a logical observable. GLTF files can be opened with a variety of programs, or opened online in viewers such as https://gltf-viewer.donmccurdy.com/ . INPUT MUST BE A DETECTOR ERROR MODEL OR A CIRCUIT. "matchgraph-3d-html": A web page containing a 3d model viewer of the decoding graph of a detector error model or circuit. INPUT MUST BE A DETECTOR ERROR MODEL OR A CIRCUIT. "detslice-text": An ASCII diagram of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. INPUT MUST BE A CIRCUIT. "detslice-svg": An SVG image of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. For example, a detector slice diagram of a CSS surface code circuit during the TICK between a measurement layer and a reset layer will produce the usual diagram of a surface code. Uses the Pauli color convention XYZ=RGB. INPUT MUST BE A CIRCUIT. "timeslice-svg": An SVG image of the operations that a circuit applies during the specified tick or range of ticks. INPUT MUST BE A CIRCUIT. "detslice-with-ops-svg": An SVG image of the operations that a circuit applies during the specified tick or range of ticks, combined with the detector slices after those operations are applied. INPUT MUST BE A CIRCUIT. EXAMPLES Example #1 >>> cat example_circuit.stim H 0 CNOT 0 1 >>> stim diagram \ --in example_circuit.stim \ --type timeline-text q0: -H-@- | q1: ---X- Example #2 >>> # Making a video of detector slices moving around >>> # First, make a circuit to animate. >>> stim gen \ --code surface_code \ --task rotated_memory_x \ --distance 5 \ --rounds 100 \ > surface_code.stim >>> # Second, use gnu-parallel and stim diagram to make video frames. >>> parallel stim diagram \ --filter_coords 2,2:4,2 \ --type detector-slice-svg \ --tick {} \ --in surface_code.stim \ --out video_frame_{}.svg \ ::: {0050..0150} >>> # Third, use ffmpeg to turn the frames into a GIF. >>> # (note: the complex filter argument is optional; it turns the background white) >>> ffmpeg output_animation.gif \ -framerate 5 \ -pattern_type glob -i 'video_frame_*.svg' \ -pix_fmt rgb8 \ -filter_complex "[0]split=2[bg][fg];[bg]drawbox=c=white@1:t=fill[bg];[bg][fg]overlay=format=auto" >>> # Alternatively, make an MP4 video instead of a GIF. >>> ffmpeg output_video.mp4 \ -framerate 5 \ -pattern_type glob -i 'video_frame_*.svg' \ -vf scale=1024:-1 \ -c:v libx264 \ -vf format=yuv420p \ -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" ``` ### stim explain_errors ``` NAME stim explain_errors SYNOPSIS stim explain_errors \ [--dem_filter filepath] \ [--in filepath] \ [--out filepath] \ [--single] DESCRIPTION Find circuit errors that produce certain detection events. Note that this command does not attempt to explain detection events by using multiple errors. This command can only tell you how to produce a set of detection events if they correspond to a specific single physical error annotated into the circuit. If you need to explain a detection event set using multiple errors, use a decoder such as pymatching to find the set of single detector error model errors that are needed and then use this command to convert those specific errors into circuit errors. OPTIONS --dem_filter Specifies a detector error model to use as a filter. If `--dem_filter` isn't specified, an explanation of every single set of symptoms that can be produced by the circuit. If `--dem_filter` is specified, only explanations of the error mechanisms present in the filter will be output. This is useful when you are interested in a specific set of detection events. The filter is specified as a detector error model file. See https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md --in Chooses the stim circuit file to read the explanatory circuit from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md --out Chooses where to write the explanations to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is in an arbitrary semi-human-readable format. --single Explain using a single simple error instead of all possible errors. When `--single` isn't specified, every single circuit error that produces a specific detector error model is output as a possible explanation of that error. When `--single` is specified, only the simplest circuit error is output. The "simplest" error is chosen by using heuristics such as "has fewer Pauli terms" and "happens earlier". EXAMPLES Example #1 >>> stim gen \ --code surface_code \ --task rotated_memory_z \ --distance 5 \ --rounds 10 \ --after_clifford_depolarization 0.001 \ > example.stim >>> echo "error(1) D97 D102" > example.dem >>> stim explain_errors \ --single \ --in example.stim \ --dem_filter example.dem ExplainedError { dem_error_terms: D97[coords 4,6,4] D102[coords 2,8,4] CircuitErrorLocation { flipped_pauli_product: Z36[coords 3,7] Circuit location stack trace: (after 25 TICKs) at instruction #83 (a REPEAT 9 block) in the circuit after 2 completed iterations at instruction #12 (DEPOLARIZE2) in the REPEAT block at targets #3 to #4 of the instruction resolving to DEPOLARIZE2(0.001) 46[coords 2,8] 36[coords 3,7] } } ``` ### stim gen ``` NAME stim gen SYNOPSIS stim gen \ [--after_clifford_depolarization probability] \ [--after_reset_flip_probability probability] \ [--before_measure_flip_probability probability] \ [--before_round_data_depolarization probability] \ --code surface_code|repetition_code|color_code \ --distance int \ [--out filepath] \ --rounds int \ --task name DESCRIPTION Generates example circuits. The generated circuits include annotations for noise, detectors, logical observables, the spatial locations of qubits, the spacetime locations of detectors, and the inexorable passage of TICKs. Note that the generated circuits are not intended to be sufficient for research. They are really just examples to make it easier to get started using Stim, so you can try things without having to first go through the entire effort of making a correctly annotated quantum error correction circuit. OPTIONS --after_clifford_depolarization Specifies a depolarizing noise level for unitary gates. Defaults to 0 when not specified. Must be a probability (a number between 0 and 1). Adds a `DEPOLARIZE1(p)` operation after every single-qubit Clifford operation, and a `DEPOLARIZE2(p)` noise operation after every two-qubit Clifford operation. When set to 0 or not set, these noise operations are not inserted at all. --after_reset_flip_probability Specifies a reset noise level. Defaults to 0 when not specified. Must be a probability (a number between 0 and 1). Adds an `X_ERROR(p)` after `R` and `RY` operations, and a `Z_ERROR(p)` after `RX` operations. When set to 0 or not set, these noise operations are not inserted at all. --before_measure_flip_probability Specifies a measurement noise level. Defaults to 0 when not specified. Must be a probability (a number between 0 and 1). Adds an `X_ERROR(p)` before `M` and `MY` operations, and a `Z_ERROR(p)` before `MX` operations. When set to 0 or not set, these noise operations are not inserted at all. --before_round_data_depolarization Specifies a quantum phenomenological noise level. Defaults to 0 when not specified. Must be a probability (a number between 0 and 1). Adds a `DEPOLARIZE1(p)` operation to each data qubit at the start of each round of stabilizer measurements. When set to 0 or not set, these noise operations are not inserted at all. --code The error correcting code to use. The available error correcting codes are: `surface_code` The surface code. A quantum code with a checkerboard pattern of alternating X and Z stabilizers. `repetition_code` The repetition code. The simplest classical code. `color_code` The color code. A quantum code with a hexagonal pattern of overlapping X and Z stabilizers. --distance The minimum number of physical errors that cause a logical error. The code distance determines how spatially large the generated circuit has to be. Conventionally, the code distance specifically refers to single-qubit errors between rounds instead of circuit errors during rounds. The distance must always be a positive integer. Different codes/tasks may place additional constraints on the distance (e.g. must be larger than 2 or must be odd or etc). --out Chooses where to write the generated circuit to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md --rounds The number of times the circuit's measurement qubits are measured. The number of rounds must be an integer between 1 and a quintillion (10^18). Different codes/tasks may place additional constraints on the number of rounds (e.g. enough rounds to have measured all the stabilizers at least once). --task What the generated circuit should do; the experiment it should run. Different error correcting codes support different tasks. The available tasks are: `memory` (repetition_code): Initialize a logical `|0>`, preserve it against noise for the given number of rounds, then measure. `rotated_memory_x` (surface_code): Initialize a logical `|+>` in a rotated surface code, preserve it against noise for the given number of rounds, then measure in the X basis. `rotated_memory_z` (surface_code): Initialize a logical `|0>` in a rotated surface code, preserve it against noise for the given number of rounds, then measure in the X basis. `unrotated_memory_x` (surface_code): Initialize a logical `|+>` in an unrotated surface code, preserve it against noise for the given number of rounds, then measure in the Z basis. `unrotated_memory_z` (surface_code): Initialize a logical `|0>` in an unrotated surface code, preserve it against noise for the given number of rounds, then measure in the Z basis. `memory_xyz` (color_code): Initialize a logical `|0>`, preserve it against noise for the given number of rounds, then measure. Use a color code that alternates between measuring X, then Y, then Z stabilizers. EXAMPLES Example #1 >>> stim gen \ --code repetition_code \ --task memory \ --distance 3 \ --rounds 100 \ --after_clifford_depolarization 0.001 # Generated repetition_code circuit. # task: memory # rounds: 100 # distance: 3 # before_round_data_depolarization: 0 # before_measure_flip_probability: 0 # after_reset_flip_probability: 0 # after_clifford_depolarization: 0.001 # layout: # L0 Z1 d2 Z3 d4 # Legend: # d# = data qubit # L# = data qubit with logical observable crossing # Z# = measurement qubit R 0 1 2 3 4 TICK CX 0 1 2 3 DEPOLARIZE2(0.001) 0 1 2 3 TICK CX 2 1 4 3 DEPOLARIZE2(0.001) 2 1 4 3 TICK MR 1 3 DETECTOR(1, 0) rec[-2] DETECTOR(3, 0) rec[-1] REPEAT 99 { TICK CX 0 1 2 3 DEPOLARIZE2(0.001) 0 1 2 3 TICK CX 2 1 4 3 DEPOLARIZE2(0.001) 2 1 4 3 TICK MR 1 3 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-2] rec[-4] DETECTOR(3, 0) rec[-1] rec[-3] } M 0 2 4 DETECTOR(1, 1) rec[-2] rec[-3] rec[-5] DETECTOR(3, 1) rec[-1] rec[-2] rec[-4] OBSERVABLE_INCLUDE(0) rec[-1] ``` ### stim help ``` NAME stim help SYNOPSIS stim help DESCRIPTION Prints helpful information about using stim. ``` ### stim m2d ``` NAME stim m2d SYNOPSIS stim m2d \ [--append_observables] \ --circuit filepath \ [--in filepath] \ [--in_format 01|b8|r8|ptb64|hits|dets] \ [--obs_out filepath] \ [--obs_out_format 01|b8|r8|ptb64|hits|dets] \ [--out filepath] \ [--out_format 01|b8|r8|ptb64|hits|dets] \ [--ran_without_feedback] \ [--skip_reference_sample] \ --sweep filepath \ [--sweep_format 01|b8|r8|ptb64|hits|dets] DESCRIPTION Convert measurement data into detection event data. When sampling data from hardware, instead of from simulators, it's necessary to convert the measurement data into detection event data that can be fed into a decoder. This is necessary both because of complexities in the exact sets of measurements being compared by a circuit to produce detection events and also because the expected parity of a detector's measurement set can vary due to (for example) spin echo operations in the circuit. Stim performs this conversion by simulating taking a reference sample from the circuit, in order to determine the expected parity of the measurements sets defining detectors and observables, and then comparing the sampled measurement data to these expectations. OPTIONS --append_observables Appends observable flips to the end of samples as extra detectors. PREFER --obs_out OVER THIS FLAG. Mixing the observable flip data into detection event data tends to require simply separating them again immediately, creating unnecessary work. For example, when testing a decoder, you do not want to give the observable flips to the decoder because that is the information the decoder is supposed to be predicting from the detection events. This flag causes observable flip data to be appended to each sample, as if the observables were extra detectors at the end of the circuit. For example, if there are 100 detectors and 10 observables in the circuit, then the output will contain 110 detectors and the last 10 are the observables. --circuit Specifies where the circuit that generated the measurements is. This argument is required, because the circuit is what specifies how to derive detection event data from measurement data. The circuit file should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md --in Chooses the file to read measurement data from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input's format is specified by `--in_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --in_format Specifies the data format to use when reading measurement data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --obs_out Specifies the file to write observable flip data to. When producing detection event data, the goal is typically to predict whether or not the logical observables were flipped by using the detection events. This argument specifies where to write that observable flip data. If this argument isn't specified, the observable flip data isn't written to a file. The output is in a format specified by `--obs_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --obs_out_format Specifies the data format to use when writing observable flip data. Irrelevant unless `--obs_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out Chooses where to write the sampled data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output's format is specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out_format Specifies the data format to use when writing output detection data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --ran_without_feedback Converts the results assuming all feedback operations were skipped. Pauli feedback operations don't need to be performed on the quantum computer. They can be performed within the classical control system. As such, it often makes sense to skip them when sampling from the circuit on hardware, and then account for this later during data analysis. Turning on this flag means that the quantum computer didn't apply the feedback operations, and it's the job of the m2d conversion to read the measurement data, rewrite it to account for feedback effects, then convert to detection events. In the python API, the same effect can be achieved by using stim.Circuit.with_inlined_feedback().compile_m2d_converter(). --skip_reference_sample Asserts the circuit can produce a noiseless sample that is just 0s. When this argument is specified, the reference sample (that the measurement data will be compared to) is generated by simply setting all measurements to 0 instead of by simulating the circuit without noise. Skipping the reference sample can significantly improve performance, because acquiring the reference sample requires using the tableau simulator. If the vacuous reference sample is actually a result that can be produced by the circuit, under noiseless execution, then specifying this flag has no observable outcome other than improving performance. CAUTION. When the all-zero sample isn't a result that can be produced by the circuit under noiseless execution, specifying this flag will cause incorrect output to be produced. --sweep Specifies a file to read sweep configuration data from. Sweep bits are used to vary whether certain Pauli gates are included in a circuit, or not, from shot to shot. For example, if a circuit contains the instruction "CX sweep[5] 0" then there is an X pauli that is included only in shots where the corresponding sweep data has the bit at index 5 set to True. If `--sweep` is not specified, all sweep bits default to OFF. If `--sweep` is specified, each shot's sweep configuratoin data is read from the specified file. The sweep data's format is specified by `--sweep_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --sweep_format Specifies the data format to use when reading sweep config data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md EXAMPLES Example #1 >>> cat example_circuit.stim X 0 M 0 1 DETECTOR rec[-2] DETECTOR rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] >>> cat example_measure_data.01 00 01 10 11 >>> stim m2d \ --append_observables \ --circuit example_circuit.stim \ --in example_measure_data.01 \ --in_format 01 \ --out_format dets shot D0 shot D0 D1 L2 shot shot D1 L2 ``` ### stim repl ``` NAME stim repl SYNOPSIS stim repl DESCRIPTION Runs stim in interactive read-evaluate-print (REPL) mode. Reads operations from stdin while immediately writing measurement results to stdout. EXAMPLES Example #1 >>> stim repl ... M 0 0 ... X 0 ... M 0 1 ... X 2 3 9 ... M 0 1 2 3 4 5 6 7 8 9 1 0 1 1 0 0 0 0 0 1 ... REPEAT 5 { ... R 0 1 ... H 0 ... CNOT 0 1 ... M 0 1 ... } 00 11 11 00 11 ``` ### stim sample ``` NAME stim sample SYNOPSIS stim sample \ [--in filepath] \ [--out filepath] \ [--out_format 01|b8|r8|ptb64|hits|dets] \ [--seed int] \ [--shots int] \ [--skip_loop_folding] \ [--skip_reference_sample] DESCRIPTION Samples measurements from a circuit. OPTIONS --in Chooses the stim circuit file to read the circuit to sample from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md --out Chooses where to write the sampled data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is in a format specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out_format Specifies the data format to use when writing output data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --seed Makes simulation results PARTIALLY deterministic. The seed integer must be a non-negative 64 bit signed integer. When `--seed` isn't specified, the random number generator is seeded using fresh entropy requested from the operating system. When `--seed #` is set, the exact same simulation results will be produced every time ASSUMING: - the exact same other flags are specified - the exact same version of Stim is being used - the exact same machine architecture is being used (for example, you're not switching from a machine that has AVX2 instructions to one that doesn't). CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary other flags and modes. For example, `--skip_reference_sample` may result in fewer calls the to the random number generator before reported sampling begins. More generally, using the same seed for `stim sample` and `stim detect` will not result in detection events corresponding to the measurement results. --shots Specifies the number of samples to take from the circuit. Defaults to 1. Must be an integer between 0 and a quintillion (10^18). --skip_loop_folding Skips loop folding logic on the reference sample calculation. When this argument is specified, the reference sample (that is used to convert measurement flip data from frame simulations into actual measurement data) is generated by iterating through the entire flattened circuit with no loop detection. Loop folding can enormously improve performance for circuits containing REPEAT blocks with large repeat counts, by detecting periodicity in loops and fast-forwarding across them when computing the reference sample for the circuit. However, in some cases the analysis is not able to detect the periodicity that is present. For example, this has been observed in honeycomb code circuits. When this happens, the folding-capable analysis is slower than simply analyzing the flattened circuit without any specialized loop logic. The `--skip_loop_folding` flag can be used to just analyze the flattened circuit, bypassing this slowdown for circuits such as honeycomb code circuits. By default, loop detection is enabled. Pass this flag to disable it (when appropriate by use case). --skip_reference_sample Asserts the circuit can produce a noiseless sample that is just 0s. When this argument is specified, the reference sample (that is used to convert measurement flip data from frame simulations into actual measurement data) is generated by simply setting all measurements to 0 instead of by performing a stabilizer tableau simulation of the circuit without noise. Skipping the reference sample can significantly improve performance, because acquiring the reference sample requires using the tableau simulator. If the vacuous reference sample is actually a result that can be produced by the circuit, under noiseless execution, then specifying this flag has no observable outcome other than improving performance. CAUTION. When the all-zero sample isn't a result that can be produced by the circuit under noiseless execution, specifying this flag will cause incorrect output to be produced. Specifically, the output measurement bits will be whether each measurement was *FLIPPED* instead of the actual absolute value of the measurement. EXAMPLES Example #1 >>> cat example_circuit.stim H 0 CNOT 0 1 M 0 1 >>> stim sample --shots 5 < example_circuit.stim 00 11 11 00 11 Example #2 >>> cat example_circuit.stim X 2 3 5 M 0 1 2 3 4 5 6 7 8 9 >>> stim sample --in example_circuit.stim --out_format dets shot M2 M3 M5 ``` ### stim sample_dem ``` NAME stim sample_dem SYNOPSIS stim sample_dem \ [--err_out filepath] \ [--err_out_format 01|b8|r8|ptb64|hits|dets] \ [--in filepath] \ [--obs_out filepath] \ [--obs_out_format 01|b8|r8|ptb64|hits|dets] \ [--out filepath] \ [--out_format 01|b8|r8|ptb64|hits|dets] \ [--replay_err_in filepath] \ [--replay_err_in_format 01|b8|r8|ptb64|hits|dets] \ [--seed int] \ [--shots int] DESCRIPTION Samples detection events from a detector error model. Supports recording and replaying the errors that occurred. OPTIONS --err_out Specifies a file to write a record of which errors occurred. For example, the errors that occurred can be analyzed, modified, and then given to `--replay_err_in` to see the effects of changes. The output is in a format specified by `--err_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --err_out_format Specifies the data format to use when writing recorded error data. Irrelevant unless `--err_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --in Chooses the file to read the detector error model to sample from. By default, the detector error model is read from stdin. When `--in $FILEPATH` is specified, the detector error model is instead read from the file at $FILEPATH. The input should be a stim detector error model. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md --obs_out Specifies the file to write observable flip data to. When sampling detection event data, the goal is typically to predict whether or not the logical observables were flipped by using the detection events. This argument specifies where to write that observable flip data. If this argument isn't specified, the observable flip data isn't written to a file. The output is in a format specified by `--obs_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --obs_out_format Specifies the data format to use when writing observable flip data. Irrelevant unless `--obs_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out Chooses where to write the sampled data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is in a format specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --out_format Specifies the data format to use when writing output data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --replay_err_in Specifies a file to read error data to replay from. When replaying error information, errors are no longer sampled randomly but are instead driven by the file data. For example, this file data could come from a previous run that wrote error data using `--err_out`. The input is in a format specified by `--err_in_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --replay_err_in_format Specifies the data format to use when reading error data to replay. Irrelevant unless `--replay_err_in` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md --seed Makes simulation results PARTIALLY deterministic. The seed integer must be a non-negative 64 bit signed integer. When `--seed` isn't specified, the random number generator is seeded using fresh entropy requested from the operating system. When `--seed #` is set, the exact same simulation results will be produced every time ASSUMING: - the exact same other flags are specified - the exact same version of Stim is being used - the exact same machine architecture is being used (for example, you're not switching from a machine that has AVX2 instructions to one that doesn't). CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary other flags and modes. For example, `--skip_reference_sample` may result in fewer calls the to the random number generator before reported sampling begins. More generally, using the same seed for `stim sample` and `stim detect` will not result in detection events corresponding to the measurement results. --shots Specifies the number of samples to take from the detector error model. Defaults to 1. Must be an integer between 0 and a quintillion (10^18). EXAMPLES Example #1 >>> cat example.dem error(0) D0 error(0.5) D1 L0 error(1) D2 D3 >>> stim sample_dem \ --shots 5 \ --in example.dem \ --out dets.01 \ --out_format 01 \ --obs_out obs_flips.01 \ --obs_out_format 01 >>> cat dets.01 0111 0011 0011 0111 0111 >>> cat obs_flips.01 1 0 0 1 1 ``` ================================================ FILE: file_lists/perf_files ================================================ src/stim/circuit/circuit.perf.cc src/stim/gates/gates.perf.cc src/stim/io/measure_record_reader.perf.cc src/stim/main.perf.cc src/stim/main_namespaced.perf.cc src/stim/mem/simd_bit_table.perf.cc src/stim/mem/simd_bits.perf.cc src/stim/mem/simd_word.perf.cc src/stim/mem/sparse_xor_vec.perf.cc src/stim/search/graphlike/algo.perf.cc src/stim/simulators/dem_sampler.perf.cc src/stim/simulators/error_analyzer.perf.cc src/stim/simulators/frame_simulator.perf.cc src/stim/simulators/tableau_simulator.perf.cc src/stim/stabilizers/clifford_string.perf.cc src/stim/stabilizers/pauli_string.perf.cc src/stim/stabilizers/pauli_string_iter.perf.cc src/stim/stabilizers/tableau.perf.cc src/stim/stabilizers/tableau_iter.perf.cc src/stim/util_bot/error_decomp.perf.cc src/stim/util_bot/probability_util.perf.cc src/stim/util_top/reference_sample_tree.perf.cc src/stim/util_top/stabilizers_to_tableau.perf.cc ================================================ FILE: file_lists/pybind_files ================================================ src/stim/circuit/circuit.pybind.cc src/stim/circuit/circuit2.pybind.cc src/stim/circuit/circuit_instruction.pybind.cc src/stim/circuit/circuit_repeat_block.pybind.cc src/stim/circuit/gate_target.pybind.cc src/stim/cmd/command_diagram.pybind.cc src/stim/dem/dem_instruction.pybind.cc src/stim/dem/detector_error_model.pybind.cc src/stim/dem/detector_error_model_repeat_block.pybind.cc src/stim/dem/detector_error_model_target.pybind.cc src/stim/gates/gates.pybind.cc src/stim/io/read_write.pybind.cc src/stim/py/base.pybind.cc src/stim/py/compiled_detector_sampler.pybind.cc src/stim/py/compiled_measurement_sampler.pybind.cc src/stim/py/march.pybind.cc src/stim/py/numpy.pybind.cc src/stim/py/stim.pybind.cc src/stim/simulators/dem_sampler.pybind.cc src/stim/simulators/frame_simulator.pybind.cc src/stim/simulators/matched_error.pybind.cc src/stim/simulators/measurements_to_detection_events.pybind.cc src/stim/simulators/tableau_simulator.pybind.cc src/stim/stabilizers/clifford_string.pybind.cc src/stim/stabilizers/flow.pybind.cc src/stim/stabilizers/pauli_string.pybind.cc src/stim/stabilizers/pauli_string_iter.pybind.cc src/stim/stabilizers/tableau.pybind.cc src/stim/stabilizers/tableau_iter.pybind.cc ================================================ FILE: file_lists/source_files_no_main ================================================ src/stim.cc src/stim/circuit/circuit.cc src/stim/circuit/circuit_instruction.cc src/stim/circuit/gate_decomposition.cc src/stim/circuit/gate_target.cc src/stim/cmd/command_analyze_errors.cc src/stim/cmd/command_convert.cc src/stim/cmd/command_detect.cc src/stim/cmd/command_diagram.cc src/stim/cmd/command_explain_errors.cc src/stim/cmd/command_gen.cc src/stim/cmd/command_help.cc src/stim/cmd/command_m2d.cc src/stim/cmd/command_repl.cc src/stim/cmd/command_sample.cc src/stim/cmd/command_sample_dem.cc src/stim/dem/dem_instruction.cc src/stim/dem/detector_error_model.cc src/stim/diagram/ascii_diagram.cc src/stim/diagram/base64.cc src/stim/diagram/basic_3d_diagram.cc src/stim/diagram/circuit_timeline_helper.cc src/stim/diagram/crumble.cc src/stim/diagram/crumble_data.cc src/stim/diagram/detector_slice/detector_slice_set.cc src/stim/diagram/diagram_util.cc src/stim/diagram/gate_data_3d.cc src/stim/diagram/gate_data_3d_texture_data.cc src/stim/diagram/gate_data_svg.cc src/stim/diagram/gltf.cc src/stim/diagram/graph/match_graph_3d_drawer.cc src/stim/diagram/graph/match_graph_svg_drawer.cc src/stim/diagram/json_obj.cc src/stim/diagram/lattice_map.cc src/stim/diagram/timeline/timeline_3d_drawer.cc src/stim/diagram/timeline/timeline_ascii_drawer.cc src/stim/diagram/timeline/timeline_svg_drawer.cc src/stim/gates/gate_data_annotations.cc src/stim/gates/gate_data_blocks.cc src/stim/gates/gate_data_collapsing.cc src/stim/gates/gate_data_controlled.cc src/stim/gates/gate_data_hada.cc src/stim/gates/gate_data_heralded.cc src/stim/gates/gate_data_noisy.cc src/stim/gates/gate_data_pair_measure.cc src/stim/gates/gate_data_pauli.cc src/stim/gates/gate_data_pauli_product.cc src/stim/gates/gate_data_period_3.cc src/stim/gates/gate_data_period_4.cc src/stim/gates/gate_data_pp.cc src/stim/gates/gate_data_swaps.cc src/stim/gates/gates.cc src/stim/gen/circuit_gen_params.cc src/stim/gen/gen_color_code.cc src/stim/gen/gen_rep_code.cc src/stim/gen/gen_surface_code.cc src/stim/io/measure_record.cc src/stim/io/measure_record_batch_writer.cc src/stim/io/measure_record_writer.cc src/stim/io/raii_file.cc src/stim/io/sparse_shot.cc src/stim/io/stim_data_formats.cc src/stim/main_namespaced.cc src/stim/mem/bit_ref.cc src/stim/mem/simd_util.cc src/stim/mem/simd_word.cc src/stim/mem/sparse_xor_vec.cc src/stim/search/graphlike/algo.cc src/stim/search/graphlike/edge.cc src/stim/search/graphlike/graph.cc src/stim/search/graphlike/node.cc src/stim/search/graphlike/search_state.cc src/stim/search/hyper/algo.cc src/stim/search/hyper/edge.cc src/stim/search/hyper/graph.cc src/stim/search/hyper/node.cc src/stim/search/hyper/search_state.cc src/stim/search/sat/wcnf.cc src/stim/simulators/error_analyzer.cc src/stim/simulators/error_matcher.cc src/stim/simulators/force_streaming.cc src/stim/simulators/graph_simulator.cc src/stim/simulators/matched_error.cc src/stim/simulators/sparse_rev_frame_tracker.cc src/stim/simulators/vector_simulator.cc src/stim/stabilizers/flex_pauli_string.cc src/stim/util_bot/arg_parse.cc src/stim/util_bot/error_decomp.cc src/stim/util_bot/probability_util.cc src/stim/util_top/circuit_inverse_qec.cc src/stim/util_top/circuit_inverse_unitary.cc src/stim/util_top/circuit_to_detecting_regions.cc src/stim/util_top/circuit_vs_amplitudes.cc src/stim/util_top/export_crumble_url.cc src/stim/util_top/export_qasm.cc src/stim/util_top/export_quirk_url.cc src/stim/util_top/has_flow.cc src/stim/util_top/mbqc_decomposition.cc src/stim/util_top/missing_detectors.cc src/stim/util_top/reference_sample_tree.cc src/stim/util_top/simplified_circuit.cc src/stim/util_top/transform_without_feedback.cc ================================================ FILE: file_lists/test_files ================================================ src/stim.test.cc src/stim/circuit/circuit.test.cc src/stim/circuit/circuit_instruction.test.cc src/stim/circuit/gate_decomposition.test.cc src/stim/circuit/gate_target.test.cc src/stim/cmd/command_analyze_errors.test.cc src/stim/cmd/command_convert.test.cc src/stim/cmd/command_detect.test.cc src/stim/cmd/command_diagram.test.cc src/stim/cmd/command_explain_errors.test.cc src/stim/cmd/command_gen.test.cc src/stim/cmd/command_m2d.test.cc src/stim/cmd/command_sample.test.cc src/stim/cmd/command_sample_dem.test.cc src/stim/dem/dem_instruction.test.cc src/stim/dem/detector_error_model.test.cc src/stim/diagram/ascii_diagram.test.cc src/stim/diagram/base64.test.cc src/stim/diagram/coord.test.cc src/stim/diagram/detector_slice/detector_slice_set.test.cc src/stim/diagram/graph/match_graph_3d_drawer.test.cc src/stim/diagram/graph/match_graph_svg_drawer.test.cc src/stim/diagram/json_obj.test.cc src/stim/diagram/timeline/timeline_3d_drawer.test.cc src/stim/diagram/timeline/timeline_ascii_drawer.test.cc src/stim/diagram/timeline/timeline_svg_drawer.test.cc src/stim/gates/gates.test.cc src/stim/gen/circuit_gen_params.test.cc src/stim/gen/gen_color_code.test.cc src/stim/gen/gen_rep_code.test.cc src/stim/gen/gen_surface_code.test.cc src/stim/io/measure_record.test.cc src/stim/io/measure_record_batch.test.cc src/stim/io/measure_record_batch_writer.test.cc src/stim/io/measure_record_reader.test.cc src/stim/io/measure_record_writer.test.cc src/stim/io/sparse_shot.test.cc src/stim/main_namespaced.test.cc src/stim/mem/bit_ref.test.cc src/stim/mem/fixed_cap_vector.test.cc src/stim/mem/monotonic_buffer.test.cc src/stim/mem/simd_bit_table.test.cc src/stim/mem/simd_bits.test.cc src/stim/mem/simd_bits_range_ref.test.cc src/stim/mem/simd_util.test.cc src/stim/mem/simd_word.test.cc src/stim/mem/sparse_xor_vec.test.cc src/stim/search/graphlike/algo.test.cc src/stim/search/graphlike/edge.test.cc src/stim/search/graphlike/graph.test.cc src/stim/search/graphlike/node.test.cc src/stim/search/graphlike/search_state.test.cc src/stim/search/hyper/algo.test.cc src/stim/search/hyper/edge.test.cc src/stim/search/hyper/graph.test.cc src/stim/search/hyper/node.test.cc src/stim/search/hyper/search_state.test.cc src/stim/search/sat/wcnf.test.cc src/stim/simulators/dem_sampler.test.cc src/stim/simulators/error_analyzer.test.cc src/stim/simulators/error_matcher.test.cc src/stim/simulators/frame_simulator.test.cc src/stim/simulators/frame_simulator_util.test.cc src/stim/simulators/graph_simulator.test.cc src/stim/simulators/matched_error.test.cc src/stim/simulators/measurements_to_detection_events.test.cc src/stim/simulators/sparse_rev_frame_tracker.test.cc src/stim/simulators/tableau_simulator.test.cc src/stim/simulators/vector_simulator.test.cc src/stim/stabilizers/clifford_string.test.cc src/stim/stabilizers/flex_pauli_string.test.cc src/stim/stabilizers/flow.test.cc src/stim/stabilizers/pauli_string.test.cc src/stim/stabilizers/pauli_string_iter.test.cc src/stim/stabilizers/pauli_string_ref.test.cc src/stim/stabilizers/tableau.test.cc src/stim/stabilizers/tableau_iter.test.cc src/stim/util_bot/arg_parse.test.cc src/stim/util_bot/error_decomp.test.cc src/stim/util_bot/probability_util.test.cc src/stim/util_bot/str_util.test.cc src/stim/util_bot/test_util.test.cc src/stim/util_bot/twiddle.test.cc src/stim/util_top/circuit_flow_generators.test.cc src/stim/util_top/circuit_inverse_qec.test.cc src/stim/util_top/circuit_inverse_unitary.test.cc src/stim/util_top/circuit_to_dem.test.cc src/stim/util_top/circuit_to_detecting_regions.test.cc src/stim/util_top/circuit_vs_amplitudes.test.cc src/stim/util_top/circuit_vs_tableau.test.cc src/stim/util_top/count_determined_measurements.test.cc src/stim/util_top/export_crumble_url.test.cc src/stim/util_top/export_qasm.test.cc src/stim/util_top/export_quirk_url.test.cc src/stim/util_top/has_flow.test.cc src/stim/util_top/mbqc_decomposition.test.cc src/stim/util_top/missing_detectors.test.cc src/stim/util_top/reference_sample_tree.test.cc src/stim/util_top/simplified_circuit.test.cc src/stim/util_top/stabilizers_to_tableau.test.cc src/stim/util_top/stabilizers_vs_amplitudes.test.cc src/stim/util_top/transform_without_feedback.test.cc src/stim_included_twice.test.cc ================================================ FILE: glue/cirq/README.md ================================================ # stimcirq Tools for interop between the quantum python package `cirq` and the stabilizer simulator `stim`. Includes: - `stimcirq.StimSampler` - `stimcirq.cirq_circuit_to_stim_circuit` - `stimcirq.stim_circuit_to_cirq_circuit` # Examples Sampling with `stimcirq.Sampler`: ```python import cirq a, b = cirq.LineQubit.range(2) c = cirq.Circuit( cirq.H(a), cirq.CNOT(a, b), cirq.measure(a, key="a"), cirq.measure(b, key="b"), ) import stimcirq sampler = stimcirq.StimSampler() result = sampler.run(c, repetitions=30) print(result) # prints something like: # a=000010100101000011001100110011 # b=000010100101000011001100110011 ``` # API Reference ## `stimcirq.StimSampler` ``` A `cirq.Sampler` backed by `stim`. Supports circuits that contain Clifford operations, measurement operations, reset operations, and noise operations that can be decomposed into probabilistic Pauli operations. Unknown operations are supported as long as they provide a decomposition into supported operations via `cirq.decompose` (i.e. via a `_decompose_` method). Note that batch sampling is significantly faster (as in potentially thousands of times faster) than individual sampling, because it amortizes the cost of parsing and analyzing the circuit. ``` ## `stimcirq.stim_circuit_to_cirq_circuit(circuit: stim.Circuit) -> cirq.Circuit` ``` Converts a stim circuit into an equivalent cirq circuit. Qubit indices are turned into cirq.LineQubit instances. Measurements are keyed by their ordering (e.g. the first measurement is keyed "0", the second is keyed "1", etc). Not all circuits can be converted: - ELSE_CORRELATED_ERROR instructions are not supported. Not all circuits can be converted with perfect 1:1 fidelity: - DETECTOR annotations are discarded. - OBSERVABLE_INCLUDE annotations are discarded. Args: circuit: The stim circuit to convert into a cirq circuit. Returns: The converted circuit. Examples: >>> import stimcirq >>> import stim >>> print(stimcirq.stim_circuit_to_cirq_circuit(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(0.25) 0 ... TICK ... M !1 0 ... '''))) 0: ───H───@───X[prob=0.25]───M('1')──── │ 1: ───────X──────────────────!M('0')─── ``` ## `def cirq_circuit_to_stim_circuit(circuit: cirq.Circuit, *, qubit_to_index_dict: Optional[Dict[cirq.Qid, int]] = None) -> stim.Circuit` ``` Converts a cirq circuit into an equivalent stim circuit. Not all circuits can be converted. In order for a circuit to be convertible, all of its operations must be convertible. An operation is convertible if: - It is a stabilizer gate or probabilistic Pauli gate from cirq - cirq.H - cirq.S - cirq.X - cirq.X**0.5 - cirq.CNOT - cirq.ResetChannel() - cirq.X.with_probability(p) - cirq.DepolarizingChannel(p, n_qubits=1 or 2) - etc - Or it has a _decompose_ method that yields convertible operations. - Or it has a correctly implemented _stim_conversion_ method. Args: circuit: The circuit to convert. qubit_to_index_dict: Optional. Which integer each qubit should get mapped to. If not specified, defaults to indexing qubits in the circuit in sorted order. Returns: The converted circuit. Examples: >>> import cirq, stimcirq >>> a = cirq.NamedQubit("zero") >>> b = cirq.NamedQubit("two") >>> stimcirq.cirq_circuit_to_stim_circuit(cirq.Circuit( ... cirq.Moment(cirq.H(a)), ... cirq.Moment(cirq.CNOT(a, b)), ... cirq.Moment( ... cirq.X(a).with_probability(0.25), ... cirq.Z(b).with_probability(0.25), ... ), ... cirq.Moment(), ... cirq.Moment(), ... cirq.Moment(cirq.DepolarizingChannel(0.125, n_qubits=2).on(b, a)), ... cirq.Moment(cirq.measure(a, b)), ... ), qubit_to_index_dict={a: 0, b: 2}) stim.Circuit(''' H 0 TICK CX 0 2 TICK X_ERROR(0.25) 0 Z_ERROR(0.25) 2 TICK TICK TICK DEPOLARIZE2(0.125) 2 0 TICK M 0 2 TICK ''') Here is an example of a _stim_conversion_ method: def _stim_conversion_( self, # The stim circuit being built. Add onto it. edit_circuit: stim.Circuit, # Metadata about measurement groupings needed by stimcirq.StimSampler. # If your gate contains a measurement, it has to append how many qubits # that measurement measures (and its key) into this list. edit_measurement_key_lengths: List[Tuple[str, int]], # The indices of qubits the gate is operating on. targets: List[int], # Forward compatibility with future arguments. **kwargs): edit_circuit.append_operation("H", targets) ``` ================================================ FILE: glue/cirq/setup.py ================================================ # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from setuptools import setup with open('README.md', encoding='UTF-8') as f: long_description = f.read() __version__ = '1.16.dev0' setup( name='stimcirq', version=__version__, author='Craig Gidney', author_email='craig.gidney@gmail.com', url='https://github.com/quantumlib/stim', license='Apache 2', packages=['stimcirq'], description='Implements a cirq.Sampler backed by stim.', long_description=long_description, long_description_content_type='text/markdown', python_requires='>=3.6.0', data_files=['README.md'], install_requires=['stim', 'cirq-core'], tests_require=['pytest', 'python3-distutils'], ) ================================================ FILE: glue/cirq/stimcirq/__init__.py ================================================ __version__ = '1.16.dev0' from ._cirq_to_stim import cirq_circuit_to_stim_circuit from ._cx_swap_gate import CXSwapGate from ._cz_swap_gate import CZSwapGate from ._det_annotation import DetAnnotation from ._obs_annotation import CumulativeObservableAnnotation from ._shift_coords_annotation import ShiftCoordsAnnotation from ._stim_sampler import StimSampler from ._stim_to_cirq import ( MeasureAndOrResetGate, stim_circuit_to_cirq_circuit, ) from ._sweep_pauli import SweepPauli from ._two_qubit_asymmetric_depolarize import TwoQubitAsymmetricDepolarizingChannel from ._i_error_gate import IErrorGate from ._ii_error_gate import IIErrorGate from ._ii_gate import IIGate JSON_RESOLVERS_DICT = { "CumulativeObservableAnnotation": CumulativeObservableAnnotation, "DetAnnotation": DetAnnotation, "MeasureAndOrResetGate": MeasureAndOrResetGate, "ShiftCoordsAnnotation": ShiftCoordsAnnotation, "SweepPauli": SweepPauli, "TwoQubitAsymmetricDepolarizingChannel": TwoQubitAsymmetricDepolarizingChannel, "CXSwapGate": CXSwapGate, "CZSwapGate": CZSwapGate, "IIGate": IIGate, "IIErrorGate": IIErrorGate, "IErrorGate": IErrorGate, } JSON_RESOLVER = JSON_RESOLVERS_DICT.get ================================================ FILE: glue/cirq/stimcirq/_cirq_to_stim.py ================================================ import functools import itertools import math from typing import Callable, cast, Dict, Iterable, List, Optional, Sequence, Tuple, Type import cirq import stim from ._i_error_gate import IErrorGate from ._ii_error_gate import IIErrorGate from ._ii_gate import IIGate def _forward_single_str_tag(op: cirq.CircuitOperation) -> str: tags = [tag for tag in op.tags if isinstance(tag, str)] if len(tags) == 1: return tags[0] return "" def cirq_circuit_to_stim_circuit( circuit: cirq.AbstractCircuit, *, qubit_to_index_dict: Optional[Dict[cirq.Qid, int]] = None, tag_func: Callable[[cirq.Operation], str] = _forward_single_str_tag, ) -> stim.Circuit: """Converts a cirq circuit into an equivalent stim circuit. Not all circuits can be converted. In order for a circuit to be convertible, all of its operations must be convertible. An operation is convertible if: - It is a stabilizer gate or probabilistic Pauli gate from cirq - cirq.H - cirq.S - cirq.X - cirq.X**0.5 - cirq.CNOT - cirq.ResetChannel() - cirq.X.with_probability(p) - cirq.DepolarizingChannel(p, n_qubits=1 or 2) - etc - Or it has a _decompose_ method that yields convertible operations. - Or it has a correctly implemented _stim_conversion_ method. Args: circuit: The circuit to convert. qubit_to_index_dict: Optional. Which integer each qubit should get mapped to. If not specified, defaults to indexing qubits in the circuit in sorted order. tag_func: Controls the tag attached to the stim instructions the cirq operation turns into. If not specified, defaults to checking for string tags on the circuit operation and if there is exactly one string tag then using that tag (otherwise not specifying a tag). Returns: The converted circuit. Examples: >>> import cirq, stimcirq >>> a = cirq.NamedQubit("zero") >>> b = cirq.NamedQubit("two") >>> stimcirq.cirq_circuit_to_stim_circuit(cirq.Circuit( ... cirq.Moment(cirq.H(a)), ... cirq.Moment(cirq.CNOT(a, b)), ... cirq.Moment( ... cirq.X(a).with_probability(0.25), ... cirq.Z(b).with_probability(0.25), ... ), ... cirq.Moment(), ... cirq.Moment(), ... cirq.Moment(cirq.DepolarizingChannel(0.125, n_qubits=2).on(b, a)), ... cirq.Moment(cirq.measure(a, b)), ... ), qubit_to_index_dict={a: 0, b: 2}) stim.Circuit(''' H 0 TICK CX 0 2 TICK X_ERROR(0.25) 0 Z_ERROR(0.25) 2 TICK TICK TICK DEPOLARIZE2(0.125) 2 0 TICK M 0 2 TICK ''') Here is an example of a _stim_conversion_ method: def _stim_conversion_( self, # The stim circuit being built. Add onto it. edit_circuit: stim.Circuit, # Metadata about measurement groupings needed by stimcirq.StimSampler. # If your gate contains a measurement, it has to append how many qubits # that measurement measures (and its key) into this list. edit_measurement_key_lengths: List[Tuple[str, int]], # The indices of qubits the gate is operating on. targets: List[int], # A custom string associated with the operation, which can be tagged # onto any operations appended to the stim circuit. tag: str, # Forward compatibility with future arguments. **kwargs): edit_circuit.append_operation("H", targets) """ return cirq_circuit_to_stim_data(circuit, q2i=qubit_to_index_dict, flatten=False, tag_func=tag_func)[0] def cirq_circuit_to_stim_data( circuit: cirq.AbstractCircuit, *, q2i: Optional[Dict[cirq.Qid, int]] = None, flatten: bool = False, tag_func: Callable[[cirq.Operation], str] = _forward_single_str_tag, ) -> Tuple[stim.Circuit, List[Tuple[str, int]]]: """Converts a Cirq circuit into a Stim circuit and also metadata about where measurements go.""" if q2i is None: q2i = {q: i for i, q in enumerate(sorted(circuit.all_qubits()))} helper = CirqToStimHelper(tag_func=tag_func) helper.q2i = q2i helper.flatten = flatten for q in sorted(circuit.all_qubits()): if isinstance(q, cirq.LineQubit): i = q2i[q] if i != q.x: helper.out.append_operation("QUBIT_COORDS", [i], [q.x]) elif isinstance(q, cirq.GridQubit): helper.out.append_operation("QUBIT_COORDS", [q2i[q]], [q.row, q.col]) helper.process_moments(circuit) return helper.out, helper.key_out StimTypeHandler = Callable[[stim.Circuit, cirq.Gate, List[int], str], None] @functools.lru_cache(maxsize=1) def gate_to_stim_append_func() -> Dict[cirq.Gate, Callable[[stim.Circuit, List[int], str], None]]: """A dictionary mapping specific gate instances to stim circuit appending functions.""" x = (cirq.X, False) y = (cirq.Y, False) z = (cirq.Z, False) nx = (cirq.X, True) ny = (cirq.Y, True) nz = (cirq.Z, True) def do_nothing(_gates, _targets, tag): pass def use( *gates: str, individuals: Sequence[Tuple[str, int]] = () ) -> Callable[[stim.Circuit, List[int], str], None]: if len(gates) == 1 and not individuals: (g,) = gates return lambda c, t, tag: c.append(g, t, tag=tag) if not individuals: def do(c, t, tag: str): for g in gates: c.append(g, t, tag=tag) else: def do(c, t, tag: str): for g in gates: c.append(g, t, tag=tag) for g, k in individuals: c.append(g, [t[k]], tag=tag) return do sqcg = cirq.SingleQubitCliffordGate.from_xz_map paulis = cast(List[cirq.Pauli], [cirq.X, cirq.Y, cirq.Z]) return { cirq.ResetChannel(): use("R"), # Identities. cirq.I: use("I"), cirq.H ** 0: do_nothing, cirq.X ** 0: do_nothing, cirq.Y ** 0: do_nothing, cirq.Z ** 0: do_nothing, cirq.ISWAP ** 0: do_nothing, cirq.SWAP ** 0: do_nothing, # Common named gates. cirq.H: use("H"), cirq.X: use("X"), cirq.Y: use("Y"), cirq.Z: use("Z"), cirq.X ** 0.5: use("SQRT_X"), cirq.X ** -0.5: use("SQRT_X_DAG"), cirq.Y ** 0.5: use("SQRT_Y"), cirq.Y ** -0.5: use("SQRT_Y_DAG"), cirq.Z ** 0.5: use("SQRT_Z"), cirq.Z ** -0.5: use("SQRT_Z_DAG"), cirq.CNOT: use("CNOT"), cirq.CZ: use("CZ"), cirq.ISWAP: use("ISWAP"), cirq.ISWAP ** -1: use("ISWAP_DAG"), cirq.ISWAP ** 2: use("Z"), cirq.SWAP: use("SWAP"), cirq.X.controlled(1): use("CX"), cirq.Y.controlled(1): use("CY"), cirq.Z.controlled(1): use("CZ"), cirq.XX ** 0.5: use("SQRT_XX"), cirq.YY ** 0.5: use("SQRT_YY"), cirq.ZZ ** 0.5: use("SQRT_ZZ"), cirq.XX ** -0.5: use("SQRT_XX_DAG"), cirq.YY ** -0.5: use("SQRT_YY_DAG"), cirq.ZZ ** -0.5: use("SQRT_ZZ_DAG"), # All 24 cirq.SingleQubitCliffordGate instances. sqcg(x, y): use("SQRT_X_DAG"), sqcg(x, ny): use("SQRT_X"), sqcg(nx, y): use("H_YZ"), sqcg(nx, ny): use("H_NYZ"), sqcg(x, z): do_nothing, sqcg(x, nz): use("X"), sqcg(nx, z): use("Z"), sqcg(nx, nz): use("Y"), sqcg(y, x): use("C_XYZ"), sqcg(y, nx): use("C_XYNZ"), sqcg(ny, x): use("C_XNYZ"), sqcg(ny, nx): use("C_NXYZ"), sqcg(y, z): use("S"), sqcg(y, nz): use("H_XY"), sqcg(ny, z): use("S_DAG"), sqcg(ny, nz): use("H_NXY"), sqcg(z, x): use("H"), sqcg(z, nx): use("SQRT_Y_DAG"), sqcg(nz, x): use("SQRT_Y"), sqcg(nz, nx): use("H_NXZ"), sqcg(z, y): use("C_ZYX"), sqcg(z, ny): use("C_ZNYX"), sqcg(nz, y): use("C_ZYNX"), sqcg(nz, ny): use("C_NZYX"), # All 36 cirq.PauliInteractionGate instances. **{ cirq.PauliInteractionGate(p0, s0, p1, s1): use( f"{p0}C{p1}", individuals=[(str(p1), 1)] * s0 + [(str(p0), 0)] * s1 ) for p0, s0, p1, s1 in itertools.product(paulis, [False, True], repeat=2) }, } @functools.lru_cache() def gate_type_to_stim_append_func() -> Dict[Type[cirq.Gate], StimTypeHandler]: """A dictionary mapping specific gate types to stim circuit appending functions.""" return { cirq.ControlledGate: cast(StimTypeHandler, _stim_append_controlled_gate), cirq.DensePauliString: cast(StimTypeHandler, _stim_append_dense_pauli_string_gate), cirq.MutableDensePauliString: cast(StimTypeHandler, _stim_append_dense_pauli_string_gate), cirq.AsymmetricDepolarizingChannel: cast( StimTypeHandler, _stim_append_asymmetric_depolarizing_channel ), cirq.BitFlipChannel: lambda c, g, t, tag: c.append( "X_ERROR", t, cast(cirq.BitFlipChannel, g).p, tag=tag ), cirq.PhaseFlipChannel: lambda c, g, t, tag: c.append( "Z_ERROR", t, cast(cirq.PhaseFlipChannel, g).p, tag=tag ), cirq.PhaseDampingChannel: lambda c, g, t, tag: c.append( "Z_ERROR", t, 0.5 - math.sqrt(1 - cast(cirq.PhaseDampingChannel, g).gamma) / 2, tag=tag ), cirq.RandomGateChannel: cast(StimTypeHandler, _stim_append_random_gate_channel), cirq.DepolarizingChannel: cast(StimTypeHandler, _stim_append_depolarizing_channel), } def _stim_append_measurement_gate( circuit: stim.Circuit, gate: cirq.MeasurementGate, targets: List[int], tag: str ): for i, b in enumerate(gate.invert_mask): if b: targets[i] = stim.target_inv(targets[i]) circuit.append("M", targets, tag=tag) def _stim_append_pauli_measurement_gate( circuit: stim.Circuit, gate: cirq.PauliMeasurementGate, targets: List[int], tag: str ): obs: cirq.DensePauliString = gate.observable() # Convert to stim Pauli product targets. if len(targets) == 0: raise NotImplementedError(f"len(targets)={len(targets)} == 0") new_targets = [] for t, p in zip(targets, obs.pauli_mask): if p == 1: t = stim.target_x(t, invert=not new_targets and obs.coefficient == -1) elif p == 2: t = stim.target_y(t, invert=not new_targets and obs.coefficient == -1) elif p == 3: t = stim.target_z(t, invert=not new_targets and obs.coefficient == -1) else: raise NotImplementedError(f"obs={obs!r}") new_targets.append(t) new_targets.append(stim.target_combiner()) new_targets.pop() # Inverted result? if obs.coefficient != 1 and obs.coefficient != -1: raise NotImplementedError(f"obs.coefficient={obs.coefficient!r} not in [1, -1]") circuit.append("MPP", new_targets, tag=tag) def _stim_append_spp_gate( circuit: stim.Circuit, gate: cirq.PauliStringPhasorGate, targets: List[int], tag: str ): obs: cirq.DensePauliString = gate.dense_pauli_string a = gate.exponent_neg b = gate.exponent_pos d = (a - b) % 2 if obs.coefficient == -1: d += 1 d %= 2 if d != 0.5 and d != 1.5: return False new_targets = [] for t, p in zip(targets, obs.pauli_mask): if p: new_targets.append(stim.target_pauli(t, p)) new_targets.append(stim.target_combiner()) if len(new_targets) == 0: return False new_targets.pop() circuit.append("SPP" if d == 0.5 else "SPP_DAG", new_targets, tag=tag) return True def _stim_append_dense_pauli_string_gate( c: stim.Circuit, g: cirq.BaseDensePauliString, t: List[int], tag: str ): gates = [None, "X", "Y", "Z"] for p, k in zip(g.pauli_mask, t): if p: c.append(gates[p], [k], tag=tag) def _stim_append_asymmetric_depolarizing_channel( c: stim.Circuit, g: cirq.AsymmetricDepolarizingChannel, t: List[int], tag: str ): if cirq.num_qubits(g) == 1: c.append("PAULI_CHANNEL_1", t, [g.p_x, g.p_y, g.p_z], tag=tag) elif cirq.num_qubits(g) == 2: c.append( "PAULI_CHANNEL_2", t, [ g.error_probabilities.get('IX', 0), g.error_probabilities.get('IY', 0), g.error_probabilities.get('IZ', 0), g.error_probabilities.get('XI', 0), g.error_probabilities.get('XX', 0), g.error_probabilities.get('XY', 0), g.error_probabilities.get('XZ', 0), g.error_probabilities.get('YI', 0), g.error_probabilities.get('YX', 0), g.error_probabilities.get('YY', 0), g.error_probabilities.get('YZ', 0), g.error_probabilities.get('ZI', 0), g.error_probabilities.get('ZX', 0), g.error_probabilities.get('ZY', 0), g.error_probabilities.get('ZZ', 0), ], tag=tag, ) else: raise NotImplementedError(f'cirq-to-stim gate {g!r}') def _stim_append_depolarizing_channel(c: stim.Circuit, g: cirq.DepolarizingChannel, t: List[int], tag: str): if g.num_qubits() == 1: c.append("DEPOLARIZE1", t, g.p, tag=tag) elif g.num_qubits() == 2: c.append("DEPOLARIZE2", t, g.p, tag=tag) else: raise TypeError(f"Don't know how to turn {g!r} into Stim operations.") def _stim_append_controlled_gate(c: stim.Circuit, g: cirq.ControlledGate, t: List[int], tag: str): if isinstance(g.sub_gate, cirq.BaseDensePauliString) and g.num_controls() == 1: gates = [None, "CX", "CY", "CZ"] for p, k in zip(g.sub_gate.pauli_mask, t[1:]): if p: c.append(gates[p], [t[0], k], tag=tag) if g.sub_gate.coefficient == 1j: c.append("S", t[:1], tag=tag) elif g.sub_gate.coefficient == -1: c.append("Z", t[:1], tag=tag) elif g.sub_gate.coefficient == -1j: c.append("S_DAG", t[:1], tag=tag) elif g.sub_gate.coefficient == 1: pass else: raise TypeError(f"Phase kickback from {g!r} isn't a stabilizer operation.") return raise TypeError(f"Don't know how to turn controlled gate {g!r} into Stim operations.") def _stim_append_random_gate_channel(c: stim.Circuit, g: cirq.RandomGateChannel, t: List[int], tag: str): if g.sub_gate in [cirq.X, cirq.Y, cirq.Z, cirq.I]: c.append(f"{g.sub_gate}_ERROR", t, g.probability, tag=tag) elif isinstance(g.sub_gate, IIGate): c.append(f"II_ERROR", t, g.probability, tag=tag) elif isinstance(g.sub_gate, cirq.DensePauliString): target_p = [None, stim.target_x, stim.target_y, stim.target_z] pauli_targets = [target_p[p](t) for t, p in zip(t, g.sub_gate.pauli_mask) if p] c.append(f"CORRELATED_ERROR", pauli_targets, g.probability, tag=tag) else: raise NotImplementedError( f"Don't know how to turn probabilistic {g!r} into Stim operations." ) class CirqToStimHelper: def __init__(self, tag_func: Callable[[cirq.Operation], str]): self.key_out: List[Tuple[str, int]] = [] self.out = stim.Circuit() self.q2i = {} self.have_seen_loop = False self.flatten = False self.tag_func = tag_func def process_circuit_operation_into_repeat_block(self, op: cirq.CircuitOperation, tag: str) -> None: if self.flatten or op.repetitions == 1: moments = cirq.unroll_circuit_op(cirq.Circuit(op), deep=False, tags_to_check=None).moments self.process_moments(moments) self.out = self.out[:-1] # Remove a trailing TICK (to avoid double TICK) return child = CirqToStimHelper(tag_func=self.tag_func) child.key_out = self.key_out child.q2i = self.q2i child.have_seen_loop = True self.have_seen_loop = True child.process_moments(op.transform_qubits(lambda q: op.qubit_map.get(q, q)).circuit) self.out.append(stim.CircuitRepeatBlock(op.repetitions, child.out, tag=tag)) def process_operations(self, operations: Iterable[cirq.Operation]) -> None: g2f = gate_to_stim_append_func() t2f = gate_type_to_stim_append_func() for op in operations: assert isinstance(op, cirq.Operation) tag = self.tag_func(op) op = op.untagged gate = op.gate targets = [self.q2i[q] for q in op.qubits] custom_method = getattr( op, '_stim_conversion_', getattr(gate, '_stim_conversion_', None) ) if custom_method is not None: custom_method( dont_forget_your_star_star_kwargs=True, edit_circuit=self.out, edit_measurement_key_lengths=self.key_out, targets=targets, have_seen_loop=self.have_seen_loop, tag=tag, ) continue if isinstance(op, cirq.CircuitOperation): self.process_circuit_operation_into_repeat_block(op, tag=tag) continue # Special case measurement, because of its metadata. if isinstance(gate, cirq.PauliStringPhasorGate): if _stim_append_spp_gate(self.out, gate, targets, tag=tag): continue if isinstance(gate, cirq.PauliMeasurementGate): self.key_out.append((gate.key, len(targets))) _stim_append_pauli_measurement_gate(self.out, gate, targets, tag=tag) continue if isinstance(gate, cirq.MeasurementGate): self.key_out.append((gate.key, len(targets))) _stim_append_measurement_gate(self.out, gate, targets, tag=tag) continue # Look for recognized gate values like cirq.H. val_append_func = g2f.get(gate) if val_append_func is not None: val_append_func(self.out, targets, tag=tag) continue # Look for recognized gate types like cirq.DepolarizingChannel. type_append_func = t2f.get(type(gate)) if type_append_func is not None: type_append_func(self.out, gate, targets, tag=tag) continue # Ask unrecognized operations to decompose themselves into simpler operations. try: self.process_operations(cirq.decompose_once(op)) except TypeError as ex: raise TypeError( f"Don't know how to translate {op!r} into stim gates.\n" f"- It doesn't have a _decompose_ method that returns stim-compatible operations.\n" f"- It doesn't have a _stim_conversion_ method.\n" ) from ex def process_moment(self, moment: cirq.Moment): length_before = len(self.out) self.process_operations(moment) # Append a TICK, unless it was already handled by an internal REPEAT block. if length_before == len(self.out) or not isinstance(self.out[-1], stim.CircuitRepeatBlock): self.out.append("TICK", []) def process_moments(self, moments: Iterable[cirq.Moment]): for moment in moments: self.process_moment(moment) ================================================ FILE: glue/cirq/stimcirq/_cirq_to_stim_test.py ================================================ import itertools from typing import Dict, List, Sequence, Tuple, Union import cirq import numpy as np import pytest import stim import stimcirq from stimcirq._cirq_to_stim import cirq_circuit_to_stim_data, gate_to_stim_append_func def solve_tableau(gate: cirq.Gate) -> Dict[cirq.PauliString, cirq.PauliString]: """Computes a stabilizer tableau for the given gate.""" result = {} n = gate.num_qubits() qs = cirq.LineQubit.range(n) for inp in [g(q) for g in [cirq.X, cirq.Z] for q in qs]: # Use superdense coding to extract X and Z flips from the generator conjugated by the gate. c = cirq.Circuit( cirq.H.on_each(qs), [cirq.CNOT(q, q + n) for q in qs], gate(*qs) ** -1, inp, gate(*qs), [cirq.CNOT(q, q + n) for q in qs], cirq.H.on_each(qs), [cirq.measure(q, q + n, key=str(q)) for q in qs], ) # Extract X/Y/Z data from sample result (which should be deterministic). s = cirq.Simulator().sample(c) out: cirq.PauliString = cirq.PauliString({q: "IXZY"[s[str(q)][0]] for q in qs}) # Use phase kickback to determine the sign of the output stabilizer. sign = cirq.NamedQubit('a') c = cirq.Circuit( cirq.H(sign), inp.controlled_by(sign), gate(*qs), out.controlled_by(sign), cirq.H(sign), cirq.measure(sign, key='sign'), ) if cirq.Simulator().sample(c)['sign'][0]: out *= -1 result[inp] = out return result def test_solve_tableau(): a, b = cirq.LineQubit.range(2) assert solve_tableau(cirq.I) == {cirq.X(a): cirq.X(a), cirq.Z(a): cirq.Z(a)} assert solve_tableau(cirq.S) == {cirq.X(a): cirq.Y(a), cirq.Z(a): cirq.Z(a)} assert solve_tableau(cirq.S ** -1) == {cirq.X(a): -cirq.Y(a), cirq.Z(a): cirq.Z(a)} assert solve_tableau(cirq.H) == {cirq.X(a): cirq.Z(a), cirq.Z(a): cirq.X(a)} assert solve_tableau( cirq.SingleQubitCliffordGate.from_xz_map((cirq.Y, False), (cirq.X, False)) ) == {cirq.X(a): cirq.Y(a), cirq.Z(a): cirq.X(a)} assert solve_tableau(cirq.CZ) == { cirq.X(a): cirq.X(a) * cirq.Z(b), cirq.Z(a): cirq.Z(a), cirq.X(b): cirq.Z(a) * cirq.X(b), cirq.Z(b): cirq.Z(b), } def assert_unitary_gate_converts_correctly(gate: cirq.Gate): n = gate.num_qubits() for pre, post in solve_tableau(gate).items(): # Create a circuit that measures pre before the gate times post after the gate. # If the gate is translated correctly, the measurement will always be zero. c = stim.Circuit() c.append("H", range(n)) for i in range(n): c.append("CNOT", [i, i + n]) c.append("H", [2 * n]) for q, p in pre.items(): c.append(f"C{p}", [2 * n, q.x]) qs = cirq.LineQubit.range(n) conv_gate, _ = cirq_circuit_to_stim_data(cirq.Circuit(gate(*qs)), q2i={q: q.x for q in qs}) c += conv_gate for q, p in post.items(): c.append(f"C{p}", [2 * n, q.x]) if post.coefficient == -1: c.append("Z", [2 * n]) c.append("H", [2 * n]) c.append("M", [2 * n]) correct = np.count_nonzero(c.compile_sampler().sample_bit_packed(10)) == 0 assert correct, f"{gate!r} failed to turn {pre} into {post}.\nConverted to:\n{conv_gate}\n" @pytest.mark.parametrize("gate", gate_to_stim_append_func().keys()) def test_unitary_gate_conversions(gate: cirq.Gate): # Note: filtering in the parametrize annotation causes a false 'gate undefined' lint error. if cirq.has_unitary(gate): assert_unitary_gate_converts_correctly(gate) def test_more_unitary_gate_conversions(): for p in [1, 1j, -1, -1j]: assert_unitary_gate_converts_correctly(p * cirq.DensePauliString("IXYZ")) assert_unitary_gate_converts_correctly((p * cirq.DensePauliString("IXYZ")).controlled(1)) a, b = cirq.LineQubit.range(2) c, _ = cirq_circuit_to_stim_data( cirq.Circuit(cirq.H(a), cirq.CNOT(a, b), cirq.measure(a, b), cirq.reset(a)) ) assert ( str(c).strip() == """ H 0 TICK CX 0 1 TICK M 0 1 TICK R 0 TICK """.strip() ) ROUND_TRIP_NOISY_GATES = [ cirq.BitFlipChannel(0.1), cirq.BitFlipChannel(0.2), cirq.PhaseFlipChannel(0.1), cirq.PhaseFlipChannel(0.2), cirq.PhaseDampingChannel(0.1), cirq.PhaseDampingChannel(0.2), cirq.X.with_probability(0.1), cirq.X.with_probability(0.2), cirq.Y.with_probability(0.1), cirq.Y.with_probability(0.2), cirq.Z.with_probability(0.1), cirq.Z.with_probability(0.2), cirq.DepolarizingChannel(0.1), cirq.DepolarizingChannel(0.2), cirq.DepolarizingChannel(0.1, n_qubits=2), cirq.DepolarizingChannel(0.2, n_qubits=2), cirq.AsymmetricDepolarizingChannel(p_x=0, p_y=0, p_z=0), cirq.AsymmetricDepolarizingChannel(p_x=0.2, p_y=0.1, p_z=0.3), cirq.AsymmetricDepolarizingChannel(p_x=0.1, p_y=0, p_z=0), cirq.AsymmetricDepolarizingChannel(p_x=0, p_y=0.1, p_z=0), cirq.AsymmetricDepolarizingChannel(p_x=0, p_y=0, p_z=0.1), *[ cirq.asymmetric_depolarize(error_probabilities={a + b: 0.1}) for a, b in list(itertools.product('IXYZ', repeat=2))[1:] ], cirq.asymmetric_depolarize(error_probabilities={'IX': 0.125, 'ZY': 0.375}), ] @pytest.mark.parametrize("gate", ROUND_TRIP_NOISY_GATES) def test_frame_simulator_sampling_noisy_gates_agrees_with_cirq_data(gate: cirq.Gate): # Create test circuit that uses superdense coding to quantify arbitrary Pauli error mixtures. n = cirq.num_qubits(gate) qs = cirq.LineQubit.range(n) circuit = cirq.Circuit( cirq.H.on_each(qs), [cirq.CNOT(q, q + n) for q in qs], gate(*qs), [cirq.CNOT(q, q + n) for q in qs], cirq.H.on_each(qs), ) expected_rates = cirq.final_density_matrix(circuit).diagonal().real # Convert test circuit to Stim and sample from it. stim_circuit, _ = cirq_circuit_to_stim_data( circuit + cirq.measure(*sorted(circuit.all_qubits())[::-1]) ) sample_count = 10000 samples = stim_circuit.compile_sampler().sample_bit_packed(sample_count).flat unique, counts = np.unique(samples, return_counts=True) # Compare sample rates to expected rates. for value, count in zip(unique, counts): expected_rate = expected_rates[value] actual_rate = count / sample_count allowed_variation = 5 * (expected_rate * (1 - expected_rate) / sample_count) ** 0.5 if not 0 <= expected_rate - allowed_variation <= 1: raise ValueError("Not enough samples to bound results away from extremes.") assert abs(expected_rate - actual_rate) <= allowed_variation, ( f"Sample rate {actual_rate} is over 5 standard deviations away from {expected_rate}.\n" f"Gate: {gate}\n" f"Test circuit:\n{circuit}\n" f"Converted circuit:\n{stim_circuit}\n" ) @pytest.mark.parametrize("gate", ROUND_TRIP_NOISY_GATES) def test_tableau_simulator_sampling_noisy_gates_agrees_with_cirq_data(gate: cirq.Gate): # Technically this be a test of the `stim` package itself, but it's so convenient to compare to cirq. # Create test circuit that uses superdense coding to quantify arbitrary Pauli error mixtures. n = cirq.num_qubits(gate) qs = cirq.LineQubit.range(n) circuit = cirq.Circuit( cirq.H.on_each(qs), [cirq.CNOT(q, q + n) for q in qs], gate(*qs), [cirq.CNOT(q, q + n) for q in qs], cirq.H.on_each(qs), ) expected_rates = cirq.final_density_matrix(circuit).diagonal().real # Convert test circuit to Stim and sample from it. stim_circuit, _ = cirq_circuit_to_stim_data( circuit + cirq.measure(*sorted(circuit.all_qubits())[::-1]) ) sample_count = 10000 samples = [] for _ in range(sample_count): sim = stim.TableauSimulator() sim.do(stim_circuit) s = 0 for k, v in enumerate(sim.current_measurement_record()): s |= v << k samples.append(s) unique, counts = np.unique(samples, return_counts=True) # Compare sample rates to expected rates. for value, count in zip(unique, counts): expected_rate = expected_rates[value] actual_rate = count / sample_count allowed_variation = 5 * (expected_rate * (1 - expected_rate) / sample_count) ** 0.5 if not 0 <= expected_rate - allowed_variation <= 1: raise ValueError("Not enough samples to bound results away from extremes.") assert abs(expected_rate - actual_rate) <= allowed_variation, ( f"Sample rate {actual_rate} is over 5 standard deviations away from {expected_rate}.\n" f"Gate: {gate}\n" f"Test circuit:\n{circuit}\n" f"Converted circuit:\n{stim_circuit}\n" ) def test_cirq_circuit_to_stim_circuit_custom_stim_method(): class DetectorGate(cirq.Gate): def _num_qubits_(self): return 1 def _measure_keys_(self): return ("custom",) def _stim_conversion_( self, edit_circuit: stim.Circuit, edit_measurement_key_lengths: List[Tuple[str, int]], targets: Sequence[int], **kwargs, ): edit_measurement_key_lengths.append(("custom", 2)) edit_circuit.append("M", [stim.target_inv(targets[0])]) edit_circuit.append("M", [targets[0]]) edit_circuit.append("DETECTOR", [stim.target_rec(-1)]) class SecondLastMeasurementWasDeterministicOperation(cirq.Operation): def _stim_conversion_(self, edit_circuit: stim.Circuit, tag: str, **kwargs): edit_circuit.append("DETECTOR", [stim.target_rec(-2)], tag=tag) def with_qubits(self, *new_qubits): raise NotImplementedError() @property def qubits(self) -> Tuple['cirq.Qid', ...]: return () a, b, c = cirq.LineQubit.range(3) cirq_circuit = cirq.Circuit( cirq.measure(a, key="a"), cirq.measure(b, key="b"), cirq.measure(c, key="c"), cirq.Moment(SecondLastMeasurementWasDeterministicOperation()), cirq.Moment(DetectorGate().on(b)), ) stim_circuit = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) assert ( str(stim_circuit).strip() == """ M 0 1 2 TICK DETECTOR rec[-2] TICK M !1 1 DETECTOR rec[-1] TICK """.strip() ) class BadGate(cirq.Gate): def num_qubits(self) -> int: return 1 def _stim_conversion_(self): pass with pytest.raises(TypeError, match="dont_forget_your_star_star_kwargs"): stimcirq.cirq_circuit_to_stim_circuit(cirq.Circuit(BadGate().on(a))) sample = stimcirq.StimSampler().sample(cirq_circuit) assert len(sample.columns) == 4 np.testing.assert_array_equal(sample["a"], [0]) np.testing.assert_array_equal(sample["b"], [0]) np.testing.assert_array_equal(sample["c"], [0]) np.testing.assert_array_equal(sample["custom"], [2]) def test_custom_qubit_indexing(): a = cirq.NamedQubit("a") b = cirq.NamedQubit("b") actual = stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit(cirq.CNOT(a, b)), qubit_to_index_dict={a: 10, b: 15} ) assert actual == stim.Circuit('CX 10 15\nTICK') actual = stimcirq.cirq_circuit_to_stim_circuit( cirq.FrozenCircuit(cirq.CNOT(a, b)), qubit_to_index_dict={a: 10, b: 15} ) assert actual == stim.Circuit('CX 10 15\nTICK') def test_on_loop(): a, b = cirq.LineQubit.range(2) c = cirq.Circuit( cirq.CircuitOperation( cirq.FrozenCircuit( cirq.X(a), cirq.X(b), cirq.measure(a, key="a"), cirq.measure(b, key="b"), ), repetitions=3, ) ) result_normal = cirq.Simulator().run(c) result_stim = stimcirq.StimSampler().run(c) assert result_normal.records.keys() == result_stim.records.keys() for k in result_normal.records.keys(): assert result_stim.records[k].shape == result_normal.records[k].shape def test_multi_moment_circuit_operation(): q0 = cirq.LineQubit(0) cc = cirq.Circuit( cirq.CircuitOperation( cirq.FrozenCircuit( cirq.Moment(cirq.H(q0)), cirq.Moment(cirq.H(q0)), cirq.Moment(cirq.H(q0)), cirq.Moment(cirq.H(q0)), ) ) ) assert stimcirq.cirq_circuit_to_stim_circuit(cc) == stim.Circuit(""" H 0 TICK H 0 TICK H 0 TICK H 0 TICK """) def test_on_tagged_loop(): a, b = cirq.LineQubit.range(2) c = cirq.Circuit( cirq.CircuitOperation( cirq.FrozenCircuit( cirq.X(a), cirq.X(b), cirq.measure(a, key="a"), cirq.measure(b, key="b"), ), repetitions=3, ).with_tags('my_tag') ) stim_circuit = stimcirq.cirq_circuit_to_stim_circuit(c) assert stim.CircuitRepeatBlock in {type(instr) for instr in stim_circuit} def test_random_gate_channel(): q0, q1 = cirq.LineQubit.range(2) circuit = cirq.Circuit(cirq.RandomGateChannel( sub_gate=cirq.DensePauliString((0, 1)), probability=0.25).on(q0, q1)) assert stimcirq.cirq_circuit_to_stim_circuit(circuit) == stim.Circuit(""" E(0.25) X1 TICK """) def test_custom_tagging(): assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.X(cirq.LineQubit(0)).with_tags('test'), cirq.X(cirq.LineQubit(0)).with_tags((2, 3, 4)), cirq.H(cirq.LineQubit(0)).with_tags('a', 'b'), ), tag_func=lambda op: "PAIR" if len(op.tags) == 2 else repr(op.tags), ) == stim.Circuit(""" X[('test',)] 0 TICK X[((2, 3, 4),)] 0 TICK H[PAIR] 0 TICK """) def test_round_trip_example_circuit(): stim_circuit = stim.Circuit.generated( "surface_code:rotated_memory_x", distance=3, rounds=1, after_clifford_depolarization=0.01, ) cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit.flattened()) circuit_back = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) assert len(circuit_back.shortest_graphlike_error()) == 3 ================================================ FILE: glue/cirq/stimcirq/_cx_swap_gate.py ================================================ from typing import Any, Dict, List import cirq import stim @cirq.value_equality class CXSwapGate(cirq.Gate): """Handles explaining stim's CXSWAP and SWAPCX gates to cirq.""" def __init__( self, *, inverted: bool, ): self.inverted = inverted def _num_qubits_(self) -> int: return 2 def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> List[str]: return ['ZSWAP', 'XSWAP'][::-1 if self.inverted else +1] def _value_equality_values_(self): return ( self.inverted, ) def _decompose_(self, qubits): a, b = qubits if self.inverted: yield cirq.SWAP(a, b) yield cirq.CNOT(a, b) else: yield cirq.CNOT(a, b) yield cirq.SWAP(a, b) def _stim_conversion_(self, edit_circuit: stim.Circuit, targets: List[int], tag: str, **kwargs): edit_circuit.append('SWAPCX' if self.inverted else 'CXSWAP', targets, tag=tag) def __pow__(self, power: int) -> 'CXSwapGate': if power == +1: return self if power == -1: return CXSwapGate(inverted=not self.inverted) return NotImplemented def __str__(self) -> str: return 'SWAPCX' if self.inverted else 'CXSWAP' def __repr__(self): return f'stimcirq.CXSwapGate(inverted={self.inverted!r})' @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return { 'inverted': self.inverted, } ================================================ FILE: glue/cirq/stimcirq/_cx_swap_test.py ================================================ import cirq import stim import stimcirq def test_stim_conversion(): a, b, c = cirq.LineQubit.range(3) cirq_circuit = cirq.Circuit( stimcirq.CXSwapGate(inverted=False).on(a, b), stimcirq.CXSwapGate(inverted=True).on(b, c), ) stim_circuit = stim.Circuit( """ CXSWAP 0 1 TICK SWAPCX 1 2 TICK """ ) assert stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) == stim_circuit assert stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) == cirq_circuit def test_diagram(): a, b = cirq.LineQubit.range(2) cirq.testing.assert_has_diagram( cirq.Circuit( stimcirq.CXSwapGate(inverted=False)(a, b), stimcirq.CXSwapGate(inverted=True)(a, b), ), """ 0: ---ZSWAP---XSWAP--- | | 1: ---XSWAP---ZSWAP--- """, use_unicode_characters=False, ) def test_inverse(): a = stimcirq.CXSwapGate(inverted=True) b = stimcirq.CXSwapGate(inverted=False) assert a**+1 == a assert a**-1 == b assert b**+1 == b assert b**-1 == a def test_repr(): val = stimcirq.CXSwapGate(inverted=True) assert eval(repr(val), {"stimcirq": stimcirq}) == val val = stimcirq.CXSwapGate(inverted=False) assert eval(repr(val), {"stimcirq": stimcirq}) == val def test_equality(): eq = cirq.testing.EqualsTester() eq.add_equality_group(stimcirq.CXSwapGate(inverted=False), stimcirq.CXSwapGate(inverted=False)) eq.add_equality_group(stimcirq.CXSwapGate(inverted=True)) def test_json_serialization(): a, b, d = cirq.LineQubit.range(3) c = cirq.Circuit( stimcirq.CXSwapGate(inverted=False)(a, b), stimcirq.CXSwapGate(inverted=False)(b, d), ) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.CXSwapGate(inverted=True) packed = '{\n "cirq_type": "CXSwapGate",\n "inverted": true\n}' assert cirq.to_json(raw) == packed assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw ================================================ FILE: glue/cirq/stimcirq/_cz_swap_gate.py ================================================ from typing import Any, Dict, List import cirq import stim @cirq.value_equality class CZSwapGate(cirq.Gate): """Handles explaining stim's CZSWAP gates to cirq.""" def _num_qubits_(self) -> int: return 2 def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> List[str]: return ['ZSWAP', 'ZSWAP'] def _value_equality_values_(self): return () def _decompose_(self, qubits): a, b = qubits yield cirq.SWAP(a, b) yield cirq.CZ(a, b) def _stim_conversion_(self, edit_circuit: stim.Circuit, targets: List[int], tag: str, **kwargs): edit_circuit.append('CZSWAP', targets, tag=tag) def __pow__(self, power: int) -> 'CZSwapGate': if power == +1: return self if power == -1: return self return NotImplemented def __str__(self) -> str: return 'CZSWAP' def __repr__(self): return f'stimcirq.CZSwapGate()' @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return {} ================================================ FILE: glue/cirq/stimcirq/_cz_swap_test.py ================================================ import cirq import stim import stimcirq def test_stim_conversion(): a, b, c = cirq.LineQubit.range(3) cirq_circuit = cirq.Circuit( stimcirq.CZSwapGate().on(a, b), stimcirq.CZSwapGate().on(b, c), ) stim_circuit = stim.Circuit( """ CZSWAP 0 1 TICK CZSWAP 1 2 TICK """ ) assert stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) == stim_circuit assert stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) == cirq_circuit def test_diagram(): a, b = cirq.LineQubit.range(2) cirq.testing.assert_has_diagram( cirq.Circuit( stimcirq.CZSwapGate()(a, b), stimcirq.CZSwapGate()(a, b), ), """ 0: ---ZSWAP---ZSWAP--- | | 1: ---ZSWAP---ZSWAP--- """, use_unicode_characters=False, ) def test_inverse(): a = stimcirq.CZSwapGate() assert a**+1 == a assert a**-1 == a def test_repr(): val = stimcirq.CZSwapGate() assert eval(repr(val), {"stimcirq": stimcirq}) == val def test_equality(): eq = cirq.testing.EqualsTester() eq.add_equality_group(stimcirq.CZSwapGate(), stimcirq.CZSwapGate()) def test_json_serialization(): a, b, d = cirq.LineQubit.range(3) c = cirq.Circuit( stimcirq.CZSwapGate()(a, b), stimcirq.CZSwapGate()(b, d), ) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.CZSwapGate() packed = '{\n "cirq_type": "CZSwapGate"\n}' assert cirq.to_json(raw) == packed assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw ================================================ FILE: glue/cirq/stimcirq/_det_annotation.py ================================================ from typing import Any, Dict, Iterable, List, Tuple import cirq import stim @cirq.value_equality class DetAnnotation(cirq.Operation): """Annotates that a particular combination of measurements is deterministic. Creates a DETECTOR operation when converting to a stim circuit. """ def __init__( self, *, parity_keys: Iterable[str] = (), relative_keys: Iterable[int] = (), coordinate_metadata: Iterable[float] = (), ): """ Args: parity_keys: The keys of some measurements with the property that their parity is always the same under noiseless execution of the circuit. relative_keys: Refers to measurements relative to this operation. For example, relative key -1 is the previous measurement. All entries must be negative. coordinate_metadata: An optional location for the detector. This has no effect on the function of the circuit, but can be used by plotting tools. """ self.parity_keys = frozenset(parity_keys) self.relative_keys = frozenset(relative_keys) self.coordinate_metadata = tuple(coordinate_metadata) @property def qubits(self) -> Tuple[cirq.Qid, ...]: return () def with_qubits(self, *new_qubits) -> 'DetAnnotation': return self def _value_equality_values_(self) -> Any: return self.parity_keys, self.relative_keys, self.coordinate_metadata def _circuit_diagram_info_(self, args: Any) -> str: items: List[str] = [repr(e) for e in sorted(self.parity_keys)] items += [f'rec[{e}]' for e in sorted(self.relative_keys)] k = ",".join(str(e) for e in items) return f"Det({k})" @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: result = { 'parity_keys': sorted(self.parity_keys), 'coordinate_metadata': self.coordinate_metadata, } if self.relative_keys: result['relative_keys'] = sorted(self.relative_keys) return result def __repr__(self) -> str: return ( f'stimcirq.DetAnnotation(' f'parity_keys={sorted(self.parity_keys)}, ' f'relative_keys={sorted(self.relative_keys)}, ' f'coordinate_metadata={self.coordinate_metadata!r})' ) def _decompose_(self): return [] def _is_comment_(self) -> bool: return True def _stim_conversion_( self, *, edit_circuit: stim.Circuit, edit_measurement_key_lengths: List[Tuple[str, int]], tag: str, have_seen_loop: bool = False, **kwargs, ): # Ideally these references would all be resolved ahead of time, to avoid the redundant # linear search overhead and also to avoid the detectors and measurements being interleaved # instead of grouped (grouping measurements is helpful for stabilizer simulation). But that # didn't happen and this is the context we're called in and we're going to make it work. if have_seen_loop and self.parity_keys: raise NotImplementedError( "Measurement key conversion is not reliable when loops are present." ) # Find indices of measurement record targets. remaining = set(self.parity_keys) rec_targets = [stim.target_rec(k) for k in sorted(self.relative_keys, reverse=True)] for offset in range(len(edit_measurement_key_lengths)): m_key, m_len = edit_measurement_key_lengths[-1 - offset] if m_len != 1: raise NotImplementedError(f"multi-qubit measurement {m_key!r}") if m_key in remaining: remaining.discard(m_key) rec_targets.append(stim.target_rec(-1 - offset)) if not remaining: break if remaining: raise ValueError( f"{self!r} was processed before measurements it referenced ({sorted(remaining)!r})." f" Make sure the referenced measurements keys are actually in the circuit, and come" f" in an earlier moment (or earlier in the same moment's operation order)." ) edit_circuit.append("DETECTOR", rec_targets, self.coordinate_metadata, tag=tag) ================================================ FILE: glue/cirq/stimcirq/_det_annotation_test.py ================================================ import cirq import numpy as np import pytest import stim import stimcirq def test_stim_conversion(): a, b, c = cirq.LineQubit.range(3) with pytest.raises(ValueError, match="earlier"): stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit(cirq.Moment(stimcirq.DetAnnotation(parity_keys=["unknown"]))) ) with pytest.raises(ValueError, match="earlier"): stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment( stimcirq.DetAnnotation(parity_keys=["later"]), cirq.measure(b, key="later") ) ) ) with pytest.raises(ValueError, match="earlier"): stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment(stimcirq.DetAnnotation(parity_keys=["later"])), cirq.Moment(cirq.measure(b, key="later")), ) ) assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment(cirq.measure(b, key="earlier")), cirq.Moment(stimcirq.DetAnnotation(parity_keys=["earlier"])), ) ) == stim.Circuit( """ QUBIT_COORDS(1) 0 M 0 TICK DETECTOR rec[-1] TICK """ ) assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment( cirq.measure(b, key="earlier"), stimcirq.DetAnnotation(parity_keys=["earlier"]) ) ) ) == stim.Circuit( """ QUBIT_COORDS(1) 0 M 0 DETECTOR rec[-1] TICK """ ) assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment(cirq.measure(a, key="a"), cirq.measure(b, key="b")), cirq.Moment( stimcirq.DetAnnotation(parity_keys=["a", "b"], coordinate_metadata=(1, 2, 3.5)) ), ) ) == stim.Circuit( """ M 0 1 TICK DETECTOR(1, 2, 3.5) rec[-1] rec[-2] TICK """ ) assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.H(a), cirq.CNOT(a, b), cirq.CNOT(a, c), cirq.Moment( cirq.measure(a, key="a"), cirq.measure(b, key="b"), stimcirq.DetAnnotation(parity_keys=["a", "b"]), cirq.measure(c, key="c"), stimcirq.DetAnnotation(parity_keys=["a", "c"]), ), ) ) == stim.Circuit( """ H 0 TICK CNOT 0 1 TICK CNOT 0 2 TICK M 0 1 DETECTOR rec[-1] rec[-2] M 2 DETECTOR rec[-1] rec[-3] TICK """ ) def test_simulation(): a = cirq.LineQubit(0) s = cirq.Simulator().sample( cirq.Circuit( cirq.X(a), cirq.measure(a, key="a"), stimcirq.DetAnnotation(parity_keys=["a"]) ), repetitions=3, ) np.testing.assert_array_equal(s["a"], [1, 1, 1]) def test_diagram(): a, b = cirq.LineQubit.range(2) cirq.testing.assert_has_diagram( cirq.Circuit( cirq.measure(a, key="a"), cirq.measure(b, key="b"), stimcirq.DetAnnotation(parity_keys=["a", "b"]), ), """ 0: ---M('a')--------- 1: ---M('b')--------- Det('a','b') """, use_unicode_characters=False, ) def test_repr(): val = stimcirq.DetAnnotation(parity_keys=["a", "b"], coordinate_metadata=(1, 2)) assert eval(repr(val), {"stimcirq": stimcirq}) == val def test_equality(): eq = cirq.testing.EqualsTester() eq.add_equality_group(stimcirq.DetAnnotation(), stimcirq.DetAnnotation()) eq.add_equality_group(stimcirq.DetAnnotation(parity_keys=["a"])) eq.add_equality_group( stimcirq.DetAnnotation(parity_keys=["a"], coordinate_metadata=[1, 2]), stimcirq.DetAnnotation(parity_keys=["a"], coordinate_metadata=(1, 2)), ) eq.add_equality_group(stimcirq.DetAnnotation(parity_keys=["a"], coordinate_metadata=(2, 1))) eq.add_equality_group(stimcirq.DetAnnotation(parity_keys=["b"])) eq.add_equality_group( stimcirq.DetAnnotation(parity_keys=["a", "b"]), stimcirq.DetAnnotation(parity_keys=["b", "a"]), ) def test_json_serialization(): c = cirq.Circuit( stimcirq.DetAnnotation(parity_keys=["a", "b"]), stimcirq.DetAnnotation(parity_keys=["a", "b"], relative_keys=[-3, -5]), stimcirq.DetAnnotation(coordinate_metadata=(1, 2)), stimcirq.DetAnnotation(parity_keys=["d", "c"], coordinate_metadata=(1, 2, 3)), ) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.DetAnnotation(parity_keys=['a'], relative_keys=[-2], coordinate_metadata=(3, 5)) packed = '{\n "cirq_type": "DetAnnotation",\n "parity_keys": [\n "a"\n ],\n "coordinate_metadata": [\n 3,\n 5\n ],\n "relative_keys": [\n -2\n ]\n}' assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed ================================================ FILE: glue/cirq/stimcirq/_i_error_gate.py ================================================ from typing import Any, Dict, List, Iterable import cirq import stim @cirq.value_equality class IErrorGate(cirq.Gate): """Handles explaining stim's I_ERROR gate to cirq.""" def __init__(self, gate_args: Iterable[float]): self.gate_args = tuple(gate_args) def _num_qubits_(self) -> int: return 1 def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> List[str]: return [f'I_ERROR({self.gate_args})'] def _value_equality_values_(self): return self.gate_args def _decompose_(self, qubits): pass def _stim_conversion_(self, edit_circuit: stim.Circuit, targets: List[int], tag: str, **kwargs): edit_circuit.append('I_ERROR', targets, arg=self.gate_args, tag=tag) def __str__(self) -> str: return 'I_ERROR(' + ",".join(str(e) for e in self.gate_args) + ')' def __repr__(self): return f'stimcirq.IErrorGate({self.gate_args!r})' @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return {'gate_args': self.gate_args} ================================================ FILE: glue/cirq/stimcirq/_i_error_gate_test.py ================================================ import cirq import stimcirq def test_repr(): r = stimcirq.IErrorGate([0.25, 0.125]) assert eval(repr(r), {'stimcirq': stimcirq}) == r r = stimcirq.IErrorGate([]) assert eval(repr(r), {'stimcirq': stimcirq}) == r def test_json_serialization(): r = stimcirq.IErrorGate([0.25, 0.125]) c = cirq.Circuit(r(cirq.LineQubit(1))) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.IErrorGate([0.25, 0.125]) packed = '{\n "cirq_type": "IErrorGate",\n "gate_args": [\n 0.25,\n 0.125\n ]\n}' assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed ================================================ FILE: glue/cirq/stimcirq/_ii_error_gate.py ================================================ from typing import Any, Dict, List, Iterable import cirq import stim @cirq.value_equality class IIErrorGate(cirq.Gate): """Handles explaining stim's II_ERROR gate to cirq.""" def __init__(self, gate_args: Iterable[float]): self.gate_args = tuple(gate_args) def _num_qubits_(self) -> int: return 2 def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> List[str]: return [f'II_ERROR({self.gate_args})', f'II_ERROR({self.gate_args})'] def _value_equality_values_(self): return self.gate_args def _decompose_(self, qubits): pass def _stim_conversion_(self, edit_circuit: stim.Circuit, targets: List[int], tag: str, **kwargs): edit_circuit.append('II_ERROR', targets, arg=self.gate_args, tag=tag) def __str__(self) -> str: return 'II_ERROR(' + ",".join(str(e) for e in self.gate_args) + ')' def __repr__(self): return f'stimcirq.IIErrorGate({self.gate_args!r})' @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return {'gate_args': self.gate_args} ================================================ FILE: glue/cirq/stimcirq/_ii_error_gate_test.py ================================================ import cirq import stimcirq def test_repr(): r = stimcirq.IIErrorGate([0.25, 0.125]) assert eval(repr(r), {'stimcirq': stimcirq}) == r r = stimcirq.IIErrorGate([]) assert eval(repr(r), {'stimcirq': stimcirq}) == r def test_json_serialization(): r = stimcirq.IIErrorGate([0.25, 0.125]) c = cirq.Circuit(r(cirq.LineQubit(1), cirq.LineQubit(3))) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.IIErrorGate([0.25, 0.125]) packed = '{\n "cirq_type": "IIErrorGate",\n "gate_args": [\n 0.25,\n 0.125\n ]\n}' assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed ================================================ FILE: glue/cirq/stimcirq/_ii_gate.py ================================================ from typing import Any, Dict, List import cirq import stim @cirq.value_equality class IIGate(cirq.Gate): """Handles explaining stim's II gate to cirq.""" def _num_qubits_(self) -> int: return 2 def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> List[str]: return ['II', 'II'] def _value_equality_values_(self): return () def _decompose_(self, qubits): pass def _stim_conversion_(self, edit_circuit: stim.Circuit, targets: List[int], tag: str, **kwargs): edit_circuit.append('II', targets, tag=tag) def __pow__(self, power: int) -> 'IIGate': return self def __str__(self) -> str: return 'II' def __repr__(self): return f'stimcirq.IIGate()' @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return {} ================================================ FILE: glue/cirq/stimcirq/_ii_gate_test.py ================================================ import cirq import stimcirq def test_repr(): r = stimcirq.IIGate() assert eval(repr(r), {'stimcirq': stimcirq}) == r def test_json_serialization(): r = stimcirq.IIGate() c = cirq.Circuit(r(cirq.LineQubit(1), cirq.LineQubit(3))) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.IIGate() packed = '{\n "cirq_type": "IIGate"\n}' assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed ================================================ FILE: glue/cirq/stimcirq/_measure_and_or_reset_gate.py ================================================ from typing import Any, Dict, List, Tuple import cirq import stim @cirq.value_equality class MeasureAndOrResetGate(cirq.Gate): """Handles explaining stim's MX/MY/MZ/MRX/MRY/MRZ/RX/RY/RZ gates to cirq.""" def __init__( self, measure: bool, reset: bool, basis: str, invert_measure: bool, key: str, measure_flip_probability: float = 0, ): self.measure = measure self.reset = reset self.basis = basis self.invert_measure = invert_measure self.key = key self.measure_flip_probability = measure_flip_probability def _num_qubits_(self) -> int: return 1 def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> str: return str(self) def resolve(self, target: cirq.Qid) -> cirq.Operation: if ( self.basis == 'Z' and self.measure and self.measure_flip_probability == 0 and not self.reset ): return cirq.MeasurementGate( num_qubits=1, key=self.key, invert_mask=(1,) if self.invert_measure else () ).on(target) return MeasureAndOrResetGate( measure=self.measure, reset=self.reset, basis=self.basis, invert_measure=self.invert_measure, key=self.key, measure_flip_probability=self.measure_flip_probability, ).on(target) def _value_equality_values_(self): return ( self.measure, self.reset, self.basis, self.invert_measure, self.key, self.measure_flip_probability, ) def _decompose_(self, qubits): (q,) = qubits if self.measure: if self.basis == 'X': yield cirq.H(q) elif self.basis == 'Y': yield cirq.X(q) ** 0.5 if self.measure_flip_probability: raise NotImplementedError("Noisy measurement as a cirq operation.") else: yield cirq.measure( q, key=self.key, invert_mask=(True,) if self.invert_measure else () ) if self.reset: yield cirq.ResetChannel().on(q) if self.measure or self.reset: if self.basis == 'X': yield cirq.H(q) elif self.basis == 'Y': yield cirq.X(q) ** -0.5 def _stim_op_name(self) -> str: result = '' if self.measure: result += "M" if self.reset: result += "R" if self.basis != 'Z': result += self.basis return result def _stim_conversion_(self, *, edit_circuit: stim.Circuit, targets: List[int], tag: str, edit_measurement_key_lengths: List[Tuple[str, int]], **kwargs): if self.measure: edit_measurement_key_lengths.append((self.key, 1)) if self.invert_measure: targets[0] = stim.target_inv(targets[0]) if self.measure_flip_probability: edit_circuit.append_operation( self._stim_op_name(), targets, self.measure_flip_probability, tag=tag ) else: edit_circuit.append_operation(self._stim_op_name(), targets, tag=tag) def __str__(self) -> str: result = self._stim_op_name() if self.invert_measure: result = "!" + result if self.measure: result += f"('{self.key}')" if self.measure_flip_probability: result += f"^{self.measure_flip_probability:%}" return result def __repr__(self): return ( f'stimcirq.MeasureAndOrResetGate(' f'measure={self.measure!r}, ' f'reset={self.reset!r}, ' f'basis={self.basis!r}, ' f'invert_measure={self.invert_measure!r}, ' f'key={self.key!r}, ' f'measure_flip_probability={self.measure_flip_probability!r})' ) @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return { 'measure': self.measure, 'reset': self.reset, 'basis': self.basis, 'invert_measure': self.invert_measure, 'key': self.key, 'measure_flip_probability': self.measure_flip_probability, } ================================================ FILE: glue/cirq/stimcirq/_measure_and_or_reset_gate_test.py ================================================ import cirq import stimcirq def test_repr(): r = stimcirq.MeasureAndOrResetGate( measure=True, reset=True, basis='Y', key='result', invert_measure=True, measure_flip_probability=0.125, ) assert eval(repr(r), {'stimcirq': stimcirq}) == r def test_json_serialization(): r = stimcirq.MeasureAndOrResetGate( measure=True, reset=True, basis='Y', key='result', invert_measure=True, measure_flip_probability=0.125, ) c = cirq.Circuit(r(cirq.LineQubit(1))) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.MeasureAndOrResetGate( measure=True, reset=True, basis='X', key='res', invert_measure=True, measure_flip_probability=0.25, ) packed = '{\n "cirq_type": "MeasureAndOrResetGate",\n "measure": true,\n "reset": true,\n "basis": "X",\n "invert_measure": true,\n "key": "res",\n "measure_flip_probability": 0.25\n}' assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed ================================================ FILE: glue/cirq/stimcirq/_obs_annotation.py ================================================ from typing import Any, Dict, Iterable, List, Tuple import cirq import stim @cirq.value_equality class CumulativeObservableAnnotation(cirq.Operation): """Annotates that a particular combination of measurements is part of a logical observable. Creates an OBSERVABLE_INCLUDE operation when converting to a stim circuit. """ def __init__( self, *, parity_keys: Iterable[str] = (), relative_keys: Iterable[int] = (), pauli_keys: Iterable[str] = (), observable_index: int, ): """ Args: parity_keys: The keys of some measurements to include in the logical observable. relative_keys: Refers to measurements relative to this operation. For example, relative key -1 is the previous measurement. All entries must be negative. observable_index: A unique index for the logical observable. """ self.parity_keys = frozenset(parity_keys) self.relative_keys = frozenset(relative_keys) self.pauli_keys = frozenset(pauli_keys) self.observable_index = observable_index @property def qubits(self) -> Tuple[cirq.Qid, ...]: return () def with_qubits(self, *new_qubits) -> 'CumulativeObservableAnnotation': return self def _value_equality_values_(self) -> Any: return self.parity_keys, self.relative_keys, self.pauli_keys, self.observable_index def _circuit_diagram_info_(self, args: Any) -> str: items: List[str] = [repr(e) for e in sorted(self.parity_keys)] items += [f'rec[{e}]' for e in sorted(self.relative_keys)] items += sorted(self.pauli_keys) k = ",".join(str(e) for e in items) return f"Obs{self.observable_index}({k})" def __repr__(self) -> str: return ( f'stimcirq.CumulativeObservableAnnotation(' f'parity_keys={sorted(self.parity_keys)}, ' f'relative_keys={sorted(self.relative_keys)}, ' f'pauli_keys={sorted(self.pauli_keys)}, ' f'observable_index={self.observable_index!r})' ) @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: result = { 'parity_keys': sorted(self.parity_keys), 'observable_index': self.observable_index, 'pauli_keys': sorted(self.pauli_keys), } if self.relative_keys: result['relative_keys'] = sorted(self.relative_keys) return result def _decompose_(self): return [] def _is_comment_(self) -> bool: return True def _stim_conversion_( self, *, edit_circuit: stim.Circuit, edit_measurement_key_lengths: List[Tuple[str, int]], have_seen_loop: bool = False, tag: str, **kwargs, ): # Ideally these references would all be resolved ahead of time, to avoid the redundant # linear search overhead and also to avoid the detectors and measurements being interleaved # instead of grouped (grouping measurements is helpful for stabilizer simulation). But that # didn't happen and this is the context we're called in and we're going to make it work. if have_seen_loop and self.parity_keys: raise NotImplementedError( "Measurement key conversion is not reliable when loops are present." ) # Find indices of measurement record targets. remaining = set(self.parity_keys) rec_targets = [stim.target_rec(k) for k in sorted(self.relative_keys, reverse=True)] for offset in range(len(edit_measurement_key_lengths)): m_key, m_len = edit_measurement_key_lengths[-1 - offset] if m_len != 1: raise NotImplementedError(f"multi-qubit measurement {m_key!r}") if m_key in remaining: remaining.discard(m_key) rec_targets.append(stim.target_rec(-1 - offset)) if not remaining: break rec_targets.extend( [ stim.target_pauli(qubit_index=int(k[1:]), pauli=k[0]) for k in sorted(self.pauli_keys) ] ) if remaining: raise ValueError( f"{self!r} was processed before measurements it referenced ({sorted(remaining)!r})." f" Make sure the referenced measurements keys are actually in the circuit, and come" f" in an earlier moment (or earlier in the same moment's operation order)." ) edit_circuit.append("OBSERVABLE_INCLUDE", rec_targets, self.observable_index, tag=tag) ================================================ FILE: glue/cirq/stimcirq/_obs_annotation_test.py ================================================ import cirq import numpy as np import pytest import stim import stimcirq def test_stim_conversion(): a, b, c = cirq.LineQubit.range(3) with pytest.raises(ValueError, match="earlier"): stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment( stimcirq.CumulativeObservableAnnotation( parity_keys=["unknown"], observable_index=0 ) ) ) ) with pytest.raises(ValueError, match="earlier"): stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment( stimcirq.CumulativeObservableAnnotation( parity_keys=["later"], observable_index=0 ), cirq.measure(b, key="later"), ) ) ) with pytest.raises(ValueError, match="earlier"): stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment( stimcirq.CumulativeObservableAnnotation( parity_keys=["later"], observable_index=0 ) ), cirq.Moment(cirq.measure(b, key="later")), ) ) assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment(cirq.measure(b, key="earlier")), cirq.Moment( stimcirq.CumulativeObservableAnnotation(parity_keys=["earlier"], observable_index=0) ), ) ) == stim.Circuit( """ QUBIT_COORDS(1) 0 M 0 TICK OBSERVABLE_INCLUDE(0) rec[-1] TICK """ ) assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment( cirq.measure(b, key="earlier"), stimcirq.CumulativeObservableAnnotation( parity_keys=["earlier"], observable_index=0 ), ) ) ) == stim.Circuit( """ QUBIT_COORDS(1) 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] TICK """ ) assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.Moment(cirq.measure(a, key="a"), cirq.measure(b, key="b")), cirq.Moment( stimcirq.CumulativeObservableAnnotation(parity_keys=["a", "b"], observable_index=2) ), ) ) == stim.Circuit( """ M 0 1 TICK OBSERVABLE_INCLUDE(2) rec[-1] rec[-2] TICK """ ) assert stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( cirq.H(a), cirq.CNOT(a, b), cirq.CNOT(a, c), cirq.Moment( cirq.measure(a, key="a"), cirq.measure(b, key="b"), stimcirq.CumulativeObservableAnnotation(parity_keys=["a", "b"], observable_index=0), cirq.measure(c, key="c"), stimcirq.CumulativeObservableAnnotation(parity_keys=["a", "c"], observable_index=0), ), ) ) == stim.Circuit( """ H 0 TICK CNOT 0 1 TICK CNOT 0 2 TICK M 0 1 OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] M 2 OBSERVABLE_INCLUDE(0) rec[-1] rec[-3] TICK """ ) def test_simulation(): a = cirq.LineQubit(0) s = cirq.Simulator().sample( cirq.Circuit( cirq.X(a), cirq.measure(a, key="a"), stimcirq.CumulativeObservableAnnotation(parity_keys=["a"], observable_index=0), ), repetitions=3, ) np.testing.assert_array_equal(s["a"], [1, 1, 1]) def test_diagram(): a, b = cirq.LineQubit.range(2) cirq.testing.assert_has_diagram( cirq.Circuit( cirq.measure(a, key="a"), cirq.measure(b, key="b"), stimcirq.CumulativeObservableAnnotation(parity_keys=["a", "b"], observable_index=2), ), """ 0: ---M('a')---------- 1: ---M('b')---------- Obs2('a','b') """, use_unicode_characters=False, ) def test_repr(): val = stimcirq.CumulativeObservableAnnotation(parity_keys=["a", "b"], observable_index=2) assert eval(repr(val), {"stimcirq": stimcirq}) == val def test_equality(): eq = cirq.testing.EqualsTester() eq.add_equality_group( stimcirq.CumulativeObservableAnnotation(observable_index=0), stimcirq.CumulativeObservableAnnotation(observable_index=0), ) eq.add_equality_group(stimcirq.CumulativeObservableAnnotation(observable_index=1)) eq.add_equality_group( stimcirq.CumulativeObservableAnnotation(parity_keys=["a"], observable_index=0) ) eq.add_equality_group( stimcirq.CumulativeObservableAnnotation(parity_keys=["a"], observable_index=1) ) eq.add_equality_group( stimcirq.CumulativeObservableAnnotation(parity_keys=["b"], observable_index=0) ) eq.add_equality_group( stimcirq.CumulativeObservableAnnotation(parity_keys=["a", "b"], observable_index=0), stimcirq.CumulativeObservableAnnotation(parity_keys=["b", "a"], observable_index=0), ) def test_json_serialization(): c = cirq.Circuit( stimcirq.CumulativeObservableAnnotation(parity_keys=["a", "b"], observable_index=5), stimcirq.CumulativeObservableAnnotation( parity_keys=["a", "b"], relative_keys=[-1, -3], observable_index=5 ), stimcirq.CumulativeObservableAnnotation(observable_index=2), stimcirq.CumulativeObservableAnnotation(parity_keys=["d", "c"], observable_index=5), ) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_serialization_with_pauli_keys(): c = cirq.Circuit( stimcirq.CumulativeObservableAnnotation(parity_keys=["a", "b"], observable_index=5, pauli_keys=["X0", "Y1", "Z2"]), stimcirq.CumulativeObservableAnnotation( parity_keys=["a", "b"], relative_keys=[-1, -3], observable_index=5, pauli_keys=["X0", "Y1", "Z2"] ), stimcirq.CumulativeObservableAnnotation(observable_index=2, pauli_keys=["X0", "Y1", "Z2"]), stimcirq.CumulativeObservableAnnotation(parity_keys=["d", "c"], observable_index=5, pauli_keys=["X0", "Y1", "Z2"]), ) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.CumulativeObservableAnnotation(parity_keys=['z'], relative_keys=[-2], observable_index=5) packed_v1 = '{\n "cirq_type": "CumulativeObservableAnnotation",\n "parity_keys": [\n "z"\n ],\n "observable_index": 5,\n "relative_keys": [\n -2\n ]\n}' packed_v2 ='{\n "cirq_type": "CumulativeObservableAnnotation",\n "parity_keys": [\n "z"\n ],\n "observable_index": 5,\n "pauli_keys": [],\n "relative_keys": [\n -2\n ]\n}' assert cirq.read_json(json_text=packed_v1, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.read_json(json_text=packed_v2, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed_v2 # With pauli_keys raw = stimcirq.CumulativeObservableAnnotation(parity_keys=['z'], relative_keys=[-2], observable_index=5, pauli_keys=["X0", "Y1", "Z2"]) packed_v2 ='{\n "cirq_type": "CumulativeObservableAnnotation",\n "parity_keys": [\n "z"\n ],\n "observable_index": 5,\n "pauli_keys": [\n "X0",\n "Y1",\n "Z2"\n ],\n "relative_keys": [\n -2\n ]\n}' assert cirq.read_json(json_text=packed_v2, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed_v2 ================================================ FILE: glue/cirq/stimcirq/_shift_coords_annotation.py ================================================ from typing import Any, Dict, Iterable, List, Tuple import cirq import stim @cirq.value_equality class ShiftCoordsAnnotation(cirq.Operation): """Annotates that the qubit/detector coordinate origin is being moved. Creates a SHIFT_COORDS operation when converting to a stim circuit. """ def __init__(self, shift: Iterable[float]): """ Args: shift: How much to shift each coordinate.. """ self.shift = tuple(shift) @property def qubits(self) -> Tuple[cirq.Qid, ...]: return () def with_qubits(self, *new_qubits) -> 'ShiftCoordsAnnotation': return self def _value_equality_values_(self) -> Any: return self.shift def _circuit_diagram_info_(self, args: Any) -> str: k = ",".join(repr(e) for e in self.shift) return f"ShiftCoords({k})" @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return {'shift': self.shift} def __repr__(self) -> str: return f'stimcirq.ShiftCoordsAnnotation({self.shift!r})' def _decompose_(self): return [] def _is_comment_(self) -> bool: return True def _stim_conversion_(self, *, edit_circuit: stim.Circuit, tag: str, **kwargs): edit_circuit.append_operation("SHIFT_COORDS", [], self.shift, tag=tag) ================================================ FILE: glue/cirq/stimcirq/_shift_coords_annotation_test.py ================================================ import cirq import numpy as np import stim import stimcirq def test_conversion(): stim_circuit = stim.Circuit(""" M 0 DETECTOR(4) rec[-1] SHIFT_COORDS(1, 2, 3) DETECTOR(5) rec[-1] TICK """) cirq_circuit = cirq.Circuit( cirq.measure(cirq.LineQubit(0), key="0"), stimcirq.DetAnnotation(parity_keys=["0"], coordinate_metadata=[4]), stimcirq.ShiftCoordsAnnotation((1, 2, 3)), stimcirq.DetAnnotation(parity_keys=["0"], coordinate_metadata=[5]), ) assert stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) == stim_circuit assert stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) == cirq_circuit assert stimcirq.stim_circuit_to_cirq_circuit(stim_circuit, flatten=False) == cirq_circuit assert stimcirq.stim_circuit_to_cirq_circuit(stim_circuit, flatten=True) == cirq.Circuit( cirq.measure(cirq.LineQubit(0), key="0"), stimcirq.DetAnnotation(parity_keys=["0"], coordinate_metadata=[4]), stimcirq.DetAnnotation(parity_keys=["0"], coordinate_metadata=[6]), ) def test_simulation(): a = cirq.LineQubit(0) s = cirq.Simulator().sample( cirq.Circuit( cirq.X(a), cirq.measure(a, key="a"), stimcirq.ShiftCoordsAnnotation((1, 2, 3)) ), repetitions=3, ) np.testing.assert_array_equal(s["a"], [1, 1, 1]) def test_diagram(): a, b = cirq.LineQubit.range(2) cirq.testing.assert_has_diagram( cirq.Circuit( cirq.measure(a, key="a"), cirq.measure(b, key="b"), stimcirq.ShiftCoordsAnnotation([1, 2, 3]), ), """ 0: ---M('a')--------------- 1: ---M('b')--------------- ShiftCoords(1,2,3) """, use_unicode_characters=False, ) def test_repr(): val = stimcirq.ShiftCoordsAnnotation([1, 2, 3]) assert eval(repr(val), {"stimcirq": stimcirq}) == val def test_equality(): eq = cirq.testing.EqualsTester() eq.add_equality_group(stimcirq.ShiftCoordsAnnotation([])) eq.add_equality_group( stimcirq.ShiftCoordsAnnotation([1, 2]), stimcirq.ShiftCoordsAnnotation((1, 2)) ) def test_json_serialization(): c = cirq.Circuit(stimcirq.ShiftCoordsAnnotation([1, 2, 3])) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.ShiftCoordsAnnotation([2, 3, 5]) packed = '{\n "cirq_type": "ShiftCoordsAnnotation",\n "shift": [\n 2,\n 3,\n 5\n ]\n}' assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed ================================================ FILE: glue/cirq/stimcirq/_stim_sampler.py ================================================ import collections from typing import Dict, List, Sequence import cirq from ._cirq_to_stim import cirq_circuit_to_stim_data try: ResultImpl = cirq.ResultDict # For cirq >= 0.14 except AttributeError: ResultImpl = cirq.Result # For cirq < 0.14 class StimSampler(cirq.Sampler): """Samples stabilizer circuits using Stim. Supports circuits that contain Clifford operations, measurement operations, reset operations, and noise operations that can be decomposed into probabilistic Pauli operations. Unknown operations are supported as long as they provide a decomposition into supported operations via `cirq.decompose` (i.e. via a `_decompose_` method). Note that batch sampling is significantly faster (as in potentially thousands of times faster) than individual sampling, because it amortizes the cost of parsing and analyzing the circuit. """ def run_sweep( self, program: cirq.Circuit, params: cirq.Sweepable, repetitions: int = 1 ) -> Sequence[cirq.Result]: results: List[cirq.Result] = [] for param_resolver in cirq.to_resolvers(params): # Request samples from stim. instance = cirq.resolve_parameters(program, param_resolver) converted_circuit, key_ranges = cirq_circuit_to_stim_data(instance, flatten=True) samples = converted_circuit.compile_sampler().sample(repetitions) # Find number of qubits (length), number of instances, and indices for each measurement key. lengths: Dict[str, int] = {} instances: Dict[str, int] = collections.Counter() indices: Dict[str, int] = collections.defaultdict(list) k = 0 for key, length in key_ranges: prev_length = lengths.get(key) if prev_length is None: lengths[key] = length elif length != prev_length: raise ValueError(f"different numbers of qubits for key {key}: {prev_length} != {length}") instances[key] += 1 indices[key].extend(range(k, k + length)) k += length # Convert unlabelled samples into keyed results. records = { key: samples[:, indices[key]].reshape((repetitions, instances[key], length)) for key, length in lengths.items() } results.append(ResultImpl(params=param_resolver, records=records)) return results ================================================ FILE: glue/cirq/stimcirq/_stim_sampler_test.py ================================================ import cirq import numpy as np import pytest import stimcirq def test_end_to_end(): sampler = stimcirq.StimSampler() a, b = cirq.LineQubit.range(2) result = sampler.run( cirq.Circuit( cirq.H(a), cirq.CNOT(a, b), cirq.X(a) ** 0.5, cirq.X(b) ** 0.5, cirq.measure(a, key='a'), cirq.measure(b, key='b'), ), repetitions=1000, ) np.testing.assert_array_equal(result.measurements['a'], result.measurements['b'] ^ 1) def test_end_to_end_repeated_keys(): sampler = stimcirq.StimSampler() a, b = cirq.LineQubit.range(2) result = sampler.run( cirq.Circuit( cirq.CircuitOperation( cirq.FrozenCircuit( cirq.H(a), cirq.CNOT(a, b), cirq.X(a) ** 0.5, cirq.X(b) ** 0.5, cirq.measure(a, b, key='m'), cirq.reset_each(a, b), ), repetitions=10, use_repetition_ids=False, ), ), repetitions=1000, ) with pytest.raises(ValueError, match='Cannot extract 2D measurements for repeated keys'): _ = result.measurements['m'] data = result.records['m'] assert data.shape == (1000, 10, 2) np.testing.assert_array_equal(data[:,:,0], data[:,:,1] ^ 1) def test_endian(): a, b = cirq.LineQubit.range(2) circuit = cirq.Circuit(cirq.X(a), cirq.measure(a, b, key='out')) s1 = cirq.Simulator().sample(circuit) s2 = stimcirq.StimSampler().sample(circuit) assert s1['out'][0] == s2['out'][0] def test_custom_gates(): s = stimcirq.StimSampler() a, b, c, d = cirq.LineQubit.range(4) class GoodGate(cirq.Gate): def _num_qubits_(self) -> int: return 1 def _decompose_(self, qubits): return [cirq.X.on_each(*qubits)] class IndirectlyGoodGate(cirq.Gate): def _num_qubits_(self) -> int: return 1 def _decompose_(self, qubits): return [GoodGate().on_each(*qubits)] class BadGate(cirq.Gate): def _num_qubits_(self) -> int: return 1 class IndirectlyBadGate(cirq.Gate): def _num_qubits_(self) -> int: return 1 def _decompose_(self): return [BadGate()] np.testing.assert_array_equal( s.sample(cirq.Circuit(GoodGate().on(a), cirq.measure(a, key='out')))['out'], [True] ) np.testing.assert_array_equal( s.sample(cirq.Circuit(IndirectlyGoodGate().on(a), cirq.measure(a, key='out')))['out'], [True], ) with pytest.raises(TypeError, match="BadGate.+into stim"): s.sample(cirq.Circuit(BadGate().on(a))) with pytest.raises(TypeError, match="IndirectlyBadGate.+into stim"): s.sample(cirq.Circuit(IndirectlyBadGate().on(a))) with pytest.raises(TypeError, match="into stim"): s.sample(cirq.Circuit(cirq.TOFFOLI(a, b, c))) def test_custom_measurement(): class XBasisMeasurement(cirq.Gate): def __init__(self, key: str): self.key = key def _num_qubits_(self) -> int: return 1 def _decompose_(self, qubits): (q,) = qubits return [cirq.H(q), cirq.measure(q, key=self.key), cirq.H(q)] s = stimcirq.StimSampler() a, b = cirq.LineQubit.range(2) out = s.sample( cirq.Circuit( cirq.H(a), cirq.X(b), cirq.H(b), XBasisMeasurement("a").on(a), XBasisMeasurement("b").on(b), ) ) np.testing.assert_array_equal(out["a"], [0]) np.testing.assert_array_equal(out["b"], [1]) def test_correlated_error(): s = stimcirq.StimSampler() a, b = cirq.LineQubit.range(2) s.run(cirq.Circuit((cirq.X(a) * cirq.Y(b)).with_probability(0.1))) ================================================ FILE: glue/cirq/stimcirq/_stim_to_cirq.py ================================================ import collections import functools from typing import ( Any, Callable, cast, DefaultDict, Dict, Iterable, List, Optional, Tuple, Union, ) import cirq import stim from ._cx_swap_gate import CXSwapGate from ._cz_swap_gate import CZSwapGate from ._det_annotation import DetAnnotation from ._ii_error_gate import IIErrorGate from ._i_error_gate import IErrorGate from ._ii_gate import IIGate from ._measure_and_or_reset_gate import MeasureAndOrResetGate from ._obs_annotation import CumulativeObservableAnnotation from ._shift_coords_annotation import ShiftCoordsAnnotation from ._sweep_pauli import SweepPauli def _stim_targets_to_dense_pauli_string( targets: List[stim.GateTarget], ) -> cirq.BaseDensePauliString: obs = cirq.MutableDensePauliString("I" * len(targets)) for k, target in enumerate(targets): if target.is_inverted_result_target: obs *= -1 if target.is_x_target: obs.pauli_mask[k] = 1 elif target.is_y_target: obs.pauli_mask[k] = 2 elif target.is_z_target: obs.pauli_mask[k] = 3 else: raise NotImplementedError(f"target={target!r}") return obs.frozen() def _proper_transform_circuit_qubits(circuit: cirq.AbstractCircuit, remap: Dict[cirq.Qid, cirq.Qid]) -> cirq.Circuit: # Note: doing this the hard way because cirq.CircuitOperation otherwise remembers the old indices in # its `remap` entry, instead of completely expunging those indices. return cirq.Circuit( cirq.Moment( cirq.CircuitOperation( circuit=_proper_transform_circuit_qubits(op.circuit, remap).freeze(), repetitions=op.repetitions, use_repetition_ids=False, ) if isinstance(op, cirq.CircuitOperation) else op.with_qubits(*[remap[q] for q in op.qubits]) for op in moment ) for moment in circuit ) class CircuitTranslationTracker: def __init__(self, flatten: bool, single_measure_key: Optional[str] = None): self.qubit_coords: Dict[int, cirq.Qid] = {} self.origin: DefaultDict[float] = collections.defaultdict(float) self.num_measurements_seen = 0 self.full_circuit = cirq.Circuit() self.tick_circuit = cirq.Circuit() self.flatten = flatten self.have_seen_loop = False self.single_measure_key = single_measure_key def get_next_measure_id(self) -> int: self.num_measurements_seen += 1 return self.num_measurements_seen - 1 def get_next_measure_key(self) -> str: if self.single_measure_key is None: return str(self.get_next_measure_id()) return self.single_measure_key def append_operation(self, op: cirq.Operation) -> None: self.tick_circuit.append(op, strategy=cirq.InsertStrategy.INLINE) def process_gate_instruction( self, gate: cirq.Gate, instruction: stim.CircuitInstruction ) -> None: targets: List[stim.GateTarget] = instruction.targets_copy() m = cirq.num_qubits(gate) if not all(t.is_qubit_target for t in targets) or len(targets) % m != 0: raise NotImplementedError(f"instruction={instruction!r}") if instruction.tag: tags = [instruction.tag] else: tags = () for k in range(0, len(targets), m): self.append_operation(gate(*[cirq.LineQubit(t.value) for t in targets[k : k + m]]).with_tags(*tags)) def process_tick(self, instruction: stim.CircuitInstruction) -> None: self.full_circuit += self.tick_circuit or cirq.Moment() self.tick_circuit = cirq.Circuit() def process_pauli_channel_1(self, instruction: stim.CircuitInstruction) -> None: args = instruction.gate_args_copy() if len(args) != 3: raise ValueError(f"len(args={args!r}) != 3") self.process_gate_instruction( cirq.AsymmetricDepolarizingChannel(p_x=args[0], p_y=args[1], p_z=args[2]), instruction ) def process_pauli_channel_2(self, instruction: stim.CircuitInstruction) -> None: args = instruction.gate_args_copy() if len(args) != 15: raise ValueError(f"len(args={args!r}) != 15") ps = { 'IX': args[0], 'IY': args[1], 'IZ': args[2], 'XI': args[3], 'XX': args[4], 'XY': args[5], 'XZ': args[6], 'YI': args[7], 'YX': args[8], 'YY': args[9], 'YZ': args[10], 'ZI': args[11], 'ZX': args[12], 'ZY': args[13], 'ZZ': args[14], } ps = {k: v for k, v in ps.items() if v} if not ps: ps['II'] = 1 gate = cirq.asymmetric_depolarize(error_probabilities=ps) self.process_gate_instruction(gate, instruction) def process_repeat_block(self, block: stim.CircuitRepeatBlock): if self.flatten or (block.repeat_count == 1 and block.tag == ""): self.process_circuit(block.repeat_count, block.body_copy()) return self.have_seen_loop = True child = CircuitTranslationTracker(flatten=self.flatten) child.origin = self.origin.copy() child.num_measurements_seen = self.num_measurements_seen child.qubit_coords = self.qubit_coords.copy() child.have_seen_loop = True child.process_circuit(1, block.body_copy()) # Circuit operation will always be in their own cirq.Moment if block.tag == "": tags = () else: tags = (block.tag,) if len(self.tick_circuit): self.full_circuit += self.tick_circuit self.full_circuit += cirq.Moment( cirq.CircuitOperation( cirq.FrozenCircuit(child.full_circuit + child.tick_circuit), repetitions=block.repeat_count, use_repetition_ids=False, ).with_tags(*tags) ) self.tick_circuit = cirq.Circuit() self.qubit_coords = child.qubit_coords self.num_measurements_seen += ( child.num_measurements_seen - self.num_measurements_seen ) * block.repeat_count for k, v in child.origin.items(): self.origin[k] += (v - self.origin[k]) * block.repeat_count def process_measurement_instruction( self, instruction: stim.CircuitInstruction, measure: bool, reset: bool, basis: str ) -> None: args = instruction.gate_args_copy() flip_probability = 0 if args: flip_probability = args[0] targets: List[stim.GateTarget] = instruction.targets_copy() if instruction.tag: tags = [instruction.tag] else: tags = () for t in targets: if not t.is_qubit_target: raise NotImplementedError(f"instruction={instruction!r}") key = self.get_next_measure_key() self.append_operation( MeasureAndOrResetGate( measure=measure, reset=reset, basis=basis, invert_measure=t.is_inverted_result_target, key=key, measure_flip_probability=flip_probability, ).resolve(cirq.LineQubit(t.value)).with_tags(*tags) ) def process_circuit(self, repetitions: int, circuit: stim.Circuit) -> None: handler_table = CircuitTranslationTracker.get_handler_table() for _ in range(repetitions): for instruction in circuit: if isinstance(instruction, stim.CircuitInstruction): handler = handler_table.get(instruction.name) if handler is None: raise NotImplementedError(f"{instruction!r}") handler(self, instruction) elif isinstance(instruction, stim.CircuitRepeatBlock): self.process_repeat_block(instruction) else: raise NotImplementedError(f"instruction={instruction!r}") def output(self) -> cirq.Circuit: out = self.full_circuit + self.tick_circuit if self.qubit_coords: remap: Dict[cirq.Qid, cirq.Qid] = { q: self.qubit_coords.get(cast(cirq.LineQubit, q).x, q) for q in out.all_qubits() } # Only remap if there are no collisions. if len(set(remap.values())) == len(remap): # Note: doing this the hard way because cirq.CircuitOperation otherwise remembers the old indices in # its `remap` entry, instead of completely expunging those indices. out = _proper_transform_circuit_qubits(out, remap) return out def process_mpp(self, instruction: stim.CircuitInstruction) -> None: args = instruction.gate_args_copy() if args and args[0]: raise NotImplementedError("Noisy MPP") targets: List[stim.GateTarget] = instruction.targets_copy() if instruction.tag: tags = [instruction.tag] else: tags = () start = 0 while start < len(targets): next_start = start + 1 while next_start < len(targets) and targets[next_start].is_combiner: next_start += 2 group = targets[start:next_start:2] start = next_start obs = _stim_targets_to_dense_pauli_string(group) qubits = [cirq.LineQubit(t.value) for t in group] key = self.get_next_measure_key() self.append_operation(cirq.PauliMeasurementGate(obs, key=key).on(*qubits).with_tags(*tags)) def process_spp_dag(self, instruction: stim.CircuitInstruction) -> None: self.process_spp(instruction, dag=True) def process_spp(self, instruction: stim.CircuitInstruction, dag: bool = False) -> None: targets: List[stim.GateTarget] = instruction.targets_copy() if instruction.tag: tags = [instruction.tag] else: tags = () start = 0 while start < len(targets): next_start = start + 1 while next_start < len(targets) and targets[next_start].is_combiner: next_start += 2 group = targets[start:next_start:2] start = next_start obs = _stim_targets_to_dense_pauli_string(group) qubits = [cirq.LineQubit(t.value) for t in group] self.append_operation(cirq.PauliStringPhasorGate( obs, exponent_neg=-0.5 if dag else 0.5, ).on(*qubits).with_tags(*tags)) def process_m_pair(self, instruction: stim.CircuitInstruction, basis: str) -> None: args = instruction.gate_args_copy() if args and args[0]: raise NotImplementedError("Noisy M" + basis*2) if instruction.tag: tags = [instruction.tag] else: tags = () targets: List[stim.GateTarget] = instruction.targets_copy() for k in range(0, len(targets), 2): obs = cirq.DensePauliString(basis * 2) if targets[0].is_inverted_result_target ^ targets[1].is_inverted_result_target: obs *= -1 qubits = [cirq.LineQubit(targets[0].value), cirq.LineQubit(targets[1].value)] key = self.get_next_measure_key() self.append_operation(cirq.PauliMeasurementGate(obs, key=key).on(*qubits).with_tags(*tags)) def process_mxx(self, instruction: stim.CircuitInstruction) -> None: self.process_m_pair(instruction, "X") def process_myy(self, instruction: stim.CircuitInstruction) -> None: self.process_m_pair(instruction, "Y") def process_mzz(self, instruction: stim.CircuitInstruction) -> None: self.process_m_pair(instruction, "Z") def process_mpad(self, instruction: stim.CircuitInstruction) -> None: targets: List[stim.GateTarget] = instruction.targets_copy() for t in targets: obs = cirq.DensePauliString("") if t.value == 1: obs *= -1 qubits = [] key = self.get_next_measure_key() self.append_operation(cirq.PauliMeasurementGate(obs, key=key).on(*qubits)) def process_correlated_error(self, instruction: stim.CircuitInstruction) -> None: if instruction.tag: tags = [instruction.tag] else: tags = () args = instruction.gate_args_copy() probability = args[0] if args else 0 targets = instruction.targets_copy() qubits = [cirq.LineQubit(t.value) for t in targets] self.append_operation( _stim_targets_to_dense_pauli_string(targets).on(*qubits).with_probability(probability).with_tags(*tags) ) def coords_after_offset( self, relative_coords: List[float], *, even_if_flattening: bool = False ) -> List[Union[float, int]]: if not self.flatten and not even_if_flattening: return list(relative_coords) result = [] for k in range(len(relative_coords)): t = relative_coords[k] + self.origin[k] if t == int(t): t = int(t) result.append(t) return result def resolve_measurement_record_keys( self, targets: Iterable[stim.GateTarget] ) -> Tuple[List[str], List[int], List[str]]: pauli_targets, meas_targets = [], [] for t in targets: if t.is_measurement_record_target: meas_targets.append(t) else: pauli_targets.append(f'{t.pauli_type}{t.value}') if self.have_seen_loop: return [], [t.value for t in meas_targets], pauli_targets else: return [str(self.num_measurements_seen + t.value) for t in meas_targets], [], pauli_targets def process_detector(self, instruction: stim.CircuitInstruction) -> None: if instruction.tag: tags = [instruction.tag] else: tags = () coords = self.coords_after_offset(instruction.gate_args_copy()) keys, rels, _ = self.resolve_measurement_record_keys(instruction.targets_copy()) self.append_operation( DetAnnotation(parity_keys=keys, relative_keys=rels, coordinate_metadata=coords).with_tags(*tags) ) def process_observable_include(self, instruction: stim.CircuitInstruction) -> None: if instruction.tag: tags = [instruction.tag] else: tags = () args = instruction.gate_args_copy() index = 0 if not args else int(args[0]) keys, rels, paulis = self.resolve_measurement_record_keys(instruction.targets_copy()) self.append_operation( CumulativeObservableAnnotation( parity_keys=keys, relative_keys=rels, pauli_keys=paulis, observable_index=index ).with_tags(*tags) ) def process_qubit_coords(self, instruction: stim.CircuitInstruction) -> None: coords = self.coords_after_offset(instruction.gate_args_copy(), even_if_flattening=True) for t in instruction.targets_copy(): if len(coords) == 1: self.qubit_coords[t.value] = cirq.LineQubit(*coords) elif len(coords) == 2: self.qubit_coords[t.value] = cirq.GridQubit(*coords) def process_shift_coords(self, instruction: stim.CircuitInstruction) -> None: if instruction.tag: tags = [instruction.tag] else: tags = () args = instruction.gate_args_copy() if not self.flatten: self.append_operation(ShiftCoordsAnnotation(args).with_tags(*tags)) for k, a in enumerate(args): self.origin[k] += a class OneToOneGateHandler: def __init__(self, gate: cirq.Gate): self.gate = gate def __call__( self, tracker: 'CircuitTranslationTracker', instruction: stim.CircuitInstruction ) -> None: tracker.process_gate_instruction(gate=self.gate, instruction=instruction) class SweepableGateHandler: def __init__(self, pauli_gate: cirq.Pauli, gate: cirq.Gate): self.pauli_gate = pauli_gate self.gate = gate def __call__( self, tracker: 'CircuitTranslationTracker', instruction: stim.CircuitInstruction ) -> None: if instruction.tag: tags = [instruction.tag] else: tags = () targets: List[stim.GateTarget] = instruction.targets_copy() for k in range(0, len(targets), 2): a = targets[k] b = targets[k + 1] if not a.is_qubit_target and not b.is_qubit_target: raise NotImplementedError(f"instruction={instruction!r}") if a.is_sweep_bit_target or b.is_sweep_bit_target: if b.is_sweep_bit_target: a, b = b, a assert not a.is_inverted_result_target tracker.append_operation( SweepPauli( stim_sweep_bit_index=a.value, cirq_sweep_symbol=f'sweep[{a.value}]', pauli=self.pauli_gate, ).on(cirq.LineQubit(b.value)).with_tags(*tags) ) else: if not a.is_qubit_target or not b.is_qubit_target: raise NotImplementedError(f"instruction={instruction!r}") tracker.append_operation( self.gate(cirq.LineQubit(a.value), cirq.LineQubit(b.value)).with_tags(*tags) ) class OneToOneMeasurementHandler: def __init__(self, *, reset: bool, measure: bool, basis: str): self.reset = reset self.measure = measure self.basis = basis def __call__( self, tracker: 'CircuitTranslationTracker', instruction: stim.CircuitInstruction ) -> None: tracker.process_measurement_instruction( measure=self.measure, reset=self.reset, basis=self.basis, instruction=instruction ) class OneToOneNoisyGateHandler: def __init__(self, prob_to_gate: Callable[[float], cirq.Gate]): self.prob_to_gate = prob_to_gate def __call__( self, tracker: 'CircuitTranslationTracker', instruction: stim.CircuitInstruction ) -> None: tracker.process_gate_instruction( self.prob_to_gate(instruction.gate_args_copy()[0]), instruction ) class MultiArgumentGateHandler: def __init__(self, args_to_gate: Callable[[List[float]], cirq.Gate]): self.args_to_gate = args_to_gate def __call__( self, tracker: 'CircuitTranslationTracker', instruction: stim.CircuitInstruction ) -> None: tracker.process_gate_instruction( self.args_to_gate(instruction.gate_args_copy()), instruction ) @staticmethod @functools.lru_cache(maxsize=1) def get_handler_table() -> Dict[ str, Callable[['CircuitTranslationTracker', stim.Circuit], None] ]: gate = CircuitTranslationTracker.OneToOneGateHandler measure_gate = CircuitTranslationTracker.OneToOneMeasurementHandler noise = CircuitTranslationTracker.OneToOneNoisyGateHandler multi_arg = CircuitTranslationTracker.MultiArgumentGateHandler sweep_gate = CircuitTranslationTracker.SweepableGateHandler def not_impl(message) -> Callable[[Any, Any], None]: def handler( tracker: CircuitTranslationTracker, instruction: stim.CircuitInstruction ) -> None: raise NotImplementedError(message) return handler return { "M": measure_gate(measure=True, reset=False, basis='Z'), "MX": measure_gate(measure=True, reset=False, basis='X'), "MY": measure_gate(measure=True, reset=False, basis='Y'), "MZ": measure_gate(measure=True, reset=False, basis='Z'), "MR": measure_gate(measure=True, reset=True, basis='Z'), "MRX": measure_gate(measure=True, reset=True, basis='X'), "MRY": measure_gate(measure=True, reset=True, basis='Y'), "MRZ": measure_gate(measure=True, reset=True, basis='Z'), "R": gate(cirq.ResetChannel()), "RX": gate( MeasureAndOrResetGate( measure=False, reset=True, basis='X', invert_measure=False, key='' ) ), "CZSWAP": gate(CZSwapGate()), "CXSWAP": gate(CXSwapGate(inverted=False)), "SWAPCX": gate(CXSwapGate(inverted=True)), "RY": gate( MeasureAndOrResetGate( measure=False, reset=True, basis='Y', invert_measure=False, key='' ) ), "RZ": gate(cirq.ResetChannel()), "I": gate(cirq.I), "II": gate(IIGate()), "X": gate(cirq.X), "Y": gate(cirq.Y), "Z": gate(cirq.Z), "H_XY": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Y, False), z_to=(cirq.Z, True)) ), "H": gate(cirq.H), "H_XZ": gate(cirq.H), "H_NXZ": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Z, True), z_to=(cirq.X, True)) ), "H_YZ": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.X, True), z_to=(cirq.Y, False)) ), "H_NXY": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Y, True), z_to=(cirq.Z, True)) ), "H_NYZ": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.X, True), z_to=(cirq.Y, True)) ), "SQRT_X": gate(cirq.X ** 0.5), "SQRT_X_DAG": gate(cirq.X ** -0.5), "SQRT_Y": gate(cirq.Y ** 0.5), "SQRT_Y_DAG": gate(cirq.Y ** -0.5), "C_XYZ": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Y, False), z_to=(cirq.X, False)) ), "C_NXYZ": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Y, True), z_to=(cirq.X, True)) ), "C_XNYZ": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Y, True), z_to=(cirq.X, False)) ), "C_XYNZ": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Y, False), z_to=(cirq.X, True)) ), "C_ZYX": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Z, False), z_to=(cirq.Y, False)) ), "C_NZYX": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Z, True), z_to=(cirq.Y, True)) ), "C_ZNYX": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Z, False), z_to=(cirq.Y, True)) ), "C_ZYNX": gate( cirq.SingleQubitCliffordGate.from_xz_map(x_to=(cirq.Z, True), z_to=(cirq.Y, False)) ), "SQRT_XX": gate(cirq.XX ** 0.5), "SQRT_YY": gate(cirq.YY ** 0.5), "SQRT_ZZ": gate(cirq.ZZ ** 0.5), "SQRT_XX_DAG": gate(cirq.XX ** -0.5), "SQRT_YY_DAG": gate(cirq.YY ** -0.5), "SQRT_ZZ_DAG": gate(cirq.ZZ ** -0.5), "S": gate(cirq.S), "S_DAG": gate(cirq.S ** -1), "SQRT_Z": gate(cirq.S), "SQRT_Z_DAG": gate(cirq.S ** -1), "SWAP": gate(cirq.SWAP), "ISWAP": gate(cirq.ISWAP), "ISWAP_DAG": gate(cirq.ISWAP ** -1), "XCX": gate(cirq.PauliInteractionGate(cirq.X, False, cirq.X, False)), "XCY": gate(cirq.PauliInteractionGate(cirq.X, False, cirq.Y, False)), "XCZ": sweep_gate(cirq.X, cirq.PauliInteractionGate(cirq.X, False, cirq.Z, False)), "YCX": gate(cirq.PauliInteractionGate(cirq.Y, False, cirq.X, False)), "YCY": gate(cirq.PauliInteractionGate(cirq.Y, False, cirq.Y, False)), "YCZ": sweep_gate(cirq.Y, cirq.PauliInteractionGate(cirq.Y, False, cirq.Z, False)), "CX": sweep_gate(cirq.X, cirq.CNOT), "CNOT": sweep_gate(cirq.X, cirq.CNOT), "ZCX": sweep_gate(cirq.X, cirq.CNOT), "CY": sweep_gate(cirq.Y, cirq.Y.controlled(1)), "ZCY": sweep_gate(cirq.Y, cirq.Y.controlled(1)), "CZ": sweep_gate(cirq.Z, cirq.CZ), "ZCZ": sweep_gate(cirq.Z, cirq.CZ), "DEPOLARIZE1": noise(lambda p: cirq.DepolarizingChannel(p, 1)), "DEPOLARIZE2": noise(lambda p: cirq.DepolarizingChannel(p, 2)), "X_ERROR": noise(cirq.X.with_probability), "Y_ERROR": noise(cirq.Y.with_probability), "Z_ERROR": noise(cirq.Z.with_probability), "I_ERROR": multi_arg(IErrorGate), "II_ERROR": multi_arg(IIErrorGate), "PAULI_CHANNEL_1": CircuitTranslationTracker.process_pauli_channel_1, "PAULI_CHANNEL_2": CircuitTranslationTracker.process_pauli_channel_2, "ELSE_CORRELATED_ERROR": not_impl( "Converting ELSE_CORRELATED_ERROR to cirq is not supported." ), "REPEAT": not_impl("[handled special]"), "TICK": CircuitTranslationTracker.process_tick, "SHIFT_COORDS": CircuitTranslationTracker.process_shift_coords, "QUBIT_COORDS": CircuitTranslationTracker.process_qubit_coords, "E": CircuitTranslationTracker.process_correlated_error, "CORRELATED_ERROR": CircuitTranslationTracker.process_correlated_error, "MPP": CircuitTranslationTracker.process_mpp, "SPP": CircuitTranslationTracker.process_spp, "SPP_DAG": CircuitTranslationTracker.process_spp_dag, "MXX": CircuitTranslationTracker.process_mxx, "MYY": CircuitTranslationTracker.process_myy, "MZZ": CircuitTranslationTracker.process_mzz, "MPAD": CircuitTranslationTracker.process_mpad, "DETECTOR": CircuitTranslationTracker.process_detector, "OBSERVABLE_INCLUDE": CircuitTranslationTracker.process_observable_include, "HERALDED_ERASE": not_impl( "Converting HERALDED_ERASE to cirq is not supported." ), "HERALDED_PAULI_CHANNEL_1": not_impl( "Converting HERALDED_PAULI_CHANNEL_1 to cirq is not supported." ), } def stim_circuit_to_cirq_circuit( circuit: stim.Circuit, *, flatten: bool = False, single_measure_key: Optional[str] = None, ) -> cirq.Circuit: """Converts a stim circuit into an equivalent cirq circuit. Qubit indices are turned into cirq.LineQubit instances. Measurements are keyed by their ordering (e.g. the first measurement is keyed "0", the second is keyed "1", etc) unless a fixed measure_key is provided. Not all circuits can be converted: - ELSE_CORRELATED_ERROR instructions are not supported. Not all circuits can be converted with perfect 1:1 fidelity: - DETECTOR annotations are discarded. - OBSERVABLE_INCLUDE annotations are discarded. Args: circuit: The stim circuit to convert into a cirq circuit. flatten: Defaults to False. When set to True, REPEAT blocks are removed by explicitly repeating their instructions multiple times. Also, SHIFT_COORDS instructions are removed by appropriately adjusting the coordinate metadata of later instructions. single_measure_key: Defaults to None. If provided, all measurements are keyed with this string instead of sequentially generated numbers. Returns: The converted circuit. Examples: >>> import stimcirq >>> import stim >>> print(stimcirq.stim_circuit_to_cirq_circuit(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(0.25) 0 ... TICK ... M !1 0 ... '''))) 0: ───H───@───X[prob=0.25]───M('1')──── │ 1: ───────X──────────────────!M('0')─── """ tracker = CircuitTranslationTracker( flatten=flatten, single_measure_key=single_measure_key ) tracker.process_circuit(repetitions=1, circuit=circuit) return tracker.output() ================================================ FILE: glue/cirq/stimcirq/_stim_to_cirq_test.py ================================================ import itertools from typing import cast import cirq import pytest import stim import stimcirq from ._stim_to_cirq import CircuitTranslationTracker def test_stim_circuit_to_cirq_circuit(): circuit = stimcirq.stim_circuit_to_cirq_circuit( stim.Circuit( """ X 0 CNOT 0 1 X_ERROR(0.125) 0 1 CORRELATED_ERROR(0.25) X0 Y1 Z2 M 1 M !1 TICK MR 0 !1 """ ) ) a, b, c = cirq.LineQubit.range(3) assert circuit == cirq.Circuit( cirq.Moment(cirq.X(a)), cirq.Moment(cirq.CNOT(a, b)), cirq.Moment(cirq.X.with_probability(0.125).on(a), cirq.X.with_probability(0.125).on(b)), cirq.Moment(cirq.PauliString({a: cirq.X, b: cirq.Y, c: cirq.Z}).with_probability(0.25)), cirq.Moment(cirq.measure(b, key="0")), cirq.Moment(cirq.measure(b, key="1", invert_mask=(True,))), cirq.Moment( stimcirq.MeasureAndOrResetGate( measure=True, reset=True, basis='Z', invert_measure=False, key='2' ).on(a), stimcirq.MeasureAndOrResetGate( measure=True, reset=True, basis='Z', invert_measure=True, key='3' ).on(b), ), ) def assert_circuits_are_equivalent_and_convert( cirq_circuit: cirq.Circuit, stim_circuit: stim.Circuit ): assert cirq_circuit == stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) assert stim_circuit == stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) @pytest.mark.parametrize( "name", [ k for k, v in CircuitTranslationTracker.get_handler_table().items() if isinstance(v, CircuitTranslationTracker.OneToOneGateHandler) ], ) def test_gates_converted_using_OneToOneGateHandler(name: str): handler = cast( CircuitTranslationTracker.OneToOneGateHandler, CircuitTranslationTracker.get_handler_table()[name], ) gate = handler.gate n = cirq.num_qubits(gate) qs = cirq.LineQubit.range(n) cirq_original = cirq.Circuit(gate.on(*qs)) stim_targets = " ".join(str(e) for e in range(n)) stim_original = stim.Circuit(f"{name} {stim_targets}\nTICK") assert_circuits_are_equivalent_and_convert(cirq_original, stim_original) @pytest.mark.parametrize( "name", [ k for k, v in CircuitTranslationTracker.get_handler_table().items() if isinstance(v, CircuitTranslationTracker.SweepableGateHandler) ], ) def test_gates_converted_using_SweepableGateHandler(name: str): handler = cast( CircuitTranslationTracker.SweepableGateHandler, CircuitTranslationTracker.get_handler_table()[name], ) gate = handler.gate n = cirq.num_qubits(gate) qs = cirq.LineQubit.range(n) # Without sweeping. cirq_original = cirq.Circuit(gate.on(*qs)) stim_targets = " ".join(str(e) for e in range(n)) stim_original = stim.Circuit(f"{name} {stim_targets}\nTICK") assert_circuits_are_equivalent_and_convert(cirq_original, stim_original) # With sweeping. q = qs[0] cirq_original = cirq.Circuit( stimcirq.SweepPauli( stim_sweep_bit_index=3, cirq_sweep_symbol="sweep[3]", pauli=handler.pauli_gate ).on(q) ) if name.startswith("C") or name.startswith("Z"): stim_original = stim.Circuit(f"{name} sweep[3] 0\nTICK") assert_circuits_are_equivalent_and_convert(cirq_original, stim_original) else: stim_original = stim.Circuit(f"{name} 0 sweep[3]\nTICK") # Round trip loses distinction between ZCX and XCZ. assert stimcirq.stim_circuit_to_cirq_circuit(stim_original) == cirq_original @pytest.mark.parametrize( "name,probability", itertools.product( [ k for k, v in CircuitTranslationTracker.get_handler_table().items() if isinstance(v, CircuitTranslationTracker.OneToOneNoisyGateHandler) ], [0, 0.125], ), ) def test_gates_converted_using_OneToOneNoisyGateHandler(name: str, probability: float): handler = cast( CircuitTranslationTracker.OneToOneNoisyGateHandler, CircuitTranslationTracker.get_handler_table()[name], ) gate = handler.prob_to_gate(probability) n = cirq.num_qubits(gate) qs = cirq.LineQubit.range(n) cirq_original = cirq.Circuit(gate.on(*qs)) stim_targets = " ".join(str(e) for e in range(n)) stim_original = stim.Circuit(f"{name}({probability}) {stim_targets}\nTICK") assert_circuits_are_equivalent_and_convert(cirq_original, stim_original) @pytest.mark.parametrize( "name,invert,probability", itertools.product( [ k for k, v in CircuitTranslationTracker.get_handler_table().items() if isinstance(v, CircuitTranslationTracker.OneToOneMeasurementHandler) ], [False, True], [0, 0.125], ), ) def test_gates_converted_using_OneToOneMeasurementHandler( name: str, invert: bool, probability: float ): handler = cast( CircuitTranslationTracker.OneToOneMeasurementHandler, CircuitTranslationTracker.get_handler_table()[name], ) if handler.basis == 'Z' and not handler.reset and not probability: cirq_original = cirq.Circuit( cirq.measure(cirq.LineQubit(5), key="0", invert_mask=(1,) * invert) ) else: cirq_original = cirq.Circuit( stimcirq.MeasureAndOrResetGate( basis=handler.basis, key="0", invert_measure=invert, reset=handler.reset, measure_flip_probability=probability, measure=handler.measure, ).on(cirq.LineQubit(5)) ) inverted = "!" if invert else "" p_str = f"({probability})" if probability else "" stim_original = stim.Circuit(f"QUBIT_COORDS(5) 0\n{name}{p_str} {inverted}0\nTICK") assert_circuits_are_equivalent_and_convert(cirq_original, stim_original) def test_round_trip_preserves_moment_structure(): a, b = cirq.LineQubit.range(2) circuit = cirq.Circuit( cirq.Moment(), cirq.Moment(cirq.H(a)), cirq.Moment(cirq.H(b)), cirq.Moment(), cirq.Moment(), cirq.Moment(cirq.CNOT(a, b)), cirq.Moment(), ) converted = stimcirq.cirq_circuit_to_stim_circuit(circuit) restored = stimcirq.stim_circuit_to_cirq_circuit(converted) assert restored == circuit def test_circuit_diagram(): cirq.testing.assert_has_diagram( stimcirq.stim_circuit_to_cirq_circuit( stim.Circuit( """ M 9 MX 9 MY 9 MZ 9 R 9 RX 9 RY 9 RZ 9 MR 9 MRX 9 MRY 9 MRZ 9 """ ) ), """ 9: ---M('0')---MX('1')---MY('2')---M('3')---R---RX---RY---R---MR('4')---MRX('5')---MRY('6')---MR('7')--- """, use_unicode_characters=False, ) def test_all_known_gates_explicitly_handled(): gates = stim.gate_data() handled = CircuitTranslationTracker.get_handler_table().keys() for gate_name in gates: assert gate_name in handled, gate_name def test_line_grid_qubit_round_trip(): c = cirq.Circuit( cirq.H(cirq.LineQubit(101)), cirq.Z(cirq.LineQubit(200.5)), cirq.X(cirq.GridQubit(2, 3.5)), cirq.Y(cirq.GridQubit(-2, 5)), ) s = stimcirq.cirq_circuit_to_stim_circuit(c) assert s == stim.Circuit( """ QUBIT_COORDS(-2, 5) 0 QUBIT_COORDS(2, 3.5) 1 QUBIT_COORDS(101) 2 QUBIT_COORDS(200.5) 3 H 2 Z 3 X 1 Y 0 TICK """ ) c2 = stimcirq.stim_circuit_to_cirq_circuit(s) for q in c2.all_qubits(): if q == cirq.LineQubit(101): assert isinstance(cast(cirq.LineQubit, q).x, int) if q == cirq.GridQubit(2, 3.5): assert isinstance(cast(cirq.GridQubit, q).row, int) assert c2 == c assert ( stimcirq.stim_circuit_to_cirq_circuit( stim.Circuit( """ QUBIT_COORDS(10) 0 SHIFT_COORDS(1, 2, 3) QUBIT_COORDS(20, 30) 1 H 0 1 """ ) ) == cirq.Circuit( stimcirq.ShiftCoordsAnnotation([1, 2, 3]), cirq.H(cirq.LineQubit(10)), cirq.H(cirq.GridQubit(21, 32)), ) ) def test_noisy_measurements(): s = stim.Circuit( """ MX(0.125) 0 MY(0.125) 1 MZ(0.125) 2 MRX(0.125) 3 MRY(0.125) 4 MRZ(0.25) 5 TICK """ ) c = stimcirq.stim_circuit_to_cirq_circuit(s) assert c == cirq.Circuit( stimcirq.MeasureAndOrResetGate( measure=True, reset=False, basis='X', invert_measure=False, key='0', measure_flip_probability=0.125, ).on(cirq.LineQubit(0)), stimcirq.MeasureAndOrResetGate( measure=True, reset=False, basis='Y', invert_measure=False, key='1', measure_flip_probability=0.125, ).on(cirq.LineQubit(1)), stimcirq.MeasureAndOrResetGate( measure=True, reset=False, basis='Z', invert_measure=False, key='2', measure_flip_probability=0.125, ).on(cirq.LineQubit(2)), stimcirq.MeasureAndOrResetGate( measure=True, reset=True, basis='X', invert_measure=False, key='3', measure_flip_probability=0.125, ).on(cirq.LineQubit(3)), stimcirq.MeasureAndOrResetGate( measure=True, reset=True, basis='Y', invert_measure=False, key='4', measure_flip_probability=0.125, ).on(cirq.LineQubit(4)), stimcirq.MeasureAndOrResetGate( measure=True, reset=True, basis='Z', invert_measure=False, key='5', measure_flip_probability=0.25, ).on(cirq.LineQubit(5)), ) assert stimcirq.cirq_circuit_to_stim_circuit(c) == s cirq.testing.assert_equivalent_repr(c, global_vals={'stimcirq': stimcirq}) def test_convert_mpp(): s = stim.Circuit( """ MPP X2*Y5*Z3 TICK MPP Y2 X1 Z3*Z4 X 0 TICK """ ) c = cirq.Circuit( cirq.Moment( cirq.PauliMeasurementGate(cirq.DensePauliString("XYZ", coefficient=+1), key='0').on( cirq.LineQubit(2), cirq.LineQubit(5), cirq.LineQubit(3) ) ), cirq.Moment( cirq.PauliMeasurementGate(cirq.DensePauliString("Y"), key='1').on(cirq.LineQubit(2)), cirq.PauliMeasurementGate(cirq.DensePauliString("X"), key='2').on(cirq.LineQubit(1)), cirq.PauliMeasurementGate(cirq.DensePauliString("ZZ"), key='3').on( cirq.LineQubit(3), cirq.LineQubit(4) ), cirq.X(cirq.LineQubit(0)), ), ) assert_circuits_are_equivalent_and_convert(c, s) cirq.testing.assert_has_diagram( c, """ 0: ---------------X----------- 1: ---------------M(X)('2')--- 2: ---M(X)('0')---M(Y)('1')--- | 3: ---M(Z)--------M(Z)('3')--- | | 4: ---|-----------M(Z)-------- | 5: ---M(Y)-------------------- """, use_unicode_characters=False, ) def test_convert_detector(): s = stim.Circuit( """ H 0 TICK CNOT 0 1 TICK M 1 0 DETECTOR(2, 3, 5) rec[-1] rec[-2] TICK """ ) a, b = cirq.LineQubit.range(2) c = cirq.Circuit( cirq.H(a), cirq.CNOT(a, b), cirq.Moment( cirq.measure(b, key='0'), cirq.measure(a, key='1'), stimcirq.DetAnnotation(parity_keys=['0', '1'], coordinate_metadata=(2, 3, 5)), ), ) cirq.testing.assert_has_diagram( c, """ 0: ---H---@---M('1')--------- | 1: -------X---M('0')--------- Det('0','1') """, use_unicode_characters=False, ) assert_circuits_are_equivalent_and_convert(c, s) def test_convert_observable(): s = stim.Circuit( """ H 0 TICK CNOT 0 1 TICK M 1 0 OBSERVABLE_INCLUDE(5) rec[-1] rec[-2] TICK """ ) a, b = cirq.LineQubit.range(2) c = cirq.Circuit( cirq.H(a), cirq.CNOT(a, b), cirq.Moment( cirq.measure(b, key='0'), cirq.measure(a, key='1'), stimcirq.CumulativeObservableAnnotation(parity_keys=['0', '1'], observable_index=5), ), ) cirq.testing.assert_has_diagram( c, """ 0: ---H---@---M('1')---------- | 1: -------X---M('0')---------- Obs5('0','1') """, use_unicode_characters=False, ) assert_circuits_are_equivalent_and_convert(c, s) def test_sweep_target(): stim_circuit = stim.Circuit( """ CX sweep[2] 0 CY sweep[3] 1 CZ sweep[5] 2 sweep[7] 3 TICK """ ) a, b, c, d = cirq.LineQubit.range(4) cirq_circuit = cirq.Circuit( stimcirq.SweepPauli(stim_sweep_bit_index=2, cirq_sweep_symbol="sweep[2]", pauli=cirq.X).on( a ), stimcirq.SweepPauli(stim_sweep_bit_index=3, cirq_sweep_symbol="sweep[3]", pauli=cirq.Y).on( b ), stimcirq.SweepPauli(stim_sweep_bit_index=5, cirq_sweep_symbol="sweep[5]", pauli=cirq.Z).on( c ), stimcirq.SweepPauli(stim_sweep_bit_index=7, cirq_sweep_symbol="sweep[7]", pauli=cirq.Z).on( d ), ) cirq.testing.assert_has_diagram( cirq_circuit, """ 0: ---X^sweep[2]='sweep[2]'--- 1: ---Y^sweep[3]='sweep[3]'--- 2: ---Z^sweep[5]='sweep[5]'--- 3: ---Z^sweep[7]='sweep[7]'--- """, use_unicode_characters=False, ) assert_circuits_are_equivalent_and_convert(cirq_circuit, stim_circuit) def test_convert_repeat_simple(): stim_circuit = stim.Circuit( """ REPEAT 1000000 { H 0 TICK CNOT 0 1 TICK } M 0 TICK """ ) a, b = cirq.LineQubit.range(2) cirq_circuit = cirq.Circuit( cirq.CircuitOperation( cirq.FrozenCircuit(cirq.H(a), cirq.CNOT(a, b)), repetitions=1000000, use_repetition_ids=False, ), cirq.measure(a, key="0"), ) assert stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) == cirq_circuit assert stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) == stim_circuit def test_convert_repeat_measurements(): stim_circuit = stim.Circuit( """ QUBIT_COORDS(2, 3) 0 QUBIT_COORDS(4, 5) 1 REPEAT 1000000 { H 0 TICK CNOT 0 1 TICK M 0 1 DETECTOR(5) rec[-1] rec[-2] SHIFT_COORDS(2) TICK } M 0 DETECTOR(7) rec[-1] rec[-3] TICK """ ) a, b = cirq.GridQubit(2, 3), cirq.GridQubit(4, 5) cirq_circuit = cirq.Circuit( cirq.Moment( cirq.CircuitOperation( cirq.FrozenCircuit( cirq.Moment(cirq.H(a)), cirq.Moment(cirq.CNOT(a, b)), cirq.Moment( cirq.measure(a, key=cirq.MeasurementKey("0")), cirq.measure(b, key=cirq.MeasurementKey("1")), stimcirq.DetAnnotation(relative_keys=[-1, -2], coordinate_metadata=[5]), stimcirq.ShiftCoordsAnnotation([2]), ), ), repetitions=1000000, use_repetition_ids=False, ) ), cirq.Moment( cirq.measure(a, key="2000000"), stimcirq.DetAnnotation(relative_keys=[-1, -3], coordinate_metadata=[7]), ), ) assert stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) == cirq_circuit assert stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) == stim_circuit def test_single_repeat_loops_always_flattened(): assert stimcirq.stim_circuit_to_cirq_circuit(stim.Circuit( """ REPEAT 1 { H 0 } """)) == cirq.Circuit(cirq.H(cirq.LineQubit(0))) assert stimcirq.cirq_circuit_to_stim_circuit(cirq.Circuit( cirq.CircuitOperation( cirq.FrozenCircuit( cirq.H(cirq.LineQubit(0)), ), repetitions=1, ), )) == stim.Circuit("H 0\nTICK") def test_circuit_operations_always_in_isolated_moment(): stim_circuit = stim.Circuit( """ REPEAT 2 { H 0 1 } H 2 3 """ ) cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) qs = cirq.LineQubit.range(4) expected_cirq_circuit = cirq.Circuit( cirq.CircuitOperation( circuit=cirq.FrozenCircuit([cirq.H(qs[0]),cirq.H(qs[1])]), repetitions=2, use_repetition_ids=False ), cirq.Moment(cirq.H(qs[2]),cirq.H(qs[3])), ) assert cirq_circuit == expected_cirq_circuit @pytest.mark.skip(reason="blocked by https://github.com/quantumlib/Cirq/issues/6136") def test_stim_circuit_to_cirq_circuit_mpad(): stim_circuit = stim.Circuit(""" MPAD 0 1 """) cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) assert cirq_circuit == cirq.Circuit( cirq.PauliMeasurementGate(cirq.DensePauliString(""), key="0").on(), cirq.PauliMeasurementGate(cirq.DensePauliString("", coefficient=-1), key="1").on(), ) def test_stim_circuit_to_cirq_circuit_mxx_myy_mzz(): stim_circuit = stim.Circuit(""" MXX 0 1 MYY 0 !1 MZZ !0 1 """) cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) a, b = cirq.LineQubit.range(2) assert cirq_circuit == cirq.Circuit( cirq.PauliMeasurementGate(cirq.DensePauliString("XX"), key='0').on(a, b), cirq.PauliMeasurementGate(cirq.DensePauliString("YY", coefficient=-1), key='1').on(a, b), cirq.PauliMeasurementGate(cirq.DensePauliString("ZZ", coefficient=-1), key='2').on(a, b), ) assert stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) == stim.Circuit(""" MPP X0*X1 TICK MPP !Y0*Y1 TICK MPP !Z0*Z1 TICK """) def test_stim_circuit_to_cirq_circuit_spp(): stim_circuit = stim.Circuit(""" SPP X1*Y2*Z3 SPP !Y4 SPP_DAG X5 SPP_DAG !Z0 """) cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) assert cirq_circuit.to_text_diagram(use_unicode_characters=False).strip() == """ 0: ---[Z]^0.5---- 1: ---[X]-------- | 2: ---[Y]-------- | 3: ---[Z]^0.5---- 4: ---[Y]^-0.5--- 5: ---[X]^-0.5--- """.strip() q0, q1, q2, q3, q4, q5 = cirq.LineQubit.range(6) assert cirq_circuit == cirq.Circuit([ cirq.Moment( cirq.PauliStringPhasor(cirq.X(q1)*cirq.Y(q2)*cirq.Z(q3), exponent_neg=0.5), cirq.PauliStringPhasor(cirq.Y(q4), exponent_neg=0, exponent_pos=0.5), cirq.PauliStringPhasor(cirq.X(q5), exponent_neg=-0.5), cirq.PauliStringPhasor(cirq.Z(q0), exponent_neg=0, exponent_pos=-0.5), ), ]) assert stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) == stim.Circuit(""" SPP X1*Y2*Z3 SPP_DAG Y4 X5 SPP Z0 TICK """) def test_tags_convert(): assert stimcirq.stim_circuit_to_cirq_circuit(stim.Circuit(""" H[my_tag] 0 """)) == cirq.Circuit( cirq.H(cirq.LineQubit(0)).with_tags('my_tag'), ) @pytest.mark.parametrize('gate', sorted(stim.gate_data().keys())) def test_every_operation_converts_tags(gate: str): if gate in [ "ELSE_CORRELATED_ERROR", "HERALDED_ERASE", "HERALDED_PAULI_CHANNEL_1", "TICK", "REPEAT", "MPAD", "QUBIT_COORDS", ]: pytest.skip() data = stim.gate_data(gate) stim_circuit = stim.Circuit() arg = None targets = [0, 1] if data.num_parens_arguments_range.start: arg = [2**-6] * data.num_parens_arguments_range.start if data.takes_pauli_targets: targets = [stim.target_x(0), stim.target_y(1)] if data.takes_measurement_record_targets and not data.is_unitary: stim_circuit.append("M", [0], tag='custom_tag') targets = [stim.target_rec(-1)] if gate == 'SHIFT_COORDS': targets = [] if gate == 'OBSERVABLE_INCLUDE': arg = [1] stim_circuit.append(gate, targets, arg, tag='custom_tag') cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) assert any(cirq_circuit.all_operations()) for op in cirq_circuit.all_operations(): assert op.tags == ('custom_tag',) restored_circuit = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) assert restored_circuit.pop() == stim.CircuitInstruction("TICK") assert all(instruction.tag == 'custom_tag' for instruction in restored_circuit) if gate not in ['MXX', 'MYY', 'MZZ']: assert restored_circuit == stim_circuit def test_loop_tagging(): stim_circuit = stim.Circuit(""" REPEAT[custom-tag] 5 { H[tag2] 0 TICK } """) cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) assert cirq_circuit == cirq.Circuit( cirq.CircuitOperation( cirq.FrozenCircuit( cirq.H(cirq.LineQubit(0)).with_tags('tag2'), ), repetitions=5, use_repetition_ids=False, ).with_tags('custom-tag') ) restored_circuit = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) assert restored_circuit == stim_circuit def test_id_error_round_trip(): stim_circuit = stim.Circuit(""" I_ERROR 0 I_ERROR(0.125, 0.25) 1 II_ERROR(0.25, 0.125) 2 3 II_ERROR 4 5 TICK """) cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) restored_circuit = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) assert restored_circuit == stim_circuit def test_round_trip_with_pauli_obs(): stim_circuit = stim.Circuit(""" QUBIT_COORDS(5, 5) 0 R 0 OBSERVABLE_INCLUDE(0) X0 TICK H 0 TICK M 0 OBSERVABLE_INCLUDE(0) rec[-1] TICK """) cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit) restored_circuit = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) assert restored_circuit == stim_circuit def test_single_measure_key_order(): stim_circuits = [ stim.Circuit( """ X 1 X 1 3 X 1 3 X 1 3 2 M 1 M 3 M 2 M 0 """ ), stim.Circuit( """ X 1 X 1 X 1 X 1 M 1 3 X 2 M 2 0 """ ) ] measure_key = "m" for stim_circuit in stim_circuits: cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit( stim_circuit, single_measure_key=measure_key ) qubits = cirq.LineQubit.range(4) expected_order = [ qubits[targ.qubit_value] for inst in stim_circuit if inst.name == "M" for targ in inst.targets_copy() ] actual_order = [] for op in cirq_circuit.all_operations(): if isinstance(op.gate, cirq.MeasurementGate): assert op.gate.key == measure_key assert len(op.qubits) == 1 actual_order.append(op.qubits[0]) assert expected_order == actual_order ================================================ FILE: glue/cirq/stimcirq/_sweep_pauli.py ================================================ from typing import AbstractSet, Any, Dict, List, Optional, Union import cirq import stim import sympy @cirq.value_equality class SweepPauli(cirq.Gate): """A Pauli gate included depending on sweep data, with compatibility across stim and cirq. Includes a sweep bit index for stim and a sweep symbol for cirq. Creates sweep-controlled operations like `CX sweep[5] 0` when converting to a stim circuit. """ def __init__( self, *, stim_sweep_bit_index: int, cirq_sweep_symbol: Optional[Union[str, sympy.Symbol]] = None, pauli: cirq.Pauli, ): r""" Args: stim_sweep_bit_index: The bit position, in some unspecified array, controlling the Pauli. cirq_sweep_symbol: The symbol used by cirq. Defaults to f"sweep_{sweep_bit_index}". pauli: The cirq Pauli operation to apply when the bit is True. """ if cirq_sweep_symbol is None: cirq_sweep_symbol = f"sweep[{stim_sweep_bit_index}]" self.stim_sweep_bit_index = stim_sweep_bit_index self.cirq_sweep_symbol = cirq_sweep_symbol self.pauli = pauli def _decomposed_into_pauli(self) -> cirq.Gate: return self.pauli ** self.cirq_sweep_symbol def _resolve_parameters_(self, resolver: cirq.ParamResolver, recursive: bool) -> cirq.Gate: new_value = resolver.value_of(self.cirq_sweep_symbol, recursive=recursive) if str(new_value) == str(self.cirq_sweep_symbol): return self return self.pauli ** new_value def _is_parameterized_(self) -> bool: return True def _parameter_names_(self) -> AbstractSet[str]: return {str(self.cirq_sweep_symbol)} def _decompose_(self, qubits) -> cirq.OP_TREE: return self.pauli.on(*qubits) ** self.cirq_sweep_symbol def _num_qubits_(self) -> int: return 1 def _value_equality_values_(self) -> Any: return self.pauli, self.stim_sweep_bit_index, self.cirq_sweep_symbol def _circuit_diagram_info_(self, args: Any) -> str: return f"{self.pauli}^sweep[{self.stim_sweep_bit_index}]='{self.cirq_sweep_symbol}'" @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return { 'pauli': self.pauli, 'stim_sweep_bit_index': self.stim_sweep_bit_index, 'cirq_sweep_symbol': self.cirq_sweep_symbol, } def __repr__(self) -> str: return ( f'stimcirq.SweepPauli(' f'pauli={self.pauli!r}, ' f'stim_sweep_bit_index={self.stim_sweep_bit_index!r}, ' f'cirq_sweep_symbol={self.cirq_sweep_symbol!r})' ) def _stim_conversion_(self, edit_circuit: stim.Circuit, targets: List[int], **kwargs): edit_circuit.append_operation( f"C{self.pauli}", [stim.target_sweep_bit(self.stim_sweep_bit_index)] + targets ) ================================================ FILE: glue/cirq/stimcirq/_sweep_pauli_test.py ================================================ import cirq import numpy as np import stim import stimcirq def test_repr(): cirq.testing.assert_equivalent_repr( stimcirq.SweepPauli(stim_sweep_bit_index=1, pauli=cirq.X, cirq_sweep_symbol="a"), global_vals={"stimcirq": stimcirq}, ) cirq.testing.assert_equivalent_repr( stimcirq.SweepPauli(stim_sweep_bit_index=2, pauli=cirq.Y, cirq_sweep_symbol="b"), global_vals={"stimcirq": stimcirq}, ) def test_stim_conversion(): actual = stimcirq.cirq_circuit_to_stim_circuit( cirq.Circuit( stimcirq.SweepPauli( stim_sweep_bit_index=5, pauli=cirq.X, cirq_sweep_symbol="init_66" ).on(cirq.GridQubit(1, 3)), stimcirq.SweepPauli( stim_sweep_bit_index=7, pauli=cirq.Y, cirq_sweep_symbol="init_63" ).on(cirq.GridQubit(2, 4)), stimcirq.SweepPauli( stim_sweep_bit_index=5, pauli=cirq.Z, cirq_sweep_symbol="init_66" ).on(cirq.GridQubit(2, 4)), ) ) assert actual == stim.Circuit( """ QUBIT_COORDS(1, 3) 0 QUBIT_COORDS(2, 4) 1 CX sweep[5] 0 CY sweep[7] 1 TICK CZ sweep[5] 1 TICK """ ) def test_resolving_without_sweep_resolver(): cirq_circuit = cirq.Circuit( stimcirq.SweepPauli( stim_sweep_bit_index=0, pauli=cirq.X, cirq_sweep_symbol="init_0" ).on(cirq.LineQubit(0)), stimcirq.SweepPauli( stim_sweep_bit_index=1, pauli=cirq.X, cirq_sweep_symbol="init_1" ).on(cirq.LineQubit(1)), ) # Resolve the cirq circuit without specifying the sweep_symbol (this should leave the op # unchanged). resolved_circuit = cirq.resolve_parameters(cirq_circuit, {"init_1":1}) op0, op1, = resolved_circuit.all_operations() assert isinstance(op0.gate, stimcirq.SweepPauli) # Operation should still be a SweepPauli assert isinstance(op1.gate, type(cirq.X)) # Operation should be converted to a cirq.X # Conversion to stim still possible assert stimcirq.cirq_circuit_to_stim_circuit(resolved_circuit) def test_parameter_names(): cirq_circuit = cirq.Circuit( stimcirq.SweepPauli( stim_sweep_bit_index=0, pauli=cirq.X, cirq_sweep_symbol="init_0" ).on(cirq.LineQubit(0)), stimcirq.SweepPauli( stim_sweep_bit_index=1, pauli=cirq.X, cirq_sweep_symbol="init_1" ).on(cirq.LineQubit(1)), ) assert cirq.parameter_names(cirq_circuit) == {'init_0', 'init_1'} def test_cirq_compatibility(): circuit = cirq.Circuit( stimcirq.SweepPauli(stim_sweep_bit_index=5, pauli=cirq.X, cirq_sweep_symbol="xx").on( cirq.LineQubit(0) ), cirq.measure(cirq.LineQubit(0), key="k"), ) np.testing.assert_array_equal( cirq.Simulator().sample(circuit, params={"xx": 0}, repetitions=3)["k"], [0, 0, 0] ) np.testing.assert_array_equal( cirq.Simulator().sample(circuit, params={"xx": 1}, repetitions=3)["k"], [1, 1, 1] ) circuit = cirq.Circuit( stimcirq.SweepPauli(stim_sweep_bit_index=5, pauli=cirq.Z, cirq_sweep_symbol="xx").on( cirq.LineQubit(0) ), cirq.measure(cirq.LineQubit(0), key="k"), ) np.testing.assert_array_equal( cirq.Simulator().sample(circuit, params={"xx": 0}, repetitions=3)["k"], [0, 0, 0] ) np.testing.assert_array_equal( cirq.Simulator().sample(circuit, params={"xx": 1}, repetitions=3)["k"], [0, 0, 0] ) circuit = cirq.Circuit( cirq.H(cirq.LineQubit(0)), stimcirq.SweepPauli(stim_sweep_bit_index=5, pauli=cirq.Z, cirq_sweep_symbol="xx").on( cirq.LineQubit(0) ), cirq.H(cirq.LineQubit(0)), cirq.measure(cirq.LineQubit(0), key="k"), ) np.testing.assert_array_equal( cirq.Simulator().sample(circuit, params={"xx": 0}, repetitions=3)["k"], [0, 0, 0] ) np.testing.assert_array_equal( cirq.Simulator().sample(circuit, params={"xx": 1}, repetitions=3)["k"], [1, 1, 1] ) def test_json_serialization(): c = cirq.Circuit( stimcirq.SweepPauli(stim_sweep_bit_index=5, pauli=cirq.X, cirq_sweep_symbol="init_66").on( cirq.GridQubit(1, 3) ), stimcirq.SweepPauli(stim_sweep_bit_index=7, pauli=cirq.Y, cirq_sweep_symbol="init_63").on( cirq.GridQubit(2, 4) ), ) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.SweepPauli(stim_sweep_bit_index=2, pauli=cirq.Z, cirq_sweep_symbol="abc") packed = '{\n "cirq_type": "SweepPauli",\n "pauli": {\n "cirq_type": "_PauliZ",\n "exponent": 1.0,\n "global_shift": 0.0\n },\n "stim_sweep_bit_index": 2,\n "cirq_sweep_symbol": "abc"\n}' assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed ================================================ FILE: glue/cirq/stimcirq/_two_qubit_asymmetric_depolarize.py ================================================ from typing import Any, Dict, List, Sequence, Tuple import cirq import stim @cirq.value_equality class TwoQubitAsymmetricDepolarizingChannel(cirq.Gate): """A two qubit error channel that applies Pauli pairs according to the given probability distribution. The 15 probabilities are in IX, IY, ... ZY, ZZ order. II is skipped; it's the leftover. This class is no longer used by stimcirq, but is kept around for backwards compatibility of json-serialized circuits. """ def __init__(self, probabilities: Sequence[float]): if len(probabilities) != 15: raise ValueError(f"len(probabilities={probabilities}) != 15") if sum(probabilities) > 1.000001: raise ValueError(f"sum(probabilities={probabilities}) > 1") self.probabilities = tuple(probabilities) def _num_qubits_(self): return 2 def _value_equality_values_(self): return self.probabilities def _has_mixture_(self): return True def _dense_mixture_(self): result = [(1 - sum(self.probabilities), cirq.DensePauliString([0, 0]))] result.extend( [ (p, cirq.DensePauliString([((k + 1) >> 2) & 3, (k + 1) & 3])) for k, p in enumerate(self.probabilities) ] ) return [(p, g) for p, g in result if p] def _mixture_(self): return [(p, cirq.unitary(g)) for p, g in self._dense_mixture_()] def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs): result = [] for p, d in self._dense_mixture_(): result.append(str(d)[1:] + ":" + args.format_real(p)) return "PauliMix(" + ",".join(result) + ")", "#2" def _stim_conversion_(self, edit_circuit: stim.Circuit, targets: List[int], **kwargs): edit_circuit.append_operation("PAULI_CHANNEL_2", targets, self.probabilities) def __repr__(self): return f"stimcirq.TwoQubitAsymmetricDepolarizingChannel({self.probabilities!r})" @staticmethod def _json_namespace_() -> str: return '' def _json_dict_(self) -> Dict[str, Any]: return {'probabilities': list(self.probabilities)} ================================================ FILE: glue/cirq/stimcirq/_two_qubit_asymmetric_depolarize_test.py ================================================ import cirq import stim import stimcirq def test_mixture(): r = stimcirq.TwoQubitAsymmetricDepolarizingChannel( [0.125, 0, 0, 0, 0, 0, 0.375, 0, 0, 0, 0, 0, 0, 0.25, 0] ) assert r._dense_mixture_() == [ (0.25, cirq.DensePauliString("II")), (0.125, cirq.DensePauliString("IX")), (0.375, cirq.DensePauliString("XZ")), (0.25, cirq.DensePauliString("ZY")), ] def test_diagram(): r = stimcirq.TwoQubitAsymmetricDepolarizingChannel( [0.125, 0, 0, 0, 0, 0, 0.375, 0, 0, 0, 0, 0, 0, 0.25, 0] ) cirq.testing.assert_has_diagram( cirq.Circuit(r.on(*cirq.LineQubit.range(2))), """ 0: ---PauliMix(II:0.25,IX:0.125,XZ:0.375,ZY:0.25)--- | 1: ---#2-------------------------------------------- """, use_unicode_characters=False, ) def test_repr(): r = stimcirq.TwoQubitAsymmetricDepolarizingChannel( [0.125, 0, 0, 0, 0, 0, 0.375, 0, 0, 0, 0, 0, 0, 0.25, 0] ) assert eval(repr(r), {'stimcirq': stimcirq}) == r def test_json_serialization(): r = stimcirq.TwoQubitAsymmetricDepolarizingChannel( [0.0125, 0.1, 0, 0.23, 0, 0, 0.0375, 0, 0, 0, 0, 0, 0, 0.25, 0] ) c = cirq.Circuit(r(cirq.LineQubit(0), cirq.LineQubit(1))) json = cirq.to_json(c) c2 = cirq.read_json(json_text=json, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) assert c == c2 def test_json_backwards_compat_exact(): raw = stimcirq.TwoQubitAsymmetricDepolarizingChannel([0.0125, 0.1, 0, 0.23, 0, 0, 0.0375, 0, 0.01, 0, 0, 0, 0, 0.25, 0]) packed = '{\n "cirq_type": "TwoQubitAsymmetricDepolarizingChannel",\n "probabilities": [\n 0.0125,\n 0.1,\n 0,\n 0.23,\n 0,\n 0,\n 0.0375,\n 0,\n 0.01,\n 0,\n 0,\n 0,\n 0,\n 0.25,\n 0\n ]\n}' assert cirq.read_json(json_text=packed, resolvers=[*cirq.DEFAULT_RESOLVERS, stimcirq.JSON_RESOLVER]) == raw assert cirq.to_json(raw) == packed def test_native_cirq_gate_converts(): c = cirq.Circuit(cirq.asymmetric_depolarize( error_probabilities={ 'IX': 0.125, 'ZY': 0.25 }).on(cirq.LineQubit(0), cirq.LineQubit(1))) s = stim.Circuit(""" PAULI_CHANNEL_2(0.125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.25, 0) 0 1 TICK """) assert stimcirq.cirq_circuit_to_stim_circuit(c) == s assert stimcirq.stim_circuit_to_cirq_circuit(s) == c ================================================ FILE: glue/crumble/README.md ================================================ # Crumble Crumble is an in-development tool for exploring and inspecting 2D stabilizer circuits, with a focus on quantum error correction. In particular, crumble automates the process of propagating and verifying the flow of Pauli products across the layers of the circuit. **Crumble is still being prototyped and developed. Crumble is not stable. Crumble is not polished.** ## Index - [Accessing Crumble](#accessing-crumble) - [Using Crumble](#using-crumble) - [Loading and Saving Circuits](#loading-saving) - [Keyboard Controls](#keyboard-commands) - [Mouse Controls](#mouse-commands) - [Building Crumble](#building-crumble) - [Testing Crumble](#testing-crumble) # Accessing Crumble Crumble can be accessed by installing stim `1.11` or later (e.g. by `pip install stim~=1.11`), printing the output of `stim.Circuit().diagram("interactive")` to an HTML file, and then opening the HTML file in a browser. Crumble can also be accessed by [building it](#building-crumble). # Using Crumble There are two core pieces to using crumble effectively: (1) editing the circuit and (2) propagating Paulis. Editing the circuit is done by selecting qubits with the mouse and hitting keyboard keys to place gates. The layers of the circuit can be navigated using the Q/E keys. Propagating Paulis is done by placing *markers* to indicate where to add terms. Each marker has a type (X, Y, or Z) and an index (0-9) indicating which indexed Pauli product the marker is modifying. For example, to place a Z term after a reset gate into the Pauli product with index 1, select the reset gate and press `Z`+`1`. This introduces a Z term into the Pauli product, and advancing through the circuit will show how the now non-empty Pauli product changes as it is rewritten by the circuit's stabilizer operations. The simplest way to use markers is as a method for seeing how errors propagate through the circuit. But the far more useful case is for seeing how *knowledge* propagates through the circuit. After a qubit is reset, its Z observable is in a known state. As the circuit executes, this piece of knowledge is transformed into different forms; it may later correspond to knowing a multi-qubit Pauli product or to knowing what the result of a measurement must be. Pauli propagation shows how these pieces of knowledge move through the circuit. Of particular interest is finding small sets of resets that match small sets of nearby measurements (i.e. resets that prepare knowledge predicting the parity of a set of measurements). These local reset-vs-measurement tautologies correspond to detectors, which can be used to correct errors. The path that the Pauli product takes, starting from the various resets and terminating on the various measurements, forms the detecting region of the detector. ## Loading and Saving Circuits - **Bookmarking**: As crumble runs, it constantly updates the web page's address so that it encodes the current circuit. For example, for an empty circuit, the URL will end with `#circuit=` whereas a circuit with a single Hadamard gate would end with `#circuit=Q(0,0)0;H_0`. Thus, the current circuit can be saved by bookmarking the page and loaded again later by opening the bookmark. - **Importing and Exporting**: In the top left of the page, there is a button "Show Import/Export". Clicking this button will reveal a large textbox containing the current circuit encoded as a Stim circuit. To save the circuit, copy this text and write it to a text file on your computer. The saved circuit can later be loaded by copying the file's contents to your clipboard, pasting over the contents of the textbox, and hitting the "↓ Import from Stim Circuit ↓" button below the textbox. The import/export text box can be hidden by clicking the "Show Import/Export" button (now labelled "Hide Import/Export") again. ## Keyboard Controls **Pauli Propagation** - `spacebar`: Clear all Pauli propagation markers at current selection. - `#`: Puts a Pauli propagation marker at the current selection for the indexed Pauli product. `#` can be any of 1, 2, 3, etc and determines which of the tracked Pauli products the marker goes into. For example, `2` will place a marker that multiplies a Pauli term into tracked Pauli product #2. The basis of the marker is inferred from context (e.g. if an `RX` gate is selected, it will be an `X` marker). - `x+#`: Put an X-type Pauli propagation marker at the current selection for the indexed Pauli product. - `y+#`: Put a Y-type Pauli propagation marker at the current selection for the indexed Pauli product. - `z+#`: Put a Z-type Pauli propagation marker at the current selection for the indexed Pauli product. - `d+#`: Converts the indexed Pauli product into a circuit `DETECTOR` declaration. - `o+#`: Converts the indexed Pauli product into a circuit `OBSERVABLE_INCLUDE` declaration. - `j+#`: Picks a `DETECTOR` or `OBSERVABLE_INCLUDE` declaration touching the current selection and converts it into a tracked Pauli product. - `k+#`: Add a marker to any dissipative gate that the indexed Pauli product overlaps in the current layer. **Editing** - `p`: Add a background polygon with corners at the current selection. The color of the polygon is affected by modifier keys: X, Y, Z, alt, shift. - `e`: Move to next layer. - `q`: Move to previous layer. - `shift+e`: Move forward 5 layers. - `shift+q`: Move backward 5 layers. - `escape`: Unselect. Set current selection to the empty set. - `delete`: Delete gates at current selection. - `backspace`: Delete gates at current selection. - `ctrl+delete`: Delete current circuit layer. - `ctrl+backspace`: Delete current circuit layer. - `ctrl+insert`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. - `ctrl+z`: Undo - `ctrl+y`: Redo - `ctrl+shift+z`: Redo - `ctrl+c`: Copy selection to clipboard (or entire layer if nothing selected). - `ctrl+v`: Past clipboard contents at current selection (or entire layer if nothing selected). - `ctrl+x`: Cut selection to clipboard (or entire layer if nothing selected). - `f`: Reverse direction of selected two qubit gates (e.g. exchange the controls and targets of a CNOT). - `g`: Reverse order of circuit layers, from the current layer to the next empty layer. - `home`: Jump to the first layer of the circuit. - `end`: Jump to the last layer of the circuit. - `t`: Rotate circuit 45 degrees clockwise. - `shift+t`: Rotate circuit 45 degrees counter-clockwise. - `v`: Translate circuit down one step. - `^`: Translate circuit up one step. - `>`: Translate circuit right one step. - `<`: Translate circuit left one step. - `.`: Translate circuit down and right by a half step. **Single Qubit Gates** Note: use `shift` to get the inverse of a gate. - `h`: Overwrite selection with `H` gate - `s`: Overwrite selection with `SQRT_Z` gate - `r`: Overwrite selection with `R` gate - `m`: Overwrite selection with `M` gate - `h+z` or `h+x+y`: Overwrite selection with `H_XY` gate - `h+x` or `h+y+z`: Overwrite selection with `H_YZ` gate - `s+x`: Overwrite selection with `SQRT_X` gate - `s+y`: Overwrite selection with `SQRT_Y` gate - `r+x`: Overwrite selection with `RX` gate - `r+y`: Overwrite selection with `RY` gate - `m+x`: Overwrite selection with `MX` gate - `m+y`: Overwrite selection with `MY` gate - `m+r+x`: Overwrite selection with `MRX` gate - `m+r+y`: Overwrite selection with `MRY` gate - `m+r`: Overwrite selection with `MR` gate - `c+t`: Overwrite selection with `C_XYZ` gate - `j+x`: Overwrite selection with **j**ust a Pauli `X` gate - `j+y`: Overwrite selection with **j**ust a Pauli `Y` gate - `j+z`: Overwrite selection with **j**ust a Pauli `Z` gate - `shift+c+t`: Overwrite selection with `C_ZYX` gate **Two Qubit Gates** Note: when a single qubit is selected and a two qubit gate is placed, the gate spans between the selected qubit and *the qubit the mouse is hovering over*. When multiple qubits are selected, the difference between the topmost leftmost qubit and *the qubit the mouse is hovering over* is computed, and then each selected qubit targets its position offset by that difference. Note: use `shift` to get the inverse of a gate. - `w`: Overwrite selection with `SWAP` gate targeting mouse - `w+i`: Overwrite selection with `ISWAP` gate targeting mouse - `w+x`: Overwrite selection with `CXSWAP` gate targeting mouse - `c+x`: Overwrite selection with `CX` gate targeting mouse - `c+y`: Overwrite selection with `CY` gate targeting mouse - `c+z`: Overwrite selection with `CZ` gate targeting mouse - `c+x+y`: Overwrite selection with `XCY` gate targeting mouse - `alt+c+x`: Overwrite selection with `XCX` gate targeting mouse - `alt+c+y`: Overwrite selection with `YCY` gate targeting mouse - `c+s+x`: Overwrite selection with `SQRT_XX` gate targeting mouse - `c+s+y`: Overwrite selection with `SQRT_YY` gate targeting mouse - `c+s+z`: Overwrite selection with `SQRT_ZZ` gate targeting mouse - `c+m+x`: Overwrite selection with `MXX` gate targeting mouse - `c+m+y`: Overwrite selection with `MYY` gate targeting mouse - `c+m+z`: Overwrite selection with `MZZ` gate targeting mouse **Multi Qubit Gates** - `m+p+x`: Overwrite selection with a single `MPP` gate targeting the tensor product of X on each selected qubit. - `m+p+y`: Overwrite selection with a single `MPP` gate targeting the tensor product of Y on each selected qubit. - `m+p+z`: Overwrite selection with a single `MPP` gate targeting the tensor product of Z on each selected qubit. **Keyboard Buttons as Gate Adjectives** Roughly speaking, the "keyboard language" for gates used by Crumble has the following "adjectives": - `x` means "X basis" - `y` means "Y basis" - `z` means "Z basis" - `shift` means "inverse" - `c` means "controlled gate" or more generally "two qubit variant of gate" - `s` means "square root" - `m` means "measure" - `r` means "reset" - `w` means "swap" - `alt` means "no not that one, a different one" Here are some examples: - `r+x` is the **X basis variant** of the **reset operation** (i.e. the gate `RX 0`). - `m+r` is the **measure** (m) and **reset** (r) operation (i.e. the gate `MR 0`). - `m+r+y` is the **Y basis variant** of the **measure** (m) and **reset** (r) operation (i.e. the gate `MRY 0`). - `c+m+x` is the **two qubit variant** (c) of **measurement** (m) in the **X basis** (x) (i.e. the gate `MXX 1 2`). - `shift+c+s+y` is the **inverse** (shift) of the **two qubit variant** (c) of the **square root** (s) of the **Y gate** (y) (i.e. the gate `SQRT_YY_DAG 1 2`). ### Mouse Controls Note: to `BoxSelect` means to press down the left mouse button, drag the mouse while holding the button down to outline a rectangular region, and then release the mouse button. Note: using `shift` modifies how the mouse updates the current selection. When `shift` is being held and selection S1 would be replaced by selection S2, the new selection will instead be set to the union of S1 and S2. Note: using `ctrl` modifies how the mouse updates the current selection. When `ctrl` is being held and selection S1 would be replaced by selection S2, the new selection will instead be set to the symmetric difference of S1 and S2. - `LeftClick`: Set current selection to clicked qubit (or to nothing, if clicking empty space). - `BoxSelect`: Set current selection to boxed area. - `Alt+BoxSelect`: Set current selection to boxed area, but only including the qubits with the same parity as the initially clicked qubit at the start of the box selection action. The specific parity being used depends on context. For example, when selecting a column of qubits, the row parity is used. When selecting a 2d region, the subgrid parity is used. # Building Crumble Crumble's source code can be served directly by a webserver. For example, serve crumble using python's built-in web server: ```bash # from the root of Stim's git repository: python -m http.server --directory glue/crumble ``` then open [http://localhost:8000/crumble.html](http://localhost:8000/crumble.html) in a web browser. A single-page version of crumble can be created using rollup-js and uglify-js: ```base # npm install -g rollup uglify-js # from the root of Stim's git repository: { cat glue/crumble/crumble.html | grep -v "^"; rollup glue/crumble/main.js | uglifyjs -c -m --mangle-props --toplevel; echo ""; } > crumble_single_page.html ``` # Testing Crumble Crumble's unit tests can be executed by opening the page `test/test.html` in a web browser: ```bash # from the root of Stim's git repository: python -m http.server --directory glue/crumble & firefox localhost:8000/test/test.html # if page says "All X tests passed" then tests passed. # see the browser console for a log of tests that were run ``` Unit tests can also be run headless using NodeJS. Unit tests for functionality such as canvas drawing will be skipped when running headless: ``` # from the root of Stim's git repository: node glue/crumble/run_tests_headless.js # will end with 'all tests passed' if all tests passed ``` ================================================ FILE: glue/crumble/base/cooldown_throttle.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ /** * Performs an action when triggered, but defers the action if it happens too soon after the last one. * * Triggering multiple times during the cooldown period only results in one action being performed. */ class CooldownThrottle { /** * @param {!function() : void} action * @param {!number} cooldownMs * @param {!number} slowActionCooldownPumpUpFactor * @param {!boolean=false} waitWithRequestAnimationFrame * @constructor */ constructor(action, cooldownMs, slowActionCooldownPumpUpFactor=0, waitWithRequestAnimationFrame=false) { /** @type {!function() : void} */ this.action = action; /** @type {!number} */ this.cooldownDuration = cooldownMs; /** @type {!number} */ this.slowActionCooldownPumpupFactor = slowActionCooldownPumpUpFactor; /** @type {!boolean} */ this._waitWithRequestAnimationFrame = waitWithRequestAnimationFrame; /** * @type {!string} * @private */ this._state = 'idle'; /** * @type {!number} * @private */ this._cooldownStartTime = -Infinity; } _triggerIdle() { // Still cooling down? let remainingCooldownDuration = this.cooldownDuration - (performance.now() - this._cooldownStartTime); if (remainingCooldownDuration > 0) { this._forceIdleTriggerAfter(remainingCooldownDuration); return; } // Go go go! this._state = 'running'; let t0 = performance.now(); try { this.action(); } finally { let dt = performance.now() - t0; this._cooldownStartTime = performance.now() + (dt * this.slowActionCooldownPumpupFactor); // Were there any triggers while we were running? if (this._state === 'running-and-triggered') { this._forceIdleTriggerAfter(this.cooldownDuration); } else { this._state = 'idle'; } } } /** * Asks for the action to be performed as soon as possible. * (No effect if the action was already requested but not performed yet.) */ trigger() { switch (this._state) { case 'idle': this._triggerIdle(); break; case 'waiting': // Already triggered. Do nothing. break; case 'running': // Re-trigger. this._state = 'running-and-triggered'; break; case 'running-and-triggered': // Already re-triggered. Do nothing. break; default: throw new Error('Unrecognized throttle state: ' + this._state); } } /** * @private */ _forceIdleTriggerAfter(duration) { this._state = 'waiting'; // setTimeout seems to refuse to run while I'm scrolling with my mouse wheel on chrome in windows. // So, for stuff that really has to come back in that case, we also support requestAnimationFrame looping. if (this._waitWithRequestAnimationFrame) { let iter; let start = performance.now(); iter = () => { if (performance.now() < start + duration) { requestAnimationFrame(iter); return; } this._state = 'idle'; this._cooldownStartTime = -Infinity; this.trigger() }; iter(); } else { setTimeout(() => { this._state = 'idle'; this._cooldownStartTime = -Infinity; this.trigger() }, duration); } } } export {CooldownThrottle} ================================================ FILE: glue/crumble/base/describe.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ const COLLECTION_CUTOFF = 1000; const BAD_TO_STRING_RESULT = new (function(){})().toString(); const RECURSE_LIMIT_DESCRIPTION = "!recursion-limit!"; const DEFAULT_RECURSION_LIMIT = 10; function try_describe_atomic(value) { if (value === null) { return "null"; } if (value === undefined) { return "undefined"; } if (typeof value === "string") { return `"${value}"`; } if (typeof value === "number") { return "" + value; } return undefined; } function try_describe_collection(value, recursionLimit) { if (recursionLimit === 0) { return RECURSE_LIMIT_DESCRIPTION; } if (value instanceof Map) { return describe_Map(value, recursionLimit); } if (value instanceof Set) { return describe_Set(value, recursionLimit); } if (value[Symbol.iterator] !== undefined) { return describe_Iterable(value, recursionLimit); } return undefined; } function describe_fallback(value, recursionLimit) { try { let defaultString = String(value); if (defaultString !== BAD_TO_STRING_RESULT) { return defaultString; } } catch { } return describe_Object(value, recursionLimit); } /** * Attempts to give a useful and unambiguous description of the given value. * * @param {*} value * @param {!int=} recursionLimit * @returns {!string} */ function describe(value, recursionLimit = DEFAULT_RECURSION_LIMIT) { return try_describe_atomic(value) || try_describe_collection(value, recursionLimit) || describe_fallback(value, recursionLimit); } /** * @param {!Map} map * @param {!int} limit * @returns {!string} */ function describe_Map(map, limit) { let entries = []; for (let [k, v] of map.entries()) { if (entries.length > COLLECTION_CUTOFF) { entries.push("[...]"); break; } //noinspection JSUnusedAssignment let keyDesc = describe(k, limit - 1); //noinspection JSUnusedAssignment let valDesc = describe(v, limit - 1); entries.push(`${keyDesc}: ${valDesc}`); } return `Map{${entries.join(", ")}}`; } /** * @param {!Set} set * @param {!int} limit * @returns {!string} */ function describe_Set(set, limit) { let entries = []; for (let e of set) { if (entries.length > COLLECTION_CUTOFF) { entries.push("[...]"); break; } entries.push(describe(e, limit - 1)); } return `Set{${entries.join(", ")}}`; } /** * @param {!Iterable} seq * @param {!int} limit * @returns {!string} */ function describe_Iterable(seq, limit) { let entries = []; for (let e of seq) { if (entries.length > COLLECTION_CUTOFF) { entries.push("[...]"); break; } entries.push(describe(e, limit - 1)); } let prefix = Array.isArray(seq) ? "" : seq.constructor.name; return `${prefix}[${entries.join(", ")}]`; } /** * @param {*} value * @param {!int} limit * @returns {!string} */ function describe_Object(value, limit) { let entries = []; for (let k in value) { if (!value.hasOwnProperty(k)) { continue; } if (entries.length > COLLECTION_CUTOFF) { entries.push("[...]"); break; } let v = value[k]; let keyDesc = describe(k, limit - 1); let valDesc = describe(v, limit - 1); entries.push(`${keyDesc}: ${valDesc}`); } if (value.constructor === undefined) { return `[an unknown non-primitive value with no constructor]`; } let typeName = value.constructor.name; let prefix = typeName === {}.constructor.name ? "" : `(Type: ${typeName})`; return `${prefix}{${entries.join(", ")}}`; } export {describe} ================================================ FILE: glue/crumble/base/describe.test.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ import {assertThat, test} from "../test/test_util.js" import {describe} from "./describe.js" class DescribableClass { constructor() { this.x = 1; } } class DescribedClass { constructor() { this.x = 1; } toString() { return "described"; } } class SomeIterable { constructor() {} //noinspection JSMethodCanBeStatic,JSUnusedGlobalSymbols [Symbol.iterator]() { return [1, 2, 3][Symbol.iterator](); } } test("describe.trivial", () => { assertThat(describe(undefined)).isEqualTo("undefined"); assertThat(describe(null)).isEqualTo("null"); assertThat(describe(false)).isEqualTo("false"); assertThat(describe("")).isEqualTo('""'); assertThat(describe(0)).isEqualTo("0"); assertThat(describe(Symbol())).isEqualTo("Symbol()"); assertThat(describe([])).isEqualTo("[]"); assertThat(describe({})).isEqualTo("{}"); assertThat(describe(new Float32Array(0))).isEqualTo("Float32Array[]"); assertThat(describe(new Int8Array(0))).isEqualTo("Int8Array[]"); assertThat(describe(new Map())).isEqualTo("Map{}"); assertThat(describe(new Set())).isEqualTo("Set{}"); }); test("describe.simple", () => { assertThat(describe(true)).isEqualTo("true"); assertThat(describe(1.5)).isEqualTo("1.5"); assertThat(describe("b")).isEqualTo('"b"'); assertThat(describe(Symbol("a"))).isEqualTo('Symbol(a)'); assertThat(describe(Infinity)).isEqualTo("Infinity"); assertThat(describe(-Infinity)).isEqualTo("-Infinity"); assertThat(describe(NaN)).isEqualTo("NaN"); assertThat(describe([1, 2, 3])).isEqualTo("[1, 2, 3]"); assertThat(describe(new Float32Array([1, 2, 3]))).isEqualTo("Float32Array[1, 2, 3]"); assertThat(describe(new Int8Array([1, 2, 3]))).isEqualTo("Int8Array[1, 2, 3]"); assertThat(describe(new Set([2]))).isEqualTo("Set{2}"); assertThat(describe(new Map([[2, "b"]]))).isEqualTo('Map{2: "b"}'); assertThat(describe({2: "b"})).isEqualTo('{"2": "b"}'); assertThat(describe(new DescribedClass())).isEqualTo("described"); assertThat(describe(new DescribableClass())).isEqualTo('(Type: DescribableClass){"x": 1}'); assertThat(describe(new SomeIterable())).isEqualTo("SomeIterable[1, 2, 3]"); }); test("describe.recursion", () => { let a = []; a.push(a); assertThat(describe(a, 2)).isEqualTo( "[[!recursion-limit!]]"); assertThat(describe(a, 10)).isEqualTo( "[[[[[[[[[[!recursion-limit!]]]]]]]]]]"); let m = new Map(); m.set(1, m); assertThat(describe(m, 2)).isEqualTo( "Map{1: Map{1: !recursion-limit!}}"); assertThat(describe(m, 10)).isEqualTo( "Map{1: Map{1: Map{1: Map{1: Map{1: Map{1: Map{1: Map{1: Map{1: Map{1: !recursion-limit!}}}}}}}}}}"); let s = new Set(); s.add(s); assertThat(describe(s, 2)).isEqualTo( "Set{Set{!recursion-limit!}}"); assertThat(describe(s, 10)).isEqualTo( "Set{Set{Set{Set{Set{Set{Set{Set{Set{Set{!recursion-limit!}}}}}}}}}}"); let o = {}; o[2] = o; assertThat(describe(o, 2)).isEqualTo( '{"2": {"2": !recursion-limit!}}'); assertThat(describe(o, 10)).isEqualTo( '{"2": {"2": {"2": {"2": {"2": {"2": {"2": {"2": {"2": {"2": !recursion-limit!}}}}}}}}}}'); // Default terminates. assertThat(describe(a)).isNotEqualTo(undefined); assertThat(describe(m)).isNotEqualTo(undefined); assertThat(describe(s)).isNotEqualTo(undefined); assertThat(describe(o)).isNotEqualTo(undefined); }); ================================================ FILE: glue/crumble/base/equate.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ /** * Determines if two values are currently equivalent. * * Values that are equal according to === are currently equivalent. * NaN is currently equivalent to NaN. * Values with an `isEqualTo` method are currently equivalent to values that return true when passed to that method. * Collections of the same type that contain currently equivalent entries are currently equivalent. * Objects of the same type with equivalent same own properties and iterables are currently equivalent. * * @param {*} subject * @param {*} other * @returns {!boolean} */ function equate(subject, other) { if (subject === other || (isExactlyNaN(subject) && isExactlyNaN(other))) { return true; } // Custom equality. let customEquality = tryEquate_custom(subject, other); if (customEquality !== undefined) { return customEquality; } if (isAtomic(subject) || isAtomic(other) || !eqType(subject, other)) { return false; } // Collection equality. if (subject instanceof Map) { return equate_Maps(subject, other); } if (subject instanceof Set) { return equate_Sets(subject, other); } if (isIndexable(subject)) { return equate_Indexables(subject, other); } // Object equality. return equate_Objects(subject, other); } const GENERIC_ARRAY_TYPES = [ Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Uint8Array, Uint16Array, Uint32Array, Uint8ClampedArray ]; /** * @param {*} v * @returns {!boolean} */ function isExactlyNaN(v) { return typeof v === "number" && isNaN(v); } /** * @param {*} subject * @param {*} other * @returns {undefined|!boolean} */ function tryEquate_custom(subject, other) { if (!isAtomic(subject) && subject.constructor !== undefined && subject.constructor.prototype.hasOwnProperty("isEqualTo")) { return subject.isEqualTo(other); } if (!isAtomic(other) && other.constructor !== undefined && other.constructor.prototype.hasOwnProperty("isEqualTo")) { return other.isEqualTo(subject); } return undefined; } /** * @param {*} value * @returns {!boolean} */ function isAtomic(value) { return value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean"; } /** * @param {*} value * @returns {!boolean} */ function isIndexable(value) { return Array.isArray(value) || !GENERIC_ARRAY_TYPES.every(t => !(value instanceof t)); } /** * @param {*} subject * @param {*} other * @returns {!boolean} */ function eqType(subject, other) { return subject.constructor.name === other.constructor.name; } /** * @param {!(*[])} subject * @param {!(*[])} other * @returns {!boolean} */ function equate_Indexables(subject, other) { if (subject.length !== other.length) { return false; } for (let i = 0; i < subject.length; i++) { if (!equate(subject[i], other[i])) { return false; } } return true; } /** * @param {!Iterable} subject * @param {!Iterable} other * @returns {!boolean} */ function equate_Iterables(subject, other) { let otherIter = other[Symbol.iterator](); for (let subjectItem of subject) { let otherItemDone = otherIter.next(); if (otherItemDone.done || !equate(subjectItem, otherItemDone.value)) { return false; } } return otherIter.next().done; } /** * @param {!Map} subject * @param {!Map} other * @returns {!boolean} */ function equate_Maps(subject, other) { if (subject.size !== other.size) { return false; } for (let [k, v] of subject) { //noinspection JSUnusedAssignment if (!other.has(k)) { return false; } //noinspection JSUnusedAssignment let otherV = other.get(k); //noinspection JSUnusedAssignment if (!equate(v, otherV)) { return false; } } return true; } /** * @param {!Set} subject * @param {!Set} other * @returns {!boolean} */ function equate_Sets(subject, other) { if (subject.size !== other.size) { return false; } for (let k of subject) { if (!other.has(k)) { return false; } } return true; } /** * @param {!object} obj * @returns {!Set} */ function objectKeys(obj) { let result = new Set(); for (let k in obj) { if (obj.hasOwnProperty(k)) { result.add(k); } } return result; } /** * @param {!object} subject * @param {!object} other * @returns {!boolean} */ function equate_Objects(subject, other) { let keys = objectKeys(subject); if (!equate_Sets(keys, objectKeys(other))) { return false; } for (let k of keys) { if (k === Symbol.iterator) { continue; } if (!equate(subject[k], other[k])) { return false; } } let hasSubjectIter = subject[Symbol.iterator] !== undefined; let hasOtherIter = other[Symbol.iterator] !== undefined; if (hasSubjectIter !== hasOtherIter) { return false; } if (hasSubjectIter && hasOtherIter) { if (!equate_Iterables(/** @type {!Iterable} */ subject, /** @type {!Iterable} */ other)) { return false; } } return true; } export {equate, equate_Maps} ================================================ FILE: glue/crumble/base/equate.test.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ import {assertThat, test} from "../test/test_util.js" import {equate} from "./equate.js" import {describe} from "./describe.js"; class EmptyClass { constructor() { } } class EmptyClass2 { constructor() { } } class PropClass { constructor(v) { //noinspection JSUnusedGlobalSymbols this.v = v; } } class SomeIterable { constructor() {} //noinspection JSMethodCanBeStatic,JSUnusedGlobalSymbols [Symbol.iterator]() { return [1, 2, 3][Symbol.iterator](); } } class One { constructor() {} isEqualTo(other) { return this !== null && other === 1 || other instanceof One; } } class First { constructor(x, y) { this.x = x; this.y = y; } isEqualTo(other) { return other instanceof First && this.x === other.x; } } class Iter { constructor(items) { this[Symbol.iterator] = () => items[Symbol.iterator](); } } class Iter2 { constructor(items) { this[Symbol.iterator] = () => items[Symbol.iterator](); } } test("Equate.groups", () => { let groups = [ [null, null], [undefined, undefined], [true, true], [false, false], [0, 0.0, 0], [1, 1.0, 1, new One(), new One()], [-1, -1.0, -1], [2, 2.0, 2], [Infinity, Infinity], [-Infinity, -Infinity], [NaN, NaN], ["", ""], ["0", "0"], [[], []], [[[]], [[]]], [new Iter([]), new Iter([])], [new Iter([1]), new Iter([1])], [new Iter([1, 2, 3]), new Iter([1, 2, 3])], [new Iter2([]), new Iter2([])], [new Float32Array(0), new Float32Array(0)], [new Int32Array(0), new Int32Array(0)], [new Int32Array([1, 2, 3]), new Int32Array([1, 2, 3])], [[1, 2, 3], [1, 2, 3]], [[1, 4, 3], [1, 4, 3]], [{}, {}], [{0: {}}, {0: {}}], [{a: 2}, {a: 2}], [new Map(), new Map()], [new Map([[1, 2]]), new Map([[1, 2]])], [new Map([[1, 3]]), new Map([[1, 3]])], [new Map([[3, 2]]), new Map([[3, 2]])], [new Map([[2, 1]]), new Map([[2, 1]])], [new Map([["2", 1]]), new Map([["2", 1]])], [new Map([[1, 2], ["a", 2]]), new Map([["a", 2], [1, 2]])], [new Set(), new Set()], [new Set(["a"]), new Set(["a"])], [new Set(["b"]), new Set(["b"])], [new Set([2, "a"]), new Set(["a", 2])], [new SomeIterable(), new SomeIterable()], [new EmptyClass(), new EmptyClass()], [new EmptyClass2(), new EmptyClass2()], [new PropClass(1), new PropClass(1)], [new PropClass(2), new PropClass(2)], [Symbol("a")], [new First(1, 2), new First(1, 3), new First(1, 2), new First(1, 3)], [new First(2, 2), new First(2, 3), new First(2, 2), new First(2, 3)], [ {frump: new Map([["trump", [1, new Set([2, "a"]), new Float32Array(5)]]])}, {frump: new Map([["trump", [1, new Set([2, "a"]), new Float32Array(5)]]])} ] ]; assertThat(equate(1, 1)).isEqualTo(true); assertThat(equate(1, 2)).isEqualTo(false); for (let g1 of groups) { for (let g2 of groups) { for (let e1 of g1) { for (let e2 of g2) { let actual = equate(e1, e2); let expected = g1 === g2; let eq = expected ? "equal" : "NOT equal"; if (actual !== expected) { // Note: not using assertThat because assertThat's correctness depends on equate throw new Error(`Expected <${describe(e1)}> to ${eq} <${describe(e2)}>.`); } } } } } }); ================================================ FILE: glue/crumble/base/history_pusher.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ /** * @return {!boolean} */ function shouldControlURL() { try { return window.self === window.top; } catch (_) { return false; } } /** * Manages interactions with the browser's history as the app's state changes and frequently updates the URL. */ class HistoryPusher { constructor() { /** * @type {!boolean} * @private */ this._historyActionsNotWorking = false; /** * @type {undefined|*} * @private */ this._currentMemorableStateObj = undefined; } /** * Indicates that the current state should be preserved in the browser history if the user transitions away from it. * * Because the state isn't known, any transition will trigger the preservation (possibly creating a duplicate * history entry). */ currentStateIsMemorableButUnknown() { this._currentMemorableStateObj = {wont_equal_this: true}; } /** * Indicates that the current state should be preserved in the browser history if the user transitions away from it. * @param {*} stateObj An ===-able object representing the current state, for identifying spurious transitions. */ currentStateIsMemorableAndEqualTo(stateObj) { this._currentMemorableStateObj = stateObj; } /** * Indicates that the current state should not be preserved in the browser history if the user transitions away. * States are not-memorable by default. */ currentStateIsNotMemorable() { this._currentMemorableStateObj = undefined; } /** * @param {*} stateObj An equatable (by ===) object representing the latest state. * @param {!string} stateUrlHash A document.location.hash value that will lead to the latest state. */ stateChange(stateObj, stateUrlHash) { if (!stateUrlHash.startsWith('#')) { throw new Error(`"Expected a hash URL: ${{stateObj, stateUrlHash}}`); } if (!shouldControlURL()) { return; } if (this._currentMemorableStateObj === stateObj) { return; } if (this._historyActionsNotWorking) { // This is worse than using the history API, since it inserts junk after every state change, but it's also // better than just randomly losing the circuit. document.location.hash = stateUrlHash; return; } try { // 'Memorable' states should stay in the history instead of being replaced. if (this._currentMemorableStateObj === undefined) { history.replaceState(stateObj, "", stateUrlHash); } else { history.pushState(stateObj, "", stateUrlHash); this._currentMemorableStateObj = undefined; } } catch (ex) { // E.g. this happens when running from the filesystem due to same-origin constraints. console.warn( "Calling 'history.replaceState/pushState' failed. Falling back to setting location.hash.", ex); this._historyActionsNotWorking = true; document.location.hash = stateUrlHash; } } } export {HistoryPusher} ================================================ FILE: glue/crumble/base/obs.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ import {CooldownThrottle} from "./cooldown_throttle.js" /** * An observable sequence of events. * * WARNING: this class is not written to be re-entrant safe! If an observable ends up triggering itself, there may be * unexpected bugs. */ class Observable { /** * @param {!function(!function(T):void): (!function():void)} subscribe * @template T */ constructor(subscribe) { /** * @type {!(function(!(function(T): void)): !(function(): void))} * @template T * @private */ this._subscribe = subscribe; } /** * @param {!function(T):void} observer * @returns {!function():void} unsubscriber * @template T */ subscribe(observer) { return this._subscribe(observer); } /** * @param {T} items * @returns {!Observable.} An observable that immediately forwards all the given items to any new subscriber. * @template T */ static of(...items) { return new Observable(observer => { for (let item of items) { observer(item); } return () => {}; }); } /** * Subscribes to the receiving observable for a moment and returns any collected items. * @returns {!Array.} * @template T */ snapshot() { let result = []; let unsub = this.subscribe(e => result.push(e)); unsub(); return result; } /** * @param {!function(TIn) : TOut} transformFunc * @returns {!Observable.} An observable with the same items, but transformed by the given function. * @template TIn, TOut */ map(transformFunc) { return new Observable(observer => this.subscribe(item => observer(transformFunc(item)))); } /** * @param {!function(T) : !boolean} predicate * @returns {!Observable.} An observable with the same items, but skipping items that don't match the predicate. * @template T */ filter(predicate) { return new Observable(observer => this.subscribe(item => { if (predicate(item)) { observer(item); }})); } /** * @param {!Observable.} other * @param {!function(T1, T2): TOut} mergeFunc * @returns {!Observable.} * @template T1, T2, TOut */ zipLatest(other, mergeFunc) { return new Observable(observer => { let has1 = false; let has2 = false; let last1; let last2; let unreg1 = this.subscribe(e1 => { last1 = e1; has1 = true; if (has2) { observer(mergeFunc(last1, last2)); } }); let unreg2 = other.subscribe(e2 => { last2 = e2; has2 = true; if (has1) { observer(mergeFunc(last1, last2)); } }); return () => { unreg1(); unreg2(); }; }); } /** * Returns an observable that keeps requesting animations frame callbacks and calling observers when they arrive. * @returns {!Observable.} */ static requestAnimationTicker() { return new Observable(observer => { let iter; let isDone = false; iter = () => { if (!isDone) { observer(undefined); window.requestAnimationFrame(iter); } }; iter(); return () => { isDone = true; }; }); } /** * @returns {!Observable.} An observable that subscribes to each sub-observables arriving on this observable * in turns, only forwarding items from the latest sub-observable. * @template T */ flattenLatest() { return new Observable(observer => { let unregLatest = () => {}; let isDone = false; let unregAll = this.subscribe(subObservable => { if (isDone) { return; } let prevUnreg = unregLatest; unregLatest = subObservable.subscribe(observer); prevUnreg(); }); return () => { isDone = true; unregLatest(); unregAll(); } }); } /** * @param {!function(T):void} action * @returns {!Observable.} * @template T */ peek(action) { return this.map(e => { action(e); return e; }); } /** * @returns {!Observable.} An observable that forwards all the items from all the observables observed by the * receiving observable of observables. * @template T */ flatten() { return new Observable(observer => { let unsubs = []; unsubs.push(this.subscribe(observable => unsubs.push(observable.subscribe(observer)))); return () => { for (let unsub of unsubs) { unsub() } } }); } /** * Starts a timer after each completed send, delays sending any more values until the timer expires, and skips * intermediate values when a newer value arrives from the source while the timer is still running down. * @param {!number} cooldownMillis * @returns {!Observable.} * @template T */ throttleLatest(cooldownMillis) { return new Observable(observer => { let latest = undefined; let isKilled = false; let throttle = new CooldownThrottle(() => { if (!isKilled) { observer(latest); } }, cooldownMillis); let unsub = this.subscribe(e => { latest = e; throttle.trigger(); }); return () => { isKilled = true; unsub(); }; }); } /** * @param {!HTMLElement|!HTMLDocument} element * @param {!string} eventKey * @returns {!Observable.<*>} An observable corresponding to an event fired from an element. */ static elementEvent(element, eventKey) { return new Observable(observer => { element.addEventListener(eventKey, observer); return () => element.removeEventListener(eventKey, observer); }); } /** * * @param {!int} count * @returns {!Observable.} * @template T */ skip(count) { return new Observable(observer => { let remaining = count; return this.subscribe(item => { if (remaining > 0) { remaining -= 1; } else { observer(item); } }) }) } /** * @returns {!Observable.} An observable with the same events, but filtering out any event value that's the same * as the previous one. * @template T */ whenDifferent(equater = undefined) { let eq = equater || ((e1, e2) => e1 === e2); return new Observable(observer => { let hasLast = false; let last = undefined; return this.subscribe(item => { if (!hasLast || !eq(last, item)) { last = item; hasLast = true; observer(item); } }); }); } } class ObservableSource { constructor() { /** * @type {!Array.} * @private * @template T */ this._observers = []; /** * @type {!Observable.} * @private * @template T */ this._observable = new Observable(observer => { this._observers.push(observer); let didRun = false; return () => { if (!didRun) { didRun = true; this._observers.splice(this._observers.indexOf(observer), 1); } }; }); } /** * @returns {!Observable.} * @template T */ observable() { return this._observable; } /** * @param {T} eventValue * @template T */ send(eventValue) { for (let obs of this._observers) { obs(eventValue); } } } class ObservableValue { /** * @param {T=undefined} initialValue * @template T */ constructor(initialValue=undefined) { this._value = initialValue; this._source = new ObservableSource(); this._observable = new Observable(observer => { observer(this._value); return this._source.observable().subscribe(observer); }); } /** * @returns {!Observable} */ observable() { return this._observable; } /** * @param {T} newValue * @template T */ set(newValue) { this._value = newValue; this._source.send(newValue); } /** * @returns {T} The current value. * @template T */ get() { return this._value; } } export {Observable, ObservableSource, ObservableValue} ================================================ FILE: glue/crumble/base/obs.test.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ import {assertThat, test} from "../test/test_util.js" import {Observable, ObservableValue, ObservableSource} from "./obs.js" let record = observable => { let out = []; let stop = observable.subscribe(e => out.push(e)); return {out, stop}; }; test("Observable.of", () => { assertThat(record(Observable.of()).out).isEqualTo([]); assertThat(record(Observable.of(1, 2, 3)).out).isEqualTo([1, 2, 3]); }); test("Observable.snapshot", () => { assertThat(Observable.of().snapshot()).isEqualTo([]); assertThat(Observable.of(1, 2, 3).snapshot()).isEqualTo([1, 2, 3]); }); test("Observable.map", () => { assertThat(Observable.of(1, 2, 3).map(e => e * 2).snapshot()).isEqualTo([2, 4, 6]); }); test("Observable.filter", () => { assertThat(Observable.of(1, 2, 3).filter(e => e !== 2).snapshot()).isEqualTo([1, 3]); }); test("Observable.zipLatest", () => { let v1 = new ObservableSource(); let v2 = new ObservableSource(); let seen = []; let unreg = v1.observable().zipLatest(v2.observable(), (e1, e2) => e1 + e2).subscribe(e => seen.push(e)); assertThat(seen).isEqualTo([]); v1.send(1); assertThat(seen).isEqualTo([]); v1.send(2); assertThat(seen).isEqualTo([]); v2.send(10); assertThat(seen).isEqualTo([12]); v1.send(3); assertThat(seen).isEqualTo([12, 13]); v1.send(4); assertThat(seen).isEqualTo([12, 13, 14]); v2.send(20); assertThat(seen).isEqualTo([12, 13, 14, 24]); unreg(); v2.send(30); assertThat(seen).isEqualTo([12, 13, 14, 24]); }); test("Observable.flattenLatest", () => { let c = new ObservableSource(); let v1 = new ObservableSource(); let v2 = new ObservableSource(); let v3 = new ObservableSource(); let seen = []; let unreg = c.observable().flattenLatest().subscribe(e => seen.push(e)); assertThat(seen).isEqualTo([]); c.send(v1.observable()); assertThat(seen).isEqualTo([]); v1.send(1); assertThat(seen).isEqualTo([1]); v1.send(2); assertThat(seen).isEqualTo([1, 2]); c.send(v2.observable()); assertThat(seen).isEqualTo([1, 2]); v1.send(3); assertThat(seen).isEqualTo([1, 2]); v2.send(4); assertThat(seen).isEqualTo([1, 2, 4]); c.send(v1.observable()); c.send(v2.observable()); c.send(v3.observable()); assertThat(seen).isEqualTo([1, 2, 4]); v1.send(5); v2.send(6); v3.send(7); assertThat(seen).isEqualTo([1, 2, 4, 7]); unreg(); v1.send(8); v2.send(9); v3.send(10); assertThat(seen).isEqualTo([1, 2, 4, 7]); }); test("Observable.whenDifferent", () => { assertThat(Observable.of(1, 2, 2, 3).whenDifferent().snapshot()).isEqualTo([1, 2, 3]); assertThat(Observable.of(undefined, 2, 2, 3).whenDifferent().snapshot()).isEqualTo([undefined, 2, 3]); assertThat(Observable.of(2, 3, 5, 7, 10, 13, 17, 19, 23).whenDifferent((e1, e2) => e1 % 3 === e2 % 3).snapshot()). isEqualTo([2, 3, 5, 7, 17, 19, 23]); }); test("Observable.skip", () => { assertThat(Observable.of(1, 2, 3).skip(0).snapshot()).isEqualTo([1, 2, 3]); assertThat(Observable.of(1, 2, 3).skip(1).snapshot()).isEqualTo([2, 3]); assertThat(Observable.of(1, 2, 3).skip(2).snapshot()).isEqualTo([3]); assertThat(Observable.of(1, 2, 3).skip(3).snapshot()).isEqualTo([]); assertThat(Observable.of(1, 2, 3).skip(5).snapshot()).isEqualTo([]); }); test("Observable.flatten", () => { assertThat(Observable.of( Observable.of(1, 2, 3), Observable.of(4, 5, 6), Observable.of(7, 8)).flatten().snapshot()).isEqualTo([1, 2, 3, 4, 5, 6, 7, 8]); let v1 = new ObservableSource(); let v2 = new ObservableSource(); let c = new ObservableSource(); let {out, stop} = record(c.observable().flatten()); assertThat(out).isEqualTo([]); c.send(v1.observable()); assertThat(out).isEqualTo([]); v1.send('a'); assertThat(out).isEqualTo(['a']); c.send(v2.observable()); assertThat(out).isEqualTo(['a']); c.send(v2.observable()); assertThat(out).isEqualTo(['a']); v2.send('b'); assertThat(out).isEqualTo(['a', 'b', 'b']); stop(); v1.send('c'); v2.send('c'); c.send(new ObservableValue('c').observable()); assertThat(out).isEqualTo(['a', 'b', 'b']); }); test("ObservableValue_setVsGet", () => { let v = new ObservableValue('a'); assertThat(v.get()).isEqualTo('a'); v.set('b'); assertThat(v.get()).isEqualTo('b'); }); test("ObservableValue_observable", () => { let v = new ObservableValue('a'); let {out, stop} = record(v.observable()); assertThat(out).isEqualTo(['a']); v.set('b'); assertThat(out).isEqualTo(['a', 'b']); v.set('c'); assertThat(out).isEqualTo(['a', 'b', 'c']); stop(); v.set('d'); assertThat(out).isEqualTo(['a', 'b', 'c']); }); test("ObservableValue_observable_multiple", () => { let v = new ObservableValue('a'); let {out: out1, stop: stop1} = record(v.observable()); let {out: out2, stop: stop2} = record(v.observable()); assertThat(out1).isEqualTo(['a']); assertThat(out2).isEqualTo(['a']); stop2(); assertThat(out1).isEqualTo(['a']); assertThat(out2).isEqualTo(['a']); v.set('b'); assertThat(out1).isEqualTo(['a', 'b']); assertThat(out2).isEqualTo(['a']); stop1(); assertThat(out1).isEqualTo(['a', 'b']); assertThat(out2).isEqualTo(['a']); }); test("ObservableSource_observable", () => { let v = new ObservableSource(); let {out, stop} = record(v.observable()); assertThat(out).isEqualTo([]); v.send('b'); assertThat(out).isEqualTo(['b']); v.send('c'); assertThat(out).isEqualTo(['b', 'c']); stop(); v.send('d'); assertThat(out).isEqualTo(['b', 'c']); }); test("Observable.observable_multiple", () => { let v = new ObservableSource(); let {out: out1, stop: stop1} = record(v.observable()); let {out: out2, stop: stop2} = record(v.observable()); assertThat(out1).isEqualTo([]); assertThat(out2).isEqualTo([]); v.send('a'); assertThat(out1).isEqualTo(['a']); assertThat(out2).isEqualTo(['a']); stop2(); assertThat(out1).isEqualTo(['a']); assertThat(out2).isEqualTo(['a']); v.send('b'); assertThat(out1).isEqualTo(['a', 'b']); assertThat(out2).isEqualTo(['a']); stop1(); assertThat(out1).isEqualTo(['a', 'b']); assertThat(out2).isEqualTo(['a']); }); test("Observable.peek", () => { let a = []; let v = new ObservableSource(); v.observable().peek(e => a.push(e)).subscribe(() => {}); assertThat(a).isEqualTo([]); v.send(2); assertThat(a).isEqualTo([2]); }); ================================================ FILE: glue/crumble/base/revision.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ import {describe} from "./describe.js" import {equate} from "./equate.js" import {ObservableSource, ObservableValue} from "./obs.js" /** * A simple linear revision history tracker, for supporting undo and redo functionality. */ class Revision { /** * @param {!Array.<*>} history * @param {!int} index * @param {!boolean} isWorkingOnCommit */ constructor(history, index, isWorkingOnCommit) { if (index < 0 || index >= history.length) { throw new Error(`Bad index: ${{history, index, isWorkingOnCommit}}`); } if (!Array.isArray(history)) { throw new Error(`Bad history: ${{history, index, isWorkingOnCommit}}`); } /** @type {!Array.<*>} */ this.history = history; /** @type {!int} */ this.index = index; /** @type {!boolean} */ this.isWorkingOnCommit = isWorkingOnCommit; /** @type {!ObservableSource} */ this._changes = new ObservableSource(); /** @type {!ObservableValue} */ this._latestActiveCommit = new ObservableValue(this.history[this.index]); } /** * @returns {!Observable.<*>} */ changes() { return this._changes.observable(); } /** * @returns {!Observable.<*>} */ latestActiveCommit() { return this._latestActiveCommit.observable(); } /** * Returns a snapshot of the current commit. * @returns {*} */ peekActiveCommit() { return this._latestActiveCommit.get(); } /** * Returns a cleared revision history, starting at the given state. * @param {*} state */ static startingAt(state) { return new Revision([state], 0, false); } /** * @returns {!boolean} */ isAtBeginningOfHistory() { return this.index === 0 && !this.isWorkingOnCommit; } /** * @returns {!boolean} */ isAtEndOfHistory() { return this.index === this.history.length - 1; } /** * Throws away all revisions and resets the given state. * @param {*} state * @returns {void} */ clear(state) { this.history = [state]; this.index = 0; this.isWorkingOnCommit = false; this._latestActiveCommit.set(state); this._changes.send(state); } /** * Indicates that there are pending changes, so that a following 'undo' will return to the current state instead of * the previous state. * @returns {void} */ startedWorkingOnCommit(newCheckpoint) { this.isWorkingOnCommit = newCheckpoint !== this.history[this.index]; this._changes.send(undefined); } /** * Indicates that pending changes were discarded, so that a following 'undo' should return to the previous state * instead of the current state. * @returns {*} The new current state. */ cancelCommitBeingWorkedOn() { this.isWorkingOnCommit = false; let result = this.history[this.index]; this._latestActiveCommit.set(result); this._changes.send(result); return result; } /** * Throws away future states, appends the given state, and marks it as the current state * @param {*} newCheckpoint * @returns {void} */ commit(newCheckpoint) { if (newCheckpoint === this.history[this.index]) { this.cancelCommitBeingWorkedOn(); return; } this.isWorkingOnCommit = false; this.index += 1; this.history.splice(this.index, this.history.length - this.index); this.history.push(newCheckpoint); this._latestActiveCommit.set(newCheckpoint); this._changes.send(newCheckpoint); } /** * Marks the previous state as the current state and returns it (or resets to the current state if * 'working on a commit' was indicated). * @returns {undefined|*} The new current state, or undefined if there's nothing to undo. */ undo() { if (!this.isWorkingOnCommit) { if (this.index === 0) { return undefined; } this.index -= 1; } this.isWorkingOnCommit = false; let result = this.history[this.index]; this._latestActiveCommit.set(result); this._changes.send(result); return result; } /** * Marks the next state as the current state and returns it (or does nothing if there is no next state). * @returns {undefined|*} The new current state, or undefined if there's nothing to redo. */ redo() { if (this.index + 1 === this.history.length) { return undefined; } this.index += 1; this.isWorkingOnCommit = false; let result = this.history[this.index]; this._latestActiveCommit.set(result); this._changes.send(result); return result; } /** * @returns {!string} A description of the revision. */ toString() { return 'Revision(' + describe({ index: this.index, count: this.history.length, workingOnCommit: this.isWorkingOnCommit, head: this.history[this.index] }) + ')'; } /** * Determines if two revisions currently have the same state. * @param {*|!Revision} other * @returns {!boolean} */ isEqualTo(other) { return other instanceof Revision && this.index === other.index && this.isWorkingOnCommit === other.isWorkingOnCommit && equate(this.history, other.history); } } export {Revision} ================================================ FILE: glue/crumble/base/revision.test.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ import {assertThat, test} from "../test/test_util.js" import {Revision} from "./revision.js" test("Revision.constructor_isEqualTo", () => { assertThat(() => new Revision([], 0, false)).throwsAnExceptionThat().matches(/Bad index/); assertThat(() => new Revision(["a"], -1, false)).throwsAnExceptionThat().matches(/Bad index/); assertThat(() => new Revision(["a"], 1, false)).throwsAnExceptionThat().matches(/Bad index/); assertThat(() => new Revision(["a", "b"], 2, false)).throwsAnExceptionThat().matches(/Bad index/); let r = new Revision(["a", "b", "c"], 1, true); assertThat(r).isEqualTo(new Revision(["a", "b", "c"], 1, true)); assertThat(r).isNotEqualTo(new Revision(["a", "b"], 1, true)); assertThat(r).isNotEqualTo(new Revision(["a", "b", "d"], 1, true)); assertThat(r).isNotEqualTo(new Revision(["a", "b", "c"], 2, true)); assertThat(r).isNotEqualTo(new Revision(["a", "b", "c"], 1, false)); assertThat(r).isNotEqualTo(undefined); assertThat(r).isNotEqualTo(5); let groups = [ [ new Revision(["a"], 0, false), new Revision(["a"], 0, false) ], [ new Revision(["a"], 0, true), new Revision(["a"], 0, true) ], [ new Revision(["a", "b"], 0, false), new Revision(["a", "b"], 0, false) ], [ new Revision(["b", "a"], 0, false), new Revision(["b", "a"], 0, false) ], [ new Revision(["b", "a"], 1, false), new Revision(["b", "a"], 1, false) ] ]; for (let g1 of groups) { for (let g2 of groups) { for (let e1 of g1) { for (let e2 of g2) { if (g1 === g2) { assertThat(e1).isEqualTo(e2); } else { assertThat(e1).isNotEqualTo(e2); } } } } } }); test("Revision.toString_exists", () => { assertThat(new Revision(["a", "b"], 1, false).toString()).isNotEqualTo(undefined); }); test("Revision.startingAt", () => { assertThat(Revision.startingAt("abc")).isEqualTo(new Revision(["abc"], 0, false)); }); test("Revision.clear", () => { let r; r = new Revision(["abc"], 0, false); r.clear("xyz"); assertThat(r).isEqualTo(new Revision(["xyz"], 0, false)); r = new Revision(["r", "s", "t"], 2, true); r.clear("wxyz"); assertThat(r).isEqualTo(new Revision(["wxyz"], 0, false)); }); test("Revision.undo", () => { let r; r = new Revision(["abc"], 0, false); assertThat(r.undo()).isEqualTo(undefined); assertThat(r).isEqualTo(new Revision(["abc"], 0, false)); r = new Revision(["abc", "def"], 0, false); assertThat(r.undo()).isEqualTo(undefined); assertThat(r).isEqualTo(new Revision(["abc", "def"], 0, false)); r = new Revision(["abc", "def"], 0, true); assertThat(r.undo()).isEqualTo("abc"); assertThat(r).isEqualTo(new Revision(["abc", "def"], 0, false)); r = new Revision(["abc", "def"], 1, false); assertThat(r.undo()).isEqualTo("abc"); assertThat(r).isEqualTo(new Revision(["abc", "def"], 0, false)); r = new Revision(["abc", "def"], 1, true); assertThat(r.undo()).isEqualTo("def"); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, false)); assertThat(new Revision(["abc", "def", "xyz"], 2, true).undo()).isEqualTo("xyz"); assertThat(new Revision(["abc", "def", "xyz"], 2, false).undo()).isEqualTo("def"); assertThat(new Revision(["abc", "def", "xyz"], 1, true).undo()).isEqualTo("def"); assertThat(new Revision(["abc", "def", "xyz"], 1, false).undo()).isEqualTo("abc"); assertThat(new Revision(["abc", "def", "xyz"], 0, true).undo()).isEqualTo("abc"); assertThat(new Revision(["abc", "def", "xyz"], 0, false).undo()).isEqualTo(undefined); }); test("Revision.isAtBeginningOfHistory", () => { assertThat(new Revision(["abc"], 0, false).isAtBeginningOfHistory()).isEqualTo(true); assertThat(new Revision(["abc"], 0, true).isAtBeginningOfHistory()).isEqualTo(false); assertThat(new Revision(["abc", "123"], 0, false).isAtBeginningOfHistory()).isEqualTo(true); assertThat(new Revision(["abc", "123"], 0, true).isAtBeginningOfHistory()).isEqualTo(false); assertThat(new Revision(["abc", "123"], 1, false).isAtBeginningOfHistory()).isEqualTo(false); assertThat(new Revision(["abc", "123"], 1, true).isAtBeginningOfHistory()).isEqualTo(false); }); test("Revision.isAtEndOfHistory", () => { assertThat(new Revision(["abc"], 0, false).isAtEndOfHistory()).isEqualTo(true); assertThat(new Revision(["abc"], 0, true).isAtEndOfHistory()).isEqualTo(true); assertThat(new Revision(["abc", "123"], 0, false).isAtEndOfHistory()).isEqualTo(false); assertThat(new Revision(["abc", "123"], 0, true).isAtEndOfHistory()).isEqualTo(false); assertThat(new Revision(["abc", "123"], 1, false).isAtEndOfHistory()).isEqualTo(true); assertThat(new Revision(["abc", "123"], 1, true).isAtEndOfHistory()).isEqualTo(true); }); test("Revision.redo", () => { let r; r = new Revision(["abc"], 0, false); assertThat(r.redo()).isEqualTo(undefined); assertThat(r).isEqualTo(new Revision(["abc"], 0, false)); r = new Revision(["abc", "def"], 0, false); assertThat(r.redo()).isEqualTo("def"); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, false)); r = new Revision(["abc", "def"], 0, true); assertThat(r.redo()).isEqualTo("def"); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, false)); r = new Revision(["abc", "def"], 1, false); assertThat(r.redo()).isEqualTo(undefined); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, false)); // Redo-ing when working on a commit with no future state shouldn't lose the in-progress work. r = new Revision(["abc", "def"], 1, true); assertThat(r.redo()).isEqualTo(undefined); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, true)); assertThat(new Revision(["abc", "def", "xyz"], 0, false).redo()).isEqualTo("def"); assertThat(new Revision(["abc", "def", "xyz"], 0, true).redo()).isEqualTo("def"); assertThat(new Revision(["abc", "def", "xyz"], 1, false).redo()).isEqualTo("xyz"); assertThat(new Revision(["abc", "def", "xyz"], 1, true).redo()).isEqualTo("xyz"); assertThat(new Revision(["abc", "def", "xyz"], 2, false).redo()).isEqualTo(undefined); assertThat(new Revision(["abc", "def", "xyz"], 2, true).redo()).isEqualTo(undefined); }); test("Revision.startedWorkingOnCommit", () => { let r; r = new Revision(["abc"], 0, false); r.startedWorkingOnCommit(); assertThat(r).isEqualTo(new Revision(["abc"], 0, true)); r.startedWorkingOnCommit(); assertThat(r).isEqualTo(new Revision(["abc"], 0, true)); r = new Revision(["abc", "def"], 0, false); r.startedWorkingOnCommit(); assertThat(r).isEqualTo(new Revision(["abc", "def"], 0, true)); r.startedWorkingOnCommit(); assertThat(r).isEqualTo(new Revision(["abc", "def"], 0, true)); r = new Revision(["abc", "def"], 1, false); r.startedWorkingOnCommit(); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, true)); r.startedWorkingOnCommit(); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, true)); }); test("Revision.cancelCommitBeingWorkedOn", () => { let r; r = new Revision(["abc"], 0, true); r.cancelCommitBeingWorkedOn(); assertThat(r).isEqualTo(new Revision(["abc"], 0, false)); r.cancelCommitBeingWorkedOn(); assertThat(r).isEqualTo(new Revision(["abc"], 0, false)); r = new Revision(["abc", "def"], 0, true); r.cancelCommitBeingWorkedOn(); assertThat(r).isEqualTo(new Revision(["abc", "def"], 0, false)); r.cancelCommitBeingWorkedOn(); assertThat(r).isEqualTo(new Revision(["abc", "def"], 0, false)); r = new Revision(["abc", "def"], 1, true); r.cancelCommitBeingWorkedOn(); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, false)); r.cancelCommitBeingWorkedOn(); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, false)); }); test("Revision.commit", () => { let r; r = new Revision(["abc"], 0, false); r.commit("def"); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, false)); r.commit("xyz"); assertThat(r).isEqualTo(new Revision(["abc", "def", "xyz"], 2, false)); r = new Revision(["abc"], 0, true); r.commit("def"); assertThat(r).isEqualTo(new Revision(["abc", "def"], 1, false)); r = new Revision(["abc", "def"], 0, true); r.commit("xyz"); assertThat(r).isEqualTo(new Revision(["abc", "xyz"], 1, false)); r = new Revision(["abc", "def", "so", "long"], 1, true); r.commit("t"); assertThat(r).isEqualTo(new Revision(["abc", "def", "t"], 2, false)); r = new Revision(["abc", "def", "so", "long"], 1, false); r.commit("t"); assertThat(r).isEqualTo(new Revision(["abc", "def", "t"], 2, false)); }); test("Revision.changes", () => { let r = Revision.startingAt('abc'); let a = []; let s = r.changes().subscribe(e => a.push(e)); assertThat(a).isEqualTo([]); r.commit('123'); assertThat(a).isEqualTo(['123']); r.undo(); assertThat(a).isEqualTo(['123', 'abc']); r.undo(); assertThat(a).isEqualTo(['123', 'abc']); r.redo(); assertThat(a).isEqualTo(['123', 'abc', '123']); r.redo(); assertThat(a).isEqualTo(['123', 'abc', '123']); r.startedWorkingOnCommit(); assertThat(a).isEqualTo(['123', 'abc', '123', undefined]); r.cancelCommitBeingWorkedOn(); assertThat(a).isEqualTo(['123', 'abc', '123', undefined, '123']); r.clear('xyz'); assertThat(a).isEqualTo(['123', 'abc', '123', undefined, '123', 'xyz']); s(); r.clear('nope'); assertThat(a).isEqualTo(['123', 'abc', '123', undefined, '123', 'xyz']); }); test("Revision.latestActiveCommit", () => { let r = Revision.startingAt('abc'); let a = []; let s = r.latestActiveCommit().subscribe(e => a.push(e)); assertThat(a).isEqualTo(['abc']); r.commit('123'); assertThat(a).isEqualTo(['abc', '123']); r.undo(); assertThat(a).isEqualTo(['abc', '123', 'abc']); r.undo(); assertThat(a).isEqualTo(['abc', '123', 'abc']); r.redo(); assertThat(a).isEqualTo(['abc', '123', 'abc', '123']); r.redo(); assertThat(a).isEqualTo(['abc', '123', 'abc', '123']); r.startedWorkingOnCommit(); assertThat(a).isEqualTo(['abc', '123', 'abc', '123']); r.cancelCommitBeingWorkedOn(); assertThat(a).isEqualTo(['abc', '123', 'abc', '123', '123']); r.clear('xyz'); assertThat(a).isEqualTo(['abc', '123', 'abc', '123', '123', 'xyz']); s(); r.clear('nope'); assertThat(a).isEqualTo(['abc', '123', 'abc', '123', '123', 'xyz']); }); ================================================ FILE: glue/crumble/base/seq.js ================================================ /** * @param {!Iterable | !Iterator}items * @param {!function(item: TItem): TKey} func * @returns {!Map>} * @template TItem * @template TKey */ function groupBy(items, func) { let result = new Map(); for (let item of items) { let key = func(item); let group = result.get(key); if (group === undefined) { result.set(key, [item]); } else { group.push(item); } } return result; } export {groupBy}; ================================================ FILE: glue/crumble/base/seq.test.js ================================================ import {assertThat, test} from "../test/test_util.js" import {groupBy} from "./seq.js" test("seq.groupBy", () => { assertThat(groupBy([], e => e)).isEqualTo(new Map([])) assertThat(groupBy([2, 3, 5, 11, 15, 17, 2], e => e % 5)).isEqualTo(new Map([ [1, [11]], [2, [2, 17, 2]], [3, [3]], [0, [5, 15]], ])) }); ================================================ FILE: glue/crumble/circuit/circuit.js ================================================ import {Operation} from "./operation.js" import {GATE_ALIAS_MAP, GATE_MAP} from "../gates/gateset.js" import {Layer} from "./layer.js" import {make_mpp_gate, make_spp_gate} from '../gates/gateset_mpp.js'; import {describe} from "../base/describe.js"; /** * @param {!string} targetText * @returns {!Array.} */ function processTargetsTextIntoTargets(targetText) { let targets = []; let flush = () => { if (curTarget !== '') { targets.push(curTarget) curTarget = ''; } } let curTarget = ''; for (let c of targetText) { if (c === ' ') { flush(); } else if (c === '*') { flush(); targets.push('*'); } else { curTarget += c; } } flush(); return targets; } /** * @param {!Array.} targets * @returns {!Array.>} */ function splitUncombinedTargets(targets) { let result = []; let start = 0; while (start < targets.length) { let end = start + 1; while (end < targets.length && targets[end] === '*') { end += 2; } if (end > targets.length) { throw Error(`Dangling combiner in ${targets}.`); } let term = []; for (let k = start; k < end; k += 2) { if (targets[k] === '*') { if (k === 0) { throw Error(`Leading combiner in ${targets}.`); } throw Error(`Adjacent combiners in ${targets}.`); } term.push(targets[k]); } result.push(term); start = end; } return result; } /** * @param {!string} tag * @param {!Float32Array} args * @param {!Array.} combinedTargets * @param {!boolean} convertIntoOtherGates * @returns {!Operation} */ function simplifiedMPP(tag, args, combinedTargets, convertIntoOtherGates) { let bases = ''; let qubits = []; for (let t of combinedTargets) { if (t[0] === '!') { t = t.substring(1); } if (t[0] === 'X' || t[0] === 'Y' || t[0] === 'Z') { bases += t[0]; let v = parseInt(t.substring(1)); if (v !== v) { throw Error(`Non-Pauli target given to MPP: ${combinedTargets}`); } qubits.push(v); } else { throw Error(`Non-Pauli target given to MPP: ${combinedTargets}`); } } let gate = undefined; if (convertIntoOtherGates) { gate = GATE_MAP.get('M' + bases); } if (gate === undefined) { gate = GATE_MAP.get('MPP:' + bases); } if (gate === undefined) { gate = make_mpp_gate(bases); } return new Operation(gate, tag, args, new Uint32Array(qubits)); } /** * @param {!string} tag * @param {!Float32Array} args * @param {!boolean} dag * @param {!Array.} combinedTargets * @returns {!Operation} */ function simplifiedSPP(tag, args, dag, combinedTargets) { let bases = ''; let qubits = []; for (let t of combinedTargets) { if (t[0] === '!') { t = t.substring(1); } if (t[0] === 'X' || t[0] === 'Y' || t[0] === 'Z') { bases += t[0]; let v = parseInt(t.substring(1)); if (v !== v) { throw Error(`Non-Pauli target given to SPP: ${combinedTargets}`); } qubits.push(v); } else { throw Error(`Non-Pauli target given to SPP: ${combinedTargets}`); } } let gate = GATE_MAP.get((dag ? 'SPP_DAG:' : 'SPP:') + bases); if (gate === undefined) { gate = make_spp_gate(bases, dag); } return new Operation(gate, tag, args, new Uint32Array(qubits)); } class Circuit { /** * @param {!Float64Array} qubitCoordData * @param {!Array} layers */ constructor(qubitCoordData, layers = []) { if (!(qubitCoordData instanceof Float64Array)) { throw new Error('!(qubitCoords instanceof Float64Array)'); } if (!Array.isArray(layers)) { throw new Error('!Array.isArray(layers)'); } if (!layers.every(e => e instanceof Layer)) { throw new Error('!layers.every(e => e instanceof Layer)'); } this.qubitCoordData = qubitCoordData; this.layers = layers; } /** * @param {!string} stimCircuit * @returns {!Circuit} */ static fromStimCircuit(stimCircuit) { let lines = stimCircuit.replaceAll(';', '\n'). replaceAll('#!pragma ERR', 'ERR'). replaceAll('#!pragma MARK', 'MARK'). replaceAll('#!pragma POLYGON', 'POLYGON'). replaceAll('_', ' '). replaceAll('Q(', 'QUBIT_COORDS('). replaceAll('DT', 'DETECTOR'). replaceAll('OI', 'OBSERVABLE_INCLUDE'). replaceAll(' COORDS', '_COORDS'). replaceAll(' ERROR', '_ERROR'). replaceAll('C XYZ', 'C_XYZ'). replaceAll('C NXYZ', 'C_NXYZ'). replaceAll('C XNYZ', 'C_XNYZ'). replaceAll('C XYNZ', 'C_XYNZ'). replaceAll('H XY', 'H_XY'). replaceAll('H XZ', 'H_XZ'). replaceAll('H YZ', 'H_YZ'). replaceAll('H NXY', 'H_NXY'). replaceAll('H NXZ', 'H_NXZ'). replaceAll('H NYZ', 'H_NYZ'). replaceAll(' INCLUDE', '_INCLUDE'). replaceAll('SQRT ', 'SQRT_'). replaceAll(' DAG ', '_DAG '). replaceAll('C ZYX', 'C_ZYX'). replaceAll('C NZYX', 'C_NZYX'). replaceAll('C ZNYX', 'C_ZNYX'). replaceAll('C ZYNX', 'C_ZYNX'). split('\n'); let layers = [new Layer()]; let num_detectors = 0; let i2q = new Map(); let used_positions = new Set(); let findEndOfBlock = (lines, startIndex, endIndex) => { let nestLevel = 0; for (let k = startIndex; k < endIndex; k++) { let line = lines[k]; line = line.split('#')[0].trim(); if (line.toLowerCase().startsWith("repeat ")) { nestLevel++; } else if (line === '}') { nestLevel--; if (nestLevel === 0) { return k; } } } throw Error("Repeat block didn't end"); }; let processLineChunk = (lines, startIndex, endIndex, repetitions) => { if (!layers[layers.length - 1].empty()) { layers.push(new Layer()); } for (let rep = 0; rep < repetitions; rep++) { for (let k = startIndex; k < endIndex; k++) { let line = lines[k]; line = line.split('#')[0].trim(); if (line.toLowerCase().startsWith("repeat ")) { let reps = parseInt(line.split(" ")[1]); let k2 = findEndOfBlock(lines, k, endIndex); processLineChunk(lines, k + 1, k2, reps); k = k2; } else { processLine(line); } } if (!layers[layers.length - 1].empty()) { layers.push(new Layer()); } } }; let measurement_locs = []; let processLine = line => { let args = []; let targets = []; let tag = ''; let name = ''; let firstSpace = line.indexOf(' '); let firstParens = line.indexOf('('); let tagStart = line.indexOf('['); let tagEnd = line.indexOf(']'); if (tagStart !== -1 && firstSpace !== -1 && firstSpace < tagStart) { tagStart = -1; } if (tagStart !== -1 && firstParens !== -1 && firstParens < tagStart) { tagStart = -1; } if (tagStart !== -1 && tagEnd > tagStart) { tag = line.substring(tagStart + 1, tagEnd).replaceAll('\\C', ']').replaceAll('\\r', '\r').replaceAll('\\n', '\n').replaceAll('\\B', '\\'); line = line.substring(0, tagStart) + ' ' + line.substring(tagEnd + 1) } if (line.indexOf(')') !== -1) { let [ab, c] = line.split(')'); let [a, b] = ab.split('('); name = a.trim(); args = b.split(',').map(e => e.trim()).map(parseFloat); targets = processTargetsTextIntoTargets(c); } else { let ab = line.split(' ').map(e => e.trim()).filter(e => e !== ''); if (ab.length === 0) { return; } let [a, ...b] = ab; name = a.trim(); args = []; targets = b.flatMap(processTargetsTextIntoTargets); } let reverse_pairs = false; if (name === '') { return; } if (args.length > 0 && ['M', 'MX', 'MY', 'MZ', 'MR', 'MRX', 'MRY', 'MRZ', 'MPP', 'MPAD'].indexOf(name) !== -1) { args = []; } let alias = GATE_ALIAS_MAP.get(name); if (alias !== undefined) { if (alias.ignore) { return; } else if (alias.name !== undefined) { reverse_pairs = alias.rev_pair !== undefined && alias.rev_pair; name = alias.name; } else { throw new Error(`Unimplemented alias ${name}: ${describe(alias)}.`); } } else if (name === 'TICK') { layers.push(new Layer()); return; } else if (name === 'MPP') { let combinedTargets = splitUncombinedTargets(targets); let layer = layers[layers.length - 1] for (let combo of combinedTargets) { let op = simplifiedMPP(tag, new Float32Array(args), combo, false); try { layer.put(op, false); } catch (_) { layers.push(new Layer()); layer = layers[layers.length - 1]; layer.put(op, false); } measurement_locs.push({layer: layers.length - 1, targets: op.id_targets}); } return; } else if (name === 'DETECTOR' || name === 'OBSERVABLE_INCLUDE') { let isDet = name === 'DETECTOR'; let argIndex = isDet ? num_detectors : args.length > 0 ? Math.round(args[0]) : 0; for (let target of targets) { if (!target.startsWith("rec[-") || ! target.endsWith("]")) { console.warn("Ignoring instruction due to non-record target: " + line); return; } let index = measurement_locs.length + Number.parseInt(target.substring(4, target.length - 1)); if (index < 0 || index >= measurement_locs.length) { console.warn("Ignoring instruction due to out of range record target: " + line); return; } let loc = measurement_locs[index]; layers[loc.layer].markers.push( new Operation(GATE_MAP.get(name), tag, new Float32Array([argIndex]), new Uint32Array([loc.targets[0]]), )); } num_detectors += isDet; return; } else if (name === 'SPP' || name === 'SPP_DAG') { let dag = name === 'SPP_DAG'; let combinedTargets = splitUncombinedTargets(targets); let layer = layers[layers.length - 1] for (let combo of combinedTargets) { try { layer.put(simplifiedSPP(tag, new Float32Array(args), dag, combo), false); } catch (_) { layers.push(new Layer()); layer = layers[layers.length - 1]; layer.put(simplifiedSPP(tag, new Float32Array(args), dag, combo), false); } } return; } else if (name.startsWith('QUBIT_COORDS')) { let x = args.length < 1 ? 0 : args[0]; let y = args.length < 2 ? 0 : args[1]; for (let targ of targets) { let t = parseInt(targ); if (i2q.has(t)) { console.warn(`Ignoring "${line}" because there's already coordinate data for qubit ${t}.`); } else if (used_positions.has(`${x},${y}`)) { console.warn(`Ignoring "${line}" because there's already a qubit placed at ${x},${y}.`); } else { i2q.set(t, [x, y]); used_positions.add(`${x},${y}`); } } return; } let has_feedback = false; for (let targ of targets) { if (targ.startsWith("rec[")) { if (name === "CX" || name === "CY" || name === "CZ" || name === "ZCX" || name === "ZCY") { has_feedback = true; } } else if (typeof parseInt(targ) !== 'number') { throw new Error(line); } } if (has_feedback) { let clean_targets = []; for (let k = 0; k < targets.length; k += 2) { let b0 = targets[k].startsWith("rec["); let b1 = targets[k + 1].startsWith("rec["); if (b0 || b1) { if (!b0) { layers[layers.length - 1].put(new Operation( GATE_MAP.get("ERR"), tag, new Float32Array([]), new Uint32Array([targets[k]]), )); } if (!b1) { layers[layers.length - 1].put(new Operation( GATE_MAP.get("ERR"), tag, new Float32Array([]), new Uint32Array([targets[k + 1]]), )); } console.warn("Feedback isn't supported yet. Ignoring", name, targets[k], targets[k + 1]); } else { clean_targets.push(targets[k]); clean_targets.push(targets[k + 1]); } } targets = clean_targets; if (targets.length === 0) { return; } } let gate = GATE_MAP.get(name); if (gate === undefined) { console.warn("Ignoring unrecognized instruction: " + line); return; } let a = new Float32Array(args); let layer = layers[layers.length - 1]; if (gate.num_qubits === undefined) { layer.put(new Operation(gate, tag, a, new Uint32Array(targets))); } else { if (targets.length % gate.num_qubits !== 0) { throw new Error("Incorrect number of targets in line " + line); } for (let k = 0; k < targets.length; k += gate.num_qubits) { let sub_targets = targets.slice(k, k + gate.num_qubits); if (reverse_pairs) { sub_targets.reverse(); } let qs = new Uint32Array(sub_targets); let op = new Operation(gate, tag, a, qs); try { layer.put(op, false); } catch (_) { layers.push(new Layer()); layer = layers[layers.length - 1]; layer.put(op, false); } if (op.countMeasurements() > 0) { measurement_locs.push({layer: layers.length - 1, targets: op.id_targets}); } } } }; processLineChunk(lines, 0, lines.length, 1); if (layers.length > 0 && layers[layers.length - 1].isEmpty()) { layers.pop(); } let next_auto_position_x = 0; let ensure_has_coords = (t) => { let b = true; while (!i2q.has(t)) { let x = b ? t : next_auto_position_x; let k = `${x},0`; if (!used_positions.has(k)) { used_positions.add(k); i2q.set(t, [x, 0]); } next_auto_position_x += !b; b = false; } }; for (let layer of layers) { for (let op of layer.iter_gates_and_markers()) { for (let t of op.id_targets) { ensure_has_coords(t); } } } let numQubits = Math.max(...i2q.keys(), 0) + 1; let qubitCoords = new Float64Array(numQubits*2); for (let q = 0; q < numQubits; q++) { ensure_has_coords(q); let [x, y] = i2q.get(q); qubitCoords[2*q] = x; qubitCoords[2*q + 1] = y; } return new Circuit(qubitCoords, layers); } /** * @returns {!Set} */ allQubits() { let result = new Set(); for (let layer of this.layers) { for (let op of layer.iter_gates_and_markers()) { for (let t of op.id_targets) { result.add(t); } } } return result; } /** * @returns {!Circuit} */ rotated45() { return this.afterCoordTransform((x, y) => [x - y, x + y]); } coordTransformForRectification() { let coordSet = new Map(); for (let k = 0; k < this.qubitCoordData.length; k += 2) { let x = this.qubitCoordData[k]; let y = this.qubitCoordData[k+1]; coordSet.set(`${x},${y}`, [x, y]); } let minX = Infinity; let minY = Infinity; let step = 256; for (let [x, y] of coordSet.values()) { minX = Math.min(x, minX); minY = Math.min(y, minY); while ((x % step !== 0 || y % step !== 0) && step > 1 / 256) { step /= 2; } } let scale; if (step <= 1 / 256) { scale = 1; } else { scale = 1 / step; let mask = 0; for (let [x, y] of coordSet.values()) { let b1 = (x - minX + y - minY) % (2 * step); let b2 = (x - minX - y + minY) % (2 * step); mask |= b1 === 0 ? 1 : 2; mask |= b2 === 0 ? 4 : 8; } if (mask === (1 | 4)) { scale /= 2; } else if (mask === (2 | 8)) { minX -= step; scale /= 2; } } let offsetX = -minX; let offsetY = -minY; return (x, y) => [(x + offsetX) * scale, (y + offsetY) * scale]; } /** * @returns {!Circuit} */ afterRectification() { return this.afterCoordTransform(this.coordTransformForRectification()); } /** * @param {!number} dx * @param {!number} dy * @returns {!Circuit} */ shifted(dx, dy) { return this.afterCoordTransform((x, y) => [x + dx, y + dy]); } /** * @return {!Circuit} */ copy() { return this.shifted(0, 0); } /** * @param {!function(!number, !number): ![!number, !number]} coordTransform * @returns {!Circuit} */ afterCoordTransform(coordTransform) { let newCoords = new Float64Array(this.qubitCoordData.length); for (let k = 0; k < this.qubitCoordData.length; k += 2) { let x = this.qubitCoordData[k]; let y = this.qubitCoordData[k + 1]; let [x2, y2] = coordTransform(x, y); newCoords[k] = x2; newCoords[k + 1] = y2; } let newLayers = this.layers.map(e => e.copy()); return new Circuit(newCoords, newLayers); } /** * @param {!boolean} orderForToStimCircuit * @returns {!{dets: !Array, qids: !Array}>, obs: !Map>}} */ collectDetectorsAndObservables(orderForToStimCircuit) { // Index measurements. let m2d = new Map(); for (let k = 0; k < this.layers.length; k++) { let layer = this.layers[k]; if (orderForToStimCircuit) { for (let group of layer.opsGroupedByNameWithArgs().values()) { for (let op of group) { if (op.countMeasurements() > 0) { let target_id = op.id_targets[0]; m2d.set(`${k}:${target_id}`, {mid: m2d.size, qids: op.id_targets}); } } } } else { for (let [target_id, op] of layer.id_ops.entries()) { if (op.id_targets[0] === target_id) { if (op.countMeasurements() > 0) { m2d.set(`${k}:${target_id}`, {mid: m2d.size, qids: op.id_targets}); } } } } } let detectors = []; let observables = new Map(); for (let k = 0; k < this.layers.length; k++) { let layer = this.layers[k]; for (let op of layer.markers) { if (op.gate.name === 'DETECTOR') { let d = Math.round(op.args[0]); while (detectors.length <= d) { detectors.push({mids: [], qids: []}); } let det_entry = detectors[d]; let key = `${k}:${op.id_targets[0]}`; let v = m2d.get(key); if (v !== undefined) { det_entry.mids.push(v.mid - m2d.size); det_entry.qids.push(...v.qids); } } else if (op.gate.name === 'OBSERVABLE_INCLUDE') { let d = Math.round(op.args[0]); let entries = observables.get(d); if (entries === undefined) { entries = [] observables.set(d, entries); } let key = `${k}:${op.id_targets[0]}`; if (m2d.has(key)) { entries.push(m2d.get(key).mid - m2d.size); } } } } let seen = new Set(); let keptDetectors = []; for (let ds of detectors) { if (ds.mids.length > 0) { ds.mids = [...new Set(ds.mids)]; ds.mids.sort((a, b) => b - a); let key = ds.mids.join(':'); if (!seen.has(key)) { seen.add(key); keptDetectors.push(ds); } } } for (let [k, vs] of observables.entries()) { vs = [...new Set(vs)] vs.sort((a, b) => b - a); observables.set(k, vs); } keptDetectors.sort((a, b) => a.mids[0] - b.mids[0]); return {dets: keptDetectors, obs: observables}; } /** * @returns {!string} */ toStimCircuit() { let usedQubits = new Set(); for (let layer of this.layers) { for (let op of layer.iter_gates_and_markers()) { for (let t of op.id_targets) { usedQubits.add(t); } } } let {dets: remainingDetectors, obs: remainingObservables} = this.collectDetectorsAndObservables(true); remainingDetectors.reverse(); let seenMeasurements = 0; let totalMeasurements = this.countMeasurements(); let packedQubitCoords = []; for (let q of usedQubits) { let x = this.qubitCoordData[2*q]; let y = this.qubitCoordData[2*q+1]; packedQubitCoords.push({q, x, y}); } packedQubitCoords.sort((a, b) => { if (a.x !== b.x) { return a.x - b.x; } if (a.y !== b.y) { return a.y - b.y; } return a.q - b.q; }); let old2new = new Map(); let out = []; for (let q = 0; q < packedQubitCoords.length; q++) { let {q: old_q, x, y} = packedQubitCoords[q]; old2new.set(old_q, q); out.push(`QUBIT_COORDS(${x}, ${y}) ${q}`); } let detectorLayer = 0; let usedDetectorCoords = new Set(); for (let layer of this.layers) { let opsByName = layer.opsGroupedByNameWithArgs(); for (let [nameWithArgs, group] of opsByName.entries()) { let targetGroups = []; let gateName = nameWithArgs.split('(')[0].split('[')[0]; if (gateName === 'DETECTOR' || gateName === 'OBSERVABLE_INCLUDE') { continue; } let gate = GATE_MAP.get(gateName); if (gate === undefined && (gateName === 'MPP' || gateName === 'SPP' || gateName === 'SPP_DAG')) { let line = [gateName + ' ']; for (let op of group) { seenMeasurements += op.countMeasurements(); let bases = op.gate.name.substring(gateName.length + 1); for (let k = 0; k < op.id_targets.length; k++) { line.push(bases[k] + old2new.get(op.id_targets[k])); line.push('*'); } line.pop(); line.push(' '); } out.push(line.join('').trim()); } else { if (gate !== undefined && gate.can_fuse) { let flatTargetGroups = []; for (let op of group) { seenMeasurements += op.countMeasurements(); flatTargetGroups.push(...op.id_targets) } targetGroups.push(flatTargetGroups); } else { for (let op of group) { seenMeasurements += op.countMeasurements(); targetGroups.push([...op.id_targets]) } } for (let targetGroup of targetGroups) { let line = [nameWithArgs]; for (let t of targetGroup) { line.push(old2new.get(t)); } out.push(line.join(' ')); } } } // Output DETECTOR lines immediately after the last measurement layer they use. let nextDetectorLayer = detectorLayer; while (remainingDetectors.length > 0) { let candidate = remainingDetectors[remainingDetectors.length - 1]; let offset = totalMeasurements - seenMeasurements; if (candidate.mids[0] + offset >= 0) { break; } remainingDetectors.pop(); let cxs = []; let cys = []; let sx = 0; let sy = 0; for (let q of candidate.qids) { let cx = this.qubitCoordData[2 * q]; let cy = this.qubitCoordData[2 * q + 1]; sx += cx; sy += cy; cxs.push(cx); cys.push(cy); } if (candidate.qids.length > 0) { sx /= candidate.qids.length; sy /= candidate.qids.length; sx = Math.round(sx * 2) / 2; sy = Math.round(sy * 2) / 2; } cxs.push(sx); cys.push(sy); let name; let dt = detectorLayer; for (let k = 0; ; k++) { if (k >= cxs.length) { k = 0; dt += 1; } name = `DETECTOR(${cxs[k]}, ${cys[k]}, ${dt})`; if (!usedDetectorCoords.has(name)) { break; } } usedDetectorCoords.add(name); let line = [name]; for (let d of candidate.mids) { line.push(`rec[${d + offset}]`) } out.push(line.join(' ')); nextDetectorLayer = Math.max(nextDetectorLayer, dt + 1); } detectorLayer = nextDetectorLayer; // Output OBSERVABLE_INCLUDE lines immediately after the last measurement layer they use. for (let [obsIndex, candidate] of [...remainingObservables.entries()]) { let offset = totalMeasurements - seenMeasurements; if (candidate[0] + offset >= 0) { continue; } remainingObservables.delete(obsIndex); let line = [`OBSERVABLE_INCLUDE(${obsIndex})`]; for (let d of candidate) { line.push(`rec[${d + offset}]`) } out.push(line.join(' ')); } out.push(`TICK`); } while (out.length > 0 && out[out.length - 1] === 'TICK') { out.pop(); } return out.join('\n'); } /** * @returns {!int} */ countMeasurements() { let total = 0; for (let layer of this.layers) { total += layer.countMeasurements(); } return total; } /** * @param {!Iterable} coords */ withCoordsIncluded(coords) { let coordMap = this.coordToQubitMap(); let extraCoordData = []; for (let [x, y] of coords) { let key = `${x},${y}`; if (!coordMap.has(key)) { coordMap.set(key, coordMap.size); extraCoordData.push(x, y); } } return new Circuit( new Float64Array([...this.qubitCoordData, ...extraCoordData]), this.layers.map(e => e.copy()), ); } /** * @returns {!Map} */ coordToQubitMap() { let result = new Map(); for (let q = 0; q < this.qubitCoordData.length; q += 2) { let x = this.qubitCoordData[q]; let y = this.qubitCoordData[q + 1]; result.set(`${x},${y}`, q / 2); } return result; } /** * @returns {!string} */ toString() { return this.toStimCircuit(); } /** * @param {*} other * @returns {!boolean} */ isEqualTo(other) { if (!(other instanceof Circuit)) { return false; } return this.toStimCircuit() === other.toStimCircuit(); } } export {Circuit, processTargetsTextIntoTargets, splitUncombinedTargets}; ================================================ FILE: glue/crumble/circuit/circuit.test.js ================================================ import {test, assertThat} from "../test/test_util.js" import {Circuit, processTargetsTextIntoTargets, splitUncombinedTargets} from "./circuit.js" import {Operation} from "./operation.js"; import {GATE_MAP} from "../gates/gateset.js"; import {make_mpp_gate} from '../gates/gateset_mpp.js'; test("circuit.fromStimCircuit", () => { let c1 = Circuit.fromStimCircuit(` QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(3, 4) 1 QUBIT_COORDS(5, 6) 2 H 0 S 1 2 TICK CX 2 0 `); assertThat(c1.qubitCoordData).isEqualTo(new Float64Array([1, 2, 3, 4, 5, 6])); let c2 = Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(2, 4) 3 QUBIT_COORDS(2, 3) 4 QUBIT_COORDS(3, 5) 5 QUBIT_COORDS(3, 6) 6 H 0 S 3 4 S[test] 1 2 CX 5 6 `); assertThat(c2.qubitCoordData).isEqualTo(new Float64Array([0, 0, 0, 1, 2, 0, 2, 4, 2, 3, 3, 5, 3, 6])); assertThat(c2.layers.length).isEqualTo(1); assertThat(c2.layers[0].id_ops).isEqualTo(new Map([ [0, new Operation(GATE_MAP.get('H'), '', new Float32Array(), new Uint32Array([0]))], [1, new Operation(GATE_MAP.get('S'), 'test', new Float32Array(), new Uint32Array([1]))], [2, new Operation(GATE_MAP.get('S'), 'test', new Float32Array(), new Uint32Array([2]))], [3, new Operation(GATE_MAP.get('S'), '', new Float32Array(), new Uint32Array([3]))], [4, new Operation(GATE_MAP.get('S'), '', new Float32Array(), new Uint32Array([4]))], [5, new Operation(GATE_MAP.get('CX'), '', new Float32Array(), new Uint32Array([5, 6]))], [6, new Operation(GATE_MAP.get('CX'), '', new Float32Array(), new Uint32Array([5, 6]))], ])); assertThat(c2.toStimCircuit()).isEqualTo(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(2, 3) 3 QUBIT_COORDS(2, 4) 4 QUBIT_COORDS(3, 5) 5 QUBIT_COORDS(3, 6) 6 CX 5 6 H 0 S 4 3 S[test] 1 2 `.trim()); }); test("circuit.fromStimCircuit_strip_measurement_noise", () => { let c1 = Circuit.fromStimCircuit(` M(0.1) 0 `); assertThat(c1.toStimCircuit()).isEqualTo(` QUBIT_COORDS(0, 0) 0 M 0 `.trim()); }); test("circuit.fromStimCircuit_detector", () => { let c = Circuit.fromStimCircuit(` QUBIT_COORDS(2, 3) 0 R 0 M 0 R 0 DETECTOR rec[-1] `); assertThat(c.toStimCircuit()).isEqualTo(` QUBIT_COORDS(2, 3) 0 R 0 TICK M 0 DETECTOR(2, 3, 0) rec[-1] TICK R 0 `.trim()); }) test("circuit.fromStimCircuit_observable", () => { let c = Circuit.fromStimCircuit(` R 0 M 0 OBSERVABLE_INCLUDE(3) rec[-1] R 0 `); assertThat(c.toStimCircuit()).isEqualTo(` QUBIT_COORDS(0, 0) 0 R 0 TICK M 0 OBSERVABLE_INCLUDE(3) rec[-1] TICK R 0 `.trim()); }) test("circuit.fromStimCircuit_mpp", () => { let c1 = Circuit.fromStimCircuit(` QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(3, 4) 1 QUBIT_COORDS(5, 6) 2 MPP X0*Z1*Y2 X2*Y1 `); assertThat(c1.qubitCoordData).isEqualTo( new Float64Array([1, 2, 3, 4, 5, 6])); assertThat(c1.layers.length).isEqualTo(2); let op1 = new Operation(make_mpp_gate("XZY"), '', new Float32Array([]), new Uint32Array([0, 1, 2])); assertThat(c1.layers[0].id_ops).isEqualTo(new Map([ [0, op1], [1, op1], [2, op1], ])); let op2 = new Operation(make_mpp_gate("XY"), '', new Float32Array([]), new Uint32Array([2, 1])); assertThat(c1.layers[1].id_ops).isEqualTo(new Map([ [1, op2], [2, op2], ])); assertThat(c1.toStimCircuit()).isEqualTo(` QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(3, 4) 1 QUBIT_COORDS(5, 6) 2 MPP X0*Z1*Y2 TICK MPP X2*Y1 `.trim()); }); test('circuit.coordToQubitMap', () => { let c = Circuit.fromStimCircuit(` QUBIT_COORDS(5, 6) 1 QUBIT_COORDS(10, 7) 2 QUBIT_COORDS(20, 17) 0 `); assertThat(c.qubitCoordData).isEqualTo(new Float64Array([20, 17, 5, 6, 10, 7])); assertThat(c.coordToQubitMap()).isEqualTo(new Map([ ['5,6', 1], ['10,7', 2], ['20,17', 0], ])); }); test('circuit.processTargetsTextIntoTargets', () => { assertThat(processTargetsTextIntoTargets('')).isEqualTo([]); assertThat(processTargetsTextIntoTargets(' X0*X1')).isEqualTo(['X0', '*', 'X1']); assertThat(processTargetsTextIntoTargets(' 0 1 2')).isEqualTo(['0', '1', '2']); assertThat(processTargetsTextIntoTargets(' X0*X1*Z3 Y1 Y2*Y4')).isEqualTo(['X0', '*', 'X1', '*', 'Z3', 'Y1', 'Y2', '*', 'Y4']); }); test('circuit.splitUncombinedTargets', () => { assertThat(splitUncombinedTargets([])).isEqualTo([]); assertThat(splitUncombinedTargets(['X1'])).isEqualTo([['X1']]); assertThat(splitUncombinedTargets(['X1', 'X2'])).isEqualTo([['X1'], ['X2']]); assertThat(splitUncombinedTargets(['X1', '*', 'X2'])).isEqualTo([['X1', 'X2']]); assertThat(splitUncombinedTargets(['X1', '*', 'X2', 'Y1', '*', 'Y2', '*', 'Y3', 'Z1', 'Z2', '*', 'Z3'])).isEqualTo([['X1', 'X2'], ['Y1', 'Y2', 'Y3'], ['Z1'], ['Z2', 'Z3']]); assertThat(() => splitUncombinedTargets(['X1', '*', '*', 'X2'])).throwsAnExceptionThat().matches(/Adjacent combiners/); assertThat(() => splitUncombinedTargets(['X1', '*'])).throwsAnExceptionThat().matches(/Dangling combiner/); assertThat(() => splitUncombinedTargets(['*', 'X1'])).throwsAnExceptionThat().matches(/Leading combiner/); }); test('circuit.withCoordsIncluded', () => { let c = Circuit.fromStimCircuit(` QUBIT_COORDS(10, 7) 1 QUBIT_COORDS(5, 6) 0 `); assertThat(c.qubitCoordData).isEqualTo(new Float64Array([5, 6, 10, 7])); let c2 = c.withCoordsIncluded([[5, 6], [2, 4], [2, 4]]) assertThat(c2.qubitCoordData).isEqualTo(new Float64Array([5, 6, 10, 7, 2, 4])); }); test("circuit.isEqualTo", () => { assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 H 0 `)).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 H 0 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 H 0 `)).isNotEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 X 0 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 H 0 `)).isNotEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(1, 0) 0 H 0 `)); }); test("circuit.copy", () => { let c = Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(2, 3) 1 H 0 CNOT 0 1 S 1 `); let c2 = c.copy(); assertThat(c !== c2).isEqualTo(true); assertThat(c).isEqualTo(c2); }); test("circuit.afterRectification", () => { assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 2) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 0.5) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.5, 0) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(2, 0) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.5, 0.5) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.5, 0.5) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0.5, 0) 0 QUBIT_COORDS(1, 0.5) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.5, 0.5) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0.5) 0 QUBIT_COORDS(0.5, 0) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0.5, 0.5) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(2, 1) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0.5, 0.5) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0.5, 0.5) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0.5, 0.5) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.25, 0.25) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.5, 0.5) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 1) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.5, 0.5) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 1) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0.5, 0.5) 0 QUBIT_COORDS(1, 0) 1 X 0 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(10, 20) 0 QUBIT_COORDS(12, 20) 1 QUBIT_COORDS(12, 21) 2 QUBIT_COORDS(10, 21) 3 H 0 X 1 Y 2 Z 3 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(2, 0) 1 QUBIT_COORDS(2, 1) 2 QUBIT_COORDS(0, 1) 3 H 0 X 1 Y 2 Z 3 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(-2, 0) 1 QUBIT_COORDS(-2, -1) 2 QUBIT_COORDS(0, -1) 3 H 0 X 1 Y 2 Z 3 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(2, 1) 0 QUBIT_COORDS(0, 1) 1 QUBIT_COORDS(0, 0) 2 QUBIT_COORDS(2, 0) 3 H 0 X 1 Y 2 Z 3 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 4) 0 QUBIT_COORDS(2, 0) 1 QUBIT_COORDS(0, 0) 2 QUBIT_COORDS(4, 0) 3 H 0 X 1 Y 2 Z 3 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 2) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(0, 0) 2 QUBIT_COORDS(2, 0) 3 H 0 X 1 Y 2 Z 3 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0.5) 0 QUBIT_COORDS(0.25, 0) 1 QUBIT_COORDS(0, 0) 2 QUBIT_COORDS(0.5, 0) 3 H 0 X 1 Y 2 Z 3 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 2) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(0, 0) 2 QUBIT_COORDS(2, 0) 3 H 0 X 1 Y 2 Z 3 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.5, 0.5) 1 QUBIT_COORDS(1, 1) 2 QUBIT_COORDS(0.5, 1.5) 3 H 0 X 1 Y 2 Z 3 `).afterRectification()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0.5, 0.5) 1 QUBIT_COORDS(1, 1) 2 QUBIT_COORDS(0.5, 1.5) 3 H 0 X 1 Y 2 Z 3 `)); }); test("circuit.rotated45", () => { let circuit = Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(2, 0) 1 QUBIT_COORDS(2, 1) 2 QUBIT_COORDS(0, 1) 3 H 0 X 1 Y 2 Z 3 `); let c45 = circuit.rotated45().afterRectification(); let c90 = c45.rotated45().afterRectification(); let c180 = c90.rotated45().rotated45().afterRectification(); assertThat(circuit.rotated45().rotated45().rotated45().rotated45()).isEqualTo( circuit.afterCoordTransform((x, y) => [-4*x, -4*y])); assertThat(c180).isEqualTo( circuit.afterCoordTransform((x, y) => [2-x, 1-y])); assertThat(c90).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(1, 0) 0 QUBIT_COORDS(1, 2) 1 QUBIT_COORDS(0, 2) 2 QUBIT_COORDS(0, 0) 3 H 0 X 1 Y 2 Z 3 `)); assertThat(c45).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(1, 0) 0 QUBIT_COORDS(2, 1) 1 QUBIT_COORDS(1.5, 1.5) 2 QUBIT_COORDS(0.5, 0.5) 3 H 0 X 1 Y 2 Z 3 `)); }); test("circuit.fromStimCircuit.automaticTicks", () => { assertThat(Circuit.fromStimCircuit(` H 0 CX 0 1 `)).isEqualTo(Circuit.fromStimCircuit(` H 0 TICK CX 0 1 `)); }); test("circuit.inferAndConvertCoordinates", () => { assertThat(Circuit.fromStimCircuit(` H 0 1 2 3 `)).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 0) 3 H 0 1 2 3 `)); assertThat(Circuit.fromStimCircuit(` H 0 3 2 1 `)).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 0) 3 H 0 3 2 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(2) 0 QUBIT_COORDS(3) 1 QUBIT_COORDS(5) 2 QUBIT_COORDS(7) 3 H 0 3 2 1 `)).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(2, 0) 0 QUBIT_COORDS(3, 0) 1 QUBIT_COORDS(5, 0) 2 QUBIT_COORDS(7, 0) 3 H 0 3 2 1 `)); assertThat(Circuit.fromStimCircuit(` QUBIT_COORDS(2, 1, 9, 9, 9) 0 QUBIT_COORDS(3, 1) 1 QUBIT_COORDS(5, 1, 9) 2 QUBIT_COORDS(7, 1, 9, 9) 3 H 0 3 2 1 `)).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(2, 1) 0 QUBIT_COORDS(3, 1) 1 QUBIT_COORDS(5, 1) 2 QUBIT_COORDS(7, 1) 3 H 0 3 2 1 `)); }); test("circuit.parse_mpp", () => { assertThat(Circuit.fromStimCircuit(` MPP Z0*Z1*Z2 Z3*Z4*Z6 `).toString()).isEqualTo(` 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 QUBIT_COORDS(6, 0) 5 MPP Z0*Z1*Z2 Z3*Z4*Z5 `.trim()) assertThat(Circuit.fromStimCircuit(` MPP Z0*Z1*Z2 Z3*Z4*Z5*X6 `).toString()).isEqualTo(` 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 QUBIT_COORDS(5, 0) 5 QUBIT_COORDS(6, 0) 6 MPP Z0*Z1*Z2 Z3*Z4*Z5*X6 `.trim()) }); test("circuit.fromStimCircuit_manygates", () => { let c = Circuit.fromStimCircuit(` QUBIT_COORDS(1, 2, 3) 0 # Pauli gates I 0 X 1 Y 2 Z 3 TICK # Single Qubit Clifford Gates C_XYZ 0 C_ZYX 1 H_XY 2 H_XZ 3 H_YZ 4 SQRT_X 0 SQRT_X_DAG 1 SQRT_Y 2 SQRT_Y_DAG 3 SQRT_Z 4 SQRT_Z_DAG 5 TICK # Two Qubit Clifford Gates CXSWAP 0 1 ISWAP 2 3 ISWAP_DAG 4 5 SWAP 6 7 SWAPCX 8 9 CZSWAP 10 11 SQRT_XX 0 1 SQRT_XX_DAG 2 3 SQRT_YY 4 5 SQRT_YY_DAG 6 7 SQRT_ZZ 8 9 SQRT_ZZ_DAG 10 11 XCX 0 1 XCY 2 3 XCZ 4 5 YCX 6 7 YCY 8 9 YCZ 10 11 ZCX 12 13 ZCY 14 15 ZCZ 16 17 TICK # Noise Channels CORRELATED_ERROR(0.01) X1 Y2 Z3 ELSE_CORRELATED_ERROR(0.02) X4 Y7 Z6 DEPOLARIZE1(0.02) 0 DEPOLARIZE2(0.03) 1 2 PAULI_CHANNEL_1(0.01, 0.02, 0.03) 3 PAULI_CHANNEL_2(0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009, 0.010, 0.011, 0.012, 0.013, 0.014, 0.015) 4 5 X_ERROR(0.01) 0 Y_ERROR(0.02) 1 Z_ERROR(0.03) 2 HERALDED_ERASE(0.04) 3 HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.03, 0.04) 6 TICK # Pauli Product Gates MPP X0*Y1*Z2 Z0*Z1 SPP X0*Y1*Z2 X3 SPP_DAG X0*Y1*Z2 X2 TICK # Collapsing Gates MRX 0 MRY 1 MRZ 2 MX 3 MY 4 MZ 5 6 RX 7 RY 8 RZ 9 TICK # Pair Measurement Gates MXX 0 1 2 3 MYY 4 5 MZZ 6 7 TICK # Control Flow REPEAT 3 { H 0 CX 0 1 S 1 TICK } TICK # Annotations MR 0 X_ERROR(0.1) 0 MR(0.01) 0 SHIFT_COORDS(1, 2, 3) DETECTOR(1, 2, 3) rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] MPAD 0 1 0 TICK # Inverted measurements. MRX !0 MY !1 MZZ !2 3 MYY !4 !5 MPP X6*!Y7*Z8 TICK # Feedback CX rec[-1] 0 CY sweep[0] 1 CZ 2 rec[-1] `); assertThat(c).isNotEqualTo(undefined); let c2 = Circuit.fromStimCircuit(`Q(1,2,3)0;I_0;X_1;Y_2;Z_3;TICK;C_XYZ_0;C_ZYX_1;H_XY_2;H_3;H_YZ_4;SQRT_X_0;SQRT_X_DAG_1;SQRT_Y_2;SQRT_Y_DAG_3;S_4;S_DAG_5;TICK;CXSWAP_0_1;ISWAP_2_3;ISWAP_DAG_4_5;SWAP_6_7;SWAPCX_8_9;CZSWAP_10_11;SQRT_XX_0_1;SQRT_XX_DAG_2_3;SQRT_YY_4_5;SQRT_YY_DAG_6_7;SQRT_ZZ_8_9;SQRT_ZZ_DAG_10_11;XCX_0_1;XCY_2_3;XCZ_4_5;YCX_6_7;YCY_8_9;YCZ_10_11;CX_12_13;CY_14_15;CZ_16_17;TICK;E(0.01)X1_Y2_Z3;ELSE_CORRELATED_ERROR(0.02)X4_Y7_Z6;DEPOLARIZE1(0.02)0;DEPOLARIZE2(0.03)1_2;PAULI_CHANNEL_1(0.01,0.02,0.03)3;PAULI_CHANNEL_2(0.001,0.002,0.003,0.004,0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015)4_5;X_ERROR(0.01)0;Y_ERROR(0.02)1;Z_ERROR(0.03)2;HERALDED_ERASE(0.04)3;HERALDED_PAULI_CHANNEL_1(0.01,0.02,0.03,0.04)6;TICK;MPP_X0*Y1*Z2_Z0*Z1;SPP_X0*Y1*Z2_X3;SPP_DAG_X0*Y1*Z2_X2;TICK;MRX_0;MRY_1;MR_2;MX_3;MY_4;M_5_6;RX_7;RY_8;R_9;TICK;MXX_0_1_2_3;MYY_4_5;MZZ_6_7;TICK;REPEAT_3_{;H_0;CX_0_1;S_1;TICK;};TICK;MR_0;X_ERROR(0.1)0;MR(0.01)0;SHIFT_COORDS(1,2,3);DETECTOR(1,2,3)rec[-1];OBSERVABLE_INCLUDE(0)rec[-1];MPAD_0_1_0;TICK;MRX_!0;MY_!1;MZZ_!2_3;MYY_!4_!5;MPP_X6*!Y7*Z8;TICK;CX_rec[-1]_0;CY_sweep[0]_1;CZ_2_rec[-1]`); assertThat(c2).isNotEqualTo(undefined); assertThat(c).isEqualTo(c2); }); test("circuit.fromStimCircuit_cx_ordering", () => { assertThat(Circuit.fromStimCircuit(` CX 0 1 `)).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 CX 0 1 `)); assertThat(Circuit.fromStimCircuit(` XCZ 1 0 `)).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 CX 0 1 `)); }); ================================================ FILE: glue/crumble/circuit/layer.js ================================================ import {Operation} from "./operation.js" import {GATE_MAP} from "../gates/gateset.js"; import {groupBy} from "../base/seq.js"; class Layer { constructor() { this.id_ops = /** @type {!Map} */ new Map(); this.markers /** @type {!Array} */ = []; } /** * @returns {!string} */ toString() { let result = 'Layer {\n'; result += " id_ops {\n"; for (let [key, val] of this.id_ops.entries()) { result += ` ${key}: ${val}\n` } result += ' }\n'; result += " markers {\n"; for (let val of this.markers) { result += ` ${val}\n` } result += ' }\n'; result += '}'; return result; } /** * @returns {Map>} */ opsGroupedByNameWithArgs() { let opsByName = groupBy(this.iter_gates_and_markers(), op => { let key = op.gate.name; if (key.startsWith('MPP:') && !GATE_MAP.has(key)) { key = 'MPP'; } if (key.startsWith('SPP:') && !GATE_MAP.has(key)) { key = 'SPP'; } if (key.startsWith('SPP_DAG:') && !GATE_MAP.has(key)) { key = 'SPP_DAG'; } if (op.tag !== '') { key += '[' + op.tag.replace('\\', '\\B').replace('\r', '\\r').replace('\n', '\\n').replace(']', '\\C') + ']'; } if (op.args.length > 0) { key += '(' + [...op.args].join(',') + ')'; } return key; }); let namesWithArgs = [...opsByName.keys()]; namesWithArgs.sort((a, b) => { let ma = a.startsWith('MARK') || a.startsWith('POLY'); let mb = b.startsWith('MARK') || b.startsWith('POLY'); if (ma !== mb) { return ma < mb ? -1 : +1; } return a < b ? -1 : a > b ? +1 : 0; }); return new Map(namesWithArgs.map(e => [e, opsByName.get(e)])); } /** * @returns {!Layer} */ copy() { let result = new Layer(); result.id_ops = new Map(this.id_ops); result.markers = [...this.markers]; return result; } /** * @returns {!int} */ countMeasurements() { let total = 0; for (let [target_id, op] of this.id_ops.entries()) { if (op.id_targets[0] === target_id) { total += op.countMeasurements(); } } return total; } /** * @returns {!boolean} */ hasDissipativeOperations() { let dissipative_gate_names = [ 'M', 'MX', 'MY', 'MR', 'MRX', 'MRY', 'MXX', 'MYY', 'MZZ', 'RX', 'RY', 'R', ] for (let op of this.id_ops.values()) { if (op.gate.name.startsWith('MPP:') || dissipative_gate_names.indexOf(op.gate.name) !== -1) { return true; } } return false; } hasSingleQubitCliffords() { let dissipative_gate_names = [ 'M', 'MX', 'MY', 'MR', 'MRX', 'MRY', 'MXX', 'MYY', 'MZZ', 'RX', 'RY', 'R', ] for (let op of this.id_ops.values()) { if (op.id_targets.length === 1 && dissipative_gate_names.indexOf(op.gate.name) === -1 && op.countMeasurements() === 0) { return true; } } return false; } /** * @returns {!boolean} */ hasResetOperations() { let gateNames = [ 'MR', 'MRX', 'MRY', 'RX', 'RY', 'R', ] for (let op of this.id_ops.values()) { if (gateNames.indexOf(op.gate.name) !== -1) { return true; } } return false; } /** * @returns {!boolean} */ hasMeasurementOperations() { let gateNames = [ 'M', 'MX', 'MY', 'MR', 'MRX', 'MRY', 'MXX', 'MYY', 'MZZ', ] for (let op of this.id_ops.values()) { if (op.gate.name.startsWith('MPP:') || gateNames.indexOf(op.gate.name) !== -1) { return true; } } return false; } /** * @return {!boolean} */ empty() { return this.id_ops.size === 0 && this.markers.length === 0; } /** * @param {!function(op: !Operation): !boolean} predicate * @returns {!Layer} */ id_filtered(predicate) { let newLayer = new Layer(); for (let op of this.id_ops.values()) { if (predicate(op)) { newLayer.put(op); } } for (let op of this.markers) { if (predicate(op)) { newLayer.markers.push(op); } } return newLayer; } /** * @param {!function(qubit: !int): !boolean} predicate * @returns {!Layer} */ id_filteredByQubit(predicate) { return this.id_filtered(op => !op.id_targets.every(q => !predicate(q))); } /** * @param {!Map} before * @param {!int} marker_index * @returns {!Map} */ id_pauliFrameAfter(before, marker_index) { let after = new Map(); let handled = new Set(); for (let k of before.keys()) { let v = before.get(k); let op = this.id_ops.get(k); if (op !== undefined) { let already_done = false; let b = ''; for (let q of op.id_targets) { if (handled.has(q)) { already_done = true; } handled.add(q); let r = before.get(q); if (r === undefined) { r = 'I'; } b += r; } let a = op.pauliFrameAfter(b); let hasErr = a.startsWith('ERR:'); for (let qi = 0; qi < op.id_targets.length; qi++) { let q = op.id_targets[qi]; if (hasErr) { after.set(q, 'ERR:' + a[4 + qi]); } else { after.set(q, a[qi]); } } } else { after.set(k, v); } } for (let op of this.markers) { if (op.gate.name === 'MARKX' && op.args[0] === marker_index) { let key = op.id_targets[0]; let pauli = after.get(key); if (pauli === undefined || pauli === 'I') { pauli = 'X'; } else if (pauli === 'X') { pauli = 'I'; } else if (pauli === 'Y') { pauli = 'Z'; } else if (pauli === 'Z') { pauli = 'Y'; } after.set(key, pauli); } else if (op.gate.name === 'MARKY' && op.args[0] === marker_index) { let key = op.id_targets[0]; let pauli = after.get(key); if (pauli === undefined || pauli === 'I') { pauli = 'Y'; } else if (pauli === 'X') { pauli = 'Z'; } else if (pauli === 'Y') { pauli = 'I'; } else if (pauli === 'Z') { pauli = 'X'; } after.set(key, pauli); } else if (op.gate.name === 'MARKZ' && op.args[0] === marker_index) { let key = op.id_targets[0]; let pauli = after.get(key); if (pauli === undefined || pauli === 'I') { pauli = 'Z'; } else if (pauli === 'X') { pauli = 'Y'; } else if (pauli === 'Y') { pauli = 'X'; } else if (pauli === 'Z') { pauli = 'I'; } after.set(key, pauli); } } return after; } /** * @returns {!boolean} */ isEmpty() { return this.id_ops.size === 0 && this.markers.length === 0; } /** * @param {!int} qubit * @returns {!Operation|undefined} */ id_pop_at(qubit) { this.markers = this.markers.filter(op => op.id_targets.indexOf(qubit) === -1); if (this.id_ops.has(qubit)) { let op = this.id_ops.get(qubit); for (let t of op.id_targets) { this.id_ops.delete(t); } return op; } return undefined; } /** * @param {!int} q * @param {undefined|!int} index */ id_dropMarkersAt(q, index=undefined) { this.markers = this.markers.filter(op => { if (index !== undefined && op.args[0] !== index) { return true; } if (op.gate.name !== 'MARKX' && op.gate.name !== 'MARKY' && op.gate.name !== 'MARKZ') { return true; } return op.id_targets[0] !== q; }); } /** * @param {!Operation} op * @param {!boolean=true} allow_overwrite */ put(op, allow_overwrite=true) { if (op.gate.is_marker) { if (op.gate.name === 'MARKX' || op.gate.name === 'MARKY' || op.gate.name === 'MARKZ') { this.id_dropMarkersAt(op.id_targets[0], op.args[0]); } this.markers.push(op); return; } for (let t of op.id_targets) { if (this.id_ops.has(t)) { if (allow_overwrite) { this.id_pop_at(t); } else { throw new Error("Collision"); } } } for (let t of op.id_targets) { this.id_ops.set(t, op); } } /** * @returns {!Iterator} */ *iter_gates_and_markers() { for (let t of this.id_ops.keys()) { let op = this.id_ops.get(t); if (op.id_targets[0] === t) { yield op; } } yield *this.markers; } } /** * @param {!Iterable} xys * @returns {![undefined | !number, undefined | !number]} */ function minXY(xys) { let minX = undefined; let minY = undefined; for (let [vx, vy] of xys) { if (minX === undefined || vx < minX || (vx === minX && vy < minY)) { minX = vx; minY = vy; } } return [minX, minY]; } export {Layer, minXY}; ================================================ FILE: glue/crumble/circuit/layer.test.js ================================================ import {test, assertThat} from "../test/test_util.js" import {PauliFrame} from "./pauli_frame.js" import {GATE_MAP} from "../gates/gateset.js" import {Operation} from "./operation.js"; import {Layer} from "./layer.js"; import {Circuit} from "./circuit.js"; test("layer.put_get", () => { let layer = new Layer(); assertThat(layer.id_ops).isEqualTo(new Map()); assertThat(layer.markers).isEqualTo([]); let op = new Operation(GATE_MAP.get("CX"), '', new Float32Array(), new Uint32Array([2, 3])); layer.put(op); assertThat(layer.id_ops).isEqualTo(new Map([ [2, op], [3, op], ])); assertThat(layer.markers).isEqualTo([]); let marker1 = new Operation(GATE_MAP.get("MARKX"), '', new Float32Array([0]), new Uint32Array([4])); let marker2 = new Operation(GATE_MAP.get("MARKZ"), '', new Float32Array([1]), new Uint32Array([5])); layer.put(marker1); layer.put(marker2); assertThat(layer.id_ops).isEqualTo(new Map([ [2, op], [3, op], ])); assertThat(layer.markers).isEqualTo([marker1, marker2]); }); test("layer.filtered", () => { let circuit = Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 2) 2 QUBIT_COORDS(3, 3) 3 H 0 1 2 S 3 TICK CNOT 0 1 CZ 2 3 TICK `); let c0 = Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 2) 2 QUBIT_COORDS(3, 3) 3 H 0 TICK CNOT 0 1 TICK `); let c01 = Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 2) 2 QUBIT_COORDS(3, 3) 3 H 0 1 TICK CNOT 0 1 TICK `); assertThat(circuit.layers[0].id_filteredByQubit(q => q === 0)).isEqualTo(c0.layers[0]); assertThat(circuit.layers[1].id_filteredByQubit(q => q === 0)).isEqualTo(c0.layers[1]); assertThat(circuit.layers[0].id_filteredByQubit(q => q === 0 || q === 1)).isEqualTo(c01.layers[0]); assertThat(circuit.layers[1].id_filteredByQubit(q => q === 0 || q === 1)).isEqualTo(c01.layers[1]); }); ================================================ FILE: glue/crumble/circuit/operation.js ================================================ import {Gate} from "../gates/gate.js" /** * @param {!string} base * @returns {!Array} */ function expandBase(base) { let result = []; for (let k = 0; k < base.length; k++) { let prefix = 'I'.repeat(k); let suffix = 'I'.repeat(base.length - k - 1); if (base[k] === 'X' || base[k] === 'Y') { result.push(prefix + 'X' + suffix); } if (base[k] === 'Z' || base[k] === 'Y') { result.push(prefix + 'Z' + suffix); } } return result; } class Operation { /** * @param {!Gate} gate * @param {!string} tag * @param {!Float32Array} args * @param {!Uint32Array} targets */ constructor(gate, tag, args, targets) { if (!(gate instanceof Gate)) { throw new Error(`!(gate instanceof Gate) gate=${gate}`); } if (!(args instanceof Float32Array)) { throw new Error('!(args instanceof Float32Array)'); } if (!(targets instanceof Uint32Array)) { throw new Error('!(targets instanceof Uint32Array)'); } this.gate = gate; this.tag = tag; this.args = args; this.id_targets = targets; } /** * @returns {!string} */ toString() { return `${this.gate.name}[${this.tag}](${[...this.args].join(', ')}) ${[...this.id_targets].join(' ')}`; } /** * @returns {!int} */ countMeasurements() { if (this.gate.name === 'M' || this.gate.name === 'MX' || this.gate.name === 'MY' || this.gate.name === 'MR' || this.gate.name === 'MRX' || this.gate.name === 'MRY') { return this.id_targets.length; } if (this.gate.name === 'MXX' || this.gate.name === 'MYY' || this.gate.name === 'MZZ') { return this.id_targets.length / 2; } if (this.gate.name.startsWith('MPP:')) { return 1; } return 0; } /** * @param {!string} before * @returns {!string} */ pauliFrameAfter(before) { let m = this.gate.tableau_map; if (m === undefined) { if (this.gate.name.startsWith('M')) { let bases; if (this.gate.name.startsWith('MPP:')) { bases = this.gate.name.substring(4); } else { bases = this.gate.name.substring(1); } let differences = 0; for (let k = 0; k < before.length; k++) { let a = 'XYZ'.indexOf(before[k]); let b = 'XYZ'.indexOf(bases[k]); if (a >= 0 && b >= 0 && a !== b) { differences++; } } if (differences % 2 !== 0) { return 'ERR:' + before; } return before; } else if (this.gate.name.startsWith('SPP:') || this.gate.name.startsWith('SPP_DAG:')) { let dag = this.gate.name.startsWith('SPP_DAG:'); let bases = this.gate.name.substring(dag ? 8 : 4); let differences = 0; let flipped = ''; for (let k = 0; k < before.length; k++) { let a = 'IXYZ'.indexOf(before[k]); let b = 'IXYZ'.indexOf(bases[k]); if (a > 0 && b > 0 && a !== b) { differences++; } flipped += 'IXYZ'[a ^ b] } if (differences % 2 !== 0) { return flipped; } return before; } else if (this.gate.name === 'POLYGON') { // Do nothing. return before; } else { throw new Error(this.gate.name); } } if (before.length !== this.gate.num_qubits) { throw new Error(`before.length !== this.gate.num_qubits`); } if (m.has(before)) { return m.get(before); } let bases = expandBase(before); bases = bases.map(e => m.get(e)); let out = [0, 0]; for (let b of bases) { for (let k = 0; k < before.length; k++) { if (b[k] === 'X') { out[k] ^= 1; } if (b[k] === 'Y') { out[k] ^= 3; } if (b[k] === 'Z') { out[k] ^= 2; } } } let result = ''; for (let k = 0; k < before.length; k++) { result += 'IXZY'[out[k]]; } return result; } /** * @param {!function(qubit: !int): ![!number, !number]} qubitCoordsFunc * @param {!CanvasRenderingContext2D} ctx */ id_draw(qubitCoordsFunc, ctx) { ctx.save(); try { this.gate.drawer(this, qubitCoordsFunc, ctx); if (this.tag !== '' && this.id_targets.length > 0) { let [x, y] = qubitCoordsFunc(this.id_targets[0]); ctx.fillText(this.tag, x, y + 16); } } finally { ctx.restore(); } } } export {Operation}; ================================================ FILE: glue/crumble/circuit/pauli_frame.js ================================================ import {describe} from "../base/describe.js"; class PauliFrame { /** * @param {!int} num_frames * @param {!int} num_qubits */ constructor(num_frames, num_qubits) { if (num_frames > 32) { throw new Error('num_frames > 32'); } this.num_qubits = num_qubits; this.num_frames = num_frames; this.xs = new Uint32Array(num_qubits); this.zs = new Uint32Array(num_qubits); this.flags = new Uint32Array(num_qubits); } /** * @returns {!PauliFrame} */ copy() { let result = new PauliFrame(this.num_frames, this.num_qubits); for (let q = 0; q < this.num_qubits; q++) { result.xs[q] = this.xs[q]; result.zs[q] = this.zs[q]; result.flags[q] = this.flags[q]; } return result; } /** * @param {!Array} qubit_keys * @returns {!Array>} */ to_dicts(qubit_keys) { if (qubit_keys.length !== this.num_qubits) { throw new Error("qubit_keys.length !== this.num_qubits"); } let result = []; for (let k = 0; k < this.num_frames; k++) { result.push(new Map()); } for (let q = 0; q < this.num_qubits; q++) { let key = qubit_keys[q]; let x = this.xs[q]; let z = this.zs[q]; let f = this.flags[q]; let m = x | z | f; let k = 0; while (m) { if (m & 1) { if (f & 1) { result[k].set(key, 'ERR:flag'); } else if (x & z & 1) { result[k].set(key, 'Y'); } else if (x & 1) { result[k].set(key, 'X'); } else { result[k].set(key, 'Z'); } } k++; x >>= 1; z >>= 1; f >>= 1; m >>= 1; } } return result; } /** * @param {!Array} strings * @returns {!PauliFrame} */ static from_strings(strings) { let num_frames = strings.length; if (num_frames === 0) { throw new Error("strings.length === 0"); } let num_qubits = strings[0].length; for (let s of strings) { if (s.length !== num_qubits) { throw new Error("Inconsistent string length."); } } let result = new PauliFrame(num_frames, num_qubits); for (let f = 0; f < num_frames; f++) { for (let q = 0; q < num_qubits; q++) { let c = strings[f][q]; if (c === 'X') { result.xs[q] |= 1 << f; } else if (c === 'Y') { result.xs[q] |= 1 << f; result.zs[q] |= 1 << f; } else if (c === 'Z') { result.zs[q] |= 1 << f; } else if (c === 'I' || c === '_') { // Identity. } else if (c === '!') { result.flags[q] |= 1 << f; } else if (c === '%') { result.flags[q] |= 1 << f; result.xs[q] |= 1 << f; } else if (c === '&') { result.flags[q] |= 1 << f; result.xs[q] |= 1 << f; result.zs[q] |= 1 << f; } else if (c === '$') { result.flags[q] |= 1 << f; result.zs[q] |= 1 << f; } else { throw new Error("Unrecognized pauli string character: '" + c + "'"); } } } return result; } /** * @returns {!Array} */ to_strings() { let result = []; for (let f = 0; f < this.num_frames; f++) { let s = ''; for (let q = 0; q < this.num_qubits; q++) { let flag = (this.flags[q] >> f) & 1; let x = (this.xs[q] >> f) & 1; let z = (this.zs[q] >> f) & 1; s += '_XZY!%$&'[x + 2*z + 4*flag]; } result.push(s); } return result; } /** * @param {!Array>} dicts * @param {!Array} qubit_keys * @returns {!PauliFrame} */ static from_dicts(dicts, qubit_keys) { let result = new PauliFrame(dicts.length, qubit_keys.length); for (let f = 0; f < dicts.length; f++) { for (let q = 0; q < qubit_keys.length; q++) { let p = dicts[f].get(qubit_keys[q]); if (p === 'X') { result.xs[q] |= 1 << f; } else if (p === 'Z') { result.zs[q] |= 1 << f; } else if (p === 'Y') { result.xs[q] |= 1 << f; result.zs[q] |= 1 << f; } else if (p !== 'I' && p !== undefined) { result.flags[q] |= 1 << f; } } } return result; } /** * @param {!Array} targets */ do_exchange_xz(targets) { for (let t of targets) { let x = this.xs[t]; let z = this.zs[t]; this.zs[t] = x; this.xs[t] = z; } } /** * @param {!Array} targets */ do_exchange_xy(targets) { for (let t of targets) { this.zs[t] ^= this.xs[t]; } } /** * @param {!Array} targets */ do_exchange_yz(targets) { for (let t of targets) { this.xs[t] ^= this.zs[t]; } } /** * @param {!Array} targets */ do_discard(targets) { for (let t of targets) { this.flags[t] |= this.xs[t]; this.flags[t] |= this.zs[t]; this.xs[t] = 0; this.zs[t] = 0; } } /** * @param {!string} observable * @param {!Array} targets */ do_measure(observable, targets) { for (let k = 0; k < targets.length; k += observable.length) { let anticommutes = 0; for (let k2 = 0; k2 < observable.length; k2++) { let t = targets[k + k2]; let obs = observable[k2]; if (obs === 'X') { anticommutes ^= this.zs[t]; } else if (obs === 'Z') { anticommutes ^= this.xs[t]; } else if (obs === 'Y') { anticommutes ^= this.xs[t] ^ this.zs[t]; } else { throw new Error(`Unrecognized measure obs: '${obs}'`); } } for (let k2 = 0; k2 < observable.length; k2++) { let t = targets[k + k2]; this.flags[t] |= anticommutes; } } } /** * @param {!string} bases * @param {!Uint32Array|!Array.} targets */ do_mpp(bases, targets) { if (bases.length !== targets.length) { throw new Error('bases.length !== targets.length'); } let anticommutes = 0; for (let k = 0; k < bases.length; k++) { let t = targets[k]; let obs = bases[k]; if (obs === 'X') { anticommutes ^= this.zs[t]; } else if (obs === 'Z') { anticommutes ^= this.xs[t]; } else if (obs === 'Y') { anticommutes ^= this.xs[t] ^ this.zs[t]; } else { throw new Error(`Unrecognized measure obs: '${obs}'`); } } for (let k = 0; k < bases.length; k++) { let t = targets[k]; this.flags[t] |= anticommutes; } } /** * @param {!string} bases * @param {!Uint32Array|!Array.} targets */ do_spp(bases, targets) { if (bases.length !== targets.length) { throw new Error('bases.length !== targets.length'); } let anticommutes = 0; for (let k = 0; k < bases.length; k++) { let t = targets[k]; let obs = bases[k]; if (obs === 'X') { anticommutes ^= this.zs[t]; } else if (obs === 'Z') { anticommutes ^= this.xs[t]; } else if (obs === 'Y') { anticommutes ^= this.xs[t] ^ this.zs[t]; } else { throw new Error(`Unrecognized spp obs: '${obs}'`); } } for (let k = 0; k < bases.length; k++) { let t = targets[k]; let obs = bases[k]; let x = 0; let z = 0; if (obs === 'X') { x = 1; } else if (obs === 'Z') { z = 1; } else if (obs === 'Y') { x = 1; z = 1; } else { throw new Error(`Unrecognized spp obs: '${obs}'`); } if (x) { this.xs[t] ^= anticommutes; } if (z) { this.zs[t] ^= anticommutes; } } } /** * @param {!string} observable * @param {!Array} targets */ do_demolition_measure(observable, targets) { if (observable === 'X') { for (let q of targets) { this.flags[q] |= this.zs[q]; this.xs[q] = 0; this.zs[q] = 0; } } else if (observable === 'Z') { for (let q of targets) { this.flags[q] |= this.xs[q]; this.xs[q] = 0; this.zs[q] = 0; } } else if (observable === 'Y') { for (let q of targets) { this.flags[q] |= this.xs[q] ^ this.zs[q]; this.xs[q] = 0; this.zs[q] = 0; } } else { throw new Error("Unrecognized demolition obs"); } } /** * @param {!Array} targets */ do_cycle_xyz(targets) { for (let t of targets) { this.xs[t] ^= this.zs[t]; this.zs[t] ^= this.xs[t]; } } /** * @param {!Array} targets */ do_cycle_zyx(targets) { for (let t of targets) { this.zs[t] ^= this.xs[t]; this.xs[t] ^= this.zs[t]; } } /** * @param {!Array} targets */ do_swap(targets) { for (let k = 0; k < targets.length; k += 2) { let a = targets[k]; let b = targets[k + 1]; let xa = this.xs[a]; let za = this.zs[a]; let xb = this.xs[b]; let zb = this.zs[b]; this.xs[a] = xb; this.zs[a] = zb; this.xs[b] = xa; this.zs[b] = za; } } /** * @param {!Array} targets */ do_iswap(targets) { for (let k = 0; k < targets.length; k += 2) { let a = targets[k]; let b = targets[k + 1]; let xa = this.xs[a]; let za = this.zs[a]; let xb = this.xs[b]; let zb = this.zs[b]; let xab = xa ^ xb; this.xs[a] = xb; this.zs[a] = zb ^ xab; this.xs[b] = xa; this.zs[b] = za ^ xab; } } /** * @param {!Array} targets */ do_sqrt_xx(targets) { for (let k = 0; k < targets.length; k += 2) { let a = targets[k]; let b = targets[k + 1]; let zab = this.zs[a] ^ this.zs[b]; this.xs[a] ^= zab; this.xs[b] ^= zab; } } /** * @param {!Array} targets */ do_sqrt_yy(targets) { for (let k = 0; k < targets.length; k += 2) { let a = targets[k]; let b = targets[k + 1]; let xa = this.xs[a]; let za = this.zs[a]; let xb = this.xs[b]; let zb = this.zs[b]; za ^= xa; zb ^= xb; xa ^= zb; xb ^= za; zb ^= xb; za ^= xa; this.xs[a] = za; this.zs[a] = xa; this.xs[b] = zb; this.zs[b] = xb; } } /** * @param {!Array} targets */ do_sqrt_zz(targets) { for (let k = 0; k < targets.length; k += 2) { let a = targets[k]; let b = targets[k + 1]; let xab = this.xs[a] ^ this.xs[b]; this.zs[a] ^= xab; this.zs[b] ^= xab; } } /** * @param {!Array} targets */ do_xcx(targets) { for (let k = 0; k < targets.length; k += 2) { let control = targets[k]; let target = targets[k + 1]; this.xs[target] ^= this.zs[control]; this.xs[control] ^= this.zs[target]; } } /** * @param {!Array} targets */ do_xcy(targets) { for (let k = 0; k < targets.length; k += 2) { let control = targets[k]; let target = targets[k + 1]; this.xs[target] ^= this.zs[control]; this.zs[target] ^= this.zs[control]; this.xs[control] ^= this.xs[target]; this.xs[control] ^= this.zs[target]; } } /** * @param {!Array} targets */ do_ycy(targets) { for (let k = 0; k < targets.length; k += 2) { let control = targets[k]; let target = targets[k + 1]; let y = this.xs[control] ^ this.zs[control]; this.xs[target] ^= y; this.zs[target] ^= y; y = this.xs[target] ^ this.zs[target]; this.xs[control] ^= y; this.zs[control] ^= y; } } /** * @param {!Array} targets */ do_cx(targets) { for (let k = 0; k < targets.length; k += 2) { let control = targets[k]; let target = targets[k + 1]; this.xs[target] ^= this.xs[control]; this.zs[control] ^= this.zs[target]; } } /** * @param {!Array} targets */ do_cx_swap(targets) { for (let k = 0; k < targets.length; k += 2) { let c = targets[k]; let t = targets[k + 1]; let xc = this.xs[c]; let zc = this.zs[c]; let xt = this.xs[t]; let zt = this.zs[t]; this.xs[c] = xt ^ xc; this.zs[c] = zt; this.xs[t] = xc; this.zs[t] = zc ^ zt; } } /** * @param {!Array} targets */ do_swap_cx(targets) { for (let k = 0; k < targets.length; k += 2) { let c = targets[k]; let t = targets[k + 1]; let xc = this.xs[c]; let zc = this.zs[c]; let xt = this.xs[t]; let zt = this.zs[t]; this.xs[c] = xt; this.zs[c] = zc ^ zt; this.xs[t] = xt ^ xc; this.zs[t] = zc; } } /** * @param {!Array} targets */ do_cz_swap(targets) { for (let k = 0; k < targets.length; k += 2) { let c = targets[k]; let t = targets[k + 1]; let xc = this.xs[c]; let zc = this.zs[c]; let xt = this.xs[t]; let zt = this.zs[t]; this.xs[c] = xt; this.zs[c] = zt ^ xc; this.xs[t] = xc; this.zs[t] = zc ^ xt; } } /** * @param {!Array} targets */ do_cy(targets) { for (let k = 0; k < targets.length; k += 2) { let control = targets[k]; let target = targets[k + 1]; this.xs[target] ^= this.xs[control]; this.zs[target] ^= this.xs[control]; this.zs[control] ^= this.zs[target]; this.zs[control] ^= this.xs[target]; } } /** * @param {!Array} targets */ do_cz(targets) { for (let k = 0; k < targets.length; k += 2) { let control = targets[k]; let target = targets[k + 1]; this.zs[target] ^= this.xs[control]; this.zs[control] ^= this.xs[target]; } } /** * @param {!Gate} gate * @param {!Array} targets */ do_gate(gate, targets) { gate.frameDo(this, targets); } /** * @param {!Gate} gate * @param {!Array} targets */ undo_gate(gate, targets) { gate.frameUndo(this, targets); } /** * @param {*} other * @returns {!boolean} */ isEqualTo(other) { if (!(other instanceof PauliFrame) || other.num_frames !== this.num_frames || other.num_qubits !== this.num_qubits) { return false; } for (let q = 0; q < this.num_qubits; q++) { if (this.xs[q] !== other.xs[q] || this.zs[q] !== other.zs[q] || this.flags[q] !== other.flags[q]) { return false; } } return true; } /** * @returns {!string} */ toString() { return this.to_strings().join('\n'); } } export {PauliFrame} ================================================ FILE: glue/crumble/circuit/pauli_frame.test.js ================================================ import {test, assertThat} from "../test/test_util.js" import {PauliFrame} from "./pauli_frame.js" import {GATE_MAP} from "../gates/gateset.js" import {make_mpp_gate, make_spp_gate} from "../gates/gateset_mpp.js" import {Operation} from "./operation.js"; test("pauli_frame.to_from", () => { let strings = [ "_XYZ", "XXXX", "!&$%", ]; let frame = PauliFrame.from_strings(strings); assertThat(frame.num_frames).isEqualTo(3); assertThat(frame.num_qubits).isEqualTo(4); assertThat(frame.toString()).isEqualTo(strings.join('\n')); }); test("pauli_frame.to_from_dicts", () => { let frame = PauliFrame.from_strings([ "_XYZ", "XXX!", ]); let qubit_keys = ["A", "B", "C", "D"]; let dicts = frame.to_dicts(qubit_keys); assertThat(dicts).isEqualTo([ new Map([["B", "X"], ["C", "Y"], ["D", "Z"]]), new Map([["A", "X"], ["B", "X"], ["C", "X"], ["D", "ERR:flag"]]), ]); assertThat(PauliFrame.from_dicts(dicts, qubit_keys)).isEqualTo(frame); }); test("pauli_frame.do_gate_vs_old_frame_updates", () => { let gates = [...GATE_MAP.values(), make_mpp_gate("XYY"), make_spp_gate("XYY")]; for (let g of gates) { if (g.name === 'DETECTOR' || g.name === 'OBSERVABLE_INCLUDE') { continue; } let before, after, returned; if (g.num_qubits === 1) { before = new PauliFrame(4, g.num_qubits); before.xs[0] = 0b0011; before.zs[0] = 0b0101; after = before.copy(); after.do_gate(g, [0]); returned = after.copy(); returned.undo_gate(g, [0]); } else { before = new PauliFrame(16, g.num_qubits); before.xs[0] = 0b0000000011111111; before.zs[0] = 0b0000111100001111; before.xs[1] = 0b0011001100110011; before.zs[1] = 0b0101010101010101; let targets = [0, 1]; for (let k = 2; k < g.num_qubits; k++) { targets.push(k); } after = before.copy(); after.do_gate(g, targets); returned = after.copy(); returned.undo_gate(g, targets); } if (!returned.flags[0]) { assertThat(returned).withInfo({'gate': g.name}).isEqualTo(before); } let before_strings = before.to_strings(); let actual_after_strings = after.to_strings(); // Broadcast failure. for (let k = 0; k < actual_after_strings.length; k++) { let a = actual_after_strings[k]; if (a.indexOf('&') !== -1 || a.indexOf('!') !== -1 || a.indexOf('%') !== -1 || a.indexOf('$') !== -1) { a = a.replaceAll('_', '!') a = a.replaceAll('X', '%') a = a.replaceAll('Y', '&') a = a.replaceAll('Z', '$') actual_after_strings[k] = a; } } let op = new Operation(g, '', new Float32Array(0), new Uint32Array([0, 1])); let expected_after_strings = []; for (let f = 0; f < before_strings.length; f++) { let t = op.pauliFrameAfter(before_strings[f].replaceAll('_', 'I')); t = t.replaceAll('I', '_'); if (t.startsWith('ERR:')) { t = t.substring(4); t = t.replaceAll('_', '!') t = t.replaceAll('X', '%') t = t.replaceAll('Y', '&') t = t.replaceAll('Z', '$') } expected_after_strings.push(t); } assertThat(actual_after_strings).withInfo(`gate = ${g.name}`).isEqualTo(expected_after_strings); } }); ================================================ FILE: glue/crumble/circuit/propagated_pauli_frames.js ================================================ import {PauliFrame} from './pauli_frame.js'; import {equate} from '../base/equate.js'; import {Layer} from './layer.js'; class PropagatedPauliFrameLayer { /** * @param {!Map} bases * @param {!Set} errors * @param {!Array} crossings */ constructor(bases, errors, crossings) { this.bases = bases; this.errors = errors; this.crossings = crossings; } /** * @param {!Set} qids * @returns {!boolean} */ touchesQidSet(qids) { for (let q of this.bases.keys()) { if (qids.has(q)) { return true; } } for (let q of this.errors.keys()) { if (qids.has(q)) { return true; } } return false; } /** * @param {!PropagatedPauliFrameLayer} other * @returns {!PropagatedPauliFrameLayer} */ mergedWith(other) { return new PropagatedPauliFrameLayer( new Map([...this.bases.entries(), ...other.bases.entries()]), new Set([...this.errors, ...other.errors]), [...this.crossings, ...other.crossings], ); } /** * @returns {!string} */ toString() { let num_qubits = 0; for (let q of this.bases.keys()) { num_qubits = Math.max(num_qubits, q + 1); } for (let q of this.errors) { num_qubits = Math.max(num_qubits, q + 1); } for (let [q1, q2] of this.crossings) { num_qubits = Math.max(num_qubits, q1 + 1); num_qubits = Math.max(num_qubits, q2 + 1); } let result = '"'; for (let q = 0; q < num_qubits; q++) { let b = this.bases.get(q); if (b === undefined) { b = '_'; } if (this.errors.has(q)) { b = 'E'; } result += b; } result += '"'; return result; } } class PropagatedPauliFrames { /** * @param {!Map} layers */ constructor(layers) { this.id_layers = layers; } /** * @param {*} other * @returns {!boolean} */ isEqualTo(other) { return other instanceof PropagatedPauliFrames && equate(this.id_layers, other.id_layers); } /** * @returns {!string} */ toString() { let layers = [...this.id_layers.keys()]; layers.sort((a, b) => a - b); let lines = ['PropagatedPauliFrames {']; for (let k of layers) { lines.push(` ${k}: ${this.id_layers.get(k)}`); } lines.push('}'); return lines.join('\n'); } /** * @param {!int} layer * @returns {!PropagatedPauliFrameLayer} */ atLayer(layer) { let result = this.id_layers.get(layer); if (result === undefined) { result = new PropagatedPauliFrameLayer(new Map(), new Set(), []); } return result; } /** * @param {!Circuit} circuit * @param {!int} marker_index * @returns {!PropagatedPauliFrames} */ static fromCircuit(circuit, marker_index) { let result = new PropagatedPauliFrames(new Map()); let bases = /** @type {!Map} */ new Map(); for (let k = 0; k < circuit.layers.length; k++) { let layer = circuit.layers[k]; let prevBases = bases; bases = layer.id_pauliFrameAfter(bases, marker_index); let errors = new Set(); for (let key of [...bases.keys()]) { let val = bases.get(key); if (val.startsWith('ERR:')) { errors.add(key); bases.set(key, val.substring(4)); } if (bases.get(key) === 'I') { bases.delete(key); } } let crossings = /** @type {!Array} */ []; for (let op of layer.iter_gates_and_markers()) { if (op.gate.num_qubits === 2 && !op.gate.is_marker) { let [q1, q2] = op.id_targets; let differences = new Set(); for (let t of op.id_targets) { let b1 = bases.get(t); let b2 = prevBases.get(t); if (b1 !== b2) { if (b1 !== undefined) { differences.add(b1); } if (b2 !== undefined) { differences.add(b2); } } } if (differences.size > 0) { let color = 'I'; if (differences.size === 1) { color = [...differences][0]; } crossings.push({q1, q2, color}); } } } if (bases.size > 0) { result.id_layers.set(k + 0.5, new PropagatedPauliFrameLayer(bases, new Set(), [])); } if (errors.size > 0 || crossings.length > 0) { result.id_layers.set(k, new PropagatedPauliFrameLayer(new Map(), errors, crossings)); } } return result; } /** * @param {!Circuit} circuit * @param {!Array} measurements * @returns {!PropagatedPauliFrames} */ static fromMeasurements(circuit, measurements) { return PropagatedPauliFrames.batchFromMeasurements(circuit, [measurements])[0]; } /** * @param {!Circuit} circuit * @param {!Array>} batchMeasurements * @returns {!Array} */ static batchFromMeasurements(circuit, batchMeasurements) { let result = []; for (let k = 0; k < batchMeasurements.length; k += 32) { let batch = []; for (let j = k; j < k + 32 && j < batchMeasurements.length; j++) { batch.push(batchMeasurements[j]); } result.push(...PropagatedPauliFrames.batch32FromMeasurements(circuit, batch)); } return result; } /** * @param {!Circuit} circuit * @param {!Array>} batchMeasurements * @returns {!Array} */ static batch32FromMeasurements(circuit, batchMeasurements) { let results = []; for (let k = 0; k < batchMeasurements.length; k++) { results.push(new PropagatedPauliFrames(new Map())); } let frame = new PauliFrame(batchMeasurements.length, circuit.allQubits().size); let measurementsBack = 0; let events = []; for (let k = 0; k < batchMeasurements.length; k++) { for (let k2 = 0; k2 < batchMeasurements[k].length; k2++) { events.push([k, batchMeasurements[k][k2]]); } } events.sort((a, b) => a[1] - b[1]); for (let k = circuit.layers.length - 1; k >= -1; k--) { let layer = k >= 0 ? circuit.layers[k] : new Layer(); let targets = [...layer.id_ops.keys()]; targets.reverse(); for (let id of targets) { let op = layer.id_ops.get(id); if (op.id_targets[0] !== id) { continue; } frame.undo_gate(op.gate, [...op.id_targets]); for (let nm = op.countMeasurements(); nm > 0; nm -= 1) { measurementsBack -= 1; let target_mask = 0; while (events.length > 0 && events[events.length - 1][1] === measurementsBack) { let ev = events[events.length - 1]; events.pop(); target_mask ^= 1 << ev[0]; } if (target_mask === 0) { continue; } for (let t_id = 0; t_id < op.id_targets.length; t_id++) { let t = op.id_targets[t_id]; let basis; if (op.gate.name === 'MX' || op.gate.name === 'MRX' || op.gate.name === 'MXX') { basis = 'X'; } else if (op.gate.name === 'MY' || op.gate.name === 'MRY' || op.gate.name === 'MYY') { basis = 'Y'; } else if (op.gate.name === 'M' || op.gate.name === 'MR' || op.gate.name === 'MZZ') { basis = 'Z'; } else if (op.gate.name === 'MPAD') { continue; } else if (op.gate.name.startsWith('MPP:')) { basis = op.gate.name[t_id + 4]; } else { throw new Error('Unhandled measurement gate: ' + op.gate.name); } if (basis === 'X') { frame.xs[t] ^= target_mask; } else if (basis === 'Y') { frame.xs[t] ^= target_mask; frame.zs[t] ^= target_mask; } else if (basis === 'Z') { frame.zs[t] ^= target_mask; } else { throw new Error('Unhandled measurement gate: ' + op.gate.name); } } } } for (let t = 0; t < batchMeasurements.length; t++) { let m = 1 << t; let bases = new Map(); let errors = new Set(); for (let q = 0; q < frame.xs.length; q++) { let x = (frame.xs[q] & m) !== 0; let z = (frame.zs[q] & m) !== 0; if (x | z) { bases.set(q, '_XZY'[x + 2 * z]); } if (frame.flags[q] & m) { errors.add(q); } } if (bases.size > 0) { results[t].id_layers.set(k - 0.5, new PropagatedPauliFrameLayer(bases, new Set(), [])); } if (errors.size > 0) { results[t].id_layers.set(k, new PropagatedPauliFrameLayer(new Map(), errors, [])); } } for (let q = 0; q < frame.xs.length; q++) { frame.flags[q] = 0; } } return results; } } export {PropagatedPauliFrames, PropagatedPauliFrameLayer}; ================================================ FILE: glue/crumble/circuit/propagated_pauli_frames.test.js ================================================ import {test, assertThat} from "../test/test_util.js" import {Circuit} from "./circuit.js"; import {PropagatedPauliFrames, PropagatedPauliFrameLayer} from './propagated_pauli_frames.js'; test("propagated_pauli_frames.fromMeasurements", () => { let propagated = PropagatedPauliFrames.fromMeasurements(Circuit.fromStimCircuit(` R 0 TICK H 0 TICK MX 0 `), [-1]); assertThat(propagated).isEqualTo(new PropagatedPauliFrames(new Map([ [0.5, new PropagatedPauliFrameLayer( new Map([[0, 'Z']]), new Set(), [], )], [1.5, new PropagatedPauliFrameLayer( new Map([[0, 'X']]), new Set(), [], )], ]))); propagated = PropagatedPauliFrames.fromMeasurements(Circuit.fromStimCircuit(` RX 0 TICK H 0 TICK MX 0 `), [-1]); assertThat(propagated).isEqualTo(new PropagatedPauliFrames(new Map([ [0, new PropagatedPauliFrameLayer( new Map(), new Set([0]), [], )], [0.5, new PropagatedPauliFrameLayer( new Map([[0, 'Z']]), new Set(), [], )], [1.5, new PropagatedPauliFrameLayer( new Map([[0, 'X']]), new Set(), [], )], ]))); propagated = PropagatedPauliFrames.fromMeasurements(Circuit.fromStimCircuit(` MX 0 TICK H 0 TICK MX 0 `), [-1]); assertThat(propagated).isEqualTo(new PropagatedPauliFrames(new Map([ [-0.5, new PropagatedPauliFrameLayer( new Map([[0, 'Z']]), new Set(), [], )], [-1.5, new PropagatedPauliFrameLayer( new Map([[0, 'Z']]), new Set(), [], )], [0, new PropagatedPauliFrameLayer( new Map(), new Set([0]), [], )], [0.5, new PropagatedPauliFrameLayer( new Map([[0, 'Z']]), new Set(), [], )], [1.5, new PropagatedPauliFrameLayer( new Map([[0, 'X']]), new Set(), [], )], ]))); propagated = PropagatedPauliFrames.fromMeasurements(Circuit.fromStimCircuit(` RX 0 1 TICK CX 0 1 TICK MX 0 `), [-1]); assertThat(propagated).isEqualTo(new PropagatedPauliFrames(new Map([ [0.5, new PropagatedPauliFrameLayer( new Map([[0, 'X'], [1, 'X']]), new Set(), [], )], [1.5, new PropagatedPauliFrameLayer( new Map([[0, 'X']]), new Set(), [], )], ]))); propagated = PropagatedPauliFrames.fromMeasurements(Circuit.fromStimCircuit(` CX 1 0 MX 1 `), [-1]); assertThat(propagated).isEqualTo(new PropagatedPauliFrames(new Map([ [-1.5, new PropagatedPauliFrameLayer( new Map([[0, 'X'], [1, 'X']]), new Set(), [], )], [-0.5, new PropagatedPauliFrameLayer( new Map([[0, 'X'], [1, 'X']]), new Set(), [], )], [0.5, new PropagatedPauliFrameLayer( new Map([[1, 'X']]), new Set(), [], )], ]))); }); ================================================ FILE: glue/crumble/crumble.html ================================================ Crumble
Crumble is a prototype stabilizer circuit editor.

Read the manual











================================================ FILE: glue/crumble/draw/config.js ================================================ const pitch = 50; const rad = 10; const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5; const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5; export {pitch, rad, OFFSET_X, OFFSET_Y}; ================================================ FILE: glue/crumble/draw/draw_util.js ================================================ import {rad} from './config.js'; /** * @param {!CanvasRenderingContext2D} ctx * @param {!Array} coords */ function beginPathPolygon(ctx, coords) { ctx.beginPath(); if (coords.length === 0) { return; } let n = coords.length; if (n === 1) { let [[x0, y0]] = coords; ctx.arc(x0, y0, rad * 1.7, 0, 2 * Math.PI); } else if (n === 2) { let [[x0, y0], [x1, y1]] = coords; let dx = x1 - x0; let dy = y1 - y0; let cx = (x1 + x0) / 2; let cy = (y1 + y0) / 2; let px = -dy; let py = dx; let pa = px*px + py*py; if (pa > 50*50) { let s = 50 / Math.sqrt(pa); px *= s; py *= s; } let ac1x = cx + px * 0.2 - dx * 0.2; let ac1y = cy + py * 0.2 - dy * 0.2; let ac2x = cx + px * 0.2 + dx * 0.2; let ac2y = cy + py * 0.2 + dy * 0.2; let bc1x = cx - px * 0.2 - dx * 0.2; let bc1y = cy - py * 0.2 - dy * 0.2; let bc2x = cx - px * 0.2 + dx * 0.2; let bc2y = cy - py * 0.2 + dy * 0.2; ctx.moveTo(x0, y0); ctx.bezierCurveTo(ac1x, ac1y, ac2x, ac2y, x1, y1); ctx.bezierCurveTo(bc2x, bc2y, bc1x, bc1y, x0, y0); } else { let [xn, yn] = coords[n - 1]; ctx.moveTo(xn, yn); for (let k = 0; k < n; k++) { let [xk, yk] = coords[k]; ctx.lineTo(xk, yk); } } } export {beginPathPolygon} ================================================ FILE: glue/crumble/draw/main_draw.js ================================================ import {pitch, rad, OFFSET_X, OFFSET_Y} from "./config.js" import {marker_placement} from "../gates/gateset_markers.js"; import {drawTimeline} from "./timeline_viewer.js"; import {PropagatedPauliFrames} from "../circuit/propagated_pauli_frames.js"; import {stroke_connector_to} from "../gates/gate_draw_util.js" import {beginPathPolygon} from './draw_util.js'; /** * @param {!number|undefined} x * @param {!number|undefined} y * @return {![undefined, undefined]|![!number, !number]} */ function xyToPos(x, y) { if (x === undefined || y === undefined) { return [undefined, undefined]; } let focusX = x / pitch; let focusY = y / pitch; let roundedX = Math.floor(focusX * 2 + 0.5) / 2; let roundedY = Math.floor(focusY * 2 + 0.5) / 2; let centerX = roundedX*pitch; let centerY = roundedY*pitch; if (Math.abs(centerX - x) <= rad && Math.abs(centerY - y) <= rad && roundedX % 1 === roundedY % 1) { return [roundedX, roundedY]; } return [undefined, undefined]; } /** * @param {!CanvasRenderingContext2D} ctx * @param {!StateSnapshot} snap * @param {!function(q: !int): ![!number, !number]} qubitCoordsFunc * @param {!PropagatedPauliFrames} propagatedMarkers * @param {!int} mi */ function drawCrossMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi) { let crossings = propagatedMarkers.atLayer(snap.curLayer).crossings; if (crossings !== undefined) { for (let {q1, q2, color} of crossings) { let [x1, y1] = qubitCoordsFunc(q1); let [x2, y2] = qubitCoordsFunc(q2); if (color === 'X') { ctx.strokeStyle = 'red'; } else if (color === 'Y') { ctx.strokeStyle = 'green'; } else if (color === 'Z') { ctx.strokeStyle = 'blue'; } else { ctx.strokeStyle = 'purple' } ctx.lineWidth = 8; stroke_connector_to(ctx, x1, y1, x2, y2); ctx.lineWidth = 1; } } } /** * @param {!CanvasRenderingContext2D} ctx * @param {!StateSnapshot} snap * @param {!function(q: !int): ![!number, !number]} qubitCoordsFunc * @param {!Map} propagatedMarkerLayers */ function drawMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkerLayers) { let obsCount = new Map(); let detCount = new Map(); for (let [mi, p] of propagatedMarkerLayers.entries()) { drawSingleMarker(ctx, snap, qubitCoordsFunc, p, mi, obsCount, detCount); } } /** * @param {!CanvasRenderingContext2D} ctx * @param {!StateSnapshot} snap * @param {!function(q: !int): ![!number, !number]} qubitCoordsFunc * @param {!PropagatedPauliFrames} propagatedMarkers * @param {!int} mi * @param {!Map} hitCount */ function drawSingleMarker(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi, hitCount) { let basesQubitMap = propagatedMarkers.atLayer(snap.curLayer + 0.5).bases; // Convert qubit indices to draw coordinates. let basisCoords = []; for (let [q, b] of basesQubitMap.entries()) { basisCoords.push([b, qubitCoordsFunc(q)]); } // Draw a polygon for the marker set. if (mi >= 0 && basisCoords.length > 0) { if (basisCoords.every(e => e[0] === 'X')) { ctx.fillStyle = 'red'; } else if (basisCoords.every(e => e[0] === 'Y')) { ctx.fillStyle = 'green'; } else if (basisCoords.every(e => e[0] === 'Z')) { ctx.fillStyle = 'blue'; } else { ctx.fillStyle = 'black'; } ctx.strokeStyle = ctx.fillStyle; let coords = basisCoords.map(e => e[1]); let cx = 0; let cy = 0; for (let [x, y] of coords) { cx += x; cy += y; } cx /= coords.length; cy /= coords.length; coords.sort((a, b) => { let [ax, ay] = a; let [bx, by] = b; let av = Math.atan2(ay - cy, ax - cx); let bv = Math.atan2(by - cy, bx - cx); if (ax === cx && ay === cy) { av = -100; } if (bx === cx && by === cy) { bv = -100; } return av - bv; }) beginPathPolygon(ctx, coords); ctx.globalAlpha *= 0.25; ctx.fill(); ctx.globalAlpha *= 4; ctx.lineWidth = 2; ctx.stroke(); ctx.lineWidth = 1; } // Draw individual qubit markers. for (let [b, [x, y]] of basisCoords) { let {dx, dy, wx, wy} = marker_placement(mi, `${x}:${y}`, hitCount); if (b === 'X') { ctx.fillStyle = 'red' } else if (b === 'Y') { ctx.fillStyle = 'green' } else if (b === 'Z') { ctx.fillStyle = 'blue' } else { throw new Error('Not a pauli: ' + b); } ctx.fillRect(x - dx, y - dy, wx, wy); } // Show error highlights. let errorsQubitSet = propagatedMarkers.atLayer(snap.curLayer).errors; for (let q of errorsQubitSet) { let [x, y] = qubitCoordsFunc(q); let {dx, dy, wx, wy} = marker_placement(mi, `${x}:${y}`, hitCount); if (mi < 0) { ctx.lineWidth = 2; } else { ctx.lineWidth = 8; } ctx.strokeStyle = 'magenta' ctx.strokeRect(x - dx, y - dy, wx, wy); ctx.lineWidth = 1; ctx.fillStyle = 'black' ctx.fillRect(x - dx, y - dy, wx, wy); } } let _defensive_draw_enabled = true; /** * @param {!boolean} val */ function setDefensiveDrawEnabled(val) { _defensive_draw_enabled = val; } /** * @param {!CanvasRenderingContext2D} ctx * @param {!function} body */ function defensiveDraw(ctx, body) { ctx.save(); try { if (_defensive_draw_enabled) { body(); } else { try { body(); } catch (ex) { console.error(ex); } } } finally { ctx.restore(); } } /** * @param {!CanvasRenderingContext2D} ctx * @param {!StateSnapshot} snap */ function draw(ctx, snap) { let circuit = snap.circuit; let numPropagatedLayers = 0; for (let layer of circuit.layers) { for (let op of layer.markers) { let gate = op.gate; if (gate.name === "MARKX" || gate.name === "MARKY" || gate.name === "MARKZ") { numPropagatedLayers = Math.max(numPropagatedLayers, op.args[0] + 1); } } } let c2dCoordTransform = (x, y) => [x*pitch - OFFSET_X, y*pitch - OFFSET_Y]; let qubitDrawCoords = q => { let x = circuit.qubitCoordData[2 * q]; let y = circuit.qubitCoordData[2 * q + 1]; return c2dCoordTransform(x, y); }; let propagatedMarkerLayers = /** @type {!Map} */ new Map(); for (let mi = 0; mi < numPropagatedLayers; mi++) { propagatedMarkerLayers.set(mi, PropagatedPauliFrames.fromCircuit(circuit, mi)); } let {dets: dets, obs: obs} = circuit.collectDetectorsAndObservables(false); let batch_input = []; for (let mi = 0; mi < dets.length; mi++) { batch_input.push(dets[mi].mids); } for (let mi of obs.keys()) { batch_input.push(obs.get(mi)); } let batch_output = PropagatedPauliFrames.batchFromMeasurements(circuit, batch_input); let batch_index = 0; for (let mi = 0; mi < dets.length; mi++) { propagatedMarkerLayers.set(~mi, batch_output[batch_index++]); } for (let mi of obs.keys()) { propagatedMarkerLayers.set(~mi ^ (1 << 30), batch_output[batch_index++]); } let operatedOnQubits = new Set(); for (let layer of circuit.layers) { for (let t of layer.id_ops.keys()) { operatedOnQubits.add(t); } } let usedQubitCoordSet = new Set(); let operatedOnQubitSet = new Set(); for (let q of circuit.allQubits()) { let qx = circuit.qubitCoordData[q * 2]; let qy = circuit.qubitCoordData[q * 2 + 1]; usedQubitCoordSet.add(`${qx},${qy}`); if (operatedOnQubits.has(q)) { operatedOnQubitSet.add(`${qx},${qy}`); } } defensiveDraw(ctx, () => { ctx.fillStyle = 'white'; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); let [focusX, focusY] = xyToPos(snap.curMouseX, snap.curMouseY); // Draw the background polygons. let lastPolygonLayer = snap.curLayer; for (let r = 0; r <= snap.curLayer; r++) { for (let op of circuit.layers[r].markers) { if (op.gate.name === 'POLYGON') { lastPolygonLayer = r; break; } } } let polygonMarkers = [...circuit.layers[lastPolygonLayer].markers]; polygonMarkers.sort((a, b) => b.id_targets.length - a.id_targets.length); for (let op of polygonMarkers) { if (op.gate.name === 'POLYGON') { op.id_draw(qubitDrawCoords, ctx); } } // Draw the grid of qubits. defensiveDraw(ctx, () => { for (let qx = 0; qx < 100; qx += 0.5) { let [x, _] = c2dCoordTransform(qx, 0); let s = `${qx}`; ctx.fillStyle = 'black'; ctx.fillText(s, x - ctx.measureText(s).width / 2, 15); } for (let qy = 0; qy < 100; qy += 0.5) { let [_, y] = c2dCoordTransform(0, qy); let s = `${qy}`; ctx.fillStyle = 'black'; ctx.fillText(s, 18 - ctx.measureText(s).width, y); } ctx.strokeStyle = 'black'; for (let qx = 0; qx < 100; qx += 0.5) { let [x, _] = c2dCoordTransform(qx, 0); let s = `${qx}`; ctx.fillStyle = 'black'; ctx.fillText(s, x - ctx.measureText(s).width / 2, 15); for (let qy = qx % 1; qy < 100; qy += 1) { let [x, y] = c2dCoordTransform(qx, qy); ctx.fillStyle = 'white'; let isUnused = !usedQubitCoordSet.has(`${qx},${qy}`); let isVeryUnused = !operatedOnQubitSet.has(`${qx},${qy}`); if (isUnused) { ctx.globalAlpha *= 0.25; } if (isVeryUnused) { ctx.globalAlpha *= 0.25; } ctx.fillRect(x - rad, y - rad, 2*rad, 2*rad); ctx.strokeRect(x - rad, y - rad, 2*rad, 2*rad); if (isUnused) { ctx.globalAlpha *= 4; } if (isVeryUnused) { ctx.globalAlpha *= 4; } } } }); for (let [mi, p] of propagatedMarkerLayers.entries()) { drawCrossMarkers(ctx, snap, qubitDrawCoords, p, mi); } for (let op of circuit.layers[snap.curLayer].iter_gates_and_markers()) { if (op.gate.name !== 'POLYGON') { op.id_draw(qubitDrawCoords, ctx); } } defensiveDraw(ctx, () => { ctx.globalAlpha *= 0.25 for (let [qx, qy] of snap.timelineSet.values()) { let [x, y] = c2dCoordTransform(qx, qy); ctx.fillStyle = 'yellow'; ctx.fillRect(x - rad * 1.25, y - rad * 1.25, 2.5*rad, 2.5*rad); } }); defensiveDraw(ctx, () => { ctx.globalAlpha *= 0.5 for (let [qx, qy] of snap.focusedSet.values()) { let [x, y] = c2dCoordTransform(qx, qy); ctx.fillStyle = 'blue'; ctx.fillRect(x - rad * 1.25, y - rad * 1.25, 2.5*rad, 2.5*rad); } }); drawMarkers(ctx, snap, qubitDrawCoords, propagatedMarkerLayers); if (focusX !== undefined) { ctx.save(); ctx.globalAlpha *= 0.5; let [x, y] = c2dCoordTransform(focusX, focusY); ctx.fillStyle = 'red'; ctx.fillRect(x - rad, y - rad, 2*rad, 2*rad); ctx.restore(); } defensiveDraw(ctx, () => { ctx.globalAlpha *= 0.25; ctx.fillStyle = 'blue'; if (snap.mouseDownX !== undefined && snap.curMouseX !== undefined) { let x1 = Math.min(snap.curMouseX, snap.mouseDownX); let x2 = Math.max(snap.curMouseX, snap.mouseDownX); let y1 = Math.min(snap.curMouseY, snap.mouseDownY); let y2 = Math.max(snap.curMouseY, snap.mouseDownY); x1 -= 1; x2 += 1; y1 -= 1; y2 += 1; x1 -= OFFSET_X; x2 -= OFFSET_X; y1 -= OFFSET_Y; y2 -= OFFSET_Y; ctx.fillRect(x1, y1, x2 - x1, y2 - y1); } for (let [qx, qy] of snap.boxHighlightPreview) { let [x, y] = c2dCoordTransform(qx, qy); ctx.fillRect(x - rad, y - rad, rad*2, rad*2); } }); }); drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords, circuit.layers.length); // Draw scrubber. ctx.save(); try { ctx.strokeStyle = 'black'; ctx.translate(Math.floor(ctx.canvas.width / 2), 0); for (let k = 0; k < circuit.layers.length; k++) { let hasPolygons = false; let hasXMarker = false; let hasYMarker = false; let hasZMarker = false; let hasResetOperations = circuit.layers[k].hasResetOperations(); let hasMeasurements = circuit.layers[k].hasMeasurementOperations(); let hasTwoQubitGate = false; let hasMultiQubitGate = false; let hasSingleQubitClifford = circuit.layers[k].hasSingleQubitCliffords(); for (let op of circuit.layers[k].markers) { hasPolygons |= op.gate.name === "POLYGON"; hasXMarker |= op.gate.name === "MARKX"; hasYMarker |= op.gate.name === "MARKY"; hasZMarker |= op.gate.name === "MARKZ"; } for (let op of circuit.layers[k].id_ops.values()) { hasTwoQubitGate |= op.id_targets.length === 2; hasMultiQubitGate |= op.id_targets.length > 2; } ctx.fillStyle = 'white'; ctx.fillRect(k * 8, 0, 8, 20); if (hasSingleQubitClifford) { ctx.fillStyle = '#FF0'; ctx.fillRect(k * 8, 0, 8, 20); } else if (hasPolygons) { ctx.fillStyle = '#FBB'; ctx.fillRect(k * 8, 0, 8, 7); ctx.fillStyle = '#BFB'; ctx.fillRect(k * 8, 7, 8, 7); ctx.fillStyle = '#BBF'; ctx.fillRect(k * 8, 14, 8, 6); } if (hasMeasurements) { ctx.fillStyle = '#DDD'; ctx.fillRect(k * 8, 0, 8, 20); } else if (hasResetOperations) { ctx.fillStyle = '#DDD'; ctx.fillRect(k * 8, 0, 4, 20); } if (hasXMarker) { ctx.fillStyle = 'red'; ctx.fillRect(k * 8 + 3, 14, 3, 3); } if (hasYMarker) { ctx.fillStyle = 'green'; ctx.fillRect(k * 8 + 3, 9, 3, 3); } if (hasZMarker) { ctx.fillStyle = 'blue'; ctx.fillRect(k * 8 + 3, 3, 3, 3); } if (hasMultiQubitGate) { ctx.strokeStyle = 'black'; ctx.beginPath(); let x = k * 8 + 0.5; for (let dx of [3, 5]) { ctx.moveTo(x + dx, 6); ctx.lineTo(x + dx, 15); } ctx.stroke(); } if (hasTwoQubitGate) { ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.moveTo(k * 8 + 0.5 + 4, 6); ctx.lineTo(k * 8 + 0.5 + 4, 15); ctx.stroke(); } } ctx.fillStyle = 'black'; ctx.beginPath(); ctx.moveTo(snap.curLayer * 8 + 0.5 + 4, 16); ctx.lineTo(snap.curLayer * 8 + 0.5 - 2, 28); ctx.lineTo(snap.curLayer * 8 + 0.5 + 10, 28); ctx.closePath(); ctx.fill(); for (let k = 0; k < circuit.layers.length; k++) { let has_errors = ![...propagatedMarkerLayers.values()].every(p => p.atLayer(k).errors.size === 0); let hasOps = circuit.layers[k].id_ops.size > 0 || circuit.layers[k].markers.length > 0; if (has_errors) { ctx.strokeStyle = 'magenta'; ctx.lineWidth = 4; ctx.strokeRect(k*8 + 0.5 - 1, 0.5 - 1, 7 + 2, 20 + 2); ctx.lineWidth = 1; } else { ctx.strokeStyle = '#000'; ctx.strokeRect(k*8 + 0.5, 0.5, 8, 20); } } } finally { ctx.restore(); } } export {xyToPos, draw, setDefensiveDrawEnabled, OFFSET_X, OFFSET_Y} ================================================ FILE: glue/crumble/draw/main_draw.test.js ================================================ import {draw, setDefensiveDrawEnabled} from './main_draw.js'; import {StateSnapshot} from './state_snapshot.js'; import {test, assertThat, skipRestOfTestIfHeadless} from "../test/test_util.js"; import {Circuit} from "../circuit/circuit.js"; test("main_draw.drawRuns", () => { skipRestOfTestIfHeadless(); setDefensiveDrawEnabled(false); let ctx = document.createElement('canvas').getContext('2d'); let circuit = Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(0, 1) 2 QUBIT_COORDS(1, 1) 3 MARKX(0) 1 CX 1 2 TICK H 0 CY 2 3 TICK S 0 1 2 3 `); let curLayer = 1; let focusedSet = new Set(['0,0', '1,0', '2,0']); let timelineSet = new Set(['0,1', '1,0']); let curMouseX = 0; let curMouseY = 0; let mouseDownX = 0; let mouseDownY = 0; let boxHighlightPreview = [[1, 1], [1, 2]]; let snap = new StateSnapshot( circuit, curLayer, focusedSet, timelineSet, curMouseX, curMouseY, mouseDownX, mouseDownY, boxHighlightPreview, ); draw(ctx, snap); assertThat(() => draw(ctx, snap)).runsWithoutThrowingAnException(); }); ================================================ FILE: glue/crumble/draw/state_snapshot.js ================================================ import {Circuit} from "../circuit/circuit.js"; import {Layer} from "../circuit/layer.js"; /** * A copy of the editor state which can be used for tasks such as drawing previews of changes. * * Technically not immutable, but should be treated as immutable. Should never be mutated. */ class StateSnapshot { /** * @param {!Circuit} circuit * @param {!int} curLayer * @param {!Map} focusedSet * @param {!Map} timelineSet * @param {!number} curMouseX * @param {!number} curMouseY * @param {!number} mouseDownX * @param {!number} mouseDownY * @param {!Array} boxHighlightPreview */ constructor(circuit, curLayer, focusedSet, timelineSet, curMouseX, curMouseY, mouseDownX, mouseDownY, boxHighlightPreview) { this.circuit = circuit.copy(); this.curLayer = curLayer; this.focusedSet = new Map(focusedSet.entries()); this.timelineSet = new Map(timelineSet.entries()); this.curMouseX = curMouseX; this.curMouseY = curMouseY; this.mouseDownX = mouseDownX; this.mouseDownY = mouseDownY; this.boxHighlightPreview = [...boxHighlightPreview]; while (this.circuit.layers.length <= this.curLayer) { this.circuit.layers.push(new Layer()); } } /** * @returns {!Set} */ id_usedQubits() { return this.circuit.allQubits(); } /** * @returns {!Array} */ timelineQubits() { let used = this.id_usedQubits(); let qubits = []; if (this.timelineSet.size > 0) { let c2q = this.circuit.coordToQubitMap(); for (let key of this.timelineSet.keys()) { let q = c2q.get(key); if (q !== undefined) { qubits.push(q); } } } else { qubits.push(...used.values()); } return qubits.filter(q => used.has(q)); } } export {StateSnapshot} ================================================ FILE: glue/crumble/draw/timeline_viewer.js ================================================ import {OFFSET_Y, rad} from "./config.js"; import {stroke_connector_to} from "../gates/gate_draw_util.js" import {marker_placement} from '../gates/gateset_markers.js'; let TIMELINE_PITCH = 32; /** * @param {!CanvasRenderingContext2D} ctx * @param {!StateSnapshot} ds * @param {!function(!int, !number): ![!number, !number]} qubitTimeCoordFunc * @param {!PropagatedPauliFrames} propagatedMarkers * @param {!int} mi * @param {!int} min_t * @param {!int} max_t * @param {!number} x_pitch * @param {!Map} hitCounts */ function drawTimelineMarkers(ctx, ds, qubitTimeCoordFunc, propagatedMarkers, mi, min_t, max_t, x_pitch, hitCounts) { for (let t = min_t - 1; t <= max_t; t++) { if (!hitCounts.has(t)) { hitCounts.set(t, new Map()); } let hitCount = hitCounts.get(t); let p1 = propagatedMarkers.atLayer(t + 0.5); let p0 = propagatedMarkers.atLayer(t); for (let [q, b] of p1.bases.entries()) { let {dx, dy, wx, wy} = marker_placement(mi, q, hitCount); if (mi >= 0 && mi < 4) { dx = 0; wx = x_pitch; wy = 5; if (mi === 0) { dy = 10; } else if (mi === 1) { dy = 5; } else if (mi === 2) { dy = 0; } else if (mi === 3) { dy = -5; } } else { dx -= x_pitch / 2; } let [x, y] = qubitTimeCoordFunc(q, t); if (x === undefined || y === undefined) { continue; } if (b === 'X') { ctx.fillStyle = 'red' } else if (b === 'Y') { ctx.fillStyle = 'green' } else if (b === 'Z') { ctx.fillStyle = 'blue' } else { throw new Error('Not a pauli: ' + b); } ctx.fillRect(x - dx, y - dy, wx, wy); } for (let q of p0.errors) { let {dx, dy, wx, wy} = marker_placement(mi, q, hitCount); dx -= x_pitch / 2; let [x, y] = qubitTimeCoordFunc(q, t - 0.5); if (x === undefined || y === undefined) { continue; } ctx.strokeStyle = 'magenta'; ctx.lineWidth = 8; ctx.strokeRect(x - dx, y - dy, wx, wy); ctx.lineWidth = 1; ctx.fillStyle = 'black'; ctx.fillRect(x - dx, y - dy, wx, wy); } for (let {q1, q2, color} of p0.crossings) { let [x1, y1] = qubitTimeCoordFunc(q1, t); let [x2, y2] = qubitTimeCoordFunc(q2, t); if (color === 'X') { ctx.strokeStyle = 'red'; } else if (color === 'Y') { ctx.strokeStyle = 'green'; } else if (color === 'Z') { ctx.strokeStyle = 'blue'; } else { ctx.strokeStyle = 'purple' } ctx.lineWidth = 8; stroke_connector_to(ctx, x1, y1, x2, y2); ctx.lineWidth = 1; } } } /** * @param {!CanvasRenderingContext2D} ctx * @param {!StateSnapshot} snap * @param {!Map} propagatedMarkerLayers * @param {!function(!int): ![!number, !number]} timesliceQubitCoordsFunc * @param {!int} numLayers */ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFunc, numLayers) { let w = Math.floor(ctx.canvas.width / 2); let qubits = snap.timelineQubits(); qubits.sort((a, b) => { let [x1, y1] = timesliceQubitCoordsFunc(a); let [x2, y2] = timesliceQubitCoordsFunc(b); if (y1 !== y2) { return y1 - y2; } return x1 - x2; }); let base_y2xy = new Map(); let prev_y = undefined; let cur_x = 0; let cur_y = 0; let max_run = 0; let cur_run = 0; for (let q of qubits) { let [x, y] = timesliceQubitCoordsFunc(q); cur_y += TIMELINE_PITCH; if (prev_y !== y) { prev_y = y; cur_x = w * 1.5; max_run = Math.max(max_run, cur_run); cur_run = 0; cur_y += TIMELINE_PITCH * 0.25; } else { cur_x += rad * 0.25; cur_run++; } base_y2xy.set(`${x},${y}`, [Math.round(cur_x) + 0.5, Math.round(cur_y) + 0.5]); } let x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25); let num_cols_half = Math.floor(ctx.canvas.width / 4 / x_pitch); let min_t_free = snap.curLayer - num_cols_half + 1; let min_t_clamp = Math.max(0, Math.min(min_t_free, numLayers - num_cols_half*2 + 1)); let max_t = Math.min(min_t_clamp + num_cols_half*2 + 2, numLayers); let t2t = t => { let dt = t - snap.curLayer; dt -= min_t_clamp - min_t_free; return dt*x_pitch; } let coordTransform_t = ([x, y, t]) => { let key = `${x},${y}`; if (!base_y2xy.has(key)) { return [undefined, undefined]; } let [xb, yb] = base_y2xy.get(key); return [xb + t2t(t), yb]; }; let qubitTimeCoords = (q, t) => { let [x, y] = timesliceQubitCoordsFunc(q); return coordTransform_t([x, y, t]); } ctx.save(); try { ctx.clearRect(w, 0, w, ctx.canvas.height); // Draw colored indicators showing Pauli propagation. let hitCounts = new Map(); for (let [mi, p] of propagatedMarkerLayers.entries()) { drawTimelineMarkers(ctx, snap, qubitTimeCoords, p, mi, min_t_clamp, max_t, x_pitch, hitCounts); } // Draw highlight of current layer. ctx.globalAlpha *= 0.5; ctx.fillStyle = 'black'; { let x1 = t2t(snap.curLayer) + w * 1.5 - x_pitch / 2; ctx.fillRect(x1, 0, x_pitch, ctx.canvas.height); } ctx.globalAlpha *= 2; ctx.strokeStyle = 'black'; ctx.fillStyle = 'black'; // Draw wire lines. for (let q of qubits) { let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); let [x1, y1] = qubitTimeCoords(q, max_t + 1); ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); } // Draw wire labels. ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; for (let q of qubits) { let [x, y] = qubitTimeCoords(q, min_t_clamp - 1); let qx = snap.circuit.qubitCoordData[q * 2]; let qy = snap.circuit.qubitCoordData[q * 2 + 1]; ctx.fillText(`${qx},${qy}:`, x, y); } // Draw layers of gates. for (let time = min_t_clamp; time <= max_t; time++) { let qubitsCoordsFuncForLayer = q => qubitTimeCoords(q, time); let layer = snap.circuit.layers[time]; if (layer === undefined) { continue; } for (let op of layer.iter_gates_and_markers()) { op.id_draw(qubitsCoordsFuncForLayer, ctx); } } // Draw links to timeslice viewer. ctx.globalAlpha = 0.5; for (let q of qubits) { let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); let [x1, y1] = timesliceQubitCoordsFunc(q); if (snap.curMouseX > ctx.canvas.width / 2 && snap.curMouseY >= y0 + OFFSET_Y - TIMELINE_PITCH * 0.55 && snap.curMouseY <= y0 + TIMELINE_PITCH * 0.55 + OFFSET_Y) { ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); ctx.fillStyle = 'black'; ctx.fillRect(x1 - 20, y1 - 20, 40, 40); ctx.fillRect(ctx.canvas.width / 2, y0 - TIMELINE_PITCH / 3, ctx.canvas.width / 2, TIMELINE_PITCH * 2 / 3); } } } finally { ctx.restore(); } } export {drawTimeline} ================================================ FILE: glue/crumble/editor/editor_state.js ================================================ import {Circuit} from "../circuit/circuit.js"; import {Chorder} from "../keyboard/chord.js"; import {Layer, minXY} from "../circuit/layer.js"; import {Revision} from "../base/revision.js"; import {ObservableValue} from "../base/obs.js"; import {pitch, rad} from "../draw/config.js"; import {xyToPos} from "../draw/main_draw.js"; import {StateSnapshot} from "../draw/state_snapshot.js"; import {Operation} from "../circuit/operation.js"; import {GATE_MAP} from "../gates/gateset.js"; import { PropagatedPauliFrameLayer, PropagatedPauliFrames } from '../circuit/propagated_pauli_frames.js'; /** * @param {!int} steps * @return {!function(x: !number, y: !number): ![!number, !number]} */ function rotated45Transform(steps) { let vx = [1, 0]; let vy = [0, 1]; let s = (x, y) => [x - y, x + y]; steps %= 8; steps += 8; steps %= 8; for (let k = 0; k < steps; k++) { vx = s(vx[0], vx[1]); vy = s(vy[0], vy[1]); } return (x, y) => [vx[0]*x + vy[0]*y, vx[1]*x + vy[1]*y]; } class EditorState { /** * @param {!HTMLCanvasElement} canvas */ constructor(canvas) { this.rev = Revision.startingAt(''); this.canvas = canvas; this.curMouseY = /** @type {undefined|!number} */ undefined; this.curMouseX = /** @type {undefined|!number} */ undefined; this.chorder = new Chorder(); this.curLayer = 0; this.focusedSet = /** @type {!Map} */ new Map(); this.timelineSet = /** @type {!Map} */ new Map(); this.mouseDownX = /** @type {undefined|!number} */ undefined; this.mouseDownY = /** @type {undefined|!number} */ undefined; this.obs_val_draw_state = /** @type {!ObservableValue} */ new ObservableValue(this.toSnapshot(undefined)); } flipTwoQubitGateOrderAtFocus(preview) { let newCircuit = this.copyOfCurCircuit(); let layer = newCircuit.layers[this.curLayer]; let flipped_op_first_targets = new Set(); let pairs = [ ['CX', 'reverse'], ['CY', 'reverse'], ['XCY', 'reverse'], ['CXSWAP', 'reverse'], ['XCZ', 'reverse'], ['XCY', 'reverse'], ['YCX', 'reverse'], ['SWAPCX', 'reverse'], ['RX', 'MX'], ['R', 'M'], ['RY', 'MY'], ]; let rev = new Map(); for (let p of pairs) { rev.set(p[0], p[1]); rev.set(p[1], p[0]); } for (let q of this.focusedSet.keys()) { let op = layer.id_ops.get(newCircuit.coordToQubitMap().get(q)); if (op !== undefined && rev.has(op.gate.name)) { flipped_op_first_targets.add(op.id_targets[0]); } } for (let q of flipped_op_first_targets) { let op = layer.id_ops.get(q); let other = rev.get(op.gate.name); if (other === 'reverse') { layer.id_ops.get(q).id_targets.reverse(); } else { op.gate = GATE_MAP.get(other); } } this.commit_or_preview(newCircuit, preview); } reverseLayerOrderFromFocusToEmptyLayer(preview) { let newCircuit = this.copyOfCurCircuit(); let end = this.curLayer; while (end < newCircuit.layers.length && !newCircuit.layers[end].empty()) { end += 1; } let layers = []; for (let k = this.curLayer; k < end; k++) { layers.push(newCircuit.layers[k]); } layers.reverse(); for (let k = this.curLayer; k < end; k++) { newCircuit.layers[k] = layers[k - this.curLayer]; } this.commit_or_preview(newCircuit, preview); } /** * @return {!Circuit} */ copyOfCurCircuit() { let result = Circuit.fromStimCircuit(this.rev.peekActiveCommit()); while (result.layers.length <= this.curLayer) { result.layers.push(new Layer()); } return result; } clearFocus() { this.focusedSet.clear(); this.force_redraw(); } /** * @param {!boolean} preview */ deleteAtFocus(preview) { let newCircuit = this.copyOfCurCircuit(); let c2q = newCircuit.coordToQubitMap(); for (let key of this.focusedSet.keys()) { let q = c2q.get(key); if (q !== undefined) { newCircuit.layers[this.curLayer].id_pop_at(q); } } this.commit_or_preview(newCircuit, preview); } /** * @param {!boolean} preview */ deleteCurLayer(preview) { let c = this.copyOfCurCircuit(); c.layers.splice(this.curLayer, 1); this.commit_or_preview(c, preview); } /** * @param {!boolean} preview */ insertLayer(preview) { let c = this.copyOfCurCircuit(); c.layers.splice(this.curLayer, 0, new Layer()); this.commit_or_preview(c, preview); } undo() { this.rev.undo(); } redo() { this.rev.redo(); } /** * @param {!Circuit} newCircuit * @param {!boolean} preview */ commit_or_preview(newCircuit, preview) { if (preview) { this.preview(newCircuit); } else { this.commit(newCircuit); } } /** * @param {!Circuit} newCircuit */ commit(newCircuit) { while (newCircuit.layers.length > 0 && newCircuit.layers[newCircuit.layers.length - 1].isEmpty()) { newCircuit.layers.pop(); } this.rev.commit(newCircuit.toStimCircuit()); } /** * @param {!Circuit} newCircuit */ preview(newCircuit) { this.rev.startedWorkingOnCommit(newCircuit.toStimCircuit()); this.obs_val_draw_state.set(this.toSnapshot(newCircuit)); } /** * @param {undefined|!Circuit} previewCircuit * @returns {!StateSnapshot} */ toSnapshot(previewCircuit) { if (previewCircuit === undefined) { previewCircuit = this.copyOfCurCircuit(); } return new StateSnapshot( previewCircuit, this.curLayer, this.focusedSet, this.timelineSet, this.curMouseX, this.curMouseY, this.mouseDownX, this.mouseDownY, this.currentPositionsBoxesByMouseDrag(this.chorder.curModifiers.has("alt")), ); } force_redraw() { let previewedCircuit = this.obs_val_draw_state.get().circuit; this.obs_val_draw_state.set(this.toSnapshot(previewedCircuit)); } clearCircuit() { this.commit(new Circuit(new Float64Array([]), [])); } clearMarkers() { let c = this.copyOfCurCircuit(); for (let layer of c.layers) { layer.markers = layer.markers.filter(e => e.gate.name !== 'MARKX' && e.gate.name !== 'MARKY' && e.gate.name !== 'MARKZ'); } this.commit(c); } /** * @param {!boolean} parityLock * @returns {!Array} */ currentPositionsBoxesByMouseDrag(parityLock) { let curMouseX = this.curMouseX; let curMouseY = this.curMouseY; let mouseDownX = this.mouseDownX; let mouseDownY = this.mouseDownY; let result = []; if (curMouseX !== undefined && mouseDownX !== undefined) { let [sx, sy] = xyToPos(mouseDownX, mouseDownY); let x1 = Math.min(curMouseX, mouseDownX); let x2 = Math.max(curMouseX, mouseDownX); let y1 = Math.min(curMouseY, mouseDownY); let y2 = Math.max(curMouseY, mouseDownY); let gap = pitch/4 - rad; x1 += gap; x2 -= gap; y1 += gap; y2 -= gap; x1 = Math.floor(x1 * 2 / pitch + 0.5) / 2; x2 = Math.floor(x2 * 2 / pitch + 0.5) / 2; y1 = Math.floor(y1 * 2 / pitch + 0.5) / 2; y2 = Math.floor(y2 * 2 / pitch + 0.5) / 2; let b = 1; if (x1 === x2 || y1 === y2) { b = 2; } for (let x = x1; x <= x2; x += 0.5) { for (let y = y1; y <= y2; y += 0.5) { if (x % 1 === y % 1) { if (!parityLock || (sx % b === x % b && sy % b === y % b)) { result.push([x, y]); } } } } } return result; } /** * @param {!function(!number, !number): ![!number, !number]} coordTransform * @param {!boolean} preview * @param {!boolean} moveFocus */ applyCoordinateTransform(coordTransform, preview, moveFocus) { let c = this.copyOfCurCircuit(); c = c.afterCoordTransform(coordTransform); if (!preview && moveFocus) { let trans = m => { let new_m = new Map(); for (let [x, y] of m.values()) { [x, y] = coordTransform(x, y); new_m.set(`${x},${y}`, [x, y]); } return new_m; } this.timelineSet = trans(this.timelineSet); this.focusedSet = trans(this.focusedSet); } this.commit_or_preview(c, preview); } /** * @param {!int} steps * @param {!boolean} preview */ rotate45(steps, preview) { let t1 = rotated45Transform(steps); let t2 = this.copyOfCurCircuit().afterCoordTransform(t1).coordTransformForRectification(); this.applyCoordinateTransform((x, y) => { [x, y] = t1(x, y); return t2(x, y); }, preview, true); } /** * @param {!int} newLayer */ changeCurLayerTo(newLayer) { this.curLayer = Math.max(newLayer, 0); this.force_redraw(); } /** * @param {!Array} newFocus * @param {!boolean} unionMode * @param {!boolean} xorMode */ changeFocus(newFocus, unionMode, xorMode) { if (!unionMode && !xorMode) { this.focusedSet.clear(); } for (let [x, y] of newFocus) { let k = `${x},${y}`; if (xorMode && this.focusedSet.has(k)) { this.focusedSet.delete(k); } else { this.focusedSet.set(k, [x, y]); } } this.force_redraw(); } /** * @param {!Iterable} affectedQubits * @returns {!Map} * @private */ _inferBases(affectedQubits) { let inferredBases = new Map(); let layer = this.copyOfCurCircuit().layers[this.curLayer]; for (let q of [...affectedQubits]) { let op = layer.id_ops.get(q); if (op !== undefined) { if (op.gate.name === 'RX' || op.gate.name === 'MX' || op.gate.name === 'MRX') { inferredBases.set(q, 'X'); } else if (op.gate.name === 'RY' || op.gate.name === 'MY' || op.gate.name === 'MRY') { inferredBases.set(q, 'Y'); } else if (op.gate.name === 'R' || op.gate.name === 'M' || op.gate.name === 'MR') { inferredBases.set(q, 'Z'); } else if (op.gate.name === 'MXX' || op.gate.name === 'MYY' || op.gate.name === 'MZZ') { let opBasis = op.gate.name[1]; for (let q of op.id_targets) { inferredBases.set(q, opBasis); } } else if (op.gate.name.startsWith('MPP:') && op.gate.tableau_map === undefined && op.id_targets.length === op.gate.name.length - 4) { // MPP special case. let bases = op.gate.name.substring(4); for (let k = 0; k < op.id_targets.length; k++) { let q = op.id_targets[k]; inferredBases.set(q, bases[k]); } } } } return inferredBases; } /** * @param {!boolean} preview * @param {!int} markIndex */ markFocusInferBasis(preview, markIndex) { let newCircuit = this.copyOfCurCircuit().withCoordsIncluded(this.focusedSet.values()); let c2q = newCircuit.coordToQubitMap(); let affectedQubits = new Set(); for (let key of this.focusedSet.keys()) { affectedQubits.add(c2q.get(key)); } // Determine which qubits have forced basis based on their operation. let forcedBases = this._inferBases(affectedQubits); for (let q of forcedBases.keys()) { affectedQubits.add(q); } // Pick a default basis for unforced qubits. let seenBases = new Set(forcedBases.values()); seenBases.delete(undefined); let defaultBasis; if (seenBases.size === 1) { defaultBasis = [...seenBases][0]; } else { defaultBasis = 'Z'; } // Mark each qubit with its inferred basis. let layer = newCircuit.layers[this.curLayer]; for (let q of affectedQubits) { let basis = forcedBases.get(q); if (basis === undefined) { basis = defaultBasis; } let gate = GATE_MAP.get(`MARK${basis}`).withDefaultArgument(markIndex); layer.put(new Operation( gate, '', new Float32Array([markIndex]), new Uint32Array([q]), )); } this.commit_or_preview(newCircuit, preview); } /** * @param {!boolean} preview */ unmarkFocusInferBasis(preview) { let newCircuit = this.copyOfCurCircuit().withCoordsIncluded(this.focusedSet.values()); let c2q = newCircuit.coordToQubitMap(); let affectedQubits = new Set(); for (let key of this.focusedSet.keys()) { affectedQubits.add(c2q.get(key)); } let inferredBases = this._inferBases(affectedQubits); for (let q of inferredBases.keys()) { affectedQubits.add(q); } for (let q of affectedQubits) { if (q !== undefined) { newCircuit.layers[this.curLayer].id_dropMarkersAt(q); } } this.commit_or_preview(newCircuit, preview); } /** * @param {!boolean} preview * @param {!Gate} gate * @param {!Array} gate_args */ _writeSingleQubitGateToFocus(preview, gate, gate_args) { let newCircuit = this.copyOfCurCircuit().withCoordsIncluded(this.focusedSet.values()); let c2q = newCircuit.coordToQubitMap(); for (let key of this.focusedSet.keys()) { newCircuit.layers[this.curLayer].put(new Operation( gate, '', new Float32Array(gate_args), new Uint32Array([c2q.get(key)]), )); } this.commit_or_preview(newCircuit, preview); } /** * @param {!boolean} preview * @param {!Gate} gate * @param {!Array} gate_args */ _writeTwoQubitGateToFocus(preview, gate, gate_args) { let newCircuit = this.copyOfCurCircuit(); let [x, y] = xyToPos(this.curMouseX, this.curMouseY); let [minX, minY] = minXY(this.focusedSet.values()); let coords = []; if (x !== undefined && minX !== undefined && !this.focusedSet.has(`${x},${y}`)) { let dx = x - minX; let dy = y - minY; for (let [vx, vy] of this.focusedSet.values()) { coords.push([vx, vy]); coords.push([vx + dx, vy + dy]); } } else if (this.focusedSet.size === 2) { for (let [vx, vy] of this.focusedSet.values()) { coords.push([vx, vy]); } } if (coords.length > 0) { newCircuit = newCircuit.withCoordsIncluded(coords) let c2q = newCircuit.coordToQubitMap(); for (let k = 0; k < coords.length; k += 2) { let [x0, y0] = coords[k]; let [x1, y1] = coords[k + 1]; let q0 = c2q.get(`${x0},${y0}`); let q1 = c2q.get(`${x1},${y1}`); newCircuit.layers[this.curLayer].put(new Operation( gate, '', new Float32Array(gate_args), new Uint32Array([q0, q1]), )); } } this.commit_or_preview(newCircuit, preview); } /** * @param {!boolean} preview * @param {!Gate} gate * @param {!Array} gate_args */ _writeVariableQubitGateToFocus(preview, gate, gate_args) { if (this.focusedSet.size === 0) { return; } let pairs = []; let cx = 0; let cy = 0; for (let xy of this.focusedSet.values()) { pairs.push(xy); cx += xy[0]; cy += xy[1]; } cx /= pairs.length; cy /= pairs.length; pairs.sort((a, b) => { let [x1, y1] = a; let [x2, y2] = b; return Math.atan2(y1 - cy, x1 - cx) - Math.atan2(y2 - cy, x2 - cx); }); let newCircuit = this.copyOfCurCircuit().withCoordsIncluded(this.focusedSet.values()); let c2q = newCircuit.coordToQubitMap(); let qs = new Uint32Array(this.focusedSet.size); for (let k = 0; k < pairs.length; k++) { let [x, y] = pairs[k]; qs[k] = c2q.get(`${x},${y}`); } newCircuit.layers[this.curLayer].put(new Operation(gate, '', new Float32Array(gate_args), qs)); this.commit_or_preview(newCircuit, preview); } /** * @param {!boolean} preview * @param {!Gate} gate * @param {undefined|!Array=} gate_args */ writeGateToFocus(preview, gate, gate_args=undefined) { if (gate_args === undefined) { if (gate.defaultArgument === undefined) { gate_args = []; } else { gate_args = [gate.defaultArgument]; } } if (gate.num_qubits === 1) { this._writeSingleQubitGateToFocus(preview, gate, gate_args); } else if (gate.num_qubits === 2) { this._writeTwoQubitGateToFocus(preview, gate, gate_args); } else { this._writeVariableQubitGateToFocus(preview, gate, gate_args); } } writeMarkerToObservable(preview, marker_index) { this._writeMarkerToDetOrObs(preview, marker_index, false); } writeMarkerToDetector(preview, marker_index) { this._writeMarkerToDetOrObs(preview, marker_index, true); } _writeMarkerToDetOrObs(preview, marker_index, isDet) { let newCircuit = this.copyOfCurCircuit(); let argIndex = isDet ? newCircuit.collectDetectorsAndObservables(false).dets.length : marker_index; let prop = PropagatedPauliFrames.fromCircuit(newCircuit, marker_index); for (let k = 0; k < newCircuit.layers.length; k++) { let before = k === 0 ? new PropagatedPauliFrameLayer(new Map(), new Set(), []) : prop.atLayer(k - 0.5); let after = prop.atLayer(k + 0.5); let layer = newCircuit.layers[k]; for (let q of new Set([...before.bases.keys(), ...after.bases.keys()])) { let b1 = before.bases.get(q); let b2 = after.bases.get(q); let op = layer.id_ops.get(q); let name = op !== undefined ? op.gate.name : undefined; let transition = undefined; if (name === 'MR' || name === 'MRX' || name === 'MRY') { transition = b1; } else if (op !== undefined && op.countMeasurements() > 0) { if (b1 === undefined) { transition = b2; } else if (b2 === undefined) { transition = b1; } else if (b1 !== b2) { let s = new Set(['X', 'Y', 'Z']); s.delete(b1); s.delete(b2); transition = [...s][0]; } } if (transition !== undefined) { layer.markers.push(new Operation( GATE_MAP.get(isDet ? 'DETECTOR' : 'OBSERVABLE_INCLUDE'), '', new Float32Array([argIndex]), op.id_targets, )); } } layer.markers = layer.markers.filter(op => !op.gate.name.startsWith('MARK') || op.args[0] !== marker_index); } this.commit_or_preview(newCircuit, preview); } addDissipativeOverlapToMarkers(preview, marker_index) { let newCircuit = this.copyOfCurCircuit(); let prop = PropagatedPauliFrames.fromCircuit(newCircuit, marker_index); let k = this.curLayer; let before = k === 0 ? new PropagatedPauliFrameLayer(new Map(), new Set(), []) : prop.atLayer(k - 0.5); let after = prop.atLayer(k + 0.5); let layer = newCircuit.layers[k]; let processedQubits = new Set(); for (let q of new Set([...before.bases.keys(), ...after.bases.keys()])) { if (processedQubits.has(q)) { continue; } let b1 = before.bases.get(q); let b2 = after.bases.get(q); let op = layer.id_ops.get(q); if (op === undefined) { continue; } let name = op.gate.name; let basis = undefined; if (name === 'R' || name === 'M' || name === 'MR') { basis = 'Z'; } else if (name === 'RX' || name === 'MX' || name === 'MRX') { basis = 'X'; } else if (name === 'RY' || name === 'MY' || name === 'MRY') { basis = 'Y'; } else if (name === 'MXX' || name === 'MYY' || name === 'MZZ') { basis = name[1]; let score = 0; for (let q2 of op.id_targets) { if (processedQubits.has(q2)) { score = -1; break; } score += before.bases.get(q2) === basis; } if (score === 2) { for (let q2 of op.id_targets) { processedQubits.add(q2); layer.markers.push(new Operation( GATE_MAP.get(`MARK${basis}`), '', new Float32Array([marker_index]), new Uint32Array([q2]), )); } } continue; } else if (name.startsWith('MPP:')) { let score = 0; for (let k = 0; k < op.id_targets.length; k++) { let q2 = op.id_targets[k]; basis = name[k + 4]; if (processedQubits.has(q2)) { score = -1; break; } score += before.bases.get(q2) === basis; } if (score > op.id_targets.length / 2) { for (let k = 0; k < op.id_targets.length; k++) { let q2 = op.id_targets[k]; basis = name[k + 4]; processedQubits.add(q2); layer.markers.push(new Operation( GATE_MAP.get(`MARK${basis}`), '', new Float32Array([marker_index]), new Uint32Array([q2]), )); } } continue; } else { continue; } if (b1 !== undefined || b2 !== undefined) { layer.markers.push(new Operation( GATE_MAP.get(`MARK${basis}`), '', new Float32Array([marker_index]), new Uint32Array([q]), )); processedQubits.add(q); } } this.commit_or_preview(newCircuit, preview); } moveDetOrObsAtFocusIntoMarker(preview, marker_index) { let circuit = this.copyOfCurCircuit(); let focusSetQids = new Set(); let c2q = circuit.coordToQubitMap(); for (let s of this.focusedSet.keys()) { focusSetQids.add(c2q.get(s)); } let find_overlapping_region = () => { let {dets: dets, obs: obs} = circuit.collectDetectorsAndObservables(false); for (let det_id = 0; det_id < dets.length; det_id++) { let prop = PropagatedPauliFrames.fromMeasurements(circuit, dets[det_id].mids); if (prop.atLayer(this.curLayer + 0.5).touchesQidSet(focusSetQids)) { return [prop, new Operation(GATE_MAP.get('DETECTOR'), '', new Float32Array([det_id]), new Uint32Array([]))]; } } for (let [obs_id, obs_val] of obs.entries()) { let prop = PropagatedPauliFrames.fromMeasurements(circuit, obs_val); if (prop.atLayer(this.curLayer + 0.5).touchesQidSet(focusSetQids)) { return [prop, new Operation(GATE_MAP.get('OBSERVABLE_INCLUDE'), '', new Float32Array([obs_id]), new Uint32Array([]))]; } } return undefined; } let overlap = find_overlapping_region(); if (overlap === undefined) { return; } let [prop, rep_op] = overlap; let newCircuit = this.copyOfCurCircuit(); for (let k = 0; k < newCircuit.layers.length; k++) { let before = k === 0 ? new PropagatedPauliFrameLayer(new Map(), new Set(), []) : prop.atLayer(k - 0.5); let after = prop.atLayer(k + 0.5); let layer = newCircuit.layers[k]; for (let q of new Set([...before.bases.keys(), ...after.bases.keys()])) { let b1 = before.bases.get(q); let b2 = after.bases.get(q); let op = layer.id_ops.get(q); let name = op !== undefined ? op.gate.name : undefined; let transition = undefined; if (name === 'MR' || name === 'MRX' || name === 'MRY' || name === 'R' || name === 'RX' || name === 'RY') { transition = b2; } else if (op !== undefined && op.countMeasurements() > 0) { if (b1 === undefined) { transition = b2; } else if (b2 === undefined) { transition = b1; } else if (b1 !== b2) { let s = new Set(['X', 'Y', 'Z']); s.delete(b1); s.delete(b2); transition = [...s][0]; } } if (transition !== undefined) { layer.markers.push(new Operation( GATE_MAP.get(`MARK${transition}`), '', new Float32Array([marker_index]), new Uint32Array([q]), )) } } layer.markers = layer.markers.filter(op => op.gate.name !== rep_op.gate.name || op.args[0] !== rep_op.args[0]); } this.commit_or_preview(newCircuit, preview); } } export {EditorState, StateSnapshot} ================================================ FILE: glue/crumble/editor/editor_state.test.js ================================================ import {test, assertThat} from "../test/test_util.js"; import {EditorState} from "./editor_state.js"; import {GATE_MAP} from "../gates/gateset.js"; import {Circuit} from "../circuit/circuit.js"; import {pitch} from "../draw/config.js"; import {Operation} from '../circuit/operation.js'; test("editor_state.changeFocus", () => { let state = new EditorState(undefined); assertThat(state.focusedSet).isEqualTo(new Map()); state.changeFocus([[0, 0], [0, 1]], false, false); assertThat(state.focusedSet).isEqualTo(new Map([ ['0,0', [0, 0]], ['0,1', [0, 1]], ])); state.changeFocus([[0, 0], [2, 1]], false, false); assertThat(state.focusedSet).isEqualTo(new Map([ ['0,0', [0, 0]], ['2,1', [2, 1]], ])); state.changeFocus([[0, 0], [1, 0]], false, true); assertThat(state.focusedSet).isEqualTo(new Map([ ['2,1', [2, 1]], ['1,0', [1, 0]], ])); state.changeFocus([[0, 0], [1, 0]], true, false); assertThat(state.focusedSet).isEqualTo(new Map([ ['0,0', [0, 0]], ['2,1', [2, 1]], ['1,0', [1, 0]], ])); }); test("editor_state.markFocusInferBasis", () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 QUBIT_COORDS(0, 2) 2 QUBIT_COORDS(1, 0) 3 QUBIT_COORDS(1, 1) 4 QUBIT_COORDS(2, 2) 5 H 0 1 RX 2 RY 3 RZ 4 RX 5 `)); state.changeFocus([[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2]], false, false); state.markFocusInferBasis(false, 1); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 QUBIT_COORDS(1, 1) 2 QUBIT_COORDS(0, 2) 3 QUBIT_COORDS(2, 2) 4 QUBIT_COORDS(1, 0) 5 QUBIT_COORDS(1, 2) 6 H 0 1 R 2 RX 3 4 RY 5 MARKX(1) 3 MARKY(1) 5 MARKZ(1) 0 1 2 6 `)); state.changeFocus([[0, 0], [0, 1], [0, 2], [1, 2]], false, false); state.markFocusInferBasis(false, 1); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 QUBIT_COORDS(1, 1) 2 QUBIT_COORDS(0, 2) 3 QUBIT_COORDS(2, 2) 4 QUBIT_COORDS(1, 0) 5 QUBIT_COORDS(1, 2) 6 H 0 1 R 2 RX 3 4 RY 5 MARKX(1) 0 1 3 6 MARKY(1) 5 MARKZ(1) 2 `)); }); test("editor_state.writeGateToFocus", () => { let state = new EditorState(undefined); state.changeFocus([[0, 0], [0, 1]], false, false); state.writeGateToFocus(false, GATE_MAP.get('H'), undefined); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 H 0 1 `)); state.changeFocus([[2, 0], [0, 1]], false, false); state.writeGateToFocus(false, GATE_MAP.get('S'), undefined); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(2, 0) 1 QUBIT_COORDS(0, 1) 2 H 0 S 1 2 `)); state.curMouseX = 5 * pitch; state.curMouseY = 6 * pitch; state.writeGateToFocus(false, GATE_MAP.get('CX'), undefined); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(2, 0) 0 QUBIT_COORDS(7, 5) 1 QUBIT_COORDS(0, 1) 2 QUBIT_COORDS(5, 6) 3 QUBIT_COORDS(0, 0) 4 CX 0 1 2 3 H 4 `)); state.clearCircuit(); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit('')); state.writeGateToFocus(false, GATE_MAP.get("POLYGON"), [1, 1, 0, 0.5]); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(2, 0) 0 QUBIT_COORDS(0, 1) 1 POLYGON(1,1,0,0.5) 0 1 `)); }); test('editor_state.writeMarkerToDetector', () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 #!pragma MARKZ(0) 0 TICK M 0 #!pragma MARKZ(0) 0 `)); assertThat(state.copyOfCurCircuit().layers[0].markers.length).isNotEqualTo(0); state.writeMarkerToDetector(false, 0); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 TICK M 0 DETECTOR(0, 0, 0) rec[-1] `)); }); test('editor_state.moveDetOrObsAtFocusIntoMarker_det', () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 TICK TICK TICK M 0 DETECTOR(0, 0, 0) rec[-1] `)); state.changeCurLayerTo(2); state.changeFocus([[0, 0]], false, false); state.moveDetOrObsAtFocusIntoMarker(false, 2); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 #!pragma MARKZ(2) 0 TICK TICK TICK M 0 #!pragma MARKZ(2) 0 `)); }); test('editor_state.moveDetOrObsAtFocusIntoMarker_obs', () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 TICK TICK TICK M 0 OBSERVABLE_INCLUDE(0) rec[-1] `)); state.changeCurLayerTo(2); state.changeFocus([[0, 0]], false, false); state.moveDetOrObsAtFocusIntoMarker(false, 2); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 #!pragma MARKZ(2) 0 TICK TICK TICK M 0 #!pragma MARKZ(2) 0 `)); }); test('editor_state.moveDetOrObsAtFocusIntoMarker_mr', () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 TICK TICK TICK MR 0 DETECTOR(0, 0, 0) rec[-1] `)); state.changeCurLayerTo(2); state.changeFocus([[0, 0]], false, false); state.moveDetOrObsAtFocusIntoMarker(false, 2); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 #!pragma MARKZ(2) 0 TICK TICK TICK MR 0 `)); }); test('editor_state.writeMarkerToDetector_mr', () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 #!pragma MARKZ(2) 0 TICK TICK TICK MR 0 `)); state.writeMarkerToDetector(false, 2); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 R 0 TICK TICK TICK MR 0 DETECTOR(0, 0, 0) rec[-1] `)); }); test('editor_state.writeMarkerToDetector_mxx', () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(1, 1) 0 QUBIT_COORDS(1, 2) 1 QUBIT_COORDS(2, 1) 2 QUBIT_COORDS(2, 2) 3 R 0 1 2 3 #!pragma MARKZ(0) 0 1 2 3 TICK MXX 0 2 1 3 #!pragma MARKX(0) 0 2 3 1 TICK MYY 3 2 1 0 #!pragma MARKY(0) 1 0 3 2 `)); state.writeMarkerToDetector(false, 0); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(1, 1) 0 QUBIT_COORDS(1, 2) 1 QUBIT_COORDS(2, 1) 2 QUBIT_COORDS(2, 2) 3 R 0 1 2 3 TICK MXX 0 2 1 3 TICK MYY 3 2 1 0 DETECTOR(1, 2, 0) rec[-1] rec[-2] rec[-3] rec[-4] `)); }); test('editor_state.writeMarkerToObservable_mxx', () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(1, 1) 0 QUBIT_COORDS(1, 2) 1 QUBIT_COORDS(2, 1) 2 QUBIT_COORDS(2, 2) 3 R 0 1 2 3 #!pragma MARKZ(0) 0 1 2 3 TICK MXX 0 2 1 3 #!pragma MARKX(0) 0 2 3 1 TICK MYY 3 2 1 0 #!pragma MARKY(0) 1 0 3 2 `)); state.writeMarkerToObservable(false, 0); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(1, 1) 0 QUBIT_COORDS(1, 2) 1 QUBIT_COORDS(2, 1) 2 QUBIT_COORDS(2, 2) 3 R 0 1 2 3 TICK MXX 0 2 1 3 TICK MYY 3 2 1 0 OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] rec[-3] rec[-4] `)); }); test('editor_state.edit_measurement_near_observables', () => { let state = new EditorState(undefined); state.commit(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(2, 0) 2 RX 1 TICK MX 2 TICK M 0 OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] `)); state.changeFocus([[1, 0]], false, false); state.changeCurLayerTo(1); state.writeGateToFocus(false, GATE_MAP.get('M')); assertThat(state.copyOfCurCircuit()).isEqualTo(Circuit.fromStimCircuit(` QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(2, 0) 2 RX 1 TICK M 1 MX 2 TICK M 0 OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] `)); }); ================================================ FILE: glue/crumble/editor/sync_url_to_state.js ================================================ /** * Copyright 2017 Google Inc. * * 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. */ import {HistoryPusher} from "../base/history_pusher.js" import {Circuit} from "../circuit/circuit.js"; /** * @param {!string} text * @return {!string} */ function urlWithCircuitHash(text) { text = text. replaceAll('QUBIT_COORDS', 'Q'). replaceAll('DETECTOR', 'DT'). replaceAll('OBSERVABLE_INCLUDE', 'OI'). replaceAll(', ', ','). replaceAll(') ', ')'). replaceAll(' ', '_'). replaceAll('\n', ';'); if (text.indexOf('%') !== -1 || text.indexOf('&') !== -1) { text = encodeURIComponent(text); } return "#circuit=" + text; } /** * @param {!Revision} revision */ function initUrlCircuitSync(revision) { // Pull initial circuit out of URL '#x=y' arguments. const getHashParameters = () => { let hashText = document.location.hash.substring(1); let paramsMap = new Map(); if (hashText !== "") { for (let keyVal of hashText.split("&")) { let eq = keyVal.indexOf("="); if (eq === -1) { continue; } let key = keyVal.substring(0, eq); let val = decodeURIComponent(keyVal.substring(eq + 1)); paramsMap.set(key, val); } } return paramsMap; }; const historyPusher = new HistoryPusher(); const loadCircuitFromUrl = () => { try { // Extract the circuit parameter. let params = getHashParameters(); if (!params.has("circuit")) { let txtDefault = /** @type {!HTMLTextAreaElement} */ document.getElementById('txtDefaultCircuit'); if (txtDefault.value.replaceAll('_', '-') === "[[[DEFAULT-CIRCUIT-CONTENT-LITERAL]]]") { params.set("circuit", ""); } else { params.set("circuit", txtDefault.value); } } // Repack the circuit data, so we can tell if there are round trip changes. let circuit = Circuit.fromStimCircuit(params.get("circuit")); let cleanedState = circuit.toStimCircuit(); revision.clear(cleanedState); // If the initial state is an empty circuit without any round trip spandrels, no need to put it in history. if (circuit.layers.every(e => e.isEmpty()) && params.size === 1 && cleanedState === params.get('circuit')) { historyPusher.currentStateIsNotMemorable(); } else { historyPusher.stateChange(cleanedState, urlWithCircuitHash(cleanedState)); } } catch (ex) { // TODO: HANDLE BETTER. throw new Error(ex); } }; window.addEventListener('popstate', loadCircuitFromUrl); loadCircuitFromUrl(); revision.latestActiveCommit().whenDifferent().skip(1).subscribe(jsonText => { historyPusher.stateChange(jsonText, urlWithCircuitHash(jsonText)); }); } export {initUrlCircuitSync} ================================================ FILE: glue/crumble/gates/gate.js ================================================ /** * Gate drawing callback. * * @callback gateDrawCallback * @param {!Operation} op * @param {!function(qubit: !int): ![!number, !number]} qubitCoordsFunc * @param {!CanvasRenderingContext2D} ctx */ /** * An operation without specified targets. */ class Gate { /** * @param {!string} name * @param {!int|undefined} num_qubits * @param {!boolean} can_fuse * @param {!boolean} is_marker * @param {!Map|undefined} tableau_map * @param {!function(!PauliFrame, !Array)} frameDo, * @param {!function(!PauliFrame, !Array)} frameUndo, * @param {!gateDrawCallback} drawer * @param {undefined|!number=undefined} defaultArgument */ constructor(name, num_qubits, can_fuse, is_marker, tableau_map, frameDo, frameUndo, drawer, defaultArgument = undefined) { this.name = name; this.num_qubits = num_qubits; this.is_marker = is_marker; this.can_fuse = can_fuse; this.tableau_map = tableau_map; this.frameDo = frameDo; this.frameUndo = frameUndo; this.drawer = drawer; this.defaultArgument = defaultArgument; } /** * @param {!number} newDefaultArgument */ withDefaultArgument(newDefaultArgument) { return new Gate( this.name, this.num_qubits, this.can_fuse, this.is_marker, this.tableau_map, this.frameDo, this.frameUndo, this.drawer, newDefaultArgument); } } export {Gate}; ================================================ FILE: glue/crumble/gates/gate_draw_util.js ================================================ import {pitch, rad} from "../draw/config.js" /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x * @param {undefined|!number} y */ function draw_x_control(ctx, x, y) { if (x === undefined || y === undefined) { return; } ctx.strokeStyle = 'black'; ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(x, y, rad, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x, y - rad); ctx.lineTo(x, y + rad); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x - rad, y); ctx.lineTo(x + rad, y); ctx.stroke(); } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x * @param {undefined|!number} y */ function draw_y_control(ctx, x, y) { if (x === undefined || y === undefined) { return; } ctx.strokeStyle = 'black'; ctx.fillStyle = '#AAA'; ctx.beginPath(); ctx.moveTo(x, y + rad); ctx.lineTo(x + rad, y - rad); ctx.lineTo(x - rad, y - rad); ctx.lineTo(x, y + rad); ctx.stroke(); ctx.fill(); } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x * @param {undefined|!number} y */ function draw_z_control(ctx, x, y) { if (x === undefined || y === undefined) { return; } ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(x, y, rad, 0, 2 * Math.PI); ctx.fill(); } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x * @param {undefined|!number} y */ function draw_xswap_control(ctx, x, y) { if (x === undefined || y === undefined) { return; } ctx.fillStyle = 'white'; ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.arc(x, y, rad, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); let r = rad * 0.4; ctx.strokeStyle = 'black'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x - r, y - r); ctx.lineTo(x + r, y + r); ctx.stroke(); ctx.moveTo(x - r, y + r); ctx.lineTo(x + r, y - r); ctx.stroke(); ctx.lineWidth = 1; } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x * @param {undefined|!number} y */ function draw_zswap_control(ctx, x, y) { if (x === undefined || y === undefined) { return; } ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.arc(x, y, rad, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); let r = rad * 0.4; ctx.strokeStyle = 'white'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x - r, y - r); ctx.lineTo(x + r, y + r); ctx.stroke(); ctx.moveTo(x - r, y + r); ctx.lineTo(x + r, y - r); ctx.stroke(); ctx.lineWidth = 1; } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x * @param {undefined|!number} y */ function draw_iswap_control(ctx, x, y) { if (x === undefined || y === undefined) { return; } ctx.fillStyle = '#888'; ctx.strokeStyle = '#222'; ctx.beginPath(); ctx.arc(x, y, rad, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); let r = rad * 0.4; ctx.lineWidth = 3; ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.moveTo(x - r, y - r); ctx.lineTo(x + r, y + r); ctx.stroke(); ctx.moveTo(x - r, y + r); ctx.lineTo(x + r, y - r); ctx.stroke(); ctx.lineWidth = 1; } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x * @param {undefined|!number} y */ function draw_swap_control(ctx, x, y) { if (x === undefined || y === undefined) { return; } let r = rad / 3; ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.moveTo(x - r, y - r); ctx.lineTo(x + r, y + r); ctx.stroke(); ctx.moveTo(x - r, y + r); ctx.lineTo(x + r, y - r); ctx.stroke(); } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x * @param {undefined|!number} y */ function stroke_degenerate_connector(ctx, x, y) { if (x === undefined || y === undefined) { return; } let r = rad * 1.1; ctx.strokeRect(x - r, y - r, r * 2, r * 2); } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x1 * @param {undefined|!number} y1 * @param {undefined|!number} x2 * @param {undefined|!number} y2 */ function stroke_connector_to(ctx, x1, y1, x2, y2) { if (x1 === undefined || y1 === undefined || x2 === undefined || y2 === undefined) { stroke_degenerate_connector(ctx, x1, y1); stroke_degenerate_connector(ctx, x2, y2); return; } if (x2 < x1 || (x2 === x1 && y2 < y1)) { stroke_connector_to(ctx, x2, y2, x1, y1); return; } let dx = x2 - x1; let dy = y2 - y1; let d = Math.sqrt(dx*dx + dy*dy); let ux = dx / d * 14; let uy = dy / d * 14; let px = uy; let py = -ux; ctx.beginPath(); ctx.moveTo(x1, y1); if (d < pitch * 1.1) { ctx.lineTo(x2, y2); } else { ctx.bezierCurveTo(x1 + ux + px, y1 + uy + py, x2 - ux + px, y2 - uy + py, x2, y2); } ctx.stroke(); } /** * @param {!CanvasRenderingContext2D} ctx * @param {undefined|!number} x1 * @param {undefined|!number} y1 * @param {undefined|!number} x2 * @param {undefined|!number} y2 */ function draw_connector(ctx, x1, y1, x2, y2) { ctx.lineWidth = 2; ctx.strokeStyle = 'black'; stroke_connector_to(ctx, x1, y1, x2, y2); ctx.lineWidth = 1; } export { draw_x_control, draw_y_control, draw_z_control, draw_swap_control, draw_iswap_control, stroke_connector_to, draw_connector, draw_xswap_control, draw_zswap_control, }; ================================================ FILE: glue/crumble/gates/gateset.js ================================================ import {Gate} from "./gate.js" import {iter_gates_controlled_paulis} from "./gateset_controlled_paulis.js"; import {iter_gates_demolition_measurements} from "./gateset_demolition_measurements.js"; import {iter_gates_hadamard_likes} from "./gateset_hadamard_likes.js"; import {iter_gates_markers} from "./gateset_markers.js"; import {iter_gates_pair_measurements} from "./gateset_pair_measurements.js"; import {iter_gates_paulis} from "./gateset_paulis.js"; import {iter_gates_quarter_turns} from "./gateset_quarter_turns.js"; import {iter_gates_resets} from "./gateset_resets.js"; import {iter_gates_solo_measurements} from "./gateset_solo_measurements.js"; import {iter_gates_sqrt_pauli_pairs} from "./gateset_sqrt_pauli_pairs.js"; import {iter_gates_swaps} from "./gateset_swaps.js"; import {iter_gates_third_turns} from "./gateset_third_turns.js"; function *iter_gates() { yield *iter_gates_controlled_paulis(); yield *iter_gates_demolition_measurements(); yield *iter_gates_hadamard_likes(); yield *iter_gates_markers(); yield *iter_gates_pair_measurements(); yield *iter_gates_paulis(); yield *iter_gates_quarter_turns(); yield *iter_gates_resets(); yield *iter_gates_solo_measurements(); yield *iter_gates_sqrt_pauli_pairs(); yield *iter_gates_swaps(); yield *iter_gates_third_turns(); } /** * @returns {!Map} */ function make_gate_map() { let result = new Map(); for (let gate of iter_gates()) { result.set(gate.name, gate); } result.set('MZ', result.get('M')) result.set('RZ', result.get('R')) result.set('MRZ', result.get('MR')) return result; } /** * @returns {!Map} */ function make_gate_alias_map() { let result = new Map(); result.set("CNOT", {name: "CX"}); result.set("MZ", {name: "M"}); result.set("MRZ", {name: "MR"}); result.set("RZ", {name: "R"}); result.set("H_XZ", {name: "H"}); result.set("SQRT_Z", {name: "S"}); result.set("SQRT_Z_DAG", {name: "S_DAG"}); result.set("ZCX", {name: "CX"}); result.set("ZCY", {name: "CY"}); result.set("ZCZ", {name: "CZ"}); result.set("SWAPCZ", {name: "CZSWAP"}); // Ordered-flipped aliases. result.set("XCZ", {name: "CX", rev_pair: true}); result.set("YCX", {name: "XCY", rev_pair: true}); result.set("YCZ", {name: "CY", rev_pair: true}); result.set("SWAPCX", {name: "CXSWAP", rev_pair: true}); // Noise. result.set("CORRELATED_ERROR", {ignore: true}); result.set("DEPOLARIZE1", {ignore: true}); result.set("DEPOLARIZE2", {ignore: true}); result.set("E", {ignore: true}); result.set("ELSE_CORRELATED_ERROR", {ignore: true}); result.set("PAULI_CHANNEL_1", {ignore: true}); result.set("PAULI_CHANNEL_2", {ignore: true}); result.set("X_ERROR", {ignore: true}); result.set("I_ERROR", {ignore: true}); result.set("II_ERROR", {ignore: true}); result.set("Y_ERROR", {ignore: true}); result.set("Z_ERROR", {ignore: true}); result.set("HERALDED_ERASE", {ignore: true}); result.set("HERALDED_PAULI_CHANNEL_1", {ignore: true}); // Annotations. result.set("MPAD", {ignore: true}); result.set("SHIFT_COORDS", {ignore: true}); return result; } const GATE_MAP = /** @type {Map} */ make_gate_map(); const GATE_ALIAS_MAP = /** @type {!Map} */ make_gate_alias_map(); export {GATE_MAP, GATE_ALIAS_MAP}; ================================================ FILE: glue/crumble/gates/gateset.test.js ================================================ import {GATE_MAP, GATE_ALIAS_MAP} from "./gateset.js" import {test, assertThat, skipRestOfTestIfHeadless} from "../test/test_util.js"; import {Operation} from '../circuit/operation.js'; import {KNOWN_GATE_NAMES_FROM_STIM} from '../test/generated_gate_name_list.test.js'; test("gateset.expected_gates", () => { let expectedGates = new Set(); for (let e of KNOWN_GATE_NAMES_FROM_STIM.split("\n")) { if (e.length > 0) { expectedGates.add(e); } } for (let e of GATE_ALIAS_MAP.keys()) { expectedGates.delete(e); } // Special cased. expectedGates.delete("REPEAT"); expectedGates.delete("QUBIT_COORDS"); expectedGates.delete("SHIFT_COORDS"); expectedGates.delete("TICK"); // Custom crumble gates and markers. expectedGates.add("POLYGON"); expectedGates.add("MARKX"); expectedGates.add("MARKY"); expectedGates.add("MARKZ"); expectedGates.add("MARK"); expectedGates.add("RZ"); expectedGates.add("MZ"); expectedGates.add("MRZ"); expectedGates.add("ERR"); // Special handling. expectedGates.delete("MPP"); expectedGates.delete("SPP"); expectedGates.delete("SPP_DAG"); assertThat(new Set([...GATE_MAP.keys()])).isEqualTo(expectedGates); }); test("gateset.allDrawCallsRun", () => { skipRestOfTestIfHeadless(); let ctx = document.createElement('canvas').getContext('2d'); for (let gate of GATE_MAP.values()) { let op = new Operation( gate, '', new Float32Array([1e-3]), new Uint32Array([5, 6]), ); assertThat(() => { gate.drawer(op, q => [2 * q, q + 1], ctx); }).withInfo(gate.name).runsWithoutThrowingAnException(); } }); ================================================ FILE: glue/crumble/gates/gateset_controlled_paulis.js ================================================ import {Gate} from "./gate.js" import { draw_x_control, draw_y_control, draw_z_control, draw_connector, draw_xswap_control, draw_zswap_control, } from "./gate_draw_util.js" function *iter_gates_controlled_paulis() { yield new Gate( 'CX', 2, true, false, new Map([ ['IX', 'IX'], ['IZ', 'ZZ'], ['XI', 'XX'], ['ZI', 'ZI'], ]), (frame, targets) => frame.do_cx(targets), (frame, targets) => frame.do_cx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_z_control(ctx, x1, y1); draw_x_control(ctx, x2, y2); }, ) yield new Gate( 'CY', 2, true, false, new Map([ ['IX', 'ZX'], ['IZ', 'ZZ'], ['XI', 'XY'], ['ZI', 'ZI'], ]), (frame, targets) => frame.do_cy(targets), (frame, targets) => frame.do_cy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_z_control(ctx, x1, y1); draw_y_control(ctx, x2, y2); }, ) yield new Gate( 'XCX', 2, true, false, new Map([ ['IX', 'IX'], ['IZ', 'XZ'], ['XI', 'XI'], ['ZI', 'ZX'], ]), (frame, targets) => frame.do_xcx(targets), (frame, targets) => frame.do_xcx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_x_control(ctx, x1, y1); draw_x_control(ctx, x2, y2); }, ) yield new Gate( 'XCY', 2, true, false, new Map([ ['IX', 'XX'], ['IZ', 'XZ'], ['XI', 'XI'], ['ZI', 'ZY'], ]), (frame, targets) => frame.do_xcy(targets), (frame, targets) => frame.do_xcy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_x_control(ctx, x1, y1); draw_y_control(ctx, x2, y2); }, ) yield new Gate( 'YCY', 2, true, false, new Map([ ['IX', 'YX'], ['IZ', 'YZ'], ['XI', 'XY'], ['ZI', 'ZY'], ]), (frame, targets) => frame.do_ycy(targets), (frame, targets) => frame.do_ycy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_y_control(ctx, x1, y1); draw_y_control(ctx, x2, y2); }, ) yield new Gate( 'CZ', 2, true, false, new Map([ ['IX', 'ZX'], ['IZ', 'IZ'], ['XI', 'XZ'], ['ZI', 'ZI'], ]), (frame, targets) => frame.do_cz(targets), (frame, targets) => frame.do_cz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_z_control(ctx, x1, y1); draw_z_control(ctx, x2, y2); }, ) } export {iter_gates_controlled_paulis}; ================================================ FILE: glue/crumble/gates/gateset_demolition_measurements.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" function *iter_gates_demolition_measurements() { yield new Gate( 'MR', 1, true, false, new Map([ ['X', 'ERR:I'], ['Y', 'ERR:I'], ['Z', 'I'], ]), (frame, targets) => frame.do_demolition_measure('Z', targets), (frame, targets) => frame.do_demolition_measure('Z', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('MR', x1, y1); }, ); yield new Gate( 'MRY', 1, true, false, new Map([ ['X', 'ERR:I'], ['Y', 'I'], ['Z', 'ERR:I'], ]), (frame, targets) => frame.do_demolition_measure('Y', targets), (frame, targets) => frame.do_demolition_measure('Y', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('MRY', x1, y1); }, ); yield new Gate( 'MRX', 1, true, false, new Map([ ['X', 'I'], ['Y', 'ERR:I'], ['Z', 'ERR:I'], ]), (frame, targets) => frame.do_demolition_measure('X', targets), (frame, targets) => frame.do_demolition_measure('X', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('MRX', x1, y1); }, ); } export {iter_gates_demolition_measurements}; ================================================ FILE: glue/crumble/gates/gateset_hadamard_likes.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" function *iter_gates_hadamard_likes() { yield new Gate( 'H', 1, true, false, new Map([ ['X', 'Z'], ['Z', 'X'], ]), (frame, targets) => frame.do_exchange_xz(targets), (frame, targets) => frame.do_exchange_xz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('H', x1, y1); }, ); yield new Gate( 'H_NXZ', 1, true, false, new Map([ ['X', 'Z'], ['Z', 'X'], ]), (frame, targets) => frame.do_exchange_xz(targets), (frame, targets) => frame.do_exchange_xz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('H', x1, y1 - rad / 3); ctx.fillText("NXZ", x1, y1 + rad / 3); }, ); yield new Gate( 'H_XY', 1, true, false, new Map([ ['X', 'Y'], ['Z', 'Z'], // -Z technically ]), (frame, targets) => frame.do_exchange_xy(targets), (frame, targets) => frame.do_exchange_xy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('H', x1, y1 - rad / 3); ctx.fillText("XY", x1, y1 + rad / 3); }, ); yield new Gate( 'H_NXY', 1, true, false, new Map([ ['X', 'Y'], ['Z', 'Z'], ]), (frame, targets) => frame.do_exchange_xy(targets), (frame, targets) => frame.do_exchange_xy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('H', x1, y1 - rad / 3); ctx.fillText("NXY", x1, y1 + rad / 3); }, ); yield new Gate( 'H_YZ', 1, true, false, new Map([ ['X', 'X'], // -X technically ['Z', 'Y'], ]), (frame, targets) => frame.do_exchange_yz(targets), (frame, targets) => frame.do_exchange_yz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('H', x1, y1 - rad / 3); ctx.fillText("YZ", x1, y1 + rad / 3); }, ); yield new Gate( 'H_NYZ', 1, true, false, new Map([ ['X', 'X'], // -X technically ['Z', 'Y'], ]), (frame, targets) => frame.do_exchange_yz(targets), (frame, targets) => frame.do_exchange_yz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('H', x1, y1 - rad / 3); ctx.fillText("NYZ", x1, y1 + rad / 3); }, ); } export {iter_gates_hadamard_likes}; ================================================ FILE: glue/crumble/gates/gateset_markers.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" import {beginPathPolygon} from '../draw/draw_util.js'; /** * @param {!int} mi * @param {* | undefined} key * @param {!Map | undefined} hitCount * @returns {!{wx: !number, wy: !number, dx: !number, dy: !number}} */ function marker_placement(mi, key, hitCount) { let dx, dy, wx, wy; if (mi < 0 && hitCount !== undefined) { // Detector. let d = hitCount.get(key) if (d === undefined) { d = 0; } hitCount.set(key, d + 1); dx = 9.5 - Math.round(d % 3.9 * 5); dy = 9.5 - Math.round(Math.floor(d / 4) % 3.8 * 5); wx = 3; wy = 3; if (mi < (-1 << 28)) { // Observable. dx += 2; wx += 4; dy += 2; wy += 4; } } else if (mi === 0) { dx = rad; dy = rad + 5; wx = rad * 2; wy = 5; } else if (mi === 1) { dx = -rad; dy = rad; wx = 5; wy = rad * 2; } else if (mi === 2) { dx = rad; dy = -rad; wx = rad * 2; wy = 5; } else if (mi === 3) { dx = rad + 5; dy = rad; wx = 5; wy = rad * 2; } else { dx = Math.cos(mi * 0.6) * rad * 1.7; dy = Math.sin(mi * 0.6) * rad * 1.7; wx = 5; wy = 5; dx += wx / 2; dy += wy / 2; } return {dx, dy, wx, wy}; } /** * @param {!string} color * @returns {!function} */ function make_marker_drawer(color) { return (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); if (x1 === undefined || y1 === undefined) { return; } let {dx, dy, wx, wy} = marker_placement(op.args[0]); ctx.fillStyle = color if (wx === wy) { ctx.fillRect(x1 - dx - 2, y1 - dy - 2, wx + 4, wy + 4); } else { let x2 = x1 + (dx < 0 ? +1 : -1) * rad; let y2 = y1 + (dy < 0 ? +1 : -1) * rad; let x3 = x2 + (wx > rad ? +1 : 0) * rad * 2; let y3 = y2 + (wy > rad ? +1 : 0) * rad * 2; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.lineTo(x1, y1); ctx.fill(); } }; } function *iter_gates_markers() { yield new Gate( 'POLYGON', undefined, false, true, undefined, () => {}, () => {}, (op, coordFunc, ctx) => { let transformedCoords = [] for (let t of op.id_targets) { let [x, y] = coordFunc(t); x -= 0.5; y -= 0.5; transformedCoords.push([x, y]); } beginPathPolygon(ctx, transformedCoords); ctx.globalAlpha *= op.args[3]; ctx.fillStyle = `rgb(${op.args[0]*255},${op.args[1]*255},${op.args[2]*255})` ctx.fill(); }, ); yield new Gate( 'DETECTOR', undefined, false, true, undefined, () => {}, () => {}, (op, coordFunc, ctx) => { }, ); yield new Gate( 'OBSERVABLE_INCLUDE', undefined, false, true, undefined, () => {}, () => {}, (op, coordFunc, ctx) => { }, ); yield new Gate( 'MARKX', 1, true, true, undefined, () => {}, () => {}, make_marker_drawer('red'), ); yield new Gate( 'MARKY', 1, true, true, undefined, () => {}, () => {}, make_marker_drawer('green'), ); yield new Gate( 'MARKZ', 1, true, true, undefined, () => {}, () => {}, make_marker_drawer('blue'), ); yield new Gate( 'MARK', 1, false, true, undefined, () => {}, () => {}, (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); if (x1 === undefined || y1 === undefined) { return; } ctx.fillStyle = 'magenta' ctx.fillRect(x1 - rad, y1 - rad, rad, rad); } ); } export {iter_gates_markers, marker_placement}; ================================================ FILE: glue/crumble/gates/gateset_mpp.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" import {draw_connector} from "./gate_draw_util.js"; /** * @param {!string} bases * @returns {!Gate} */ function make_mpp_gate(bases) { return new Gate( 'MPP:' + bases, bases.length, true, false, undefined, (frame, targets) => frame.do_mpp(bases, targets), (frame, targets) => frame.do_mpp(bases, targets), (op, coordFunc, ctx) => { let prev_x = undefined; let prev_y = undefined; for (let k = 0; k < op.id_targets.length; k++) { let t = op.id_targets[k]; let [x, y] = coordFunc(t); if (prev_x !== undefined) { draw_connector(ctx, x, y, prev_x, prev_y); } prev_x = x; prev_y = y; } for (let k = 0; k < op.id_targets.length; k++) { let t = op.id_targets[k]; let [x, y] = coordFunc(t); ctx.fillStyle = 'gray'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.font = 'bold 12pt monospace' ctx.fillText(bases[k], x, y - 1); ctx.font = '5pt monospace' ctx.fillText('MPP', x, y + 8); } }, ); } /** * @param {!string} bases * @param {!boolean} dag * @returns {!Gate} */ function make_spp_gate(bases, dag) { return new Gate( (dag ? 'SPP_DAG:' : 'SPP:') + bases, bases.length, true, false, undefined, (frame, targets) => frame.do_spp(bases, targets), (frame, targets) => frame.do_spp(bases, targets), (op, coordFunc, ctx) => { let prev_x = undefined; let prev_y = undefined; for (let k = 0; k < op.id_targets.length; k++) { let t = op.id_targets[k]; let [x, y] = coordFunc(t); if (prev_x !== undefined) { draw_connector(ctx, x, y, prev_x, prev_y); } prev_x = x; prev_y = y; } for (let k = 0; k < op.id_targets.length; k++) { let t = op.id_targets[k]; let [x, y] = coordFunc(t); ctx.fillStyle = 'gray'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.font = 'bold 12pt monospace' ctx.fillText(bases[k], x, y - 1); ctx.font = '5pt monospace' ctx.fillText(dag ? 'SPP†' : 'SPP', x, y + 8); } }, ); } export {make_mpp_gate, make_spp_gate}; ================================================ FILE: glue/crumble/gates/gateset_pair_measurements.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" import {draw_connector} from "./gate_draw_util.js"; function *iter_gates_pair_measurements() { yield new Gate( 'MXX', 2, true, false, new Map([ ['II', 'II'], ['IX', 'IX'], ['IY', 'ERR:IY'], ['IZ', 'ERR:IZ'], ['XI', 'XI'], ['XX', 'XX'], ['XY', 'ERR:XY'], ['XZ', 'ERR:XZ'], ['YI', 'ERR:YI'], ['YX', 'ERR:YX'], ['YY', 'YY'], ['YZ', 'YZ'], ['ZI', 'ERR:ZI'], ['ZX', 'ERR:ZX'], ['ZY', 'ZY'], ['ZZ', 'ZZ'], ]), (frame, targets) => frame.do_measure('XX', targets), (frame, targets) => frame.do_measure('XX', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillRect(x2 - rad, y2 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeRect(x2 - rad, y2 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('MXX', x1, y1); ctx.fillText('MXX', x2, y2); }, ); yield new Gate( 'MYY', 2, true, false, new Map([ ['II', 'II'], ['IX', 'ERR:IX'], ['IY', 'IY'], ['IZ', 'ERR:IZ'], ['XI', 'ERR:XI'], ['XX', 'XX'], ['XY', 'ERR:XY'], ['XZ', 'XZ'], ['YI', 'YI'], ['YX', 'ERR:YX'], ['YY', 'YY'], ['YZ', 'ERR:YZ'], ['ZI', 'ERR:ZI'], ['ZX', 'ZX'], ['ZY', 'ERR:ZY'], ['ZZ', 'ZZ'], ]), (frame, targets) => frame.do_measure('YY', targets), (frame, targets) => frame.do_measure('YY', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillRect(x2 - rad, y2 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeRect(x2 - rad, y2 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('MYY', x1, y1); ctx.fillText('MYY', x2, y2); }, ); yield new Gate( 'MZZ', 2, true, false, new Map([ ['II', 'II'], ['IX', 'ERR:IX'], ['IY', 'ERR:IY'], ['IZ', 'IZ'], ['XI', 'ERR:XI'], ['XX', 'XX'], ['XY', 'XY'], ['XZ', 'ERR:XZ'], ['YI', 'ERR:YI'], ['YX', 'YX'], ['YY', 'YY'], ['YZ', 'ERR:YZ'], ['ZI', 'ZI'], ['ZX', 'ERR:ZX'], ['ZY', 'ERR:ZY'], ['ZZ', 'ZZ'], ]), (frame, targets) => frame.do_measure('ZZ', targets), (frame, targets) => frame.do_measure('ZZ', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillRect(x2 - rad, y2 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeRect(x2 - rad, y2 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('MZZ', x1, y1); ctx.fillText('MZZ', x2, y2); }, ); } export {iter_gates_pair_measurements}; ================================================ FILE: glue/crumble/gates/gateset_paulis.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" function *iter_gates_paulis() { yield new Gate( 'ERR', 1, true, false, new Map([ ['X', 'X'], ['Z', 'Z'], ]), () => {}, () => {}, (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'red'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('ERR', x1, y1); }, ); yield new Gate( 'I', 1, true, false, new Map([ ['X', 'X'], ['Z', 'Z'], ]), () => {}, () => {}, (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'white'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('I', x1, y1); }, ); yield new Gate( 'X', 1, true, false, new Map([ ['X', 'X'], ['Z', 'Z'], ]), () => {}, () => {}, (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'white'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('X', x1, y1); }, ); yield new Gate( 'Y', 1, true, false, new Map([ ['X', 'X'], ['Z', 'Z'], ]), () => {}, () => {}, (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'white'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('Y', x1, y1); }, ); yield new Gate( 'Z', 1, true, false, new Map([ ['X', 'X'], ['Z', 'Z'], ]), () => {}, () => {}, (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'white'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('Z', x1, y1); }, ); } export {iter_gates_paulis}; ================================================ FILE: glue/crumble/gates/gateset_quarter_turns.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" function *iter_gates_quarter_turns() { yield new Gate( 'S', 1, true, false, new Map([ ['X', 'Y'], ['Z', 'Z'], ]), (frame, targets) => frame.do_exchange_xy(targets), (frame, targets) => frame.do_exchange_xy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('S', x1, y1); }, ) yield new Gate( 'S_DAG', 1, true, false, new Map([ ['X', 'Y'], ['Z', 'Z'], ]), (frame, targets) => frame.do_exchange_xy(targets), (frame, targets) => frame.do_exchange_xy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('S†', x1, y1); }, ) yield new Gate( 'SQRT_X', 1, true, false, new Map([ ['X', 'X'], ['Z', 'Y'], ]), (frame, targets) => frame.do_exchange_yz(targets), (frame, targets) => frame.do_exchange_yz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√X', x1, y1); }, ) yield new Gate( 'SQRT_X_DAG', 1, true, false, new Map([ ['X', 'X'], ['Z', 'Y'], ]), (frame, targets) => frame.do_exchange_yz(targets), (frame, targets) => frame.do_exchange_yz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√X†', x1, y1); }, ) yield new Gate( 'SQRT_Y', 1, true, false, new Map([ ['X', 'Z'], ['Z', 'X'], ]), (frame, targets) => frame.do_exchange_xz(targets), (frame, targets) => frame.do_exchange_xz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√Y', x1, y1); }, ) yield new Gate( 'SQRT_Y_DAG', 1, true, false, new Map([ ['X', 'Z'], ['Z', 'X'], ]), (frame, targets) => frame.do_exchange_xz(targets), (frame, targets) => frame.do_exchange_xz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'yellow'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√Y†', x1, y1); }, ) } export {iter_gates_quarter_turns}; ================================================ FILE: glue/crumble/gates/gateset_resets.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" function *iter_gates_resets() { yield new Gate( 'R', 1, true, false, new Map([ ['X', 'ERR:I'], ['Y', 'ERR:I'], ['Z', 'ERR:I'], ]), (frame, targets) => frame.do_discard(targets), (frame, targets) => frame.do_demolition_measure('Z', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('R', x1, y1); }, ); yield new Gate( 'RX', 1, true, false, new Map([ ['X', 'ERR:I'], ['Y', 'ERR:I'], ['Z', 'ERR:I'], ]), (frame, targets) => frame.do_discard(targets), (frame, targets) => frame.do_demolition_measure('X', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('RX', x1, y1); }, ); yield new Gate( 'RY', 1, true, false, new Map([ ['X', 'ERR:I'], ['Y', 'ERR:I'], ['Z', 'ERR:I'], ]), (frame, targets) => frame.do_discard(targets), (frame, targets) => frame.do_demolition_measure('Y', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('RY', x1, y1); }, ); } export {iter_gates_resets}; ================================================ FILE: glue/crumble/gates/gateset_solo_measurements.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" function *iter_gates_solo_measurements() { yield new Gate( 'M', 1, true, false, new Map([ ['X', 'ERR:X'], ['Y', 'ERR:Y'], ['Z', 'Z'], ]), (frame, targets) => frame.do_measure('Z', targets), (frame, targets) => frame.do_measure('Z', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('M', x1, y1); ctx.textAlign = "left"; }, ); yield new Gate( 'MX', 1, true, false, new Map([ ['X', 'X'], ['Y', 'ERR:Y'], ['Z', 'ERR:Z'], ]), (frame, targets) => frame.do_measure('X', targets), (frame, targets) => frame.do_measure('X', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('MX', x1, y1); ctx.textAlign = "left"; }, ); yield new Gate( 'MY', 1, true, false, new Map([ ['X', 'ERR:X'], ['Y', 'Y'], ['Z', 'ERR:Z'], ]), (frame, targets) => frame.do_measure('Y', targets), (frame, targets) => frame.do_measure('Y', targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'gray'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('MY', x1, y1); ctx.textAlign = "left"; }, ); } export {iter_gates_solo_measurements}; ================================================ FILE: glue/crumble/gates/gateset_sqrt_pauli_pairs.js ================================================ import {Gate} from "./gate.js" import {draw_connector} from "./gate_draw_util.js"; import {rad} from "../draw/config.js"; function *iter_gates_sqrt_pauli_pairs() { yield new Gate( 'II', 2, true, false, new Map([ ['IX', 'IX'], ['IZ', 'IZ'], ['XI', 'XI'], ['ZI', 'ZI'], ]), (frame, targets) => undefined, (frame, targets) => undefined, (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); for (let [x, y] of [[x1, y1], [x2, y2]]) { ctx.fillStyle = 'white'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('II', x, y); } }, ) yield new Gate( 'SQRT_XX', 2, true, false, new Map([ ['IX', 'IX'], ['IZ', 'XY'], ['XI', 'XI'], ['ZI', 'YX'], ]), (frame, targets) => frame.do_sqrt_xx(targets), (frame, targets) => frame.do_sqrt_xx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); for (let [x, y] of [[x1, y1], [x2, y2]]) { ctx.fillStyle = 'yellow'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√XX', x, y); } }, ) yield new Gate( 'SQRT_XX_DAG', 2, true, false, new Map([ ['IX', 'IX'], ['IZ', 'XY'], ['XI', 'XI'], ['ZI', 'YX'], ]), (frame, targets) => frame.do_sqrt_xx(targets), (frame, targets) => frame.do_sqrt_xx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); for (let [x, y] of [[x1, y1], [x2, y2]]) { ctx.fillStyle = 'yellow'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√XX†', x, y); } }, ) yield new Gate( 'SQRT_YY', 2, true, false, new Map([ ['IX', 'YZ'], ['IZ', 'YX'], ['XI', 'ZY'], ['ZI', 'XY'], ]), (frame, targets) => frame.do_sqrt_yy(targets), (frame, targets) => frame.do_sqrt_yy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); for (let [x, y] of [[x1, y1], [x2, y2]]) { ctx.fillStyle = 'yellow'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√YY', x, y); } }, ) yield new Gate( 'SQRT_YY_DAG', 2, true, false, new Map([ ['IX', 'YZ'], ['IZ', 'YX'], ['XI', 'ZY'], ['ZI', 'XY'], ]), (frame, targets) => frame.do_sqrt_yy(targets), (frame, targets) => frame.do_sqrt_yy(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); for (let [x, y] of [[x1, y1], [x2, y2]]) { ctx.fillStyle = 'yellow'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√YY†', x, y); } }, ) yield new Gate( 'SQRT_ZZ', 2, true, false, new Map([ ['IX', 'ZY'], ['IZ', 'IZ'], ['XI', 'YZ'], ['ZI', 'ZI'], ]), (frame, targets) => frame.do_sqrt_zz(targets), (frame, targets) => frame.do_sqrt_zz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); for (let [x, y] of [[x1, y1], [x2, y2]]) { ctx.fillStyle = 'yellow'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√ZZ', x, y); } }, ) yield new Gate( 'SQRT_ZZ_DAG', 2, true, false, new Map([ ['IX', 'ZY'], ['IZ', 'IZ'], ['XI', 'YZ'], ['ZI', 'ZI'], ]), (frame, targets) => frame.do_sqrt_zz(targets), (frame, targets) => frame.do_sqrt_zz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); for (let [x, y] of [[x1, y1], [x2, y2]]) { ctx.fillStyle = 'yellow'; ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2); ctx.strokeStyle = 'black'; ctx.strokeRect(x - rad, y - rad, rad * 2, rad * 2); ctx.fillStyle = 'black'; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('√ZZ†', x, y); } }, ) } export {iter_gates_sqrt_pauli_pairs}; ================================================ FILE: glue/crumble/gates/gateset_swaps.js ================================================ import {Gate} from "./gate.js" import { draw_connector, draw_swap_control, draw_iswap_control, draw_zswap_control, draw_xswap_control } from './gate_draw_util.js'; function *iter_gates_swaps() { yield new Gate( 'ISWAP', 2, true, false, new Map([ ['IX', 'YZ'], ['IZ', 'ZI'], ['XI', 'ZY'], ['ZI', 'IZ'], ]), (frame, targets) => frame.do_iswap(targets), (frame, targets) => frame.do_iswap(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_iswap_control(ctx, x1, y1); draw_iswap_control(ctx, x2, y2); }, ) yield new Gate( 'ISWAP_DAG', 2, true, false, new Map([ ['IX', 'YZ'], ['IZ', 'ZI'], ['XI', 'ZY'], ['ZI', 'IZ'], ]), (frame, targets) => frame.do_iswap(targets), (frame, targets) => frame.do_iswap(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_iswap_control(ctx, x1, y1); draw_iswap_control(ctx, x2, y2); }, ) yield new Gate( 'SWAP', 2, true, false, new Map([ ['IX', 'XI'], ['IZ', 'ZI'], ['XI', 'IX'], ['ZI', 'IZ'], ]), (frame, targets) => frame.do_swap(targets), (frame, targets) => frame.do_swap(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_swap_control(ctx, x1, y1); draw_swap_control(ctx, x2, y2); }, ); yield new Gate( 'CXSWAP', 2, true, false, new Map([ ['IX', 'XI'], ['IZ', 'ZZ'], ['XI', 'XX'], ['ZI', 'IZ'], ]), (frame, targets) => frame.do_cx_swap(targets), (frame, targets) => frame.do_swap_cx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_zswap_control(ctx, x1, y1); draw_xswap_control(ctx, x2, y2); }, ) yield new Gate( 'CZSWAP', 2, true, false, new Map([ ['IX', 'XZ'], ['IZ', 'ZI'], ['XI', 'ZX'], ['ZI', 'IZ'], ]), (frame, targets) => frame.do_cz_swap(targets), (frame, targets) => frame.do_cz_swap(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); let [x2, y2] = coordFunc(op.id_targets[1]); draw_connector(ctx, x1, y1, x2, y2); draw_zswap_control(ctx, x1, y1); draw_zswap_control(ctx, x2, y2); }, ) } export {iter_gates_swaps}; ================================================ FILE: glue/crumble/gates/gateset_third_turns.js ================================================ import {rad} from "../draw/config.js" import {Gate} from "./gate.js" function *iter_gates_third_turns() { yield new Gate( 'C_XYZ', 1, true, false, new Map([ ['X', 'Y'], ['Z', 'X'], ]), (frame, targets) => frame.do_cycle_xyz(targets), (frame, targets) => frame.do_cycle_zyx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'teal'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('C', x1, y1 - rad / 3); ctx.fillText("XYZ", x1, y1 + rad / 3); }, ) yield new Gate( 'C_NXYZ', 1, true, false, new Map([ ['X', 'Y'], ['Z', 'X'], ]), (frame, targets) => frame.do_cycle_xyz(targets), (frame, targets) => frame.do_cycle_zyx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'teal'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('C', x1, y1 - rad / 3); ctx.fillText("NXYZ", x1, y1 + rad / 3); }, ) yield new Gate( 'C_XNYZ', 1, true, false, new Map([ ['X', 'Y'], ['Z', 'X'], ]), (frame, targets) => frame.do_cycle_xyz(targets), (frame, targets) => frame.do_cycle_zyx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'teal'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('C', x1, y1 - rad / 3); ctx.fillText("XNYZ", x1, y1 + rad / 3); }, ) yield new Gate( 'C_XYNZ', 1, true, false, new Map([ ['X', 'Y'], ['Z', 'X'], ]), (frame, targets) => frame.do_cycle_xyz(targets), (frame, targets) => frame.do_cycle_zyx(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'teal'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('C', x1, y1 - rad / 3); ctx.fillText("XYNZ", x1, y1 + rad / 3); }, ) yield new Gate( 'C_ZYX', 1, true, false, new Map([ ['X', 'Z'], ['Z', 'Y'], ]), (frame, targets) => frame.do_cycle_zyx(targets), (frame, targets) => frame.do_cycle_xyz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'teal'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('C', x1, y1 - rad / 3); ctx.fillText("ZYX", x1, y1 + rad / 3); }, ) yield new Gate( 'C_ZYNX', 1, true, false, new Map([ ['X', 'Z'], ['Z', 'Y'], ]), (frame, targets) => frame.do_cycle_zyx(targets), (frame, targets) => frame.do_cycle_xyz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'teal'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('C', x1, y1 - rad / 3); ctx.fillText("ZYNX", x1, y1 + rad / 3); }, ) yield new Gate( 'C_ZNYX', 1, true, false, new Map([ ['X', 'Z'], ['Z', 'Y'], ]), (frame, targets) => frame.do_cycle_zyx(targets), (frame, targets) => frame.do_cycle_xyz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'teal'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('C', x1, y1 - rad / 3); ctx.fillText("ZNYX", x1, y1 + rad / 3); }, ) yield new Gate( 'C_NZYX', 1, true, false, new Map([ ['X', 'Z'], ['Z', 'Y'], ]), (frame, targets) => frame.do_cycle_zyx(targets), (frame, targets) => frame.do_cycle_xyz(targets), (op, coordFunc, ctx) => { let [x1, y1] = coordFunc(op.id_targets[0]); ctx.fillStyle = 'teal'; ctx.fillRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.strokeRect(x1 - rad, y1 - rad, rad*2, rad*2); ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText('C', x1, y1 - rad / 3); ctx.fillText("NZYX", x1, y1 + rad / 3); }, ) } export {iter_gates_third_turns}; ================================================ FILE: glue/crumble/keyboard/chord.js ================================================ import {describe} from "../base/describe.js"; import {equate} from "../base/equate.js"; class ChordEvent { /** * @param {!boolean} inProgress * @param {!Set} chord * @param {!boolean} altKey * @param {!boolean} ctrlKey * @param {!boolean} metaKey * @param {!boolean} shiftKey */ constructor(inProgress, chord, altKey, ctrlKey, metaKey, shiftKey) { this.inProgress = inProgress; this.chord = chord; this.altKey = altKey; this.shiftKey = shiftKey; this.ctrlKey = ctrlKey; this.metaKey = metaKey; } /** * @param {*} other * @return {!boolean} */ isEqualTo(other) { return other instanceof ChordEvent && this.inProgress === other.inProgress && equate(this.chord, other.chord) && this.altKey === other.altKey && this.shiftKey === other.shiftKey && this.ctrlKey === other.ctrlKey && this.metaKey === other.metaKey; } /** * @return {!string} */ toString() { return `ChordEvent( inProgress=${this.inProgress}, chord=${describe(this.chord)}, altKey=${this.altKey}, shiftKey=${this.shiftKey}, ctrlKey=${this.ctrlKey}, metaKey=${this.metaKey}, )`; } } const MODIFIER_KEYS = new Set(["alt", "shift", "control", "meta"]); const ACTION_KEYS = new Set(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\\', '`']); class Chorder { constructor() { this.curModifiers = /** @type {!Set} */ new Set(); this.curPressed = /** @type {!Set} */ new Set(); this.curChord = /** @type {!Set} */ new Set(); this.queuedEvents = /** @type {!Array} */ []; } /** * @param {!boolean} inProgress */ toEvent(inProgress) { return new ChordEvent( inProgress, new Set(this.curChord), this.curModifiers.has("alt"), this.curModifiers.has("control"), this.curModifiers.has("meta"), this.curModifiers.has("shift") ); } /** * @param {!boolean} inProgress * @private */ _queueEvent(inProgress) { this.queuedEvents.push(this.toEvent(inProgress)); } handleFocusChanged() { this.curPressed.clear(); this.curChord.clear(); this.curModifiers.clear(); } /** * @param {!KeyboardEvent} ev */ handleKeyEvent(ev) { let key = ev.key.toLowerCase(); if (key === 'escape') { this.handleFocusChanged(); } if (ev.type === 'keydown') { let flag_key_pairs = [ [ev.altKey, "alt"], [ev.shiftKey, "shift"], [ev.ctrlKey, "control"], [ev.metaKey, "meta"], ]; for (let [b, k] of flag_key_pairs) { if (b) { this.curModifiers.add(k); } else { this.curModifiers.delete(k); } } if (!MODIFIER_KEYS.has(key)) { this.curPressed.add(key); this.curChord.add(key); } this._queueEvent(true); } else if (ev.type === 'keyup') { if (!MODIFIER_KEYS.has(key)) { this.curPressed.delete(key); this._queueEvent(this.curPressed.size > 0 && !ACTION_KEYS.has(key)); if (ACTION_KEYS.has(key)) { this.curChord.delete(key); } if (this.curPressed.size === 0) { this.curModifiers.clear(); this.curChord.clear(); } } } else { throw new Error("Not a recognized key event type: " + ev.type); } } } export {Chorder, ChordEvent}; ================================================ FILE: glue/crumble/keyboard/chord.test.js ================================================ import {test, assertThat, skipRestOfTestIfHeadless} from "../test/test_util.js" import {Chorder, ChordEvent} from "./chord.js" test("chorder.usage", () => { let c = new Chorder(); assertThat(c.curChord).isEqualTo(new Set()); assertThat(c.curPressed).isEqualTo(new Set()); assertThat(c.queuedEvents).isEqualTo([]); skipRestOfTestIfHeadless(); c.handleKeyEvent(new KeyboardEvent('keydown', {key: 'x', ctrlKey: true})); assertThat(c.queuedEvents.shift()).isEqualTo(new ChordEvent(true, new Set(["x"]), false, true, false, false)); assertThat(c.curChord).isEqualTo(new Set(["x"])); assertThat(c.curPressed).isEqualTo(new Set(["x"])); assertThat(c.curModifiers).isEqualTo(new Set(["control"])); assertThat(c.queuedEvents).isEqualTo([]); c.handleKeyEvent(new KeyboardEvent('keydown', {key: 'r', altKey: true})); assertThat(c.queuedEvents.shift()).isEqualTo(new ChordEvent(true, new Set(["r", "x"]), true, false, false, false)); assertThat(c.curChord).isEqualTo(new Set(["r", "x"])); assertThat(c.curPressed).isEqualTo(new Set(["r", "x"])); assertThat(c.curModifiers).isEqualTo(new Set(["alt"])); assertThat(c.queuedEvents).isEqualTo([]); c.handleKeyEvent(new KeyboardEvent('keyup', {key: 'x'})); assertThat(c.queuedEvents.shift()).isEqualTo(new ChordEvent(true, new Set(["r", "x"]), true, false, false, false)); assertThat(c.curChord).isEqualTo(new Set(["r", "x"])); assertThat(c.curPressed).isEqualTo(new Set(["r"])); assertThat(c.curModifiers).isEqualTo(new Set(["alt"])); assertThat(c.queuedEvents).isEqualTo([]); c.handleKeyEvent(new KeyboardEvent('keyup', {key: 'r'})); assertThat(c.queuedEvents.shift()).isEqualTo(new ChordEvent(false, new Set(["r", "x"]), true, false, false, false)); assertThat(c.curChord).isEqualTo(new Set([])); assertThat(c.curPressed).isEqualTo(new Set([])); assertThat(c.curModifiers).isEqualTo(new Set([])); assertThat(c.queuedEvents).isEqualTo([]); }); ================================================ FILE: glue/crumble/keyboard/toolbox.js ================================================ import {GATE_MAP} from "../gates/gateset.js" let toolboxCanvas = /** @type {!HTMLCanvasElement} */ document.getElementById('toolbox'); let DIAM = 28; let PITCH = DIAM + 4; let PAD = 10.5; let COLUMNS = ['H', 'S', 'R', 'M', 'MR', 'C', 'W', 'SC', 'MC', 'P', '1-9']; let DEF_ROW = [1, 2, 2, 2, 2, 1, 2, 2, 2, -1, -1, -1]; /** * @param {!ChordEvent} ev * @returns {undefined|!{row: !int, strength: !number}} */ function getFocusedRow(ev) { if (ev.ctrlKey) { return undefined; } let hasX = +ev.chord.has('x'); let hasY = +ev.chord.has('y'); let hasZ = +ev.chord.has('z'); if ((hasX && !hasY && !hasZ) || (!hasX && hasY && hasZ)) { return {row: 0, strength: Math.max(hasX, Math.min(hasY, hasZ))}; } if ((!hasX && hasY && !hasZ) || (hasX && !hasY && hasZ)) { return {row: 1, strength: Math.max(hasY, Math.min(hasX, hasZ))}; } if ((!hasX && !hasY && hasZ) || (hasX && hasY && !hasZ)) { return {row: 2, strength: Math.max(hasZ, Math.min(hasX, hasY))}; } return undefined; } /** * @param {!ChordEvent} ev * @returns {undefined|!{col: !int, strength: !number}} */ function getFocusedCol(ev) { if (ev.ctrlKey) { return undefined; } let best = undefined; for (let k = 0; k < COLUMNS.length; k++) { let s = 0; for (let c of COLUMNS[k].toLowerCase()) { s += ev.chord.has(c.toLowerCase()); } if (s === COLUMNS[k].length) { if (best === undefined || s >= best.strength) { best = {col: k, strength: s / COLUMNS[k].length}; } } } return best; } function make_pos_to_gate_dict() { let result = new Map([ ['0,0', GATE_MAP.get("H_YZ")], ['0,1', GATE_MAP.get("H")], ['0,2', GATE_MAP.get("H_XY")], ['1,0', GATE_MAP.get("SQRT_X")], ['1,1', GATE_MAP.get("SQRT_Y")], ['1,2', GATE_MAP.get("S")], ['2,0', GATE_MAP.get("RX")], ['2,1', GATE_MAP.get("RY")], ['2,2', GATE_MAP.get("R")], ['3,0', GATE_MAP.get("MX")], ['3,1', GATE_MAP.get("MY")], ['3,2', GATE_MAP.get("M")], ['4,0', GATE_MAP.get("MRX")], ['4,1', GATE_MAP.get("MRY")], ['4,2', GATE_MAP.get("MRZ")], ['5,0', GATE_MAP.get("CX")], ['5,1', GATE_MAP.get("CY")], ['5,2', GATE_MAP.get("CZ")], ['6,0', GATE_MAP.get("CXSWAP")], ['6,1', GATE_MAP.get("SWAP")], ['6,2', GATE_MAP.get("CZSWAP")], ['7,0', GATE_MAP.get("SQRT_XX")], ['7,1', GATE_MAP.get("SQRT_YY")], ['7,2', GATE_MAP.get("SQRT_ZZ")], ['8,0', GATE_MAP.get("MXX")], ['8,1', GATE_MAP.get("MYY")], ['8,2', GATE_MAP.get("MZZ")], ]); let x = 9; for (let k = 0; k < 4; k++) { result.set(`${x},0`, GATE_MAP.get("MARKX").withDefaultArgument(k)); result.set(`${x},1`, GATE_MAP.get("MARKY").withDefaultArgument(k)); result.set(`${x},2`, GATE_MAP.get("MARKZ").withDefaultArgument(k)); result.set(`${x},-1`, GATE_MAP.get("MARK").withDefaultArgument(k)); x += 1; } return result; } let POS_TO_GATE_DICT = make_pos_to_gate_dict(); /** * @param {!ChordEvent} ev * @returns {{focusedRow: (!{row: !int, strength: !number}|undefined), partialFocusedRow: (!{row: !int, strength: !number}|undefined), focusedCol: (!{col: !int, strength: !number}|undefined), chosenGate: undefined|!Gate}} */ function getToolboxFocusedData(ev) { let partialFocusedRow = getFocusedRow(ev); let focusedCol = getFocusedCol(ev); let focusedRow = partialFocusedRow; if (focusedCol !== undefined && partialFocusedRow === undefined) { let row = DEF_ROW[focusedCol.col]; if (row === undefined) { focusedRow = undefined; } else { focusedRow = {strength: 0, row: row}; } } let chosenGate = undefined; if (focusedRow !== undefined && focusedCol !== undefined) { let key = `${focusedCol.col},${focusedRow.row}`; if (POS_TO_GATE_DICT.has(key)) { chosenGate = POS_TO_GATE_DICT.get(key); } } return {partialFocusedRow, focusedRow, focusedCol, chosenGate}; } /** * @param {!ChordEvent} ev */ function drawToolbox(ev) { toolboxCanvas.width = toolboxCanvas.scrollWidth; toolboxCanvas.height = toolboxCanvas.scrollHeight; let ctx = toolboxCanvas.getContext('2d'); ctx.clearRect(0, 0, toolboxCanvas.width, toolboxCanvas.height); ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.fillText('X', PAD - 3, PAD + DIAM / 2); ctx.fillText('Y', PAD - 3, PAD + DIAM / 2 + PITCH); ctx.fillText('Z', PAD - 3, PAD + DIAM / 2 + PITCH * 2); ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; for (let k = 0; k < COLUMNS.length; k++) { ctx.fillText(COLUMNS[k], PAD + DIAM / 2 + PITCH * k, PAD); } ctx.fillStyle = 'white'; ctx.strokeStyle = 'black'; let xGates = ['H_YZ', 'S_X', 'R_X', 'M_X', 'MR_X', 'C_X', 'CXSWAP', '√XX', 'M_XX', 'PX', 'X1']; let yGates = ['H', 'S_Y', 'R_Y', 'M_Y', 'MR_Y', 'C_Y', 'SWAP', '√YY', 'M_YY', 'PY', 'Y1']; let zGates = ['H_XY', 'S', 'R', 'M', 'MR', 'C_Z', 'CZSWAP', '√ZZ', 'M_ZZ', 'PZ', 'Z1']; let gates = [xGates, yGates, zGates]; for (let k = 0; k < COLUMNS.length; k++) { for (let p = 0; p < 3; p++) { ctx.fillRect(PAD + PITCH * k, PAD + PITCH * p, DIAM, DIAM); } } for (let k = 0; k < COLUMNS.length; k++) { for (let p = 0; p < 3; p++) { let text = gates[p][k]; let cx = PAD + PITCH * k + DIAM / 2; let cy = PAD + PITCH * p + DIAM / 2; if (text.startsWith('P')) { ctx.beginPath(); let numPoints = 3; if (text === 'PX') { numPoints = 4; ctx.fillStyle = 'red'; } else if (text === 'PY') { numPoints = 5; ctx.fillStyle = 'green'; cy += 1; } else if (text === 'PZ') { numPoints = 3; ctx.fillStyle = 'blue'; cy += 2; } let pts = []; for (let k = 0; k < numPoints; k++) { let t = 2 * Math.PI / numPoints * (k + 0.5); pts.push([ Math.round(cx + 0.3 * DIAM * Math.sin(t)), Math.round(cy + 0.3 * DIAM * Math.cos(t)), ]); } ctx.moveTo(pts[pts.length - 1][0], pts[pts.length - 1][1]); for (let pt of pts) { ctx.lineTo(pt[0], pt[1]); } ctx.closePath(); ctx.globalAlpha *= 0.25; ctx.fill(); ctx.globalAlpha *= 4; continue; } if (text.endsWith('1')) { ctx.beginPath(); ctx.moveTo(cx + PITCH * 0.15, cy - PITCH * 0.25); ctx.lineTo(cx, cy + PITCH * 0.1); ctx.lineTo(cx - PITCH * 0.15, cy - PITCH * 0.25); ctx.closePath(); let color = text === 'X1' ? 'red' : text === 'Y1' ? 'green' : 'blue'; ctx.fillStyle = color; ctx.strokeStyle = color; ctx.fill(); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + DIAM * 0.5, cy); ctx.stroke(); ctx.lineWidth = 1; continue; } ctx.fillStyle = 'black'; if (text.indexOf('_') !== -1) { let [main, sub] = text.split('_'); let k = 16; let w1 = 0; let w2 = 0; while (k > 4) { ctx.font = `${k}pt monospace`; w1 = ctx.measureText(main).width; ctx.font = `${k * 0.6}pt monospace`; w2 = ctx.measureText(sub).width; if (w1 + w2 <= 26) { break; } k -= 1; } cx -= (w1 + w2) / 2; ctx.font = `${k}pt monospace`; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(main, cx, cy); ctx.font = `${k * 0.6}pt monospace`; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText(sub, cx + w1, cy); } else { let k = 16; while (k > 4) { ctx.font = `${k}pt monospace`; if (ctx.measureText(text).width <= 28) { break; } k -= 1; } ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, cx, cy); } } } ctx.strokeStyle = 'black'; for (let k = 0; k < COLUMNS.length; k++) { for (let p = 0; p < 3; p++) { ctx.strokeRect(PAD + PITCH * k, PAD + PITCH * p, DIAM, DIAM); } } let focus = getToolboxFocusedData(ev); if (focus.partialFocusedRow !== undefined) { ctx.fillStyle = 'rgba(255, 255, 0, ' + (0.5 * focus.partialFocusedRow.strength) + ')'; ctx.fillRect(0, PAD + PITCH * focus.partialFocusedRow.row - (PITCH - DIAM) / 2, PAD + PITCH * COLUMNS.length, PITCH); } if (focus.focusedCol !== undefined) { ctx.fillStyle = 'rgba(255, 255, 0, ' + (0.5 * focus.focusedCol.strength) + ')'; ctx.fillRect( PAD + PITCH * focus.focusedCol.col - (PITCH - DIAM) / 2, 0, PITCH, PAD + PITCH * 3); } if (focus.focusedRow !== undefined && focus.focusedCol !== undefined) { ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; ctx.fillRect( PAD + PITCH * focus.focusedCol.col - (PITCH - DIAM) / 2, PAD + PITCH * focus.focusedRow.row - (PITCH - DIAM) / 2, PITCH, PITCH); } ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'black'; } export {getToolboxFocusedData, drawToolbox}; ================================================ FILE: glue/crumble/main.js ================================================ import {Circuit} from "./circuit/circuit.js" import {minXY} from "./circuit/layer.js" import {pitch} from "./draw/config.js" import {GATE_MAP} from "./gates/gateset.js" import {EditorState} from "./editor/editor_state.js"; import {initUrlCircuitSync} from "./editor/sync_url_to_state.js"; import {draw} from "./draw/main_draw.js"; import {drawToolbox} from "./keyboard/toolbox.js"; import {Operation} from "./circuit/operation.js"; import {make_mpp_gate} from './gates/gateset_mpp.js'; import {PropagatedPauliFrames} from './circuit/propagated_pauli_frames.js'; const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5; const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5; const btnInsertLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnInsertLayer'); const btnDeleteLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnDeleteLayer'); const btnUndo = /** @type{!HTMLButtonElement} */ document.getElementById('btnUndo'); const btnRedo = /** @type{!HTMLButtonElement} */ document.getElementById('btnRedo'); const btnClearMarkers = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearMarkers'); const btnImportExport = /** @type{!HTMLButtonElement} */ document.getElementById('btnShowHideImportExport'); const btnNextLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnNextLayer'); const btnPrevLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnPrevLayer'); const btnRotate45 = /** @type{!HTMLButtonElement} */ document.getElementById('btnRotate45'); const btnRotate45Counter = /** @type{!HTMLButtonElement} */ document.getElementById('btnRotate45Counter'); const btnExport = /** @type {!HTMLButtonElement} */ document.getElementById('btnExport'); const btnImport = /** @type {!HTMLButtonElement} */ document.getElementById('btnImport'); const btnClear = /** @type {!HTMLButtonElement} */ document.getElementById('clear'); const txtStimCircuit = /** @type {!HTMLTextAreaElement} */ document.getElementById('txtStimCircuit'); const btnTimelineFocus = /** @type{!HTMLButtonElement} */ document.getElementById('btnTimelineFocus'); const btnClearTimelineFocus = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearTimelineFocus'); const btnClearSelectedMarkers = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearSelectedMarkers'); const btnShowExamples = /** @type {!HTMLButtonElement} */ document.getElementById('btnShowExamples'); const divExamples = /** @type{!HTMLDivElement} */ document.getElementById('examples-div'); // Prevent typing in the import/export text editor from causing changes in the main circuit editor. txtStimCircuit.addEventListener('keyup', ev => ev.stopPropagation()); txtStimCircuit.addEventListener('keydown', ev => ev.stopPropagation()); let editorState = /** @type {!EditorState} */ new EditorState(document.getElementById('cvn')); btnExport.addEventListener('click', _ev => { exportCurrentState(); }); btnImport.addEventListener('click', _ev => { let text = txtStimCircuit.value; let circuit = Circuit.fromStimCircuit(text); editorState.commit(circuit); }); btnImportExport.addEventListener('click', _ev => { let div = /** @type{!HTMLDivElement} */ document.getElementById('divImportExport'); if (div.style.display === 'none') { div.style.display = 'block'; btnImportExport.textContent = "Hide Import/Export"; exportCurrentState(); } else { div.style.display = 'none'; btnImportExport.textContent = "Show Import/Export"; txtStimCircuit.value = ''; } setTimeout(() => { window.scrollTo(0, 0); }, 0); }); btnClear.addEventListener('click', _ev => { editorState.clearCircuit(); }); btnUndo.addEventListener('click', _ev => { editorState.undo(); }); btnTimelineFocus.addEventListener('click', _ev => { editorState.timelineSet = new Map(editorState.focusedSet.entries()); editorState.force_redraw(); }); btnClearSelectedMarkers.addEventListener('click', _ev => { editorState.unmarkFocusInferBasis(false); editorState.force_redraw(); }); btnShowExamples.addEventListener('click', _ev => { if (divExamples.style.display === 'none') { divExamples.style.display = 'block'; btnShowExamples.textContent = "Hide Example Circuits"; } else { divExamples.style.display = 'none'; btnShowExamples.textContent = "Show Example Circuits"; } }); btnClearTimelineFocus.addEventListener('click', _ev => { editorState.timelineSet = new Map(); editorState.force_redraw(); }); btnRedo.addEventListener('click', _ev => { editorState.redo(); }); btnClearMarkers.addEventListener('click', _ev => { editorState.clearMarkers(); }); btnRotate45.addEventListener('click', _ev => { editorState.rotate45(+1, false); }); btnRotate45Counter.addEventListener('click', _ev => { editorState.rotate45(-1, false); }); btnInsertLayer.addEventListener('click', _ev => { editorState.insertLayer(false); }); btnDeleteLayer.addEventListener('click', _ev => { editorState.deleteCurLayer(false); }); btnNextLayer.addEventListener('click', _ev => { editorState.changeCurLayerTo(editorState.curLayer + 1); }); btnPrevLayer.addEventListener('click', _ev => { editorState.changeCurLayerTo(editorState.curLayer - 1); }); window.addEventListener('resize', _ev => { editorState.canvas.width = editorState.canvas.scrollWidth; editorState.canvas.height = editorState.canvas.scrollHeight; editorState.force_redraw(); }); function exportCurrentState() { let validStimCircuit = editorState.copyOfCurCircuit().toStimCircuit(). replaceAll('\nPOLYGON', '\n#!pragma POLYGON'). replaceAll('\nERR', '\n#!pragma ERR'). replaceAll('\nMARK', '\n#!pragma MARK'); let txt = txtStimCircuit; txt.value = validStimCircuit + '\n'; txt.focus(); txt.select(); } editorState.canvas.addEventListener('mousemove', ev => { editorState.curMouseX = ev.offsetX + OFFSET_X; editorState.curMouseY = ev.offsetY + OFFSET_Y; // Scrubber. let w = editorState.canvas.width / 2; if (isInScrubber && ev.buttons === 1) { editorState.changeCurLayerTo(Math.floor((ev.offsetX - w) / 8)); return; } editorState.force_redraw(); }); let isInScrubber = false; editorState.canvas.addEventListener('mousedown', ev => { editorState.curMouseX = ev.offsetX + OFFSET_X; editorState.curMouseY = ev.offsetY + OFFSET_Y; editorState.mouseDownX = ev.offsetX + OFFSET_X; editorState.mouseDownY = ev.offsetY + OFFSET_Y; // Scrubber. let w = editorState.canvas.width / 2; isInScrubber = ev.offsetY < 20 && ev.offsetX > w && ev.buttons === 1; if (isInScrubber) { editorState.changeCurLayerTo(Math.floor((ev.offsetX - w) / 8)); return; } editorState.force_redraw(); }); editorState.canvas.addEventListener('mouseup', ev => { let highlightedArea = editorState.currentPositionsBoxesByMouseDrag(ev.altKey); editorState.mouseDownX = undefined; editorState.mouseDownY = undefined; editorState.curMouseX = ev.offsetX + OFFSET_X; editorState.curMouseY = ev.offsetY + OFFSET_Y; editorState.changeFocus(highlightedArea, ev.shiftKey, ev.ctrlKey); if (ev.buttons === 1) { isInScrubber = false; } }); /** * @return {!Map} */ function makeChordHandlers() { let res = /** @type {!Map} */ new Map(); res.set('shift+t', preview => editorState.rotate45(-1, preview)); res.set('t', preview => editorState.rotate45(+1, preview)); res.set('escape', () => editorState.clearFocus()); res.set('delete', preview => editorState.deleteAtFocus(preview)); res.set('backspace', preview => editorState.deleteAtFocus(preview)); res.set('ctrl+delete', preview => editorState.deleteCurLayer(preview)); res.set('ctrl+insert', preview => editorState.insertLayer(preview)); res.set('ctrl+backspace', preview => editorState.deleteCurLayer(preview)); res.set('ctrl+z', preview => { if (!preview) editorState.undo() }); res.set('ctrl+y', preview => { if (!preview) editorState.redo() }); res.set('ctrl+shift+z', preview => { if (!preview) editorState.redo() }); res.set('ctrl+c', async preview => { await copyToClipboard(); }); res.set('ctrl+v', pasteFromClipboard); res.set('ctrl+x', async preview => { await copyToClipboard(); if (editorState.focusedSet.size === 0) { let c = editorState.copyOfCurCircuit(); c.layers[editorState.curLayer].id_ops.clear(); c.layers[editorState.curLayer].markers.length = 0; editorState.commit_or_preview(c, preview); } else { editorState.deleteAtFocus(preview); } }); res.set('l', preview => { if (!preview) { editorState.timelineSet = new Map(editorState.focusedSet.entries()); editorState.force_redraw(); } }); res.set(' ', preview => editorState.unmarkFocusInferBasis(preview)); for (let [key, val] of [ ['1', 0], ['2', 1], ['3', 2], ['4', 3], ['5', 4], ['6', 5], ['7', 6], ['8', 7], ['9', 8], ['0', 9], ['-', 10], ['=', 11], ['\\', 12], ['`', 13], ]) { res.set(`${key}`, preview => editorState.markFocusInferBasis(preview, val)); res.set(`${key}+x`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKX').withDefaultArgument(val))); res.set(`${key}+y`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKY').withDefaultArgument(val))); res.set(`${key}+z`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKZ').withDefaultArgument(val))); res.set(`${key}+d`, preview => editorState.writeMarkerToDetector(preview, val)); res.set(`${key}+o`, preview => editorState.writeMarkerToObservable(preview, val)); res.set(`${key}+j`, preview => editorState.moveDetOrObsAtFocusIntoMarker(preview, val)); res.set(`${key}+k`, preview => editorState.addDissipativeOverlapToMarkers(preview, val)); } let defaultPolygonAlpha = 0.25; res.set('p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 0, defaultPolygonAlpha])); res.set('alt+p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 0, defaultPolygonAlpha])); res.set('shift+p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 0, 1, defaultPolygonAlpha])); res.set('p+x', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 0, defaultPolygonAlpha])); res.set('p+y', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 0, defaultPolygonAlpha])); res.set('p+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 0, 1, defaultPolygonAlpha])); res.set('p+x+y', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 1, 0, defaultPolygonAlpha])); res.set('p+x+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 1, defaultPolygonAlpha])); res.set('p+y+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 1, defaultPolygonAlpha])); res.set('p+x+y+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 1, 1, defaultPolygonAlpha])); res.set('m+p+x', preview => editorState.writeGateToFocus(preview, make_mpp_gate("X".repeat(editorState.focusedSet.size)), [])); res.set('m+p+y', preview => editorState.writeGateToFocus(preview, make_mpp_gate("Y".repeat(editorState.focusedSet.size)), [])); res.set('m+p+z', preview => editorState.writeGateToFocus(preview, make_mpp_gate("Z".repeat(editorState.focusedSet.size)), [])); res.set('f', preview => editorState.flipTwoQubitGateOrderAtFocus(preview)); res.set('g', preview => editorState.reverseLayerOrderFromFocusToEmptyLayer(preview)); res.set('shift+>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview, false)); res.set('shift+<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview, false)); res.set('shift+v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview, false)); res.set('shift+^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview, false)); res.set('>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview, false)); res.set('<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview, false)); res.set('v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview, false)); res.set('^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview, false)); res.set('.', preview => editorState.applyCoordinateTransform((x, y) => [x + 0.5, y + 0.5], preview, false)); /** * @param {!Array} chords * @param {!string} name * @param {undefined|!string=}inverse_name */ function addGateChords(chords, name, inverse_name=undefined) { for (let chord of chords) { if (res.has(chord)) { throw new Error("Chord collision: " + chord); } res.set(chord, preview => editorState.writeGateToFocus(preview, GATE_MAP.get(name))); } if (inverse_name !== undefined) { addGateChords(chords.map(e => 'shift+' + e), inverse_name); } } addGateChords(['h', 'h+y', 'h+x+z'], "H", "H"); addGateChords(['h+z', 'h+x+y'], "H_XY", "H_XY"); addGateChords(['h+x', 'h+y+z'], "H_YZ", "H_YZ"); addGateChords(['s+x', 's+y+z'], "SQRT_X", "SQRT_X_DAG"); addGateChords(['s+y', 's+x+z'], "SQRT_Y", "SQRT_Y_DAG"); addGateChords(['s', 's+z', 's+x+y'], "S", "S_DAG"); addGateChords(['r+x', 'r+y+z'], "RX"); addGateChords(['r+y', 'r+x+z'], "RY"); addGateChords(['r', 'r+z', 'r+x+y'], "R"); addGateChords(['m+x', 'm+y+z'], "MX"); addGateChords(['m+y', 'm+x+z'], "MY"); addGateChords(['m', 'm+z', 'm+x+y'], "M"); addGateChords(['m+r+x', 'm+r+y+z'], "MRX"); addGateChords(['m+r+y', 'm+r+x+z'], "MRY"); addGateChords(['m+r', 'm+r+z', 'm+r+x+y'], "MR"); addGateChords(['c'], "CX", "CX"); addGateChords(['c+x'], "CX", "CX"); addGateChords(['c+y'], "CY", "CY"); addGateChords(['c+z'], "CZ", "CZ"); addGateChords(['j+x'], "X", "X"); addGateChords(['j+y'], "Y", "Y"); addGateChords(['j+z'], "Z", "Z"); addGateChords(['c+x+y'], "XCY", "XCY"); addGateChords(['alt+c+x'], "XCX", "XCX"); addGateChords(['alt+c+y'], "YCY", "YCY"); addGateChords(['w'], "SWAP", "SWAP"); addGateChords(['w+x'], "CXSWAP", undefined); addGateChords(['c+w+x'], "CXSWAP", undefined); addGateChords(['i+w'], "ISWAP", "ISWAP_DAG"); addGateChords(['w+z'], "CZSWAP", undefined); addGateChords(['c+w+z'], "CZSWAP", undefined); addGateChords(['c+w'], "CZSWAP", undefined); addGateChords(['c+t'], "C_XYZ", "C_ZYX"); addGateChords(['c+s+x'], "SQRT_XX", "SQRT_XX_DAG"); addGateChords(['c+s+y'], "SQRT_YY", "SQRT_YY_DAG"); addGateChords(['c+s+z'], "SQRT_ZZ", "SQRT_ZZ_DAG"); addGateChords(['c+s'], "SQRT_ZZ", "SQRT_ZZ_DAG"); addGateChords(['c+m+x'], "MXX", "MXX"); addGateChords(['c+m+y'], "MYY", "MYY"); addGateChords(['c+m+z'], "MZZ", "MZZ"); addGateChords(['c+m'], "MZZ", "MZZ"); return res; } let fallbackEmulatedClipboard = undefined; async function copyToClipboard() { let c = editorState.copyOfCurCircuit(); c.layers = [c.layers[editorState.curLayer]] if (editorState.focusedSet.size > 0) { c.layers[0] = c.layers[0].id_filteredByQubit(q => { let x = c.qubitCoordData[q * 2]; let y = c.qubitCoordData[q * 2 + 1]; return editorState.focusedSet.has(`${x},${y}`); }); let [x, y] = minXY(editorState.focusedSet.values()); c = c.shifted(-x, -y); } let content = c.toStimCircuit() fallbackEmulatedClipboard = content; try { await navigator.clipboard.writeText(content); } catch (ex) { console.warn("Failed to write to clipboard. Using fallback emulated clipboard.", ex); } } /** * @param {!boolean} preview */ async function pasteFromClipboard(preview) { let text; try { text = await navigator.clipboard.readText(); } catch (ex) { console.warn("Failed to read from clipboard. Using fallback emulated clipboard.", ex); text = fallbackEmulatedClipboard; } if (text === undefined) { return; } let pastedCircuit = Circuit.fromStimCircuit(text); if (pastedCircuit.layers.length !== 1) { throw new Error(text); } let newCircuit = editorState.copyOfCurCircuit(); if (editorState.focusedSet.size > 0) { let [x, y] = minXY(editorState.focusedSet.values()); pastedCircuit = pastedCircuit.shifted(x, y); } // Include new coordinates. let usedCoords = []; for (let q = 0; q < pastedCircuit.qubitCoordData.length; q += 2) { usedCoords.push([pastedCircuit.qubitCoordData[q], pastedCircuit.qubitCoordData[q + 1]]); } newCircuit = newCircuit.withCoordsIncluded(usedCoords); let c2q = newCircuit.coordToQubitMap(); // Remove existing content at paste location. for (let key of editorState.focusedSet.keys()) { let q = c2q.get(key); if (q !== undefined) { newCircuit.layers[editorState.curLayer].id_pop_at(q); } } // Add content to paste location. for (let op of pastedCircuit.layers[0].iter_gates_and_markers()) { let newTargets = []; for (let q of op.id_targets) { let x = pastedCircuit.qubitCoordData[2*q]; let y = pastedCircuit.qubitCoordData[2*q+1]; newTargets.push(c2q.get(`${x},${y}`)); } newCircuit.layers[editorState.curLayer].put(new Operation( op.gate, op.tag, op.args, new Uint32Array(newTargets), )); } editorState.commit_or_preview(newCircuit, preview); } const CHORD_HANDLERS = makeChordHandlers(); /** * @param {!KeyboardEvent} ev */ function handleKeyboardEvent(ev) { editorState.chorder.handleKeyEvent(ev); if (ev.type === 'keydown') { if (ev.key.toLowerCase() === 'q') { let d = ev.shiftKey ? 5 : 1; editorState.changeCurLayerTo(editorState.curLayer - d); return; } if (ev.key.toLowerCase() === 'e') { let d = ev.shiftKey ? 5 : 1; editorState.changeCurLayerTo(editorState.curLayer + d); return; } if (ev.key === 'Home') { editorState.changeCurLayerTo(0); ev.preventDefault(); return; } if (ev.key === 'End') { editorState.changeCurLayerTo(editorState.copyOfCurCircuit().layers.length - 1); ev.preventDefault(); return; } } let evs = editorState.chorder.queuedEvents; if (evs.length === 0) { return; } let chord_ev = evs[evs.length - 1]; while (evs.length > 0) { evs.pop(); } let pressed = [...chord_ev.chord]; if (pressed.length === 0) { return; } pressed.sort(); let key = ''; if (chord_ev.altKey) { key += 'alt+'; } if (chord_ev.ctrlKey) { key += 'ctrl+'; } if (chord_ev.metaKey) { key += 'meta+'; } if (chord_ev.shiftKey) { key += 'shift+'; } for (let e of pressed) { key += `${e}+`; } key = key.substring(0, key.length - 1); let handler = CHORD_HANDLERS.get(key); if (handler !== undefined) { handler(chord_ev.inProgress); ev.preventDefault(); } else { editorState.preview(editorState.copyOfCurCircuit()); } } document.addEventListener('keydown', handleKeyboardEvent); document.addEventListener('keyup', handleKeyboardEvent); editorState.canvas.width = editorState.canvas.scrollWidth; editorState.canvas.height = editorState.canvas.scrollHeight; editorState.rev.changes().subscribe(() => { editorState.obs_val_draw_state.set(editorState.toSnapshot(undefined)); drawToolbox(editorState.chorder.toEvent(false)); }); initUrlCircuitSync(editorState.rev); editorState.obs_val_draw_state.observable().subscribe(ds => requestAnimationFrame(() => draw(editorState.canvas.getContext('2d'), ds))); window.addEventListener('focus', () => { editorState.chorder.handleFocusChanged(); }); window.addEventListener('blur', () => { editorState.chorder.handleFocusChanged(); }); // Intercept clicks on the example circuit links, and load them without actually reloading the page, to preserve undo history. for (let anchor of document.getElementById('examples-div').querySelectorAll('a')) { anchor.onclick = ev => { // Don't stop the user from e.g. opening the example in a new tab using ctrl+click. if (ev.shiftKey || ev.ctrlKey || ev.altKey || ev.button !== 0) { return undefined; } let circuitText = anchor.href.split('#circuit=')[1]; editorState.rev.commit(circuitText); return false; }; } ================================================ FILE: glue/crumble/package.json ================================================ { "type": "module" } ================================================ FILE: glue/crumble/run_tests_headless.js ================================================ import {run_tests} from "./test/test_util.js" import "./test/test_import_all.js" let total = await run_tests(() => {}, _name => true); if (!total.passed) { throw new Error("Some tests failed"); } ================================================ FILE: glue/crumble/test/generated_gate_name_list.test.js ================================================ const KNOWN_GATE_NAMES_FROM_STIM = ` I X Y Z C_NXYZ C_NZYX C_XNYZ C_XYNZ C_XYZ C_ZNYX C_ZYNX C_ZYX H H_NXY H_NXZ H_NYZ H_XY H_XZ H_YZ S SQRT_X SQRT_X_DAG SQRT_Y SQRT_Y_DAG SQRT_Z SQRT_Z_DAG S_DAG CNOT CX CXSWAP CY CZ CZSWAP II ISWAP ISWAP_DAG SQRT_XX SQRT_XX_DAG SQRT_YY SQRT_YY_DAG SQRT_ZZ SQRT_ZZ_DAG SWAP SWAPCX SWAPCZ XCX XCY XCZ YCX YCY YCZ ZCX ZCY ZCZ CORRELATED_ERROR DEPOLARIZE1 DEPOLARIZE2 E ELSE_CORRELATED_ERROR HERALDED_ERASE HERALDED_PAULI_CHANNEL_1 II_ERROR I_ERROR PAULI_CHANNEL_1 PAULI_CHANNEL_2 X_ERROR Y_ERROR Z_ERROR M MR MRX MRY MRZ MX MY MZ R RX RY RZ MXX MYY MZZ MPP SPP SPP_DAG REPEAT DETECTOR MPAD OBSERVABLE_INCLUDE QUBIT_COORDS SHIFT_COORDS TICK ` export {KNOWN_GATE_NAMES_FROM_STIM}; ================================================ FILE: glue/crumble/test/test.html ================================================ Test Crumble
Loading tests...
================================================ FILE: glue/crumble/test/test_import_all.js ================================================ import "../base/describe.test.js" import "../base/equate.test.js" import "../base/obs.test.js" import "../base/revision.test.js" import "../base/seq.test.js" import "../circuit/circuit.test.js" import "../circuit/layer.test.js" import "../circuit/pauli_frame.test.js" import "../circuit/propagated_pauli_frames.test.js" import "../draw/main_draw.test.js" import "../editor/editor_state.test.js" import "../gates/gateset.test.js" import "../keyboard/chord.test.js" import "../test/generated_gate_name_list.test.js" import "../test/test_util.test.js" ================================================ FILE: glue/crumble/test/test_main.js ================================================ import {run_tests, test, assertThat} from "./test_util.js" let imported = import("./test_import_all.js"); test("init", () => assertThat(imported).asyncResolvesToAValueThat().isNotEqualTo(undefined)); await imported.catch(() => {}); let status = /** @type {!HTMLDivElement} */ document.getElementById("status"); let acc = /** @type {!HTMLDivElement} */ document.getElementById("acc"); let testFilter = undefined; let hash = document.location.hash; if (hash.startsWith('#')) { hash = hash.substring(1); } if (hash.startsWith('test=')) { hash = hash.substring(5); testFilter = hash; console.log(`Only running '${testFilter}'`) } status.textContent = "Running tests..."; let total = await run_tests(progress => { status.textContent = `${progress.num_tests_left - progress.num_tests}/${progress.num_tests} ${progress.name} ${progress.passed ? 'passed' : 'failed'} (${progress.num_tests_failed} failed)`; if (!progress.passed) { let d = document.createElement("div"); d.textContent = `Test '${progress.name}' failed:`; acc.appendChild(d); let a = document.createElement("a"); a.textContent = `(click here to only run this test)`; a.style.marginLeft = '5px'; a.href = `#test=${progress.name}`; d.appendChild(a); acc.appendChild(d); let p = document.createElement("pre"); p.style.marginLeft = '40px'; p.style.backgroundColor = 'lightgray'; p.textContent = `${progress.error}`; acc.appendChild(p); } else if (progress.skipped) { let d = document.createElement("pre"); d.textContent = `Test '${progress.name}' skipped`; acc.appendChild(d); } }, name => testFilter === undefined || name === testFilter); if (!total.passed) { if (total.num_skipped > 0) { status.textContent = `${total.num_tests_failed} tests failed out of ${total.num_tests - total.num_skipped} (${total.num_skipped} skipped).`; } else { status.textContent = `${total.num_tests_failed} tests failed out of ${total.num_tests}.`; } } else if (total.num_skipped > 0) { status.textContent = `All ${total.num_tests} tests passed (${total.num_skipped} skipped).`; } else { status.textContent = `All ${total.num_tests} tests passed.`; } ================================================ FILE: glue/crumble/test/test_util.js ================================================ import {describe} from "../base/describe.js"; import {equate} from "../base/equate.js"; let _tests = /** @type {!Array} */ []; let _usedAssertIndices = 0; let _unawaitedAsserts = 0; /** * @param {!string} name * @param {!function} body */ function test(name, body) { _tests.push({name, body}); } /** * @param {!string} text * @param {!int} indentation * @param {!boolean} indent_first * @returns {!string} */ function indent(text, indentation, indent_first=true) { let lines = text.split('\n'); let prefix = ' '.repeat(indentation); for (let k = 0; k < lines.length; k++) { let p = k > 0 || indent_first ? prefix : ''; lines[k] = p + lines[k]; } return lines.join('\n'); } /** * @param {*} obj * @returns {!boolean} */ function isSequence(obj) { return obj instanceof Array || obj instanceof Uint32Array; } /** * @param {*} actual * @param {*} expected * @returns {!string} */ function diff(actual, expected) { let differences = []; if (isSequence(actual) && isSequence(expected)) { for (let k = 0; k < Math.max(actual.length, expected.length); k++) { if (!equate(actual[k], expected[k])) { differences.push(`${describe(k)}: ${diff(actual[k], expected[k])}`); } } } if ((actual instanceof Set || actual instanceof Map) && ((expected instanceof Set || expected instanceof Map))) { for (let k of actual.keys()) { if (!expected.has(k)) { differences.push(`${describe(k)} missing from expected`); } } for (let k of expected.keys()) { if (!actual.has(k)) { differences.push(`${describe(k)} missing from actual`); } } } if (actual instanceof Map && expected instanceof Map) { for (let k of actual.keys()) { if (expected.has(k)) { let v1 = actual.get(k); let v2 = expected.get(k); if (!equate(v1, v2)) { differences.push(`${describe(k)}: ${diff(actual.get(k), expected.get(k))}`); } } } } if (typeof actual === 'number' && typeof expected === 'number') { differences.push(`${actual} != ${expected}`); } if (differences.length === 0) { let lines1 = describe(actual).split('\n'); let lines2 = describe(expected).split('\n'); for (let k = 0; k < Math.max(lines1.length, lines2.length); k++) { let line1 = lines1[k]; let line2 = lines2[k]; if ((line1 === undefined && line2 === '') || (line2 === undefined && line1 === '')) { differences.push('▯'); } else if (line1 === undefined) { differences.push('█'.repeat(line2.length)); } else if (line2 === undefined) { differences.push('█'.repeat(line1.length)); } else { let t = ''; for (let x = 0; x < Math.max(line1.length, line2.length); x++) { let c1 = line1[x]; let c2 = line2[x]; t += c1 === c2 ? c1 : '█'; } differences.push(t); } } } return differences.map(e => indent(e, 4, false)).join('\n'); } class AsyncAssertSubject { /** * @param {!Promise} asyncSubject */ constructor(asyncSubject) { this.asyncSubject = asyncSubject; } /** * @param {!string} info * @returns {!AsyncAssertSubject} */ withInfo(info) { return new AsyncAssertSubject(this.asyncSubject.then(subject => subject.withInfo(info))); } /** * @returns {!AsyncAssertSubject} */ runsWithoutThrowingAnException() { return new AsyncAssertSubject(this.asyncSubject.then(subject => subject.runsWithoutThrowingAnException())); } /** * @returns {!AsyncAssertSubject} */ resolvesToAValueThat() { return new AsyncAssertSubject(this.asyncSubject.then(async subject => await subject.asyncResolvesToAValueThat())); } /** * @returns {!AsyncAssertSubject} */ rejectsWithAnExceptionThat() { return new AsyncAssertSubject(this.asyncSubject.then(async subject => await subject.asyncRejectsWithAnExceptionThat())); } /** * @returns {!AsyncAssertSubject} */ throwsAnExceptionThat() { return new AsyncAssertSubject(this.asyncSubject.then(subject => subject.throwsAnExceptionThat())); } /** * @param {!RegExp} pattern * @returns {!AsyncAssertSubject} */ matches(pattern) { return new AsyncAssertSubject(this.asyncSubject.then(subject => subject.matches(pattern))); } /** * @param {*} expected * @returns {!AsyncAssertSubject} */ isEqualTo(expected) { return new AsyncAssertSubject(this.asyncSubject.then(subject => subject.isEqualTo(expected))); } /** * @param {*} unexpected * @returns {!AsyncAssertSubject} */ isNotEqualTo(unexpected) { return new AsyncAssertSubject(this.asyncSubject.then(subject => subject.isNotEqualTo(unexpected))); } then(resolve, reject) { this.asyncSubject.then(e => { _unawaitedAsserts -= 1; resolve(e); }, ex => { _unawaitedAsserts -= 1; reject(ex); }); } } class AssertSubject { /** * @param {*} value * @param {!int} index * @param {!*} info */ constructor(value, index, info) { this.value = value; this.index = index; this.info = info; } /** * @param {!*} info * @returns {!AssertSubject} */ withInfo(info) { return new AssertSubject(this.value, this.index, info); } /** * @returns {!string} * @private */ _trace() { try { throw new Error(''); } catch (ex) { let lines = `${ex.stack}`.split('\n'); return 'Stack trace:\n' + lines.slice(3).join('\n') } } /** * @returns {!string} * @private */ _whichAssertion() { if (this.info === undefined) { return `Assertion #${this.index}`; } return `Assertion #${this.index} with info ${describe(this.info)}`; } runsWithoutThrowingAnException() { try { this.value(); } catch (ex) { throw new Error(`${this._whichAssertion()} failed. Got an exception: ${indent('' + ex, 4)} but expected the following not to throw: ${indent('' + this.value, 4)} `); } } /** * @returns {!AsyncAssertSubject} */ asyncResolvesToAValueThat() { _unawaitedAsserts += 1; return new AsyncAssertSubject( new Promise((resolve, reject) => { this.value.then(v => resolve(new AssertSubject(v, this.index, this.info))).catch( ex => reject(new Error(`Promise should have resolved, but instead rejected with: ${describe(ex)} `, {cause: ex}))); })); } /** * @returns {!AsyncAssertSubject} */ asyncRejectsWithAnExceptionThat() { _unawaitedAsserts += 1; return new AsyncAssertSubject( new Promise((resolve, reject) => { this.value.then(v => reject(new Error(`Promise should have rejected, but instead resolved to: ${describe(v)} `))).catch(ex => resolve(new AssertSubject(ex, this.index, this.info))); })); } /** * @returns {!AssertSubject} */ throwsAnExceptionThat() { try { this.value(); } catch (ex) { return new AssertSubject(ex, this.index, this.info); } throw new Error(`${this._whichAssertion()} failed. Expected the following to throw an exception: ${indent('' + this.value, 4)} `); } /** * @param {!RegExp} pattern * @returns void */ matches(pattern) { if (!pattern.test('' + this.value)) { throw new Error(`${this._whichAssertion()} failed. Expected the following to match: Actual value { ${indent(describe(this.value), 4)} } Expected pattern { ${pattern} } `); } } /** * @param {*} expected * @returns void */ isEqualTo(expected) { if (!equate(this.value, expected)) { let s1 = describe(this.value); let s2 = describe(expected); throw new Error(`${this._whichAssertion()} failed. Expected the following objects to be equal. ${s1 === s2 ? 'WARNING! Objects not equal but strings equal!' : ''} Actual value { ${indent(s1, 4)} } Expected value { ${indent(s2, 4)} } Differences { ${indent(diff(this.value, expected), 4)} } ${this._trace()} `); } } /** * @param {*} unexpected * @returns void */ isNotEqualTo(unexpected) { if (equate(this.value, unexpected)) { let s1 = describe(this.value); let s2 = describe(unexpected); throw new Error(`${this._whichAssertion()} failed. Expected the following objects to NOT be equal. Actual value { ${indent(s1, 4)} } Expected different value { ${indent(s2, 4)} } `); } } } /** * @param {*} value * @returns {!AssertSubject} */ function assertThat(value) { _usedAssertIndices++; return new AssertSubject(value, _usedAssertIndices, undefined); } function skipRestOfTestIfHeadless() { try { // Actually, even attempting to read the variable will raise an error. if (document === undefined) { throw new Error(); } } catch (_) { throw new Error("skipRestOfTestIfHeadless:document === undefined"); } } class TestProgress { /** * @param {!string} name * @param {!boolean} passed * @param {*} error * @param {!int} num_tests * @param {!int} num_tests_failed * @param {!int} num_tests_left * @param {!int} num_skipped */ constructor(name, passed, error, num_tests, num_tests_failed, num_tests_left, num_skipped) { this.name = name; this.passed = passed; this.error = error; this.num_tests = num_tests; this.num_tests_failed = num_tests_failed; this.num_tests_left = num_tests_left; this.num_skipped = num_skipped; } } /** * @param {!function(progress: !TestProgress)} callback * @param {!function(name: !string): !boolean} name_filter */ async function run_tests(callback, name_filter) { let num_tests = _tests.length; let num_tests_left = _tests.length; let num_tests_failed = 0; let all_passed = true; let num_skipped = 0; for (let test of _tests) { if (!name_filter(test.name)) { num_skipped += 1; continue; } console.log("run test", test.name); _usedAssertIndices = 0; let name = test.name; let passed = false; let error = undefined; let skipped = false; try { await test.body(); if (_usedAssertIndices === 0) { throw new Error("No assertions in test."); } if (_unawaitedAsserts > 0) { throw new Error("An async assertion wasn't awaited."); } passed = true; } catch (ex) { if (ex instanceof Error && ex.message === "skipRestOfTestIfHeadless:document === undefined") { console.warn(`skipped part of test '${test.name}' because tests are running headless`); skipped = true; num_skipped += 1; passed = true; } else { error = ex; all_passed = false; num_tests_failed++; console.error("failed test", test.name, ex); } } num_tests_left--; callback(new TestProgress(name, passed, error, num_tests, num_tests_failed, num_tests_left, num_skipped)); } let msg = `done running tests: ${num_tests_failed} failed, ${num_skipped} skipped`; if (num_tests_failed > 0) { console.error(msg); } else if (num_skipped > 0) { console.warn(msg); } else { console.log(msg); } return new TestProgress('', all_passed, undefined, num_tests, num_tests_failed, 0, num_skipped); } export {test, run_tests, assertThat, skipRestOfTestIfHeadless} ================================================ FILE: glue/crumble/test/test_util.test.js ================================================ import {test, assertThat} from "./test_util.js"; function expectFailure(func, match) { try { func(); } catch (ex) { if (match.test('' + ex)) { return; } throw new Error('message "' + ex + '" does not match ' + match, {cause: ex}); } throw new Error('Expected an exception matching ' + match); } /** * @param {!function(): void} func * @param {!RegExp} match */ async function asyncExpectFailure(func, match) { try { await func(); } catch (ex) { if (match.test('' + ex)) { return; } throw new Error('message "' + ex + '" does not match ' + match, {cause: ex}); } throw new Error('Expected an exception matching ' + match); } test("test_util.throws", () => { assertThat(() => { throw new Error('test'); }).throwsAnExceptionThat().matches(/test/); let a = 1; assertThat(() => { a += 1; }).runsWithoutThrowingAnException(); assertThat(a).isEqualTo(2); expectFailure(() => { assertThat(() => { throw new Error('XYZ'); }).runsWithoutThrowingAnException(); }, /expected the following not to throw:/); expectFailure(() => { assertThat(() => { }).throwsAnExceptionThat(); }, /Expected the following to throw/); expectFailure(() => { assertThat(() => { throw new Error('xyz') }).throwsAnExceptionThat().matches(/XYZ/); }, /Expected the following to match/); }); test("test_util.isEqualTo", () => { assertThat(undefined).isEqualTo(undefined); assertThat(null).isEqualTo(null); assertThat("1").isEqualTo("1"); assertThat(1).isEqualTo(1); assertThat("1").isNotEqualTo(1); assertThat([1, 2, 3]).isEqualTo([1, 2, 3]); assertThat([1, 2, 3]).isNotEqualTo([1, 2, 4]); assertThat(new Set([1, 2, 3])).isEqualTo(new Set([1, 2, 3])); assertThat(new Set([1, 2, 3])).isNotEqualTo(new Set([1, 2, 4])); assertThat(new Set([1, 2, 3])).isNotEqualTo(new Set([1, 2])); assertThat(new Set([1, 2, 3])).isNotEqualTo(new Set([1, 2, 3, 4])); assertThat(new Map([[1, 2], [3, 4]])).isEqualTo(new Map([[1, 2], [3, 4]])); assertThat(new Map([[1, 2], [3, 4]])).isNotEqualTo(new Map([[1, 2], [3, 5]])); assertThat(new Map([[1, 2], [3, 4]])).isNotEqualTo(new Map([[1, 2]])); assertThat(new Map([[1, 2], [3, 4]])).isNotEqualTo(new Map([[1, 2], [3, 4], [5, 6]])); expectFailure(() => { assertThat(undefined).isNotEqualTo(undefined); }, /Expected the following objects to NOT be equal/); expectFailure(() => { assertThat("1").isEqualTo(1); }, /Expected the following objects to be equal/); }); test("test_util.async", async () => { let e = (async () => 5)(); let ex = (async () => { throw new Error("test"); })(); await assertThat(e).asyncResolvesToAValueThat().isEqualTo(5); await assertThat(ex).asyncRejectsWithAnExceptionThat().matches(/test/); expectFailure(() => { assertThat(e).isEqualTo(5); }, /to be equal/); await asyncExpectFailure(async () => { await assertThat(e).asyncRejectsWithAnExceptionThat(); }, /should have rejected/); await asyncExpectFailure(async () => { await assertThat(e).asyncResolvesToAValueThat().isEqualTo(6); }, /to be equal/); expectFailure(() => { assertThat(ex).isEqualTo(5); }, /to be equal/); await asyncExpectFailure(async () => { await assertThat(ex).asyncResolvesToAValueThat(); }, /should have resolved/); await asyncExpectFailure(async () => { await assertThat(ex).asyncRejectsWithAnExceptionThat().matches(/bla/); }, /test/); }); ================================================ FILE: glue/javascript/README.md ================================================ # Stim in Javascript This directory contains glue code for exposing stim to javascript. For build instructions, see [the "build javascript bindings" sections of the dev documentation](/dev/README.md#build-javascript-bindings). ## Exposed API ``` stim.Circuit static stim.Circuit. stim.Circuit.append_operation stim.Circuit.append_from_stim_program_text stim.Circuit.copy stim.Circuit.isEqualTo stim.Circuit.repeated stim.Circuit.toString stim.Tableau static stim.Tableau. static stim.Tableau.random static stim.Tableau.from_named_gate static stim.Tableau.from_conjugated_generators_xs_zs stim.Tableau.x_output stim.Tableau.y_output stim.Tableau.z_output stim.Tableau.toString stim.Tableau.isEqualTo stim.Tableau.length stim.TableauSimulator static stim.TableauSimulator. stim.TableauSimulator.CNOT stim.TableauSimulator.CY stim.TableauSimulator.CZ stim.TableauSimulator.H stim.TableauSimulator.X stim.TableauSimulator.Y stim.TableauSimulator.Z stim.TableauSimulator.copy stim.TableauSimulator.current_inverse_tableau stim.TableauSimulator.do_circuit stim.TableauSimulator.do_pauli_string stim.TableauSimulator.do_tableau stim.TableauSimulator.measure stim.TableauSimulator.measure_kickback stim.TableauSimulator.set_inverse_tableau stim.PauliString static stim.PauliString. static stim.PauliString.random stim.PauliString.commutes stim.PauliString.isEqualTo stim.PauliString.length stim.PauliString.neg stim.PauliString.pauli stim.PauliString.sign stim.PauliString.times stim.PauliString.toString ``` ================================================ FILE: glue/javascript/build_wasm.sh ================================================ #!/bin/bash set -e # Get to this script's git repo root. cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" # Find relevant source files using naming conventions. readarray -t stim_src_files < <( \ find src \ | grep "\\.cc" \ | grep -v "\\.\(test\|perf\|pybind\)\\.cc" \ | grep -v "main\\.cc" \ ) readarray -t glue_src_files < <( \ find glue/javascript \ | grep "\\.js\\.cc" \ ) readarray -t js_test_files < <( \ find glue/javascript \ | grep "\\.test\\.js" \ ) mkdir -p out echo '' > out/all_stim_tests.html echo '' >> out/all_stim_tests.html # Build web assembly module using emscripten. emcc \ -std=c++20 \ -s NO_DISABLE_EXCEPTION_CATCHING \ -s EXPORT_NAME="load_stim_module" \ -s MODULARIZE=1 \ -s SINGLE_FILE=1 \ -o out/stim.js \ --no-entry \ --bind \ -I src \ "${glue_src_files[@]}" \ "${stim_src_files[@]}" ================================================ FILE: glue/javascript/circuit.js.cc ================================================ #include "circuit.js.h" #include #include using namespace stim; ExposedCircuit::ExposedCircuit() : circuit() { } ExposedCircuit::ExposedCircuit(Circuit circuit) : circuit(circuit) { } ExposedCircuit::ExposedCircuit(const std::string &text) : circuit(Circuit(text)) { } void ExposedCircuit::append_operation( const std::string &name, const emscripten::val &targets, const emscripten::val &args) { circuit.safe_append_u( name, emscripten::convertJSArrayToNumberVector(targets), emscripten::convertJSArrayToNumberVector(args)); } void ExposedCircuit::append_from_stim_program_text(const std::string &text) { circuit.append_from_text(text); } ExposedCircuit ExposedCircuit::repeated(size_t repetitions) const { return ExposedCircuit(circuit * repetitions); } ExposedCircuit ExposedCircuit::copy() const { return ExposedCircuit(circuit); } std::string ExposedCircuit::toString() const { return circuit.str(); } bool ExposedCircuit::isEqualTo(const ExposedCircuit &other) const { return circuit == other.circuit; } void emscripten_bind_circuit() { auto c = emscripten::class_("Circuit"); c.constructor(); c.constructor(); c.function("toString", &ExposedCircuit::toString); c.function("repeated", &ExposedCircuit::repeated); c.function("copy", &ExposedCircuit::copy); c.function("append_operation", &ExposedCircuit::append_operation); c.function("append_from_stim_program_text", &ExposedCircuit::append_from_stim_program_text); c.function("isEqualTo", &ExposedCircuit::isEqualTo); } ================================================ FILE: glue/javascript/circuit.js.h ================================================ #ifndef STIM_CIRCUIT_JS_H #define STIM_CIRCUIT_JS_H #include #include #include #include "stim/circuit/circuit.h" struct ExposedCircuit { stim::Circuit circuit; ExposedCircuit(); explicit ExposedCircuit(stim::Circuit circuit); explicit ExposedCircuit(const std::string &text); void append_operation(const std::string &name, const emscripten::val &targets, const emscripten::val &args); void append_from_stim_program_text(const std::string &text); ExposedCircuit repeated(size_t repetitions) const; ExposedCircuit copy() const; bool isEqualTo(const ExposedCircuit &other) const; std::string toString() const; }; void emscripten_bind_circuit(); #endif ================================================ FILE: glue/javascript/circuit.test.js ================================================ test("circuit.constructor", ({stim, assert}) => { let c = new stim.Circuit(); assert(c.toString() === ''); c = new stim.Circuit('H 0\n H 1 # comment'); assert(c.toString() === 'H 0 1'); }); test("circuit.repeated", ({stim, assert}) => { let c = new stim.Circuit('H 0\nCNOT 0 1'); assert(c.repeated(0).isEqualTo(new stim.Circuit())); assert(c.repeated(1).isEqualTo(c)); assert(!c.repeated(5).isEqualTo(c)); assert(c.repeated(5).toString().trim() === ` REPEAT 5 { H 0 CX 0 1 } `.trim()); }); test("circuit.copy", ({stim, assert}) => { let c = new stim.Circuit(` REPEAT 2 { H 0 CNOT 0 1 } `); let c2 = c.copy(); assert(c.isEqualTo(c2)); c.append_operation("X", [0], []); assert(!c.isEqualTo(c2)); }); test("circuit.append_operation", ({stim, assert}) => { let c = new stim.Circuit(); c.append_operation("H", [0, 1], []); c.append_operation("X_ERROR", [0, 1, 2], [0.25]); assert(c.toString().trim() === ` H 0 1 X_ERROR(0.25) 0 1 2 `.trim()); }); test("circuit.append_from_stim_program_text", ({stim, assert}) => { let c = new stim.Circuit(); let c2 = new stim.Circuit("H 0\nCNOT 0 1"); c.append_from_stim_program_text("H 0\nCNOT 0 1"); assert(c.isEqualTo(c2)); }); test("circuit.isEqualTo", ({stim, assert}) => { assert(new stim.Circuit().isEqualTo(new stim.Circuit())); assert(new stim.Circuit('').isEqualTo(new stim.Circuit())); assert(!new stim.Circuit('H 0').isEqualTo(new stim.Circuit())); assert(!new stim.Circuit('H 0').isEqualTo(new stim.Circuit('X 0'))); assert(!new stim.Circuit('H 0').isEqualTo(new stim.Circuit('H 1'))); assert(new stim.Circuit('H 1').isEqualTo(new stim.Circuit('H 1'))); assert(!new stim.Circuit('H 1').isEqualTo(new stim.Circuit(` REPEAT 1 { H 1 } `))); }); ================================================ FILE: glue/javascript/common.js.cc ================================================ #include "common.js.h" using namespace stim; uint32_t js_val_to_uint32_t(const emscripten::val &val) { double v = val.as(); double f = floor(v); if (v != f || v < 0 || v > UINT32_MAX) { throw std::out_of_range("Number isn't a uint32_t: " + std::to_string(v)); } return (uint32_t)f; } ================================================ FILE: glue/javascript/common.js.h ================================================ #ifndef STIM_COMMON_JS_H #define STIM_COMMON_JS_H #include #include "stim/util_bot/probability_util.h" template emscripten::val vec_to_js_array(const std::vector &items) { emscripten::val result = emscripten::val::array(); for (size_t k = 0; k < items.size(); k++) { result.set(k, items[k]); } return result; } uint32_t js_val_to_uint32_t(const emscripten::val &val); #endif ================================================ FILE: glue/javascript/pauli_string.js.cc ================================================ #include "pauli_string.js.h" #include #include "common.js.h" using namespace stim; ExposedPauliString::ExposedPauliString(PauliString pauli_string) : pauli_string(pauli_string) { } ExposedPauliString::ExposedPauliString(const emscripten::val &arg) : pauli_string(0) { std::string t = arg.typeOf().as(); if (arg.isNumber()) { pauli_string = PauliString(js_val_to_uint32_t(arg)); } else if (arg.isString()) { pauli_string = PauliString::from_str(arg.as()); } else { throw std::invalid_argument("Expected an int or a string. Got " + t); } } ExposedPauliString ExposedPauliString::random(size_t n) { auto rng = externally_seeded_rng(); return ExposedPauliString(PauliString::random(n, rng)); } ExposedPauliString ExposedPauliString::times(const ExposedPauliString &other) const { PauliString result = pauli_string; uint8_t r = result.ref().inplace_right_mul_returning_log_i_scalar(other.pauli_string); if (r & 1) { throw std::invalid_argument("Multiplied non-commuting."); } if (r & 2) { result.sign ^= 1; } return ExposedPauliString(result); } ExposedPauliString ExposedPauliString::neg() const { PauliString result = pauli_string; result.sign ^= 1; return ExposedPauliString(result); } bool ExposedPauliString::isEqualTo(const ExposedPauliString &other) const { return pauli_string == other.pauli_string; } bool ExposedPauliString::commutes(const ExposedPauliString &other) const { return pauli_string.ref().commutes(other.pauli_string); } std::string ExposedPauliString::toString() const { return pauli_string.str(); } uint8_t ExposedPauliString::pauli(size_t index) const { if (index >= pauli_string.num_qubits) { return 0; } uint8_t x = pauli_string.xs[index]; uint8_t z = pauli_string.zs[index]; return pauli_xz_to_xyz(x, z); } size_t ExposedPauliString::getLength() const { return pauli_string.num_qubits; } int8_t ExposedPauliString::getSign() const { return pauli_string.sign ? -1 : +1; } void emscripten_bind_pauli_string() { auto c = emscripten::class_("PauliString"); c.constructor(); c.class_function("random", &ExposedPauliString::random); c.function("neg", &ExposedPauliString::neg); c.function("times", &ExposedPauliString::times); c.function("commutes", &ExposedPauliString::commutes); c.function("isEqualTo", &ExposedPauliString::isEqualTo); c.function("toString", &ExposedPauliString::toString); c.function("pauli", &ExposedPauliString::pauli); c.property("length", &ExposedPauliString::getLength); c.property("sign", &ExposedPauliString::getSign); } ================================================ FILE: glue/javascript/pauli_string.js.h ================================================ #ifndef STIM_PAULI_STRING_JS_H #define STIM_PAULI_STRING_JS_H #include #include #include #include "stim/stabilizers/pauli_string.h" struct ExposedPauliString { stim::PauliString pauli_string; explicit ExposedPauliString(stim::PauliString pauli_string); explicit ExposedPauliString(const emscripten::val &arg); static ExposedPauliString random(size_t n); ExposedPauliString times(const ExposedPauliString &other) const; bool commutes(const ExposedPauliString &other) const; size_t getLength() const; int8_t getSign() const; uint8_t pauli(size_t index) const; ExposedPauliString neg() const; bool isEqualTo(const ExposedPauliString &other) const; std::string toString() const; }; void emscripten_bind_pauli_string(); #endif ================================================ FILE: glue/javascript/pauli_string.test.js ================================================ test("pauli_string.constructor", ({stim, assert}) => { let p = new stim.PauliString(0); assert(p.length === 0); assert(p.sign === +1); assert(p.toString() === "+"); p = new stim.PauliString(1); assert(p.length === 1); assert(p.sign === +1); assert(p.pauli(0) === 0); assert(p.toString() === "+_"); p = new stim.PauliString(2); assert(p.length === 2); assert(p.sign === +1); assert(p.pauli(0) === 0); assert(p.pauli(1) === 0); assert(p.toString() === "+__"); p = new stim.PauliString(1000); assert(p.length === 1000); assert(p.sign === +1); assert(p.pauli(0) === 0); assert(p.pauli(1) === 0); assert(p.pauli(999) === 0); assert(p.toString() === "+" + "_".repeat(1000)); }); test("pauli_string.constructor", ({stim, assert}) => { let p = new stim.PauliString(""); assert(p.length === 0); assert(p.sign === +1); assert(p.toString() === "+"); p = new stim.PauliString("-"); assert(p.length === 0); assert(p.sign === -1); assert(p.toString() === "-"); p = new stim.PauliString("-IXYZ"); assert(p.length === 4); assert(p.sign === -1); assert(p.pauli(0) === 0); assert(p.pauli(1) === 1); assert(p.pauli(2) === 2); assert(p.pauli(3) === 3); assert(p.toString() === "-_XYZ"); try { new stim.PauliString([]); assert(false); } catch { } }); test("pauli_string.equality", ({stim, assert}) => { let ps = k => new stim.PauliString(k); assert(ps("+").isEqualTo(ps("+"))); assert(ps("XX").isEqualTo(ps("XX"))); assert(!ps("XX").isEqualTo(ps("XY"))); assert(!ps("XX").isEqualTo(ps("XXX"))); assert(!ps("__").isEqualTo(ps("___"))); assert(!ps("XX").isEqualTo(ps("ZX"))); assert(!ps("XX").isEqualTo(ps("__"))); assert(!ps("+").isEqualTo(ps("-"))); }); test("pauli_string.random", ({stim, assert}) => { assert(!stim.PauliString.random(100).isEqualTo(stim.PauliString.random(100))); }); test("pauli_string.times", ({stim, assert}) => { let ps = k => new stim.PauliString(k); assert(ps("XX").times(ps("YY")).isEqualTo(ps("-ZZ"))); assert(ps("XY").times(ps("YX")).isEqualTo(ps("+ZZ"))); assert(ps("ZZ").times(ps("YY")).isEqualTo(ps("-XX"))); }); test("pauli_string.commutes", ({stim, assert}) => { let ps = k => new stim.PauliString(k); assert(ps('+').commutes(ps('+'))); assert(ps('+XX').commutes(ps('+YY'))); assert(ps('+XX').commutes(ps('-YY'))); assert(ps('+XX').commutes(ps('+ZZ'))); assert(!ps('+XX').commutes(ps('+XZ'))); assert(!ps('+XX').commutes(ps('+XZ'))); }); test("pauli_string.neg", ({stim, assert}) => { let ps = k => new stim.PauliString(k); assert(ps('+').neg().isEqualTo(ps('-'))); assert(ps('-').neg().isEqualTo(ps('+'))); assert(ps('+XYZ').neg().isEqualTo(ps('-XYZ'))); }); ================================================ FILE: glue/javascript/stim.js.cc ================================================ #include #include #include "circuit.js.h" #include "pauli_string.js.h" #include "tableau.js.h" #include "tableau_simulator.js.h" EMSCRIPTEN_BINDINGS(stim) { emscripten_bind_circuit(); emscripten_bind_pauli_string(); emscripten_bind_tableau(); emscripten_bind_tableau_simulator(); } ================================================ FILE: glue/javascript/stim.test_harness.js ================================================ let tests = []; let __any_failures = false; function test(name, handler) { tests.push({name, handler}); } function tryPromiseRun(method) { try { return Promise.resolve(method()); } catch (ex) { return Promise.reject(ex); } } async function run_all_tests() { let stim = await load_stim_module(); let results = []; for (let {name, handler} of tests) { let result = tryPromiseRun(() => { let count = 0; let assert = condition => { count += 1; if (!condition) { throw new Error(`Assert #${count} failed.`); } }; return handler({stim, assert}); }); results.push({name, result}); } for (let {result, name} of results) { try { await result; console.log(`pass ${name}`); } catch (ex) { __any_failures = true; console.error(`FAIL ${name}: ${ex}`); } } try { if (__any_failures) { console.error("THERE WERE TEST FAILURES"); throw Error("THERE WERE TEST FAILURES"); } else { console.info("All tests passed."); } } finally { var doneDiv = document.createElement('div'); doneDiv.id = 'done'; doneDiv.innerText = 'Done'; document.body.appendChild(doneDiv); } } setTimeout(run_all_tests, 0); ================================================ FILE: glue/javascript/tableau.js.cc ================================================ #include "tableau.js.h" #include #include "common.js.h" #include "stim/gates/gates.h" using namespace stim; ExposedTableau::ExposedTableau(Tableau tableau) : tableau(tableau) { } ExposedTableau::ExposedTableau(int n) : tableau(n) { } ExposedTableau ExposedTableau::random(int n) { auto rng = externally_seeded_rng(); return ExposedTableau(Tableau::random(n, rng)); } ExposedTableau ExposedTableau::from_named_gate(const std::string &name) { const Gate &gate = GATE_DATA.at(name); if (!(gate.flags & GATE_IS_UNITARY)) { throw std::out_of_range("Recognized name, but not unitary: " + name); } return ExposedTableau(gate.tableau()); } std::string ExposedTableau::str() const { return tableau.str(); } ExposedPauliString ExposedTableau::x_output(size_t index) const { if (index >= tableau.num_qubits) { throw std::invalid_argument("target >= tableau.length"); } return ExposedPauliString(tableau.xs[index]); } ExposedPauliString ExposedTableau::y_output(size_t index) const { if (index >= tableau.num_qubits) { throw std::invalid_argument("target >= tableau.length"); } // Compute Y using Y = i*X*Z. uint8_t log_i = 1; PauliString copy = tableau.xs[index]; log_i += copy.ref().inplace_right_mul_returning_log_i_scalar(tableau.zs[index]); if (log_i & 1) { throw std::out_of_range("Malformed tableau. X_k commutes with Z_k."); } copy.sign ^= (log_i & 2) != 0; return ExposedPauliString(copy); } ExposedPauliString ExposedTableau::z_output(size_t index) const { if (index >= tableau.num_qubits) { throw std::invalid_argument("target >= tableau.length"); } return ExposedPauliString(tableau.zs[index]); } ExposedTableau ExposedTableau::from_conjugated_generators_xs_zs( const emscripten::val &xs_val, const emscripten::val &zs_val) { auto xs = emscripten::vecFromJSArray(xs_val); auto zs = emscripten::vecFromJSArray(zs_val); size_t n = xs.size(); if (zs.size() != n) { throw std::invalid_argument("xs.length != zs.length"); } for (const auto &e : xs) { if (e.pauli_string.num_qubits != n) { throw std::invalid_argument("x.length != xs.length"); } } for (const auto &e : zs) { if (e.pauli_string.num_qubits != n) { throw std::invalid_argument("z.length != zs.length"); } } Tableau result(n); for (size_t q = 0; q < n; q++) { result.xs[q] = xs[q].pauli_string; result.zs[q] = zs[q].pauli_string; } if (!result.satisfies_invariants()) { throw std::invalid_argument( "The given generator outputs don't describe a valid Clifford operation.\n" "They don't preserve commutativity.\n" "Everything must commute, except for X_k anticommuting with Z_k for each k."); } return ExposedTableau(result); } size_t ExposedTableau::getLength() const { return tableau.num_qubits; } bool ExposedTableau::isEqualTo(const ExposedTableau &other) const { return tableau == other.tableau; } void emscripten_bind_tableau() { auto c = emscripten::class_("Tableau"); c.constructor(); c.class_function("random", &ExposedTableau::random); c.class_function("from_named_gate", &ExposedTableau::from_named_gate); c.class_function("from_conjugated_generators_xs_zs", &ExposedTableau::from_conjugated_generators_xs_zs); c.function("x_output", &ExposedTableau::x_output); c.function("y_output", &ExposedTableau::y_output); c.function("z_output", &ExposedTableau::z_output); c.function("toString", &ExposedTableau::str); c.function("isEqualTo", &ExposedTableau::isEqualTo); c.property("length", &ExposedTableau::getLength); } ================================================ FILE: glue/javascript/tableau.js.h ================================================ #ifndef STIM_TABLEAU_JS_H #define STIM_TABLEAU_JS_H #include #include #include "pauli_string.js.h" #include "stim/stabilizers/tableau.h" struct ExposedTableau { stim::Tableau tableau; explicit ExposedTableau(stim::Tableau tableau); explicit ExposedTableau(int n); static ExposedTableau random(int n); static ExposedTableau from_named_gate(const std::string &name); static ExposedTableau from_conjugated_generators_xs_zs( const emscripten::val &xs_val, const emscripten::val &zs_val); ExposedPauliString x_output(size_t index) const; ExposedPauliString y_output(size_t index) const; ExposedPauliString z_output(size_t index) const; size_t getLength() const; std::string str() const; bool isEqualTo(const ExposedTableau &other) const; }; void emscripten_bind_tableau(); #endif ================================================ FILE: glue/javascript/tableau.test.js ================================================ test("tableau.constructor", ({stim, assert}) => { let p = new stim.Tableau(0); assert(p.length === 0); p = new stim.Tableau(1); assert(p.length === 1); assert(p.x_output(0).toString() === "+X"); assert(p.y_output(0).toString() === "+Y"); assert(p.z_output(0).toString() === "+Z"); p = new stim.Tableau(8); assert(p.length === 8); assert(p.x_output(5).toString() === "+_____X__"); assert(p.y_output(5).toString() === "+_____Y__"); assert(p.z_output(5).toString() === "+_____Z__"); }); test("tableau.from_conjugated_generators_xs_zs", ({stim, assert}) => { assert(new stim.Tableau(0).isEqualTo(stim.Tableau.from_conjugated_generators_xs_zs([], []))); assert(new stim.Tableau(1).isEqualTo(stim.Tableau.from_conjugated_generators_xs_zs( [new stim.PauliString("+X")], [new stim.PauliString("+Z")] ))); let t = stim.Tableau.from_conjugated_generators_xs_zs( [ new stim.PauliString("+XX"), new stim.PauliString("+_X"), ], [ new stim.PauliString("+Z_"), new stim.PauliString("+ZZ"), ], ); assert(t.toString().trim() === ` +-xz-xz- | ++ ++ | XZ _Z | X_ XZ `.trim()); }); test("tableau.from_named_gate_vs_output", ({stim, assert}) => { assert(new stim.Tableau(3).toString().trim() === ` +-xz-xz-xz- | ++ ++ ++ | XZ __ __ | __ XZ __ | __ __ XZ `.trim()); assert(stim.Tableau.from_named_gate("H").toString().trim() === ` +-xz- | ++ | ZX `.trim()); assert(stim.Tableau.from_named_gate("X").toString().trim() === ` +-xz- | +- | XZ `.trim()); let cnot = stim.Tableau.from_named_gate("CNOT"); assert(cnot.toString().trim() === ` +-xz-xz- | ++ ++ | XZ _Z | X_ XZ `.trim()); assert(cnot.x_output(0).toString() === "+XX"); assert(cnot.x_output(1).toString() === "+_X"); assert(cnot.y_output(0).toString() === "+YX"); assert(cnot.y_output(1).toString() === "+ZY"); assert(cnot.z_output(0).toString() === "+Z_"); assert(cnot.z_output(1).toString() === "+ZZ"); }); test("tableau.random", ({stim, assert}) => { assert(!stim.Tableau.random(10).isEqualTo(stim.Tableau.random(10))); }); test("tableau.equality", ({stim, assert}) => { let h = stim.Tableau.from_named_gate("H"); let x = stim.Tableau.from_named_gate("X"); let id = stim.Tableau.from_named_gate("I"); let cnot = stim.Tableau.from_named_gate("CNOT"); assert(h.isEqualTo(h)); assert(h.isEqualTo(stim.Tableau.from_named_gate("H"))); assert(!h.isEqualTo(x)); assert(!id.isEqualTo(x)); assert(id.isEqualTo(id)); assert(x.isEqualTo(x)); assert(cnot.isEqualTo(cnot)); assert(!x.isEqualTo(cnot)); assert(new stim.Tableau(2).isEqualTo(new stim.Tableau(2))); assert(!new stim.Tableau(3).isEqualTo(new stim.Tableau(2))); }); ================================================ FILE: glue/javascript/tableau_simulator.js.cc ================================================ #include "tableau_simulator.js.h" #include #include #include "common.js.h" using namespace stim; struct JsCircuitInstruction { GateType gate_type; std::vector targets; JsCircuitInstruction(GateType gate_type, std::vector targets) : gate_type(gate_type), targets(std::move(targets)) { } JsCircuitInstruction(GateType gate_type, std::vector init_targets) : gate_type(gate_type) { for (auto e : init_targets) { targets.push_back(GateTarget{e}); } } operator CircuitInstruction() const { return {gate_type, {}, targets, ""}; } }; static JsCircuitInstruction args_to_targets(TableauSimulator &self, GateType gate_type, const emscripten::val &args) { std::vector result = emscripten::convertJSArrayToNumberVector(args); uint32_t max_q = 0; for (uint32_t q : result) { max_q = std::max(max_q, q & TARGET_VALUE_MASK); } // Note: quadratic behavior. self.ensure_large_enough_for_qubits((size_t)max_q + 1); return JsCircuitInstruction(gate_type, result); } static JsCircuitInstruction safe_targets(TableauSimulator &self, GateType gate_type, uint32_t target) { uint32_t max_q = target & TARGET_VALUE_MASK; self.ensure_large_enough_for_qubits((size_t)max_q + 1); return JsCircuitInstruction(gate_type, {GateTarget{target}}); } static JsCircuitInstruction safe_targets(TableauSimulator &self, GateType gate_type, uint32_t target1, uint32_t target2) { uint32_t max_q = std::max(target1 & TARGET_VALUE_MASK, target2 & TARGET_VALUE_MASK); self.ensure_large_enough_for_qubits((size_t)max_q + 1); if (target1 == target2) { throw std::invalid_argument("target1 == target2"); } return JsCircuitInstruction(gate_type, {GateTarget{target1}, GateTarget{target2}}); } static JsCircuitInstruction args_to_target_pairs(TableauSimulator &self, GateType gate_type, const emscripten::val &args) { auto result = args_to_targets(self, gate_type, args); if (result.targets.size() & 1) { throw std::out_of_range("Two qubit operation requires an even number of targets."); } return result; } ExposedTableauSimulator::ExposedTableauSimulator() : sim(externally_seeded_rng(), 0) { } bool ExposedTableauSimulator::measure(size_t target) { sim.ensure_large_enough_for_qubits(target + 1); sim.do_MZ(JsCircuitInstruction(GateType::M, {GateTarget{target}})); return (bool)sim.measurement_record.storage.back(); } emscripten::val ExposedTableauSimulator::measure_kickback(size_t target) { sim.ensure_large_enough_for_qubits(target + 1); auto result = sim.measure_kickback_z(GateTarget{(uint32_t)target}); emscripten::val returned = emscripten::val::object(); returned.set("result", result.first); if (result.second.num_qubits) { returned.set("kickback", ExposedPauliString(result.second)); } else { returned.set("kickback", emscripten::val::undefined()); } return returned; } void ExposedTableauSimulator::do_circuit(const ExposedCircuit &circuit) { sim.ensure_large_enough_for_qubits(circuit.circuit.count_qubits()); circuit.circuit.for_each_operation([&](const CircuitInstruction &op) { sim.do_gate(op); }); } void ExposedTableauSimulator::do_pauli_string(const ExposedPauliString &pauli_string) { sim.ensure_large_enough_for_qubits(pauli_string.pauli_string.num_qubits); sim.paulis(pauli_string.pauli_string); } void ExposedTableauSimulator::do_tableau(const ExposedTableau &tableau, const emscripten::val &targets) { auto conv = emscripten::convertJSArrayToNumberVector(targets); uint32_t max_q = 0; for (uint32_t q : conv) { max_q = std::max(max_q, q & TARGET_VALUE_MASK); } sim.ensure_large_enough_for_qubits((size_t)max_q + 1); sim.inv_state.inplace_scatter_prepend(tableau.tableau.inverse(), conv); } void ExposedTableauSimulator::X(uint32_t target) { sim.do_X(safe_targets(sim, GateType::X, target)); } void ExposedTableauSimulator::Y(uint32_t target) { sim.do_Y(safe_targets(sim, GateType::Y, target)); } void ExposedTableauSimulator::Z(uint32_t target) { sim.do_Z(safe_targets(sim, GateType::Z, target)); } void ExposedTableauSimulator::H(uint32_t target) { sim.do_H_XZ(safe_targets(sim, GateType::H, target)); } void ExposedTableauSimulator::CNOT(uint32_t control, uint32_t target) { sim.do_ZCX(safe_targets(sim, GateType::CX, control, target)); } void ExposedTableauSimulator::SWAP(uint32_t target1, uint32_t target2) { sim.do_SWAP(safe_targets(sim, GateType::SWAP, target1, target2)); } void ExposedTableauSimulator::CY(uint32_t control, uint32_t target) { sim.do_ZCY(safe_targets(sim, GateType::CY, control, target)); } void ExposedTableauSimulator::CZ(uint32_t control, uint32_t target) { sim.do_ZCZ(safe_targets(sim, GateType::CZ, control, target)); } ExposedTableau ExposedTableauSimulator::current_inverse_tableau() const { return ExposedTableau(sim.inv_state); } void ExposedTableauSimulator::set_inverse_tableau(const ExposedTableau &tableau) { sim.inv_state = tableau.tableau; } ExposedTableauSimulator ExposedTableauSimulator::copy() const { return *this; } void emscripten_bind_tableau_simulator() { auto c = emscripten::class_("TableauSimulator"); c.constructor(); c.function("current_inverse_tableau", &ExposedTableauSimulator::current_inverse_tableau); c.function("set_inverse_tableau", &ExposedTableauSimulator::set_inverse_tableau); c.function("measure", &ExposedTableauSimulator::measure); c.function("measure_kickback", &ExposedTableauSimulator::measure_kickback); c.function("do_circuit", &ExposedTableauSimulator::do_circuit); c.function("do_tableau", &ExposedTableauSimulator::do_tableau); c.function("do_pauli_string", &ExposedTableauSimulator::do_pauli_string); c.function("X", &ExposedTableauSimulator::X); c.function("Y", &ExposedTableauSimulator::Y); c.function("Z", &ExposedTableauSimulator::Z); c.function("H", &ExposedTableauSimulator::H); c.function("CNOT", &ExposedTableauSimulator::CNOT); c.function("CY", &ExposedTableauSimulator::CY); c.function("CZ", &ExposedTableauSimulator::CZ); c.function("SWAP", &ExposedTableauSimulator::SWAP); c.function("copy", &ExposedTableauSimulator::copy); } ================================================ FILE: glue/javascript/tableau_simulator.js.h ================================================ #ifndef STIM_TABLEAU_SIMULATOR_JS_H #define STIM_TABLEAU_SIMULATOR_JS_H #include #include #include #include "circuit.js.h" #include "pauli_string.js.h" #include "stim/simulators/tableau_simulator.h" #include "tableau.js.h" struct ExposedTableauSimulator { stim::TableauSimulator sim; ExposedTableauSimulator(); ExposedTableauSimulator copy() const; bool measure(size_t target); emscripten::val measure_kickback(size_t target); ExposedTableau current_inverse_tableau() const; void set_inverse_tableau(const ExposedTableau &tableau); void do_circuit(const ExposedCircuit &circuit); void do_pauli_string(const ExposedPauliString &pauli_string); void do_tableau(const ExposedTableau &tableau, const emscripten::val &targets); void X(uint32_t target); void Y(uint32_t target); void Z(uint32_t target); void H(uint32_t target); void CNOT(uint32_t control, uint32_t target); void CY(uint32_t control, uint32_t target); void CZ(uint32_t control, uint32_t target); void SWAP(uint32_t target1, uint32_t target12); }; void emscripten_bind_tableau_simulator(); #endif ================================================ FILE: glue/javascript/tableau_simulator.test.js ================================================ test("tableau_simulator.constructor", ({stim, assert}) => { let s = new stim.TableauSimulator(); assert(s.current_inverse_tableau().isEqualTo(new stim.Tableau(0))); }); test("tableau_simulator.set_inverse_tableau", ({stim, assert}) => { let s = new stim.TableauSimulator(); let t = new stim.Tableau(10); s.set_inverse_tableau(t); assert(s.current_inverse_tableau().isEqualTo(t)); }); test("tableau_simulator.measure", ({stim, assert}) => { let s = new stim.TableauSimulator(); assert(s.measure(0) === false); assert(s.measure(1) === false); assert(s.measure(2) === false); s.X(0); s.X(2); assert(s.measure(0) === true); assert(s.measure(1) === false); assert(s.measure(2) === true); assert(s.measure(500) === false); }); test("tableau_simulator.gate_methods_vs_do_tableau", ({stim, assert}) => { for (let gate of ["X", "Y", "Z", "H", "CNOT", "CY", "CZ"]) { let t = stim.Tableau.from_named_gate(gate); let n = t.length; let s = new stim.TableauSimulator(); for (let k = 0; k < n; k++) { s.H(k); s.CNOT(k, k + n); } let s2 = s.copy(); if (n === 1) { s[gate](2); s2.do_tableau(t, [2]); } else { s[gate](2, 3); s2.do_tableau(t, [2, 3]); } assert(s.current_inverse_tableau().isEqualTo(s2.current_inverse_tableau())); } }); test("tableau_simulator.do_tableau", ({stim, assert}) => { let a = new stim.TableauSimulator(); a.do_tableau(stim.Tableau.from_named_gate("X"), [0]); assert(a.measure(0) === true); assert(a.measure(1) === false); assert(a.measure(2) === false); a.do_tableau(stim.Tableau.from_named_gate("CNOT"), [0, 2]); assert(a.measure(0) === true); assert(a.measure(1) === false); assert(a.measure(2) === true); }); test("tableau_simulator.copy", ({stim, assert}) => { let a = new stim.TableauSimulator(); a.X(2); let b = a.copy(); assert(a.current_inverse_tableau().isEqualTo(b.current_inverse_tableau())); a.Y(1); assert(!a.current_inverse_tableau().isEqualTo(b.current_inverse_tableau())); }); test("tableau_simulator.do_circuit", ({stim, assert}) => { let a = new stim.TableauSimulator(); a.do_circuit(new stim.Circuit(` X 0 2 SWAP 2 3 `)); assert(a.measure(0) === true); assert(a.measure(1) === false); assert(a.measure(2) === false); assert(a.measure(3) === true); }); test("tableau_simulator.do_pauli_string", ({stim, assert}) => { let a = new stim.TableauSimulator(); a.do_circuit(new stim.Circuit(` H_XZ 4 5 6 7 H_YZ 8 9 10 11 `)); a.do_pauli_string(new stim.PauliString("_XYZ_XYZ_XYZ")); a.do_circuit(new stim.Circuit(` H_XZ 4 5 6 7 H_YZ 8 9 10 11 `)); assert(a.measure(0) === false); assert(a.measure(1) === true); assert(a.measure(2) === true); assert(a.measure(3) === false); assert(a.measure(4) === false); assert(a.measure(5) === false); assert(a.measure(6) === true); assert(a.measure(7) === true); assert(a.measure(8) === false); assert(a.measure(9) === true); assert(a.measure(10) === false); assert(a.measure(11) === true); }); test("tableau_simulator.measure_kickback", ({stim, assert}) => { let a = new stim.TableauSimulator(); a.do_circuit(new stim.Circuit(` H 0 CNOT 0 1 0 3 `)); let v1 = a.measure_kickback(1); let v2 = a.measure_kickback(3); assert(v1.kickback.isEqualTo(new stim.PauliString('+XX_X'))); assert(v2.kickback === undefined); assert(v1.result === v2.result); }); test("tableau_simulator.collision", ({stim, assert}) => { let s = new stim.TableauSimulator(); try { s.CNOT(0, 0); assert(false); } catch { } try { s.SWAP(2, 2); assert(false); } catch { } s.SWAP(0, 1); }); ================================================ FILE: glue/lattice_surgery/README.md ================================================ # Lattice Surgery Subroutine Synthesizer (LaSsynth) A lattice surgery subroutine (LaS) is a confined volume with a set of ports. Within this volume, lattice surgery merges and splits are performed. The function of a LaS is characterized by a set of stabilizers on these ports. The lattice surgery subroutine synthesizer (LaSsynth) uses SAT/SMT solvers to synthesize LaS given the volume, the ports, and the stabilizers. LaSsynth outputs a textual representation of LaS (LaSRe) which is a JSON file with filename extension `.lasre`. LaSsynth can also generate 3D modelling files in the [GLTF](https://www.khronos.org/gltf/) format from LaSRe files. The main ideas of this project is provided in the paper [A SAT Scalpel for Lattice Surgery](http://arxiv.org/abs/2404.18369) by Tan, Niu, and Gidney. For files specific to the paper, please refer to [its Zenodo archive](https://zenodo.org/doi/10.5281/zenodo.11051465). ## Installation It is recommended to create a virtual Python environment. Once inside the environment, in this directory, `pip install .` Apart from LaSsynth, this will install a few packages that we need: - `z3-solver` version `4.12.1.0`, from pip - `networkx` default version, from pip - `stim` default version, from pip - `stimzx` from files included in sirectory `./stimzx/`. We copied these files from [here](https://github.com/quantumlib/Stim/tree/0fdddef863cfe777f3f2086a092ba99785725c07/glue/zx). - `ipykernel` default version, from pip, to view the demo Jupyter notebook. We have a dependency [kissat](https://github.com/arminbiere/kissat) which is a SAT solver, not a Python package. It is recommended to install it and find out the directory of the executable `kissat` because we will need it later. LaSsynth can be used without Kissat, in which case it just uses `z3-solver`, but on certain cases Kissat can offer big runtime improvements. ## How to use See the [demo notebook in the docs directory](docs/demo.ipynb) ## Cite this work ```bibtex @inproceedings{tan-niu-gidney_lattice_surgery, author = {Tan, Daniel Bochen and Niu, Murphy Yuezhen and Gidney, Craig}, title = {A {SAT} Scalpel for Lattice Surgery: Representation and Synthesis of Subroutines for Surface-Code Fault-Tolerant Quantum Computing}, shorttitle = {A {SAT} Scalpel for Lattice Surgery}, booktitle = {2024 ACM/IEEE 51st Annual International Symposium on Computer Architecture ({ISCA})}, year = {2024}, url = {http://arxiv.org/abs/2404.18369}, } ``` ================================================ FILE: glue/lattice_surgery/docs/demo.ipynb ================================================ { "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Introduction to the LaSSynth, Lattice Surgery Subroutine Synthesizer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Prerequisites" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "This Jupyter notebook aims at giving a minimal demo on how to use this software.\n", "The reader needs to know how lattice surgery works to fully understand this notebook.\n", "The most direct reference is [our paper](http://arxiv.org/abs/2404.18369) which, in itself, also provides more pointers to background knowledge references.\n", "There are two we would like to mention here.\n", "- [arXiv:1704.08670](https://arxiv.org/abs/1704.08670) links merging and spliting operations in lattice surgery to ZX calculus.\n", "We leverage this connection a lot in our software.\n", "- [arXiv:1808.02892](https://arxiv.org/abs/1808.02892) is helpful because it works through some examples of composing lattice surgery operations to perform quantum computation." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introduction" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "\n", "In what follows, we are assuming a surface code quantum memory with nearest-neighbor connectivity among the qubits (in both the physical and the logical sense).\n", "We perform fault-tolerant quantum computing with lattice surgery between patches of physical qubits.\n", "Some of these patches correspond to logical qubits whereas others can be temporary ancilla during computation.\n", "\n", "Since the logical qubits are in a 2D grid, and there is the time dimension, the compilation problem is laying out operations in a 3D grid to realize certain computation.\n", "We consider only a bounded spacetime and what *can* be realized within the bounds is called a *lattice surgery subroutine* (LaS), because it should be considered as a subroutine in the whole quantum algorithm.\n", "\n", "Because of the connection between lattice surgery and ZX calculus, a LaS can be seen as a ZX diagram with nodes at points in a 3D grid and edges only between nearest neighbors, as seen in the figure below.\n", "If you have worked with ZX calculus, you would know that this ZX diagram is a CNOT.\n", "However, it seems that there are two \"unnecessary\" identity nodes in the middle.\n", "This is because there are other constraints when it comes to realizing the CNOT in a surface code memory.\n", "Our representation of a LaS, the \"pipe diagram\" below, does account for these extra constraints." ] }, { "attachments": { "e1d30228-b8e9-4608-942a-e09fe765c155.png": { "image/png": "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" } }, "cell_type": "markdown", "metadata": {}, "source": [ "![Screenshot from 2023-09-14 05-14-00.png](attachment:e1d30228-b8e9-4608-942a-e09fe765c155.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Pipe diagrams" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "We use a 3D coordinate system with basis I, J, and K.\n", "We avoid using X, Y, and Z because these letters are also used in ZX calculus for different purposes.\n", "K is the time dimension while I and J are the space dimensions.\n", "\n", "If we want the pipe diagram to look like exactly what happens on chip, we need to draw them in scale, e.g, shown on the right below.\n", "There are four patches of surface codes, and only three are used in the computation.\n", "These three are identified by their coordinates in the I-J plane: (1,0), (1,1), and (0,1) from left to right in the picture.\n", "Between the patches, there are some \"gaps\" which are lines on physical qubits.\n", "We can perform merging and splitting of patches with these gaps.\n", "Since the gap is very narrow compared to the patches, but what happens there are really what decides the computation.\n", "Thus, to see these merging and splitting more clearly, we often stretch the gaps in the pipe diagram, resulting in something like the picture on the left below.\n", "\n", "The unit in all three dimension is the code distance.\n", "So, a patch going through a full QEC cycle will become a cube.\n", "These cubes are sitting at integer points in the I-J-K grid.\n", "Nontrivial logical operations are done by connecting these cubes with pipes.\n", "For example, two cubes connected in the I-J plane correspond to performing merging and splitting of two patches; a cube that has a vertical connection below but not above is a logical measurement; etc.\n", "At this point, we see that the problem of compiling LaS is laying out these cubes and pipes in a limited spacetime." ] }, { "attachments": { "dac9ce3a-5960-445f-a5d9-f13e0ac44c80.png": { "image/png": "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" } }, "cell_type": "markdown", "metadata": {}, "source": [ "![Screenshot from 2023-09-14 05-55-57.png](attachment:dac9ce3a-5960-445f-a5d9-f13e0ac44c80.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## LaS Specification" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are some constraints in the construction of these diagrams, e.g., matching colors at intersection of two pipes, but we are not going to introduce them here.\n", "After all, the purpose of a synthesizer is to let a computer consider those constraints instead of humans.\n", "The reader can refer to our paper, or even to the code in this repo for these constraints later on.\n", "What we are going to detail now is how to specify a problem to the compiler, so that the reader can start using the software." ] }, { "attachments": { "4fef384b-1f6b-4712-8dcf-8e3f7b973a2f.png": { "image/png": "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iaQX1tbOwr6srAwyM5PIyEggIyMRtToVqVSOQq4gTe6E1NAcmcwImdQAA6kcU5kBKtVN0P6vPACKKuW8yOVy6tZ1QqPRcP36nzRu3Ahra+uKNkskD7y9vfH29hbe//rrr/z2229MmTKFoUOHFrvftWvXCtWkO3XqREREBCtWrGDmzJlIpVKio6Pp3bs3o0aNokmTJkC2eNqRI0fYtm2bjuNy+PBhGjduLCivtmrVCltbWxYsWMCjR4+E2mSBgYHCMd9++22etp04cQKVSsWqVauEgoReXl6888473L17t8iOy/Lly0lOTuby5cvCzISvr69wY8+hXr16giMQGhrKmjVrCuxbKpVy5coVvQ8AZ86c4eeff2bjxo3Cjb1NmzbExsayYMECJk2ahEKhIC0tjVmzZjF06FChjpG3tzdWVlZ069aN06dP07lzZ2rXri0s+UilUurWrZvnElD79u1p3749kF0ZPC/HZfbs2WRlZXH48GFhrENDQ3FzcyMkJIQDBw4IbVUqFfv378fW1haAjh07sn//fg4cOKDXcbGysuL69esFjmF1QKPRMHjwYO7fv8+nn35aZo7aP8JxOXr0KC1atMDGxqaiTalwnI3gLU00W69uQ9usJ1lZCaSnx6HVajEyssLAwAIzMyeMjWsikRQUfJu9fp9TWTpNk4axtOD13sqCQqGgfn03FAo5f/75J02aNBGdlyrAzZs3CQoKolu3bsybN69Efb1ez6pv377s3buXe/fuUb9+fZycnFi6dClxcXEcOnSIJ0+eoFKpyMzMJCsrS+dYf39/goODmTRpEoGBgXh5eeHr61vsGSFfX18MDAx45513CA4Opl27dlhaWrJ3795i9XfhwgU8PT11llOUSmWp1NgJCgrKc9Zy3759SCQSzM3NdWT+a9SoQVxcHI8ePcLFxYXbt28TGxvLoEGDdI738/MjPDy8TIOY9+7dS48ePXQqVhsYGDBw4ECWL19ORkaGUKfO0NBQcFog23mqV68ekZGRZWZfVSEzM5MTJ07QunVrFi9ezLBhw0o1KDeHau+43L59m8ePH//jZ1tepb4SemalsOnyb3h4NMXevjMKhRJJCWdMjCRGaLSaKjXzAuDs7IxEIuXvv++jVquxsbEpk+lNkZITFxdHnz59sLW15aeffir1zLach5tXY5+++uorPv/8cxwdHXF1dcXExISYmJhc8RLjxo3D3NycRYsWsXTpUiwsLOjVqxczZ86kQYMGRbbF09OTEydOMHv2bCFlukOHDkyZMoWuXbsWub/Hjx+XWUV6hSLvoP9nz56h1Wp1ZppykMvlREVF4eLiItz4Xx9XmUxWrPErCtHR0XrrHTk5OaFWq4mPj883PkYmk6FSqcrSxCrDt99+S48ePWjYsCEjR47k7Nmzpf49rfa/zocPHwayn4ZE/h9rBVhbf0J0dB2eP39IVlbea9hFQYMGlVaVZ6BaZcXJyZE33qjLgwcPiYiIyHdNX6RiyMrKIjAwkJiYGPbs2YOlpWWpnyNH1yWn71OnTjF9+nSWLVvGnTt32L9/P9u3b9eJCXmV4cOHc/36daKioli+fDmXL1+mXbt2OsGlRaFt27YcOXKEuLg4du3ahUKh4K233ipWgUJbW1tiYmKKZUdJMDU1xdDQkJcvX6JSqXK9cmakcmYxKkJbx8rKSm/gbExMDFKptNrFpvz555/Mnz8/V3JCamoqQLEVxo2MjBgxYgS1atVixYoVXLx4UVj2K03+EY5LrVq1aNmyZUWbUulQKptTs+YsoqIsePToRon7k0gkyCVypEhRUbWePqRSKTY2Nri7NyAi4hF3794TnZdKxqRJkzhx4gQ//vgj7u6lU3H89ZvV/v37MTMzE9R0c4S9unfvLrTRarW5HBG1Wk3nzp1ZtmwZkH0THjFiBN9//z0vXrzIU+AtP4KDg3n33XeB7Jt/z5492bdvH6ampuzbt6/I/Xl5eREWFsbTp0+FbZmZmaSkpBS5r6Lg5+dHRkYGO3fu1Nmu1Wp5/vy58N7DwwMzMzP27Nmj0+727du0aNGCM2fO5OrbwsKCxMTEEtvo7+/PwYMHc4nZ7d69m7Zt2xYq5Tk/YmNjS8XO0iIjI4OQkJBcn6OjR48ClIpuz+DBg+nduzezZ88mPDy8xP29SrVeKkpKSiIsLIwBAwaIEu95YGjojI3NVzx48C4ZGcfx8Ch5dVKZRIYMGWmklYKF5YdUKqVmzZr4+LTi/PkLyGQyXFzqVSmhverK999/z4oVK+jXrx+mpqY6aapyuZx27doVq98PP/wQjUZD/fr12blzp5AxlJOdkiPwNXnyZKZMmUJycjIrVqzg0qVLOssucrkcGxsbPvvsM2rWrEmnTp2Ii4tj0aJFGBkZCVkpAMePHyc5ORnIlvzPzMwUbtYODg54enoC2dk9H374IQ4ODowcORKtVsv27dtJTk7Gx8enyNc6ceJEVq9eTZ8+fZg/fz5WVlbMmTMnlxMWFRVFWFgYgPDv6dOnBRkJf3//IumrBAQE4O/vz/vvv098fDzdunUjISGBzz//nJs3bxIeHo5cLsfMzIxPP/2UGTNm4ObmxqBBg3j48CHBwcFkZGQI4/IqrVq1YufOnfTv3x9nZ2cePHiAr6+vUELh+vXrPHjwAIC7d++i0WiEsTYyMhKqJ8+ZM4fdu3cTEBDA3LlzMTAwYMGCBdy6davEqsGPHj2iQYMGGBsb8/Dhw2LFFKnVakJDQ4HsZBNAJ0W9U6dORerX09MTf39/JkyYgEQioUWLFpw8eZKvvvqKESNGFLlKdl58/fXXNGzYkFGjRnHmzJlSW4Kv1o7LyZMnUavVxVoP/ichl1vi7PxfHjwYhURyhnr1PDEwKJ0gWy2aKhf3olQqadasKeHhfyGTZWctlJbwl0jx2L17NwC7du1i165dOvssLS2F9OKi8vH/tXfm4TGd7R//zJ6Z7IkREhJJRATVELGrnSKKV2lVG9X2pUVJUUtVVX9VxNKWarXaUrpoLdWithZvG1uVotaKKkmIbLIvs53fH+lMjUz2ycb5XNdczJnnPOc+J2e5z/Pc9/eeMoVx48aRlJSETCZj/PjxvP7665bfO3bsyLvvvsucOXPYtGkTcrmcUaNGMWrUKDZv3mwlkvbxxx8za9Ysxo8fT15eodPevHlzvv32W6t6SFOmTLE8fMwMHToUKAxy3bBhAwDjx49Hp9OxYMECFixYAICnpydLly7lySefLPe+NmjQgH379vHoo4/Sq1cvJBIJjz/+eBGH4MiRI5ZMKzPmTCkozM65Wy+lNLZt28bMmTOJioqylAzp1KkTW7dutbq2Zs2ahVwuZ968eUyfPh2pVMrgwYNZuXKlzVGPt99+m6FDh/Lwww8D4OzszLFjxwgJCQFgzZo1vPfee1brmI91o0aNLHE1gYGBxMTE8OSTT1qmARs1asTu3bsr7BSbMU+bV+blOScnx2K3mTsD00+dOmXlHJeGRCKxZMWNHTsWo9GIQqFg4sSJxWoUVQRvb2+WLVvGc889xzvvvMPUqVPt0q9EuCMY4b333uPw4cO8+uqrVlHTdZVJkybx7bffcunSJZycnGranFKZO3cuOTk5TJpUcUGtO1m0aDHaQ1t50Maux+XDZs0BXFx6WJYZDJkkJy/F0fE4QUHtUChKVo4sKDhN06YNih1GzcvLIzc/l5DmIQgIyErNUiofeXkCgiCg0djfKTKZTCQnJ3Px4iU8PDwICAhAoym6n4cPH8bXtzFPPDHK7jbcSevWoSgUlatsPWfOHN566y127NjBoEGD7GRZ7WLAgAHs3r3b5m+tWrXijz/+KLLcYDAQFxeHu7t7sXEzBoOB69evU69evVLfbE0mE3FxcTg6OtqtoF9iYiIGgwFvb+8ib61qtbrY+juTJk1i5cqVVssMBgN///03Hh4eVlk01UFubi4JCQl4eHiUqJ1UUFBAXFwc3t7epRagFASBuLg4jEYjvr6+lR4hjYuLw2Aw4OfnZ7cRgrS0NBQKRZkUg+1FTk4OTk5OnD17tsQRFPOxbtCgQanPyfDwcJ5++ulq0UMzq1R36NCBo0ePWv12z7xGfvPNN+zatYt33nnHMu/5/fff89hjj9UJp6U2IJe7oNVOJTV1DRcu7KZ168pPG0mRIZVIa32wrtFoJCcnh5ycHDIzM7l9Ox29XgCcSU7OIz//PC1ahNRoFV+R0omOjmbWrFk2fytuekMulxfRMrHVJiAgoEw2mLVF7ElJGS179+4tVsHaVqaMXC6vsYrYGo2GoKCgUtupVKoy2yiRSPD19a2saRbuTpG3B9XtIJaH8hzr2sI947hER0dz48YNcnNzcXV15dVXX8Xd3Z25c+fWtGl1CrncDS+vl7lxQ8/PP39Khw4jUSpLruFSFiQSCTpBhwKF5Xt1IggCJpMJQSgcpdHpdGRlZXP7dgbp6elkZeUilzujUrmiUmlxdm6OQlHo8Bbkx5OV+htn/zhLqwdaic5LLcYsEnc/URnlYBGRusg947gMGzaMb7/9liNHjrBt2zby8vLYvXs37u7uNW1ancTb+xVSUnw4c2YdzZq1wsXFq9LOhgIFBgzIkCGhehwXo9HI7du3ycnJJysrj4ICA/n5RoxGKQqFI0qlFienQNzdnZBKbV8OUqmEAHdn4jOyOX/uHCEtWti1+JuIiIiIvXFwcGD37t12Hf1bsWKFzVG86uaecVzmzJnDiBEjSExMZMGCBVUy3He/4ek5mrQ0CX/+uZ7gYCkuLvUr1Z9EIkGBAqNgRBCEahl1yc/P58L5C+j0Dri7N0OhcESjcUQmK1+8iEouw8/NiWvp2fzxx1lCQprfc9oOIiIi9w4ymcySNWUvKlrd2t7UnVSPMtCsWTMeeugh0WmxExKJHHf3kSgUT3HmzP/Iy8uyS79SpBgxlt7QDhiNRjyd1Lgq8tHpslAqXcrttJhxUMgI8HDG0VjA2bPnKpzJIiIiIiJScawcF3MtBnO6mkj1UlBQgFJZvHR2TSCVOlCv3hjc3N7i8OGvyMxMqnSgrVmoziAYKBAK7GRp8SikMvzdVUjzr5CV9TeCYDuQsUx9yaT4uTviJjFw7uxZ0tNrj6iUiMi9wqBBgxgwYAADBgzg1KlTNW2OSBWwaNEiy9/YLNxYVqwcF/PQd1UrKYoUxZzVUlsDP93cBtKw4XouXrxOSsrfxWYxlAe5RI4CRbVkHCllMvzcHCD3IpkZf2EyVUzZV28wkJWbh9KkJyclye6KkPcbs2bNYtGiReVap6CgwCKGdjfHjh2z6KyYP5cuXSq1z8TERLuc0/ca2dnZ/P3330UKSlY1e/fuJS8vj1atWtWqeLJ79fwq6ZqqKho3bkyrVq04fvw4586dK9e6Nh2XzMxM+1knUibMx7w2XaR34+zcDTe317lypYCbN+37wM4zVb3KrkYpx9/dAQf9JdJSz2Iw2Na+MGM0GsjKSiY+/gq/XbzMnmMn+T7mInuOSzlw2pfY+EHk5XWvcrtrI6mpqUXk0SvCnj17rFRwy8LTTz+Nn5+fTe0SQRAwGAwWbZaNGzeWekM2Vx42i7yJFIrQdenSBWdnZ/z9/fHy8mLp0qWVcmAMBgOpqallLkY4ePBglixZYjN9esSIEUgkEsvH1dWVhx56yCJUWFXUhvPLXtfenZR0TVUVo0ePZsmSJRXSjLMKzjXnwsfFxdWaIJz7BbOCo1arrWFLikcikaJWP0C9evO5ePFJmjQpoGnT4vUlyoJZUVeFigJTASqpyh6mFotKLsPXVcOt7FvcSsnHs14b5HIHBMFEfn42ycnXSUq6yu3bN0lLS0IiaYyDQyga9TgUyhbI1Q2RS51QShVIJA7ojZ8Cf1apzbWRBg0aEBUVxZIlS6p928899xxt27bFwaGoQGLHjh3ZtGkTADt37iQiIqLU/vz8/Hj11VeLKJPer5w7d45+/frRqlUr9u7di6enJ9u2bWPGjBkkJSURHR1doX6PHTtG165d7SaA6Ovry/r16wFIT0/n22+/ZdiwYaxatYoJEyZUun9b1IbzqyquvZKuqdqIlePSrFkzVCoVp06dslmCXKTqMCt6+vvbV7jK3kgkUhwcAvD338Lly2H4+FymZcuWlVarlEqkyJHbRR7bFoIgYDAaMRhNGE0mVOjRp13k1ytXyMtXkZycgF7viFrdFkfHITg4NMPPrzVyeckqqVWdGSWYh5erUb8vNzfXrlOWZe3PaDSi1+tLvXn27t2b3r1728s81Go1b7zxht36g8IpFo1GU2bl1dzcXORyebFVeXU6HVKptNTSE0lJSezfv5+BAwdWqCYOwPvvv09ubi7bt2+3KP+2bduWmzdvsmzZMl566aUib8mCIJCdnV2tyrAajYbu3f8d8RwyZAj5+fm8/PLLPP3000XOufz8fFQqVbVrSJXn/LL3tXdnvyWdX+W5pvLy8kotOmkOe6iqY211VclkMh544AEyMjKsKoiKVD2nTp1CLpfXmVILSmVDHByacft2OhcuXLRLJWWZRIYRo10qS+fl5ZGUlMT163FcuX6NUxcvc+LUGY4fPczxg/s5uW8Xwh+HyY6XIpXOxc/vJ0JCjtGkyQdotc/g7Ny1VKfF3gj/HENTRgam3FwEvR5jcjKCTodgqtoYg6ysLCZOnIi7u7tFqn7KlClW8W7bt29Hq9Wi1WoxGAysWrXK8v3u2jXZ2dlMnTqV+vXr4+joiKurK88995zNTKz8/Hz++9//4ujoiEajITw8nJMnT1q1mTVrlmVbWq22RCXZspCWlmbVn1arZfny5UXabd68uUg788dWBd3PP/+cwMBAnJ2dcXR0ZPz48TaH9fv06cOYMWPYu3cvLVu2xNHREbVazYEDB6za7dmzh7Zt26LRaFCr1YSHh7Nv375i92vixImMGjWKV199tQJHpZC8vDykUmmRh9zUqVOJjo62itXQ6/XMmTMHT09PXFxc0Gq1vPvuu1brmf925lGWJ554wnIMIyMjK2ynLXr37k1ubi7Xr1+3LHv//fdp2rSp5Rj269evSK2ohQsX2hzt7tSpU4VsLOv5Bfa/9qBs51dZr6mePXsyc+ZMVq9ejY+PDxqNBl9fX7766qsibWNiYmjVqhVOTk44Ozszbdo0hgwZwjPPPFPeQ1giRdz3nj178ttvv3HgwAGeeOIJu25MxDYnT54kNzeXtm1D7VYbozpwdHRk1Kih/Pbbb5YiXwpF5bKizNlGAAbBgFxSutSQXq8nLS2N5OQUUlKSSU1NRSaT4+LiTH5+PldPHKO7pgBHKTjLwEUBjh4gl0Bqni8G566VsrkyGG/dQuLqipCdDSYTsvr1EXQ6JP8cR6mDA1IHByRVXKH62WefZf/+/axcuZLg4GCOHDnCzJkzSUpKstygOnTowMaNGwHo168fERERjB8/HqDISMC4cePYu3cvK1eutATgTZ48mdu3b7NlyxartgcPHsTT05Pvv/+e9PR0Zs6cSf/+/bl06ZJFKj0iIsIiy79582YOHjxYqf3VaDS8+eabQKHjFBUVZSmMeCfh4eFFivQdPHiQ1atX85///Mdq+QcffMCECROYOnUqw4cP59KlS8yYMYPr16+za9cuq7YZGRmcO3eO7777jhEjRjBmzBjS09Ot4jlOnz7N4MGDefTRR1m5ciV5eXksXLiQiIgIjh8/btNx6ty5M4cPH6Zz584VPjZPPvkkn332GSNHjuTDDz+0CJiFhIRYihfe2XbXrl289dZbhIWFsXfvXqZNm0Z+fj4zZ84EYOzYsfTt25c//viDl156iRkzZtCxY0fA/lPjFy5cQCaT4eXlBcBHH33EpEmTWLBgAQMHDiQ+Pp5p06YxaNAgzp8/b4kpzM3NtRmrUqiqXX4ZiLKeX2D/aw/Kdn6V9ZpKT0/nk08+ISwsjNWrVyOTyVi4cCFjxowhPDzcUi4gKSmJAQMG0LBhQ7Zu3YparWbFihXs2rWrSNHOylJkj9u0aYOfnx+HDh2iZ8+edWYEoK5iNBrZunUrMpmM9u3Da9qcciGRgI+PN23aPMcnn3zK7t17ePjh/nZxXgCEYuZHbt++TVxcHNeuJRAfH09ubiYBAQEEBQXRuXMnGjVqhLu7GwqFgj/+OMuW338hvPpGsIvFkHgLmbYexps3kTo7g1yO1MMDZDIkdxT3k3p6IvnHgZVUk8jd3r17iYyMtFQdDg8PRy6Xs3//fotYYP369S3DyeZ6PMUNLzdu3Jg1a9YwbNgwoFCK//z587z99tvodDqrt/kHHniATZs2WYaVmzZtSlhYGJ9//jmTJ08GoGvXrnTtWuhgxsbGVtpxcXBwsNz4c3JyiIqKstnOz8/PSnn06tWrTJw4kc6dO1sVL8zOzmb27Nk8/fTTLFu2DCh0IlxcXHj00Uc5efIkbdu2teo7OTmZw4cP0759e5vbPnDgAHq9nvfee8/iwLVr146nnnqKy5cv23RcXnrpJV566aVyHImi9OrVi/Xr1/Piiy8SGBhI9+7diYyM5IknnrC6tmNiYvjmm29Yt24dY8aMAQpHKFJSUoiOjmbq1KkoFAqCg4MJDg62TAGGhobaZarPaDSSkVEoR5CRkcG2bdt47733eOGFF6wU0xcvXszLL78MwIMPPoggCAwePJgjR47Qp0+fStthi7KeX2D/a89MaedXea4pHx8fdu3aZXmxDgwMpHnz5vz4448Wx2XFihXk5uaya9cuyyhQr169aNKkSYl2VgSbr7OPPfYY0dHRfPrpp8yaNavS8QsixbNt2zZu375NWFibWp1RVBIajYYXX5zEmjUfc/DgQTp16oyjY/nnac11hAwGA3q9Ab1BR25eLimpKaQlp3HrVhJJSUmo1WrCwtoQEtKZhx9uSvfugcWeo2q1A9JqmNIWjEYQBJDJQK9H+Cc633DjBoqAAIwpKYVOilSKvFGjoh3cMdImqYFRtz59+rB+/Xr8/f0ZOHAgwcHBTJgwocJBjosXL0an0/G///2P69evk5uby99/F6bR5+XlWTkuPj4+VnPhbdu2pXHjxhw9etTiuNQGcnJyGDJkCGq1mq1bt1rtQ0xMDBkZGQQEBLBnzx7LcnPM1u+//17EcenYsWOxDxUorEGkVCp56qmnmDx5Ml26dMHNzY3t27fbec+KMnr0aAYMGMC6dev47LPPePrpp1m6dClbt261vLXv2LEDiUSCi4uL1T67u7uTlpbG9evXbU5j2IvLly9bVfNWq9VMmzaN+fPnW5aNGzcOKDz+sbGxpKWlce3aNaDw71kbsPe1Z6a086s8+Pr6Ws0GmM8Bc1IJwG+//UZYWJjV31ypVFaJwrhNxyU0NJT27dvz66+/8s033zBq1Ci7b1gEzp49y/79+3F2dqZz57qfxfXUU08ilUo5c+Y0bdq0KbW9yWQiNzeX7OxscnNzycrKQqfTodfrMRiMGI165HIFrt4utG8fjq9vYxo29MbFxRmpVMrffxsoKBBq3rE2GjFlZoLRiNTNDWNSErJ/5ouVzZsDIPf2rkkLS2XDhg0sWrSIZcuWMXXqVBo3bszo0aOZMWNGhep9md8iAVq1aoWbmxt//ln27CsvLy/S0tLKvd2qQhAEIiMjiY2N5ZdffrFMRZhJTEwEYP78+UUCEuVyuc2YwdJGJsPCwjhw4ADz5s1j8ODBAHTv3p2XX36Zfv36VWZ3yoSHhwdTp05l6tSpHDx4kJEjRzJo0CDOnj2LUqkkMTERQRBsJnLI5XJu3LhRpY5LkyZNLOnPjo6O+Pv7F7kXxMXFERERQWxsLGFhYWi1WtLT06vMpopg72vPTGVHvkvC7MTcGe9048aNMldQryzFBhC88MILJCQkEBMTQ0BAAB06dKgWg+4Xbt26xaeffopMJmP48KHFRnvXJRwcHHj00eHI5Qp++SWG8PB2cEcxxby8PFJTUy2xKJmZmajVapycnPHwcMfHxxsXFxdcXV3x8PDA1dUVNzdXZDIZ+aZ8HKS1NFVPKkWq0SD5R3na5ohKLUetVjN//nzmz5/PhQsX2L59O4sWLeLnn3/m0KFD5eorKyuLESNGMGTIED755BPLDXTZsmVMnz69TH2kpqZW6UOvvMyfP5+tW7fy5ZdfEhYWVuR3J6fCSuIHDhywa7Xmzp07s2/fPrKzszl48CDvv/8+Dz/8MLt27bJ7HRozy5Yto3Xr1vTt29eyrEePHixevJhnnnmGY8eO0a1bN5ycnFCpVGRlZVXpQ7I4HBwcePDBB0tsExkZadFcMU+3nThxgnbt2lm1Mz+IjUZjtb8I2fPaq0m8vLxITk6ulm0V67ioVCqmTZvG7Nmz2bBhA9nZ2XZNQbyfuXr1KqtWraKgoICBAx/G09Ozpk2yG66urowePQpXVxc++OBDgoODOX/+PFev/oXBYCQ4uBkhISH07dub4ODmODlSdwuNAAAgAElEQVQ5IpVKkclkyGSyYoOTVRIVBsGACRNKSS1z8iQSi9NSFzl58iTTpk1j3rx59OjRwxKEKZFImDFjBqmpqUXOUVdXV0t8wd1cvXqVzMxM+vbta/VAKy5T8e6b3YULF7h69SrPP/98JffMPmzdupU33niDmTNnFjv63LVrV1QqFV988UURx+XmzZsVihWcPHkymZmZrFu3DicnJyIiIhg4cCBubm7s2LHDpuNiNBqJi4urVFzBxx9/jCAInDlzxuqFypzlYv6b9u7dm1WrVrFlyxYef/xxSztBEEhKSioyKmWeMijuvKkKTp8+zbBhwyxOC9g+D80ZNXceO4PBUOWjM/a+9mqSNm3a8M4771id70ajsULBzaVRYspGgwYNmDt3rmVuMzk5mUcffbRULQGR4jl69CgbNmwAoG/f3oSENK9hi+yPXC5n6NAhNG3alAsXLuLt3ZDAwEAaNPAqfeVikEgkyJGTaEjETepWe0df6iDNmzcnLi6OF154gVWrVhEcHMyVK1f44osvCAoKsjlc3b59e7Zs2cLw4cPx9/fn6tWrdOvWDY1GY1lnyZIl+Pj44OTkxObNm3n77beBwgyOO+e9T5w4wbhx43jxxRdJS0vjxRdfxNXV1SqF8syZM1y9ehUojG0wmUx89913QOGbt/khfuPGDY4fPw5g+ffnn3+2ZIz06dMHR0dHdDqdJdPHnO1x/vx5S5+hoaH4+flx7tw5IiMjCQwMpF+/fkVUftu1a4ejo6PlXjl37lwcHR155plnUCqVrFu3jiVLlnDu3DmbCrAl0bJlS55//nkaNWrE2LFjEQSBTZs2kZWVVewI+OTJk3n//ff5v//7vwqnRK9YsYKHH36YiIgIZs2aRcOGDTl06BBz586lc+fOlriJYcOG0adPH8aNG8ft27fp378/6enpzJ8/n3PnznHx4kWrZ0VQUBBubm688847lmDdxMTEKn0h7tChA5s3b6Zv376WjB3zcbkzTb1///7IZDKmT5/O66+/TlZWFtHR0UWmK+19ftn72isPZb2mykpUVBTvvfceQ4YMYfHixZZ7QEJCQrn6KQuleiABAQEsWLCA6OhofvnlF06dOkWfPn3o3r17jQwP1kUEQeD06dP88MMPJCQkoFKpGDJkMI0b170phfLQqlVLWrVqadc+G8gbkGsqm9y1oNcjGKq3xkpdRKPRsGfPHiZPnkzfvn0t89a9e/dm9erVNkfB3n77bYYOHcrDDz8MgLOzM8eOHSMkJAS1Ws2WLVsYO3as5aHUpUsX5s6dy/z58zl79qzVCMSQIUNISkqyZMn4+fmxa9cui/gZwJo1a4qkJZuVSBs1amQJEjxy5EiR1MtXXnnF8v/Y2FgCAwPJysoqomT65Zdf8uWXXwKFow7PPvssP/74Izk5OcTGxtp8wJplAMzb8fDwYM6cORbNDn9/f8tDqLyMHz8enU7HggULLHLxnp6eLF261JKBcjfmv11lhL/69u3LwYMHiYqKsuyzSqUiMjKS6Ohoq/Nh27ZtzJw5k6ioKEtx3k6dOrF169YiL7gqlYp169YxduxYy1RN06ZNuXz5coVtLY0PP/yQ0aNHW0bKmjRpwuLFixk3bpyVlktAQABr165l4sSJbNmyBZVKxezZs4sE8Nr7/LL3tVceynpNlRVvb2/27dvHiBEj6NWrF1KplMcee8wqK89eSIQyVrgzGo3s2rWLrVu3kp+fj4ODA6GhoXTo0IGgoKBqVyOsCyQlJXH06FGOHz9u8dxbtmxBt25dK5R1U14WLVqM9tBWHnQq+ltcPmzWHMDFpUeF+791axDLl4+pknS3kigwFaCUKPnz7zzQKQgOVhQqzBoMGNPTkbm7Y7hxA5lWy/HfTrBx4RxGu9muQv1Z3hgMDddV2JakpNUMH36JJ56o2gD21q1DUSgqN0U2Z84c3nrrrRIl1wsKCoiPj8fLy8sSt1EcgiAQFxeH0WjE19fXZmxAfHw8CoWiyLSBLdLS0sjIyMDPz69O6RndjcFg4Nq1a6jVaho2bGiXe2NiYiIGgwFvb+8Sj43JZCIhIYHGjRtXeptQGIuXk5ODr69viSPtubm5JCQk4OHhUerUt16v59q1a2g0GryLCVpXKBQsWrSIadOmVcp+M2lpaWRmZuLn51fi38NgMHD9+nXq169f6vlvb+x97dUU5vPf09PznzhFNx5//HE+/PBDm+0feOABOnbsyJo1a6yW5+fno1ar6dChA0ePHrX6rcxzPjKZjIiICLp168bGjRs5fPgwR48eLdKhiG0aN25E9+4P4eVVv9q22bt3L1bu3IqfA7jdQ7N7KqkKfcINJL9doMCvMfnndKiaBWNMu42svhYkksIAWakUSR2pvVFbUKlUZQ6KlUgklvpmxdGoHIHKHh4eVrEIdRW5XG73wOKyKgVLpVK7OS1AmRxOwDJFWBYUCoVF+6Mk1q5dS0xMDPPnz7epWVMeynpuyeXyasuMuRt7X3vVTUJCAlFRUaxYscKyH0uXLiUzM9Oi53Qny5YtIyYmhmvXrlkECctKuR9nrq6ujB8/nmeeeYbTp09z9OhRmzLe9xqFtTjKF2SkUCgICmpKQEBAtYyw3E14eDhDo2aw74uPeUhIo768UDSutiMYDAg5OQj6QgVdQVeAISEB481EBH3hcLS8vheNurTgrOtNbijdaSwHxZ0xNP+8gdSF/RUREbFmwoQJlmkTVR0OfL+fUKvV3Lx5k4CAANq2bUtSUhJXrlxh+vTplmmtO/Hw8KBRo0aMGTOm3EWdK/werlAoaNeuXZG0snsVg8HA6dMnS29Yyxj8yCO4u7mzb91qwnOu0biW3QMEnQ5jWhqm9IxCp8RoAkFA0OnAWBifIlEqkTdsiOrBB5HckeWQl2eipcmLv4TzXNJdoqmiqRi0KyJyD3B3vSOR2o+HhwcxMTEcPHiQI0eO4OLiQpcuXQgNDbXZfuzYsYwdO7ZC27qHJhBEbKFQKOjSrSsaJyfWLl5Ap5xEWlZCoFcQDOj1KeTmnkIuzy59hTvXNZkwZWRgjI/HcDMRBAGJQoHU1RWJs3OhDL7JhESpQurpiVRdshNiMhX6Ng84tiLOEMdl/WWaKZqhktYy70xERETkPqFHjx706NGjSrchOi73AUqlkg4d2uO7ajVTnnsORW4K6jLGPhqNuWRnHyYz8yfy8s6Sn38Cb28HwsLa0qPH8BIDcwW9Ht2fl9FfuoQpJQWkUmRaLYqQENR9elda2t7R8d/1G8sL5/Uv6f+khTKkTMUZRURERETqHuLd/T6iYcOGvL5kCeui30KbdAU0YDBkYDRmYDJlYzRmIpUmI5H8hVR6GYUiDkfHHB54wIemTQPx9h6Cv38ULi6FFQtNefkYk5IQDP/EouTlYbhxs1D+3mRCIpch92mEulfPwlEVOwecZGebMBrB1bXQgWksb0y+UMA53Xn8FU1wkbrYdXsiIiIiIjWP6LjcZzRv3pwnoqaz7YvPKbi6BYViD+7uOjw9BTw8jGi1KrRaLVptO3x9/4Ozc2FanqDTYUxOxnj1Lwr+cVQsxZv/8UckKgcUzYKQ1auHpBrS8+RyCVKpYKmgChCkaEq8IZ5Y/RV85fbLrhCpGxw7dox33nnHatnrr79OcHBwieslJiZSv379WpuGXVBQQFZWlpW2jYjI/YrouNxnSKVSWrduTcOGM0hKSkKhUODk5ISTkyNOTk7I5XIEvR5jcgqGM6fJSSvMGJMoFMjqa5F61kOiKDxtJA4OSJ1dLN+rG50ul2XLlqHX68jLyyM0NJQxY56ikbwRzlIXruhjSTPWnkJ99xNardaiJgqF6bydOnXitddeKzZYzx6Yq4tDobT7oUOHmDRpUomOy8WLF2nRogXz589n7ty5lbYhNTUVtVpdbiXTknj66af5/vvvSU1NxUFM8Re5zxEdl/sQqVSKl1d9i6aMKSsL3fkL5F/5C1lKMgYXF2Q+PihbtcKhS9n0I6obo9HIlClTadGiNS+/PBGj0chzz43DYNDz7LPP4Cp1obmiOUcNv2IQDDVt7n3JI488wtSpUxEEgRs3bvD+++/TsWNHDh8+TNu2batkmx07dmTTpk0A7Ny5k4iIiFLX8fPz49VXXy2idFpRGjRoQFRUFEuWLLFLfwDPPfccbdu2FZ0WERFEx+WeRzCZEHJyMeVkF6bhCAKm7BxMyckY/9HfkarVyP38CIw5hNemzZzdsgmdj21Fy9rC999v59y5MyxduhgoFEh87LGRvP76G/Tv359GjXzQSDWEq8K4LPkSEKX/qxtvb2+6d+9u+f6f//yHkJAQpk2bxoEDB4q0z8/PR6VSVbsKt1qt5o033ihT29zcXLuOpNzZr1wuL7ZKfO/evctc0ycvLw+1Wl1im5ycHDQaTanHWq/Xs23bNsLCwmpMmE1E5G5q54SuSIUx5eWhv/IXBSd/p+DECXQnf0d/6RLG+ASMN29ivHULAEWLEBwHR+A4OAJ1n94ogpoiODshMRiot3NnDe9F6WzZ8i3+/v6o1f8G4LZu/QAGg4Hvv99eg5aJFIeDgwOdO3fmwoULVsvff/99mjZtikajQa1W069fP6s6MgALFy5Eq9UW6bNTp05ERkaW25a0tLR/Yrn+/ZjrC91NVlYWEydOxN3dHUdHR+rVq8eUKVMs1ZIBtm/fbunHYDCwatUqy3dbaqh9+vRhzJgx7N27l5YtW+Lo6IharbZy6GbNmmVlX3HquT179mTmzJmsXr0aHx8fNBoNvr6+fPXVV0XaxsTE0KpVK5ycnHB2dmbatGkMGTLEqqDlnXz00UeMHDnSbqNRIiL2QBxxqePor17F8PffmLILi4FJHNTIvRsUSt9LpSCRIHV0ROLohERWsp+aMnAgDdd8gsfOXdz473O1VnY2PT2dS5cuER7eAb3+3+Xe3t7I5XJ+//33mjNOpEQuXrxoVZ/mo48+YtKkSSxYsICBAwcSHx/PtGnTGDRoEOfPn8fRsVB0KDc31ypmxkx6ejpZWeVTtIZCifo333wTKBzpiYqKslTxvZtnn32W/fv3s3LlSkuF4ZkzZ5KUlGRxDjp06MDGjRsB6NevHxEREYwfPx7AZo2fjIwMzp07x3fffceIESMYM2YM6enpVrL5ERER+Pv7A7B582YOHjxo07709HQ++eQTwsLCWL16NTKZjIULFzJmzBjCw8Mt8vpJSUkMGDCAhg0bsnXrVtRqNStWrGDXrl1FCgeaefDBB2ncuLHN0Z6ff/6ZefPm2VwPYO/evWIhXpEqQXRc6hCGpGR0Z85gTLiBUJAPgKJFC5ShbZC5u1W6f10jH7LbtsHpxEkcz54j54FWle6zKrh58yaCIODh4WpJhTbj6KipkjLqIuWnoKCAjIwMBEEgMTGRt99+m5MnT1piUMwsXryYl19+GSh8UAqCwODBgzly5Ah9+vSpEtscHBwsjkVOTg5RUVHFtt27dy+RkZGWiszh4eHI5XL2799vyWirX7++5eEulUrx8/MrdWonOTmZw4cP0759e5u/d+3ala5duwKFVYeLc1wAfHx82LVrlyUrKjAwkObNm/Pjjz9aHJcVK1aQm5vLrl27LKNAvXr1KlGLqWvXrly/ft3mby4uLrRqVfw9Qiy8K1JViI5LLUHQGzBlZRXW6DEawGjElJuL6XY6wj9vglIXF5QtWyJ7qJuV9L09SYkYhNOJk3js3VtrHZeMjEwANBpnMjNNuLj867woFEry8vJryjSRO1i7di1r1661fPf29uazzz6zersfN24cAL///juxsbGkpaVx7do1oNChqA306dOH9evX4+/vz8CBAwkODmbChAlMmDChUv127NixWKelvPj6+lqlcptHbuLi4izLfvvtN8LCwqymrpRKJa6urhXaZmhoKCtXrqygxSIiFUd0XGoAwWTClJ6OIT4BwTxPLpEgUShAoQCpBIlMjtTFFUVAAFLHSmj0l5PbvXvhu2QZHnv2ET9lMkIJpexrCrm8UCNGEATulovR63XVXo5exDaPPvoor776KgD16tXDx8enSJu4uDgiIiKIjY0lLCwMrVZLenp6dZtaIhs2bGDRokUsW7aMqVOn0rhxY0aPHs2MGTNwd3evcL9VOY1idmLMhQoBbty4YdcA24SEBI4ePVrs78OGDau1ujgidZva91S6BxEMBvSxV9BfvoygKwCJFJmHB3LfxkjN8/0yKRKVAxKVslrE24rDpNGQ0bEDmj8vo7iVVCuzi8xBijk5WahU/w5HF1bwzsHPz8+qfb6YUFQj1KtXjwcffLDENpGRkRgMBuLi4vDw8ADgxIkTRYq3mh+ARqMRWTVfH2q1mvnz5zN//nwuXLjA9u3bWbRoET///DOHDh2qVlsqg5eXF8nJyXbr7+jRo8XGxsC/WWIiIvZGdFwqiSAI/1b7EwqlZI0pqYWxKCkpCEYjErkcZfPmqPv2QVoHdBiuz56Jwc2t1gbn+vj44Onpyc2bSWRmmvDwKHyQJScnYzQarTRCwsPDed8niP2J5+juKiCrnbt033L69GmGDRtmcVqgMIbpbszOalxcnCUmw2AwVPnozMmTJ5k2bRrz5s2jR48ehISEEBISgkQiYcaMGaSmpuLp6Wm1jqurKxkZGVVqV0Vo06YN77zzDjdv3qRhw4ZAoSNYWnBzXFwcXl5eRVK1hw8fXnj/ExGpZkTHpZyY8gsQMjMR8vMKNVL0BoTsbEzp6Qg6HQBSV1dU7cORurnV6OhJRTFUYvi7OpBIJAwc+DBff70JhUIPFB7jCxcuIJFI6NvXOqBz+fKlLH3zTY7E/ko7Bx0O4uh1raFDhw5s3ryZvn37WjJ2zNNLubm5lnb9+/dHJpMxffp0Xn/9dbKysoiOjiYtzVoZ+caNGxw/fhzA8u/PP/9syUjq06cPjo6O6HQ6du3aBWDJJjp//jzfffcdUBi/4efnR/PmzYmLi+OFF15g1apVBAcHc+XKFb744guCgoJsThW1b9+eLVu2MHz4cPz9/bl69SrdunUrt/7LmTNnuHr1KgCXL1/GZDJZ7HNwcKB///7l6i8qKor33nuPIUOGsHjxYtzd3VmyZEmJweybNm1i5MiR9O7dmx9//LFc2xMRqSpEx6WMCAYD+YePgFRaGBgrlxXGpcgVyLzqowgJQeogDotWF2PGjGHnzl3ExBxiwIDC7I3vv9/BI48MJji4mVVbd3d3Jk2fzjdrPyHmyB56OOqQiyMvtYIPP/yQ0aNHM2rUKACaNGnC4sWLGTdunJWWS0BAAGvXrmXixIls2bIFlUrF7NmziwTwHjlypMj0xSuvvGL5f2xsLIGBgWRlZRXRJvnyyy/58ssvAfj444959tln0Wg07Nmzh8mTJ9O3b19LzEjv3r1ZvXq1zRiOt99+m6FDh/Lwww8D4OzszLFjxwgJCSnXsVmzZg3vvfee1TKzzY0aNbIKvC0L3t7e7Nu3jxEjRtCrVy+kUimPPfZYkanVOzHvr5ghJFKbkAjiWF+ZMOh1nNz/ExK5HInaARQKJPd64JnJVKgFU0s5efICy5dHM2hQf2JjryCTSZkx42WbuhmCIJCZmck3X3xB7LbPecTdiEoKn+WNwdBwXbm2Kwgm8vMvk5d3gdTU9Uyf3oaHHy7f2295ad06FIWicplkc+bM4a233mLHjh0MGjTITpbZh7S0NDIzM/Hz8yvxIWkwGLh+/Tr169ev9iDsgoIC4uPj8fLyKnXbgiAQFxeH0WjE19e32uNySsJgMHDt2jU8PT1xdXXFzc2Nxx9/nA8//NBm+4SEBOrXry9qsohUK/n5+ajVajp06FAkCFwccSkrEimyep6lt7sHUMYn0HDdZ0jz8ri64P9q2pxiadOmOZ9++hFxcXEMGPAwbm7Fa9lIJBJcXV3574QJfCRXsP6bz3jCQ2+zrSCYEAQDYEQQDBgMqej1F8nLO05e3nEKCk7i5+dJ27bNaN68RZU7LfcDHh4eVnEuxSGXy2tMel6lUtlUwbWFRCLB19e3ii0qHwkJCURFRbFixQrLfixdupTMzEyGDRtW7Hq2ssFERGoS0XERscL5t99w/+kA9bZ9x+1ePWvanBLJyxMwmWQEBpbvQTZu3H/51tOTH776lHSDHqUuEYPhFnp9EgZDGnJ5ChqX20jVSbir0vBwL8DX15eGDRvi7x+Jl9d0sdidSJ1DrVZz8+ZNAgICaNu2LUlJSVy5coXp06dbprVEROoCouNSBdTb9h3uP/6E88lC6XmTgwPJ/xlGyiMRFNSyt7C7yWrXjqx27fDYu7emTalSBgwaiATYsfN3TMI8GjVS0bChCq1WiaenM271PUlyNBHs0ZlAd7G4nEjdx8PDg5iYGA4ePMiRI0dwcXGhS5cuhIaG1rRpIiLlQnRcqoCUoUPIbB/OA48UDr/+tXwpmR071LBV5cPoUHJ12dqAg4OEikZoOTg4MHBwBA/17IHJJKBUKlEqFSgUCmQyGYIgoEPHkfxjOBucqC+vb1/jRURqiB49etCjR4+aNkNEpMKIjksVofpHi6LAt3Gdc1qAWh2UayY7W8BoBHf3imU8KJXKYuMqJBIJKlT0UD/E/rwDtMBEfVl9pJLaf1xERERE7mXEu3AV4XysUEMi/aGHatiSexcHBwlqddWmaRowkN4og8ktXiLOFIdJMJW+koiIiIhIlSE6LlWE66HDAGR06VzDlohUlH3uPzKy1eO81WQhcU5x/Or4G3HG8mlniFQdx48fp6CgoKbNqFOkpaVZ6eOUh9TUVKvaR7UJnU7Hr7/+WtNmVDmHDh3CaCy9hklKSkqt/VvZA9FxqQLkKalo/vwTk0ZDdmjJtVpEKk5+vkBenv1liNJlGSz0XczsgDlcV8XRK70nm85+zZC8wSQbUjie/5vdtylSPn744Qf69u1bbhG2+534+Hgeeughfv755zKvo9PpeOqpp2jcuDF///23ZXlWVhajRo2y+nz77bdVYHXJFBQUMHDgQNasWVPt265uVq1axfDhwzEYDCW2Gzt2LIGBgZw+fbqaLKteRMelCnA9dBgEgcz24QiiaFOV4eQkwcXF/lNFr/vPZ4t2K03ym/DhpQ+IvrKIRjoflBIl7RzCcJe580fBH+gF2zowIlXL1atXGTlyJJ988glNmzatdH87d+5Eq9Xa/G306NEolUquXLlitfyHH35AIpHw5ptvFlknJycHiUTCuXPnivzWo0cPtFqtpcyAmdu3b+Pk5FSsnkp4eDirVq0qsvzEiRNIJJJiP6tXr7Zq37p1a6Kjoxk2bBi3bt2yua27efPNNzlw4ADXrl0roqFjMBgwGAzk5+ezceNGzp8/X6Y+7cnUqVPJyMgoojJcGzAYDKSmpqLX2+desWbNGi5cuMC8efNKbLd9+3YGDx7MsGHDyjRCU9cQHZcqwPXwP9NEnTvVsCX3NuUZcTFR9mHTCQkvEBU/mY3nviAsu22R3wPk/mikGi7rL6MTdGXuV8Q+TJkyhR49ejB8+PAq31Z0dDRKpZI5c+ZYlplMJmbOnIm/vz/Tp08vV3+vvfYaKSkprFu3zmr5Bx98QE5ODnPnzi1Xf4GBgWzbtq3IZ/DgwUilUquCo2aeffZZAgMDefnll8u0jXXr1jF06NAizp2zszObNm1i06ZNfP755+Wy21789ttvrF69mvfff79WVqI+duwY9erVY6+d5CUcHR1Zvnw50dHR/PnnnyW2feaZZ7h69SoHDx60y7ZrE6LjYm8MBlyOFc61ZnQW41uqEnNh7pL43ekUE4JeZLN2S5n7bZYXxJO3RiMvJulOKpHSRN4EF6krf+j+KI/JIpXkwoUL7Nixg6ioqGrZno+PD3PmzOGbb76xFG1ct24dZ8+eZfny5eUWIuzVqxddu3Zl+fLllhiEgoICVq5cyeDBg206GiXh5ubGkCFDrD6tW7fmp59+4vnnn6d9+/ZF1pFIJERFRfHVV1+VOtWm0+mIj4+nfn37yAEIglBqNeo725YWw7R06VI6depEeHh4qf0ZjUarwp3FodPpyM/PL5ONUOjIlnWfSqMs9g0aNIiAgACWL19eYjtzBfDY2Fi72FabEB0XO+N0+gyy7GzyAgPRN/CqaXMqjESnQ2qn4c2qQqOR4ORU/FTRDs+drG24jl9dfsXekTAyiQwfmTfeMh8O5v3Pzr2LFMdXX32Fp6cnvXr1qrZtTp06lYCAAGbMmEFubi6vvfYaffr0KVKksazMmzeP2NhYS6XnL774gsTERF577TW72DthwgTc3NxYuHBhsW2GDh2KQqFg8+bNJfal1+sRBKHStZb0ej1z5szB09MTFxcXtFot7777rlWbzMxMtFotX3/9NVOmTMHZ2RmNRkO7du04efJkkT7z8vLYtm0bI0eOLHa73bp1Y/z48URGRqLRaHB0dKRDhw6cOXOmSNvTp0/Ts2dPHBwcUKvVhIeH87//Fb229+7di1ar5dixYzz//PM4OTnh4uJCz57/Ko3PmjULrVZrqQn2xBNPoNVq0Wq1REZGWvWXlZXFxIkTcXd3x9HRkXr16jFlyhSys7OL3a9Ro0bx9ddflxiAa67ZVh4nrK4gOi52xpxNlNmlbk4Tqf+8jPfqD1GkpeF88iT1N36NIiWlps2ySXa2QGZm8S5JROogJsdPqrLtSyQSGsob0FQeyLH8X8kz5SHWLK1a9u/fT7t27WxWZa4qVCoVb7/9NgcPHmTo0KHcunWryEO3PPTp04fOnTuzdOlSBEFg2bJlDBo0iHbt2lXa1q+++ordu3ezYsUKXFxcim2n0WgIDQ1l//79JfaXnp4OFE4LVYYnn3ySlStX8sYbb3D48GEmTZrEtGnTWLx4saWNIAikpKQwadIkBEFg27ZtbNiwgVu3bvH4448XCUg9dOgQBQUFdOhQvE5Weno6H330EQ4ODuzevZu1a9dy+fJlBgwYYDWac/PmTbp3705WVhb79qTjUJUAAA43SURBVO3j559/xs3Njb59+xYJcNXr9aSkpDBixAhOnDjBjBkzWLhwIaNHj7a0GTt2LBs3buT1118HYMaMGWzcuJGNGzcWmV589tln+frrr1m5ciW//vorr732Gh999BH//e9/i92vzp07k56ezu+//15sG2dnZ2QyWYkOUF1FFKCzM3U9DTqvWRB5zYK48fz4mjalVJRKSalTRQ7GqlcA9pH7IJFIOa+/QHNFMI4Sxyrf5v3KxYsXrR4Q1cXgwYPp378/e/bsISoqihYtWlSqv3nz5tG/f3/mzp3L+fPnWbt2baVtTE9P56WXXmLw4MFliv8JCAgoNYX4q6++QiaT0a9fvwrbFRMTwzfffMO6desYM2YMAJ06dSIlJYXo6GimTp1qVXn6scceY8WKFZbvWVlZPP/888TGxtK8eXPL8osXLwLQrFmzErffuXNnPvroI8v3+vXrM2jQIL788kvGjh0LFP49jEYje/futYhS7ty5k6CgIGbNmsWuXbuK9NumTRu+/fZbm050cHAwwcHBlqnE0NBQevfubdO+vXv3EhkZyZNPPgkUBmLL5XL279+PIAg2q6Wbg6QvXbpEWFiYzX6VSiU9e/bk22+/ZcqUKdVeTb0qEUdc7IjiVhLqK1cwOWrIflBMg65qpFIobQRbStUK1EHhyIuP3Bt/eRNOFJwk3Zhe5du8HzEYDKSlpeHu7l7t274zNiMzM7PS/fXr14+OHTuyYMECBgwYYDMWpbzMmDGDnJwcm9lHtvDw8Cg2s+iXX34hLCyMBQsWsH79eiuHobzs2LEDiUSCi4sLe/bssXzc3d1JS0vj+vXrVu39/f2tvgcFBQEUicdJSkqyVH0vibvVsQcMGICrq6vV9NP27dsZNGiQVVulUsmIESP48ccfbcbavPjii3YZ+evTpw/r16/n7bff5tKlS0DhdN/mzZttOi137lNpmWHmFHGzA3avIDoudsT9wAEAssLCEOTiYFZVk5srkJ1de0SWPGQedHLoyG+6E2VyXj5rsJ4nWjxJ57bd6Ny2GwNaR/BOoxVccfirGqyte5jjLeQ1cG1t2LCBw4cPM2rUKNauXWsXsbOXXnrJ6t/KEBMTw8cff8ybb75J48aNy7SOUqksNv7B2dmZgIAAdDpdpbVyEhMTEQSBkSNHEhERYfksXLgQuVzOjRs3SlzfHF9zdzxHQUEBMpms3M6DRCLBx8fHartJSUn4+PgUaevr64vBYOD27dtFflPYSepiw4YNTJkyhWXLltG8eXN8fX2ZPXu2zW2aUSqVQOnxK5mZmSQnJ+Pj42Nz/+oqouNiL4xGtN9uAyDvrjcGkaqhUMeldp3CComCrqouXDbEkmZMKzHmZUxiJPOvzkMn0aGT6FhyZRFR8ZMJzBerUdtCrVbj6OhotwyOspKZmcnMmTMZMGAAGzZsICQkhBdffLHS8Uz16tWz+rei6HQ6xo8fT1hYGJMmlT2mKyMjo9hsodDQUDZt2sQHH3zArFmzbAbHlhUnJydUKhW5ubno9foin27dulWoX61Wi8FgKKKJUxZu3bqFl9e/yRMeHh4kJSUVaZecnIxUKi11VKcyqNVq5s+fT3x8POfPn2fSpEl8+OGHREREFLtORkYGQKnZXubU9+PHj/Piiy/a1e6apHbd9esgsvQMGqxdR/D4F3C4UvimrN22jUYrVlq+i1QNOp1AQUHtC4Z1kDrQVB7IdUMcyaaSA5vjVPEAtMgNoVVOq+owr07TuHFjEhISytS2oKDALsq68+fPJzk5mSVLliCTyViyZAm//vprpeNSzCMFlZ1uWLx4MZcuXeKjjz4qV/ZPXFwcjRo1KrFN//79AcqltHs3vXv3pqCggC1brCUJBEEoswieLcwjS6WdD3dP8xw5coTU1FRCQ0Mty/r06cPu3buLpCNv27aNzp07o1ZXLFbO7PCYHY27OXnyJD179rRorYSEhDBjxgxmz57N4cOHSU1Ntbme+bwu6e+Xl5fHiRMn6Nu3b7FTTnUVcT6jkhjdXEkc+zSJY5+uaVPuO4zGwk9txF3mjkqi4pzuPCnGZFoobQdzHnU9CkD3292r07w6y0MPPURMTEyZ2g4cOJD9+/fzzTffMGLEiApt78KFC6xYsYJnn32Wli1bWvrt3bs3s2fPZvjw4RV+G7eH4/LXX3+xYMECunXrxo0bN4pMu/j4+Fg9oO/k1KlTPP300yX2bw7ozMnJKfKbTqezBK2aRz3Onz9vSfMODQ3Fz8+PYcOG0adPH8aNG8ft27fp378/6enpzJ8/n3PnznHx4sUKTf899E8B299//71EBeWffvqJpUuXMnLkSK5fv05kZCRardYSDAuF6sDbtm1j2LBhvPXWWyiVSqKjozl//nylnLagoCDc3Nx45513LMG6iYmJlkDd5s2bExcXxwsvvMCqVasIDg7mypUrfPHFFwQFBRUbz/X7778jl8vpXIJWWG5uLoIgoNFoKmx/bUUccRGpszg5SXF1rb2nsEaqIdyhHSYEzuqKyr8DHHItzELrklE3s9Cqm0ceeYTz58/z11+lj2aaYyIq87b54osv4uDgwBtvvGG1fMmSJaSkpJQqvV4SZoelMvadO3eOgoICDh48aBU/Yv4sW7bM5nonTpzg1q1bDB48uMT+VSoVcrncZnBqVlYWQ4cOZejQoYwaNQqAL7/80rLsxx9/tLTdtm0bkZGRREVFERgYSFhYGMnJyWzdurXCMUsNGzakXbt27Ny5s8R2YWFh7Nq1Cz8/P8u01I8//mj1QA8MDCQmJobr16/Trl07WrduzcGDB9m9ezddunSpkH1QePzWrVtHbGws7dq1o1WrVjz//POW3zUaDXv27CEgIIC+ffvSqFEjunfvTr169fjhhx+KdWp/+OEHevToUWKautmZvJeyicyIIy4idZbsbBNGI7XaeQEIUTTnsj6Wv/RXaSL3QyoptPdP9WVuKZKop69HcF7JKZ0ihQwYMICmTZvy8ccf89Zbb5XYds+ePaSmploURCvCnQ/fO2nTpk2la8B07dq10nEygwcPrlAfa9euJSwsrNSHslwup3nz5pZslzvx9PQs87YdHR157733iI6OJiEhAQ8PDzw9Pa3auLq62uyve/fuxW5n8uTJTJgwgeXLlxfJHjLj5eXF9u3bSU5OJjMzE39/f5sOQVhYGBcuXCAuLg6DwYCfn5/NdoMGDSrXMR8yZAi3bt3i2rVraDQavL29rX4PDAxk586dFBQUEB8fj5eXV4nORkJCAnv37i0y9XY3Fy5cAKBVq3tvCrp23/FFREpALpdQWmC/TlKo/quX1lxNIZlERoDCH51QwJ/6y5blh1wPAdA5oxOSakjbvheQSqUsW7aMVatWlRrboFQqK+W03KtcvnyZtWvXFjsaczcvvfQS3333XZmn6EpCo9EQFBRUxGmpKE888QQhISEsWLCg1LZarZbAwMBSp+YaN25crHNTURQKBU2bNi3itNyJSqUiMDCw1BGS1157ja5du1pUeW2Rnp7OggULaN++fYkCfXUV0XERqbPI5aBQFP/AP+xyhI98CnUMttfbwXf1vi9XsUV7opQoCVQEIkPKsfxjAMSI00QV4pFHHiEyMpJHH33ULpoq9xPJycn85z//Ydq0aXTvXra4qmeeeYYFCxYwbNgwLl++XPoK1YhMJuPzzz9n/fr1bNy4sabNqXI++OAD9uzZw7p160qcYhw+fDgqlYodO3ZUo3XVhzhVJFJnyc4WMBgE6tWznUnRObMTnTM78RZvVrNltlFIFAQpg4g3xPOz8RfOOp1FLsjpkHnvvRFVNe+++y6zZ88mKyurRGn7stC+fXu+/vprO1mGRV7ez8/Pbn2uWLHCLjocSUlJjBo1ildeeaVc602bNo1p06ZVevtVQbNmzdi3bx979uwp8tsrr7xS6fOjNnHr1i32799fqlbPTz/9VE0W1Qyi4yJSZ3FykiAIdW+KxVvmzT7XHzFipE12KE4msURAeZFKpVZ1biqDVqu1a9FGmUxmSSO2F5062af2WcuWLS3ZUfcSoaGhNrOnzEHD9wrm2kf3O+JUkUidxWAAvb726biUhlQi5arH3wAEpgaSYyqaaioiIiIiYhvRcRGps+j1Arqai7mtMEaMHPlHv2V4xn84pTslOi8iIiIiZUR0XETqLIWS/7V7quiA20FOOFnLpR9z+ZXb8tt0zOxAkKEpDygf4KL+Irmm3GJ6ERERERExIzouInWWwiKLtXeq6Lt63/Ny4EwmBE8iS1ZYX8eEiU8arkUmyBh/YxwALlIXGsgacEn/JynGkksEiIiIiNzviMG5InUWO8osVAmJykQAwjPb4WwsVLj82PsTzjidYVrcSzxwR20iH7kPSomS64Y4CoQCfOT3TiVXEREREXsiOi4idRalUkIlhUerlCHJQ9jh+QN50nw21v+aIy5HiXeIZ+mVxXRPL6qhoZVpcZY6c0F3Eb1goInCfum0IiIiIvcKouMiUmfJzhYwGgU8PMpeEbc6aaD3YsvZbzjldJocWQ4TEl6gWV5QiSq5DhIHWihDOKc7j9qoxktWctl6ERERkfsN0XERqbNoNLVfx0UpKGmfFV6udVQSFSGK5pzXX8AkmGgob1BF1omIiIjUPUTHRaTOYjIVfu5F1FI1LRQhXNRfIsuUSWuKimtVlIiICLv1JSIiIlLdiI6LSJ1FpxMwGkGtrt2jLhVFLVXTStmSy/pY8oUCFCgr1V/Hjh3LXJ9GREREpDbQs2fPIsskQmXrqouIiIiIiIiIVBO1PKFURERERERERORfRMdFREREREREpM4gOi4iIiIiIiIidQbRcRERERERERGpM4iOi4iIiIiIiEid4f8ByoWM4yWEBMwAAAAASUVORK5CYII=" } }, "cell_type": "markdown", "metadata": {}, "source": [ "![Screenshot from 2023-09-14 06-26-00.png](attachment:4fef384b-1f6b-4712-8dcf-8e3f7b973a2f.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The LaS specification is shown in part b) of the figure above.\n", "`max_i`, `max_j` and `max_k` are the bounds of spacetime.\n", "In our example, they are 2, 2, and 3, which means all the cubes and pipes are within 2x2x3 volume." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "input_dict = {\n", " \"max_i\": 2, \"max_j\": 2, \"max_k\": 3\n", "}" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "There are certain *ports* that connects the LaS to the outside (which makes sense since a subroutine in classical computing always has some arguements and some returns).\n", "In this example, there are two ports on the bottom floor corresponding to the two qubits before the CNOT; then, there is some manipulation of these two qubits implmented with the pipes in the gray box; on the top floor, the two ports are the qubits after going through the CNOT.\n", "\n", "We need to provide three things to specify each port.\n", "Let us look at the port for the output of control qubit in the CNOT indicated in the callout in part a) of the figure above.\n", "In the code block below, it is the third port in `input_dict[\"ports\"]`.\n", "- Its `location` is `[1,0,3]` because that is where the information is going out of the LSS.\n", "- In general, the pipe connecting a port can also be in I, J or K direction.\n", "Additionally, we need another character (`-` or `+`) to indicate the direction from the port to the other parts of the LaS.\n", "In this example, the pipe is in the K direction, and we need to go downward from `[1, 0, 3]` to everything else, so the `direction` of the port is `-K`.\n", "- Finally, surface code patches have a space orientation of the X and Z boundaries indicated by red and blue above.\n", "We provide which one of I, J, and K is orthogonal to the face of Z boundary (blue).\n", "In this example, it is J that is orthogonal to the blue faces, so the `z_basis_direction` of this port is `J`." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "input_dict[\"ports\"] = [\n", " {\"location\": [1, 0, 0], \"direction\": \"+K\", \"z_basis_direction\": \"J\"},\n", " {\"location\": [0, 1, 0], \"direction\": \"+K\", \"z_basis_direction\": \"J\"},\n", " {\"location\": [1, 0, 3], \"direction\": \"-K\", \"z_basis_direction\": \"J\"},\n", " {\"location\": [0, 1, 3], \"direction\": \"-K\", \"z_basis_direction\": \"J\"},\n", "]" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we need to provide the stabilizer constraints on the ports to ensure that the LaS indeed realizes the logical operations we want to perform.\n", "Although intuitively there are input and output ports for the CNOT, in a LaS, there is no inherent distinction between inputs and outputs.\n", "What matters is that the given stabilizers have to match the ordering of the ports.\n", "Our ordering is (control qubit input, target qubit input, control qubit output, target qubit output), so the correct stabilizers are ZIZI, IZZZ, XIXX, and IXIX.\n", "If we change the ordering of the `\"ports\"` list above, we also need to change the stabilizers." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "input_dict[\"stabilizers\"] = [\"Z.Z.\", \".ZZZ\", \"X.XX\", \".X.X\"]\n", "# Note that we use a . for an identity in a stabilizer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Solving LaS" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "By now we have finished preparing the specification of the LaS.\n", "We can use our software package `lassynth`, specifically the class `LatticeSurgerySynthesizer` to solve the problem.\n", "When we invoke `solve` method, the synthesizer gives us a solution with respect to a `specification`." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from lassynth import LatticeSurgerySynthesizer\n", "\n", "las_synth = LatticeSurgerySynthesizer()\n", "result = las_synth.solve(specification=input_dict)\n", "result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you have noticed, the return value is of a class `LatticeSurgerySolution`.\n", "We implement a few methods for this class to help us further manipulate the solution.\n", "To see the \"raw\" solution, i.e., LaSRe (lattice surgery subroutine representation) in the paper, you can access the `lasre` of this result.\n", "Due to technical reasons, the `ports` here is another encoding compared to the `ports` in the specification.\n", "Intersted readers can refer to comments in the code to understand this encoding, but it is not too important in this notebook." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'n_i': 2,\n", " 'n_j': 2,\n", " 'n_k': 3,\n", " 'n_p': 4,\n", " 'n_s': 4,\n", " 'ports': [{'i': 1, 'j': 0, 'k': 0, 'd': 'K', 'e': '-', 'c': 1},\n", " {'i': 0, 'j': 1, 'k': 0, 'd': 'K', 'e': '-', 'c': 1},\n", " {'i': 1, 'j': 0, 'k': 2, 'd': 'K', 'e': '+', 'c': 1},\n", " {'i': 0, 'j': 1, 'k': 2, 'd': 'K', 'e': '+', 'c': 1}],\n", " 'stabs': [[{'KI': 0, 'KJ': 1},\n", " {'KI': 0, 'KJ': 0},\n", " {'KI': 0, 'KJ': 1},\n", " {'KI': 0, 'KJ': 0}],\n", " [{'KI': 0, 'KJ': 0},\n", " {'KI': 0, 'KJ': 1},\n", " {'KI': 0, 'KJ': 1},\n", " {'KI': 0, 'KJ': 1}],\n", " [{'KI': 1, 'KJ': 0},\n", " {'KI': 0, 'KJ': 0},\n", " {'KI': 1, 'KJ': 0},\n", " {'KI': 1, 'KJ': 0}],\n", " [{'KI': 0, 'KJ': 0},\n", " {'KI': 1, 'KJ': 0},\n", " {'KI': 0, 'KJ': 0},\n", " {'KI': 1, 'KJ': 0}]],\n", " 'port_cubes': [(1, 0, 0), (0, 1, 0), (1, 0, 3), (0, 1, 3)],\n", " 'optional': {},\n", " 'ExistI': [[[0, 1, 0], [0, 1, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " 'ExistJ': [[[0, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " 'ExistK': [[[0, 1, 0], [1, 1, 1]], [[1, 1, 1], [1, 1, 0]]],\n", " 'ColorI': [[[0, 1, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " 'ColorJ': [[[0, 0, 0], [0, 0, 0]], [[0, 0, 1], [0, 0, 0]]],\n", " 'NodeY': [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [1, 0, 1]]],\n", " 'CorrIJ': [[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 1, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]],\n", " 'CorrIK': [[[[0, 0, 0], [0, 0, 1]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 1, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 1, 0]], [[0, 0, 0], [0, 0, 0]]]],\n", " 'CorrJK': [[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]],\n", " 'CorrJI': [[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]],\n", " 'CorrKI': [[[[1, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[1, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[1, 1, 1], [0, 0, 1]], [[1, 1, 1], [0, 0, 0]]],\n", " [[[0, 0, 0], [1, 1, 1]], [[0, 0, 0], [1, 1, 0]]]],\n", " 'CorrKJ': [[[[0, 0, 1], [0, 0, 0]], [[1, 1, 1], [0, 0, 0]]],\n", " [[[1, 1, 1], [1, 1, 1]], [[0, 1, 1], [0, 0, 0]]],\n", " [[[1, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [1, 1, 0]]]]}" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "result.lasre" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Post-process and Output LaS" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We provide a few rewrite passes to remove valid but unnecessary structures in the solution, and also color the K-pipes.\n", "These can be applied with the follow call. " ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "result = result.after_default_optimizations()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can export the result to a few formats.\n", "The most direct one is to save the LaSRe, which is now just a dictionary" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "result.save_lasre(\"cnot.lasre.json\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also create a 3D modelling file in the [GLTF](https://www.khronos.org/gltf/) format.\n", "This can be opened in many software, a lot of them are also web-based.\n", "The `attach_axes` flag attaches I (red), J (green), and K (blue) axis to the GLTF." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "result.to_3d_model_gltf(\"cnot.gltf\", attach_axes=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Like mentioned previously, the generated LaS can be easily mapped to a ZX-diagram.\n", "We can use this connection to verify our result.\n", "Internally, we construct the ZX-diagram and let [Stim ZX](https://github.com/quantumlib/Stim/tree/main/glue/zx) to derive the stabilizers.\n", "Then, we check whether these stabilizers are commutable with the ones in the specification.\n", "If all are commutable, then our LaS is correct." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "specified:\n", "+Z_Z_\n", "+_ZZZ\n", "+X_XX\n", "+_X_X\n", "==============================================================\n", "resulting:\n", "+X_XX\n", "+Z_Z_\n", "+_X_X\n", "+_ZZZ\n", "==============================================================\n", "specified and resulting stabilizers are equivalent.\n" ] }, { "data": { "text/plain": [ "True" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "result.verify_stabilizers_stimzx(specification=input_dict, print_stabilizers=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using Other SAT solver" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So far, we are using the [Z3 SMT solver](https://github.com/Z3Prover/z3) to do everything.\n", "In our experience, it may be faster to generate an SAT problem with Z3 and solve it using other solvers, like Kissat.\n", "For the user, it is very easy to change: just initiate the `LatticeSurgerySynthesizer` with the directory where Kissat is installed in your system." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "las_synth = LatticeSurgerySynthesizer(solver=\"kissat\", kissat_dir=\"\")\n", "# you need to add the kissat dir based on where kissat is on your computer" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Adding constraints time: 0.207474946975708\n", "CNF generation time: 0.004982948303222656\n", "c ---- [ banner ] ------------------------------------------------------------\n", "c\n", "c Kissat SAT Solver\n", "c \n", "c Copyright (c) 2021-2023 Armin Biere University of Freiburg\n", "c Copyright (c) 2019-2021 Armin Biere Johannes Kepler University Linz\n", "c \n", "c Version 3.1.1 71caafb4d182ced9f76cef45b00f37cc598f2a37\n", "c Apple clang version 15.0.0 (clang-1500.3.9.4) -W -Wall -O3 -DNDEBUG\n", "c Sun May 12 13:03:10 PDT 2024 Darwin MacBook-Pro-2 23.4.0 arm64\n", "c\n", "c ---- [ parsing ] -----------------------------------------------------------\n", "c\n", "c opened and reading DIMACS file:\n", "c\n", "c cnot.dimacs\n", "c\n", "c parsed 'p cnf 462 2231' header\n", "c closing input after reading 40739 bytes (40 KB)\n", "c finished parsing after 0.00 seconds\n", "c\n", "c ---- [ options ] -----------------------------------------------------------\n", "c\n", "c --seed=916189 (different from default '0')\n", "c\n", "c ---- [ solving ] -----------------------------------------------------------\n", "c\n", "c seconds switched conflicts irredundant variables\n", "c MB reductions redundant trail remaining\n", "c level restarts binary glue\n", "c\n", "c * 0.00 2 0 0 0 0 0 0 614 1557 0% 0 402 87%\n", "c { 0.00 2 0 0 0 0 0 0 614 1557 0% 0 402 87%\n", "c i 0.00 2 22 0 0 0 38 23 623 1556 44% 2 398 86%\n", "c i 0.00 2 22 0 0 0 39 23 623 1556 44% 2 397 86%\n", "c } 0.00 2 22 0 0 0 39 23 623 1556 44% 2 397 86%\n", "c 1 0.00 2 22 0 0 0 39 23 623 1556 44% 2 397 86%\n", "c\n", "c ---- [ result ] ------------------------------------------------------------\n", "c\n", "s SATISFIABLE\n", "v 1 -2 -3 -4 5 -6 -7 -8 9 10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22\n", "v 23 -24 -25 -26 -27 28 -29 -30 -31 -32 33 -34 35 -36 37 38 39 40 41 42 43 44\n", "v 45 46 47 48 -49 50 -51 -52 -53 54 -55 -56 -57 -58 -59 60 -61 62 -63 64 65\n", "v -66 -67 -68 69 -70 71 -72 -73 -74 75 -76 -77 -78 79 -80 -81 -82 -83 -84 85\n", "v 86 -87 -88 -89 90 91 92 93 94 95 -96 -97 -98 99 100 101 -102 -103 -104 -105\n", "v -106 -107 -108 109 110 111 112 113 -114 115 116 117 118 119 120 121 122 -123\n", "v -124 -125 -126 127 128 129 130 131 132 133 134 135 -136 137 -138 139 -140\n", "v 141 142 143 -144 145 -146 147 148 -149 -150 -151 152 153 154 -155 -156 -157\n", "v 158 159 160 -161 -162 163 -164 165 166 -167 168 169 -170 171 172 173 174\n", "v -175 176 177 178 179 180 -181 182 183 184 185 -186 187 188 189 190 191 192\n", "v 193 -194 -195 196 197 198 -199 -200 201 202 -203 204 205 206 207 208 209\n", "v -210 -211 212 213 214 215 216 217 218 219 -220 221 -222 -223 -224 225 226\n", "v 227 228 -229 230 -231 232 -233 234 235 236 -237 -238 239 240 241 242 243\n", "v -244 245 246 247 -248 249 -250 251 -252 -253 254 255 256 257 258 -259 260\n", "v 261 262 263 -264 -265 266 267 268 -269 270 -271 272 273 -274 275 276 277 278\n", "v 279 280 281 282 283 284 -285 286 -287 288 289 290 -291 292 293 -294 -295 296\n", "v 297 -298 299 300 301 302 303 -304 305 -306 307 -308 309 -310 311 312 313\n", "v -314 315 -316 317 318 319 -320 321 322 323 -324 -325 326 327 328 -329 330\n", "v 331 332 333 334 335 336 -337 -338 339 340 341 -342 -343 344 345 346 347 348\n", "v 349 350 351 352 353 354 355 -356 357 -358 359 -360 361 362 363 -364 365 -366\n", "v 367 368 369 -370 371 -372 373 -374 -375 376 -377 378 -379 380 -381 382 -383\n", "v -384 385 386 -387 388 389 390 391 392 393 394 395 -396 -397 398 -399 400\n", "v -401 402 403 -404 405 406 407 408 -409 -410 411 412 413 414 415 -416 -417\n", "v 418 -419 420 421 422 423 424 425 426 427 428 429 430 -431 432 433 434 435\n", "v 436 437 438 439 -440 441 442 443 444 445 446 447 448 -449 450 451 452 453\n", "v 454 455 456 457 -458 459 -460 461 462 0\n", "c\n", "c ---- [ profiling ] ---------------------------------------------------------\n", "c\n", "c 0.00 39.95 % parse\n", "c 0.00 36.66 % search\n", "c 0.00 34.35 % focused\n", "c 0.00 0.00 % simplify\n", "c =============================================\n", "c 0.00 100.00 % total\n", "c\n", "c ---- [ statistics ] --------------------------------------------------------\n", "c\n", "c conflicts: 39 12268.01 per second\n", "c decisions: 186 4.77 per conflict\n", "c jumped_reasons: 1002 29 % propagations\n", "c propagations: 3417 1074866 per second\n", "c queue_decisions: 186 100 % decision\n", "c random_decisions: 0 0 % decision\n", "c random_sequences: 0 0 interval\n", "c score_decisions: 0 0 % decision\n", "c switched: 0 0 interval\n", "c vivify_checks: 0 0 per vivify\n", "c vivify_units: 0 0 % variables\n", "c\n", "c ---- [ resources ] ---------------------------------------------------------\n", "c\n", "c maximum-resident-set-size: 1828716544 bytes 1744 MB\n", "c process-time: 0.00 seconds\n", "c\n", "c ---- [ shutting down ] -----------------------------------------------------\n", "c\n", "c exit 10\n", "kissat runtime: 0.008579015731811523\n", "kissat SAT!\n", "Construct a Z3 SMT model and solve...\n", "elapsed time: 0.021113s\n", "Z3 SAT\n", "Total solving time: 0.04399609565734863\n" ] } ], "source": [ "result = las_synth.solve(\n", " specification=input_dict,\n", " print_detail=True,\n", " dimacs_file_name=\"cnot\",\n", " sat_log_file_name=\"cnot\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We used a few optional arguments above.\n", "`print_detail` will display the output of Kissat on the screen. \n", "`dimacs_file_name` specifies where to store the SAT problem instance in the DIMACS format.\n", "This instance is generated by Z3 and then solved by Kissat.\n", "`sat_log_file_name` saves the output of Kissat, which is basically what you have seen as the output (from `c ---- [ banner ]` to `c exit 10`)." ] } ], "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.8.19" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: glue/lattice_surgery/lassynth/__init__.py ================================================ from .lattice_surgery_synthesis import LatticeSurgerySynthesizer from .lattice_surgery_synthesis import LatticeSurgerySolution ================================================ FILE: glue/lattice_surgery/lassynth/lattice_surgery_synthesis.py ================================================ """Two wrapper classes, rewrite passes, and translators.""" import functools import itertools import json import time import multiprocessing import random import networkx from typing import Any, Literal, Mapping, Optional, Sequence from lassynth.rewrite_passes.attach_fixups import attach_fixups from lassynth.rewrite_passes.color_z import color_z from lassynth.rewrite_passes.remove_unconnected import remove_unconnected from lassynth.sat_synthesis.lattice_surgery_sat import LatticeSurgerySAT from lassynth.tools.verify_stabilizers import verify_stabilizers from lassynth.translators.gltf_generator import gltf_generator from lassynth.translators.textfig_generator import textfig_generator from lassynth.translators.zx_grid_graph import ZXGridGraph from lassynth.translators.networkx_generator import networkx_generator def check_lasre(lasre: Mapping[str, Any]) -> None: """check aspects of LaSRe other than SMT constraints, i.e., data layout.""" if "n_i" not in lasre: raise ValueError( f"upper bound of I dimension, `n_i`, is missing in lasre.") if lasre["n_i"] <= 0: raise ValueError("n_i <= 0.") if "n_j" not in lasre: raise ValueError( f"upper bound of J dimension, `n_j`, is missing in lasre.") if lasre["n_j"] <= 0: raise ValueError("n_j <= 0.") if "n_k" not in lasre: raise ValueError( f"upper bound of K dimension, `n_k`, is missing in lasre.") if lasre["n_k"] <= 0: raise ValueError("n_k <= 0.") if "n_p" not in lasre: raise ValueError(f"number of ports, `n_p`, is missing in lasre.") if lasre["n_p"] <= 0: raise ValueError("n_p <= 0.") if "n_s" not in lasre: raise ValueError(f"number of stabilizers, `n_s`, is missing in lasre.") if lasre["n_s"] < 0: raise ValueError("n_s < 0.") if lasre["n_s"] == 0: print("no stabilizer!") if "ports" not in lasre: raise ValueError(f"`ports` is missing in lasre.") if len(lasre["ports"]) != lasre["n_p"]: raise ValueError("number of ports in `ports` is different from `n_p`.") for port in lasre["ports"]: if "i" not in port: raise ValueError(f"location `i` missing from port {port}.") if port["i"] not in range(lasre["n_i"]): raise ValueError(f"i out of range in port {port}.") if "j" not in port: raise ValueError(f"location `j` missing from port {port}.") if port["j"] not in range(lasre["n_j"]): raise ValueError(f"j out of range in port {port}.") if "k" not in port: raise ValueError(f"location `k` missing from port {port}.") if port["k"] not in range(lasre["n_k"]): raise ValueError(f"k out of range in port {port}.") if "d" not in port: raise ValueError(f"direction `d` missing from port {port}.") if port["d"] not in ["I", "J", "K"]: raise ValueError(f"direction not I, J, or K in port {port}.") if "e" not in port: raise ValueError(f"open end `e` missing from port {port}.") if port["e"] not in ["-", "+"]: raise ValueError(f"open end not - or + in port {port}.") if "c" not in port: raise ValueError(f"color `c` missing from port {port}.") if port["c"] not in [0, 1]: raise ValueError(f"color not 0 or 1 in port {port}.") if "stabs" not in lasre: raise ValueError(f"`stabs` is missing in lasre.") if len(lasre["stabs"]) != lasre["n_s"]: raise ValueError("number of stabs in `stabs` is different from `n_s`.") for stab in lasre["stabs"]: if len(stab) != lasre["n_p"]: raise ValueError("number of boundary corrsurf is not `n_p`.") for i, corrsurf in enumerate(stab): for (k, v) in corrsurf.items(): if lasre["ports"][i]["d"] == "I" and k not in ["IJ", "IK"]: raise ValueError(f"stabs[{i}] key invalid {stab}.") if lasre["ports"][i]["d"] == "J" and k not in ["JI", "JK"]: raise ValueError(f"stabs[{i}] key invalid {stab}.") if lasre["ports"][i]["d"] == "K" and k not in ["KI", "KJ"]: raise ValueError(f"stabs[{i}] key invalid {stab}.") if v not in [0, 1]: raise ValueError(f"stabs[{i}] value not 0 or 1 {stab}.") port_cubes = [] for p in lasre["ports"]: # if e=-, (i,j,k); otherwise, +1 in the proper direction if p["e"] == "-": port_cubes.append((p["i"], p["j"], p["k"])) elif p["d"] == "I": port_cubes.append((p["i"] + 1, p["j"], p["k"])) elif p["d"] == "J": port_cubes.append((p["i"], p["j"] + 1, p["k"])) elif p["d"] == "K": port_cubes.append((p["i"], p["j"], p["k"] + 1)) lasre["port_cubes"] = port_cubes if "optional" not in lasre: lasre["optional"] = {} for key in [ "NodeY", "ExistI", "ExistJ", "ExistK", "ColorI", "ColorJ", ]: if key not in lasre: raise ValueError(f"`{key}` missing from lasre.") if len(lasre[key]) != lasre["n_i"]: raise ValueError(f"dimension of {key} is wrong.") for tmp in lasre[key]: if len(tmp) != lasre["n_j"]: raise ValueError(f"dimension of {key} is wrong.") for tmptmp in tmp: if len(tmptmp) != lasre["n_k"]: raise ValueError(f"dimension of {key} is wrong.") if lasre["n_s"] > 0: for key in [ "CorrIJ", "CorrIK", "CorrJI", "CorrJK", "CorrKI", "CorrKJ", ]: if key not in lasre: raise ValueError(f"`{key}` missing from lasre.") if len(lasre[key]) != lasre["n_s"]: raise ValueError(f"dimension of {key} is wrong.") for tmp in lasre[key]: if len(tmp) != lasre["n_i"]: raise ValueError(f"dimension of {key} is wrong.") for tmptmp in tmp: if len(tmptmp) != lasre["n_j"]: raise ValueError(f"dimension of {key} is wrong.") for tmptmptmp in tmptmp: if len(tmptmptmp) != lasre["n_k"]: raise ValueError(f"dimension of {key} is wrong.") class LatticeSurgerySolution: """A class for the result of synthesizer lattice surgery subroutine. It internally saves an LaSRe (Lattice Surgery Subroutine Representation) and we can apply rewrite passes to it, or use translators to derive other formats of the LaS solution """ def __init__( self, lasre: Mapping[str, Any], ) -> None: """initialization for LatticeSurgerySubroutine Args: lasre (Mapping[str, Any]): LaSRe """ check_lasre(lasre) self.lasre = lasre def get_depth(self) -> int: """get the depth/height of the LaS in LaSRe. Returns: int: depth/height of the LaS in LaSRe """ return self.lasre["n_k"] def after_removing_disconnected_pieces(self): """remove_unconnected.""" return LatticeSurgerySolution(lasre=remove_unconnected(self.lasre)) def after_color_k_pipes(self): """coloring K pipes.""" return LatticeSurgerySolution(lasre=color_z(self.lasre)) def after_default_optimizations(self): """default optimizations: remove unconnected, and then color K pipes.""" solution = LatticeSurgerySolution(lasre=remove_unconnected(self.lasre)) solution = LatticeSurgerySolution(lasre=color_z(solution.lasre)) return solution def after_t_factory_default_optimizations(self): """default optimization for T-factories.""" solution = LatticeSurgerySolution(lasre=remove_unconnected(self.lasre)) solution = LatticeSurgerySolution(lasre=color_z(solution.lasre)) solution = LatticeSurgerySolution(lasre=attach_fixups(solution.lasre)) return solution def save_lasre(self, file_name: str) -> None: """save the current LaSRe to a file. Args: file_name (str): file name including extension to save the LaSRe """ with open(file_name, "w") as f: json.dump(self.lasre, f) def to_3d_model_gltf(self, output_file_name: str, stabilizer: int = -1, tube_len: float = 2.0, no_color_z: bool = False, attach_axes: bool = False, rm_dir: Optional[str] = None) -> None: """generate gltf file (for 3D modelling). Args: output_file_name (str): file name including extension to save gltf stabilizer (int, optional): Defaults to -1 meaning do not draw correlation surfaces. If the value is in [0, n_s), the correlation surfaces corresponding to that stabilizer are drawn and faces in one of the directions are revealed to unveil the correlation surfaces. tube_len (float, optional): Length of the pipe comapred to the cube. Defaults to 2.0. no_color_z (bool, optional): Do not color the K pipes. Defaults to False. attach_axes (bool, optional): attach IJK axes. Defaults to False. If attached, the color coding is I->red, J->green, K->blue. rm_dir (str, optional): the (+|-)(I|J|K) faces to remove. Intended to reveal correlation surfaces. Default to None. """ gltf = gltf_generator( self.lasre, stabilizer=stabilizer, tube_len=tube_len, no_color_z=no_color_z, attach_axes=attach_axes, rm_dir=rm_dir if rm_dir else (":+J" if stabilizer >= 0 else None), ) with open(output_file_name, "w") as f: json.dump(gltf, f) def to_zigxag_url( self, io_spec: Optional[Sequence[str]] = None, ) -> str: """generate a link that leads to a ZigXag figure. Args: io_spec (Optional[Sequence[str]], optional): Specify whether each port is an input or an output. Length must be the same with the number of ports. Defaults to None, which means all ports are outputs. Returns: str: the ZigXag link """ zxgridgraph = ZXGridGraph(self.lasre) return zxgridgraph.to_zigxag_url(io_spec=io_spec) def to_text_diagram(self) -> str: """generate the text figure of LaS time slices. Returns: str: text figure of the LaS """ return textfig_generator(self.lasre) def to_networkx_graph(self) -> networkx.Graph: """generate a annotated networkx.Graph correponding to the LaS. Returns: networkx.Graph: """ return networkx_generator(self.lasre) def verify_stabilizers_stimzx(self, specification: Mapping[str, Any], print_stabilizers: bool = False) -> bool: """verify the stabilizer of the LaS. Use StimZX to deduce the stabilizers from the annotated networkx graph. Then use Stim to ensure that this set of stabilizers and the set of stabilizers specified in the input are equivalent. Args: specification (Mapping[str, Any]): the LaS specification to verify the current solution against. print_stabilizers (bool, optional): If True, print the two sets of stabilizers. Defaults to False. Returns: bool: True if the two sets are equivalent; otherwise False. """ paulistrings = [ paulistring.replace(".", "_") for paulistring in specification["stabilizers"] ] return verify_stabilizers( paulistrings, self.to_networkx_graph(), print_stabilizers=print_stabilizers, ) class LatticeSurgerySynthesizer: """A class to synthesize LaS.""" def __init__( self, solver: Literal["kissat", "z3"] = "z3", kissat_dir: Optional[str] = None, ) -> None: """initialize. Args: solver (Literal["kissat", "z3"], optional): the solver to use. Defaults to "z3". "kissat" is recommended. kissat_dir (Optional[str], optional): directory of the kissat executable. Defaults to None. """ self.solver = solver self.kissat_dir = kissat_dir def solve( self, specification: Mapping[str, Any], given_arrs: Optional[Mapping[str, Any]] = None, given_vals: Optional[Sequence[Mapping[str, Any]]] = None, print_detail: bool = False, dimacs_file_name: Optional[str] = None, sat_log_file_name: Optional[str] = None, ) -> Optional[LatticeSurgerySolution]: """solve an LaS synthesis problem. Args: specification (Mapping[str, Any]): the LaS specification to solve. given_arrs (Optional[Mapping[str, Any]], optional): given array of known values to plug in. Defaults to None. given_vals (Optional[Sequence[Mapping[str, Any]]], optional): given known values to plug in. Defaults to None. Format should be a sequence of dicts. Each one contains three fields: "array", the name of the array, e.g., "ExistI"; "indices", a sequence of the indices, e.g., [0, 0, 0]; and "value", 0 or 1. print_detail (bool, optional): whether to print details in SAT solving. Defaults to False. dimacs_file_name (Optional[str], optional): file to save the DIMACS. Defaults to None. sat_log_file_name (Optional[str], optional): file to save the SAT solver log. Defaults to None. Returns: Optional[LatticeSurgerySubroutine]: if the problem is unsatisfiable, this is None; otherwise, a LatticeSurgerySolution initialized by the compiled result. """ start_time = time.time() sat_synthesis = LatticeSurgerySAT( input_dict=specification, given_arrs=given_arrs, given_vals=given_vals, ) if print_detail: print(f"Adding constraints time: {time.time() - start_time}") start_time = time.time() if self.solver == "z3": if_sat = sat_synthesis.check_z3(print_progress=print_detail) else: if_sat = sat_synthesis.check_kissat( dimacs_file_name=dimacs_file_name, sat_log_file_name=sat_log_file_name, print_progress=print_detail, kissat_dir=self.kissat_dir, ) if print_detail: print(f"Total solving time: {time.time() - start_time}") if if_sat: solver_result = sat_synthesis.get_result() return LatticeSurgerySolution(lasre=solver_result) else: return None def optimize_depth( self, specification: Mapping[str, Any], start_depth: Optional[int] = None, print_detail: bool = False, dimacs_file_name_prefix: Optional[str] = None, sat_log_file_name_prefix: Optional[str] = None, ) -> LatticeSurgerySolution: """find the optimal solution in terms of depth/height of the LaS. Args: specification (Mapping[str, Any]): the LaS specification to solve. start_depth (int, optional): starting depth of the exploration. If not provided, use the depth given in the specification print_detail (bool, optional): whether to print details in SAT solving. Defaults to False. dimacs_file_name_prefix (Optional[str], optional): file prefix to save the DIMACS. The full file name will contain the specific depth after this prefix. Defaults to None. sat_log_file_name_prefix (Optional[str], optional): file prefix to save the SAT log. The full file name will contain the specific depth after this prefix. Defaults to None. result_file_name_prefix (Optional[str], optional): file prefix to save the variable assignments. The full file name will contain the specific depth after this prefix. Defaults to None. post_optimization (str, optional): optimization to perform when initializing the LatticeSurgerySubroutine object for the result. Defaults to "default". Raises: ValueError: starting depth is too low. Returns: LatticeSurgerySolution: compiled result with the optimal depth. """ self.specification = dict(specification) if start_depth is None: depth = self.specification["max_k"] else: depth = int(start_depth) if depth < 2: raise ValueError("depth too low.") checked_depth = {} while True: # the ports on the top floor will still be on the top floor when we # increase the height. This is an assumption. Adapt to your case. for port in self.specification["ports"]: if port["location"][2] == self.specification["max_k"]: port["location"][2] = depth self.specification["max_k"] = depth result = self.solve( specification=self.specification, print_detail=print_detail, dimacs_file_name=dimacs_file_name_prefix + f"_d={depth}" if dimacs_file_name_prefix else None, sat_log_file_name=sat_log_file_name_prefix + f"_d={depth}" if sat_log_file_name_prefix else None, ) if result is None: checked_depth[str(depth)] = "UNSAT" if str(depth + 1) in checked_depth: # since this depth leads to UNSAT, we need to increase # the depth, but if depth+1 is already checked, we can stop break else: depth += 1 else: checked_depth[str(depth)] = "SAT" self.sat_result = LatticeSurgerySolution(lasre=result.lasre) if str(depth - 1) in checked_depth: # since this depth leads to SAT, we need to try decreasing # the depth, but if depth-1 is already checked, we can stop break else: depth -= 1 return self.sat_result def try_one_permutation( self, perm: Sequence[int], specification: Mapping[str, Any], print_detail: bool = False, dimacs_file_name_prefix: Optional[str] = None, sat_log_file_name_prefix: Optional[str] = None, ) -> Optional[LatticeSurgerySolution]: """check if the problem is satisfiable given a port permutation. Args: specification (Mapping[str, Any]): the LaS specification to solve. perm (Sequence[int]): the given permutation, which is an integer tuple of length n (n being the number of ports permuted). print_detail (bool, optional): whether to print details in SAT solving. Defaults to False. dimacs_file_name_prefix (Optional[str], optional): file prefix to save the DIMACS. The full file name will contain the specific permutation after this prefix. Defaults to None. sat_log_file_name_prefix (Optional[str], optional): file prefix to save the SAT log. The full file name will contain the specific permutation after this prefix. Defaults to None. Returns: Optional[LatticeSurgerySubroutine]: if the problem is unsatisfiable, this is None; otherwise, a LatticeSurgerySolution initialized by the compiled result. """ # say `perm` is [0,3,2], then `original` is in order, i.e., [0,2,3] # the full permutation is 0,1,2,3 -> 0,1,3,2 original = sorted(perm) this_spec = dict(specification) new_ports = [] for p, port in enumerate(specification["ports"]): if p not in perm: # the p-th port is not involved in `perm`, e.g., 1 is unchanged new_ports.append(port) else: # after the permutation, the index of the p-th port in # specification is the k-th port in `perm` where k is the place # of p in `original`. In this example, when p=0 and 1, nothing # changed. When p=2, we find `place` to be 1, and perm[place]=3 # so we attach port_3. When p=3, we end up attach port_2 place = original.index(p) new_ports.append(specification["ports"][perm[place]]) this_spec["ports"] = new_ports result = self.solve( specification=this_spec, print_detail=print_detail, dimacs_file_name=dimacs_file_name_prefix + "_" + perm.__repr__().replace(" ", "") if dimacs_file_name_prefix else None, sat_log_file_name=sat_log_file_name_prefix + "_" + perm.__repr__().replace(" ", "") if sat_log_file_name_prefix else None, ) print(f"{perm}: {'SAT' if result else 'UNSAT'}") return result def solve_all_port_permutations( self, permute_ports: Sequence[int], parallelism: int = 1, shuffle: bool = True, **kwargs, ) -> Mapping[str, Sequence[Sequence[int]]]: """try all the permutations of given ports, which ones are satisfiable. Note that we do not check that the LaS after permuting the ports (we do not permute the stabilizers accordingly) is functionally equivalent. The user should use this method based on their judgement. Also, the number of permutations scales exponentially with the number of ports to permute, so this method can easily take an immense amount of time. Args: permute_ports (Sequence[int]): the indices of ports to permute parallelism (int, optional): number of parallel process. Each one try one permutation. A New proess starts when an old one finishes. Defaults to 1. shuffle (bool, optional): whether using a random order to start the processes. Defaults to True. **kwargs: other arguments to `try_one_permutation`. Returns: Mapping[str, Sequence[Sequence[int]]]: a dict with two keys. "SAT": [.] a list containing all the satisfiable permutations; "UNSAT": [.] all the unsatisfiable permutations. """ perms = list(itertools.permutations(permute_ports)) if shuffle: random.shuffle(perms) pool = multiprocessing.Pool(parallelism) # issue the job one by one (chuck=1) results = pool.map( functools.partial( self.try_one_permutation, **kwargs, ), perms, chunksize=1, ) sat_perms = [] unsat_perms = [] for p, result in enumerate(results): if result is None: unsat_perms.append(perms[p]) else: sat_perms.append(perms[p]) return { "SAT": sat_perms, "UNSAT": unsat_perms, } ================================================ FILE: glue/lattice_surgery/lassynth/rewrite_passes/__init__.py ================================================ ================================================ FILE: glue/lattice_surgery/lassynth/rewrite_passes/attach_fixups.py ================================================ """assuming all T injections requiring fixup are on the top floor. The output is also on the top floor""" from typing import Mapping, Any def attach_fixups(lasre: Mapping[str, Any]) -> Mapping[str, Any]: n_s = lasre["n_s"] n_i = lasre["n_i"] n_j = lasre["n_j"] n_k = lasre["n_k"] fixup_locs = [] for p in lasre["optional"]["top_fixups"]: fixup_locs.append((lasre["ports"][p]["i"], lasre["ports"][p]["j"])) lasre["n_k"] += 2 # we add two layers on the top. The lower layer will contain fixups dressed # as Y cubes that is connecting downwards. The upper layer will contain no # new cubes. This corresponds to waiting the machine to apply the fixups # because there is a finite interaction time from the machine knows whether # the injection is T or T^dagger to apply the fixups. for i in range(n_i): for j in range(n_j): if (i, j) in fixup_locs: lasre["NodeY"][i][j].append(1) # fixup dressed as Y cubes lasre["ExistK"][i][j][n_k - 1] = 1 # connect fixup downwards else: lasre["NodeY"][i][j].append(0) # no fixup lasre["ExistK"][i][j][n_k - 1] = 0 lasre["NodeY"][i][j].append(0) # the upper layer is empty # do not add any new pipes in the added layer for arr in ["ExistI", "ExistJ", "ExistK"]: lasre[arr][i][j].append(0) lasre[arr][i][j].append(0) for arr in ["ColorI", "ColorJ", "ColorKM", "ColorKP"]: lasre[arr][i][j].append(-1) lasre[arr][i][j].append(-1) for s in range(n_s): for arr in [ "CorrIJ", "CorrIK", "CorrJK", "CorrJI", "CorrKI", "CorrKJ" ]: lasre[arr][s][i][j].append(0) lasre[arr][s][i][j].append(0) # the output ports need to be extended in the two added layers for port in lasre["ports"]: if "f" in port and port["f"] == "output": ii, jj = port["i"], port["j"] lasre["ExistK"][ii][jj][n_k - 1] = 1 lasre["ExistK"][ii][jj][n_k] = 1 lasre["ExistK"][ii][jj][n_k + 1] = 1 lasre["ColorKM"][ii][jj][n_k] = lasre["ColorKP"][ii][jj][n_k - 1] lasre["ColorKM"][ii][jj][n_k + 1] = lasre["ColorKM"][ii][jj][n_k] lasre["ColorKP"][ii][jj][n_k] = lasre["ColorKM"][ii][jj][n_k] lasre["ColorKP"][ii][jj][n_k + 1] = lasre["ColorKP"][ii][jj][n_k] for s in range(n_s): lasre["CorrKI"][s][ii][jj][n_k] = lasre["CorrKI"][s][ii][jj][ n_k - 1] lasre["CorrKI"][s][ii][jj][n_k + 1] = lasre["CorrKI"][s][ii][jj][n_k] lasre["CorrKJ"][s][ii][jj][n_k] = lasre["CorrKJ"][s][ii][jj][ n_k - 1] lasre["CorrKJ"][s][ii][jj][n_k + 1] = lasre["CorrKJ"][s][ii][jj][n_k] port["k"] += 2 new_cubes = [] for c in lasre["port_cubes"]: if c[0] == port["i"] and c[1] == port["j"]: new_cubes.append((c[0], c[1], port["k"] + 1)) else: new_cubes.append(c) lasre["port_cubes"] = new_cubes t_injections = [] for port in lasre["ports"]: if port["f"] == "T": if port["e"] == "+": t_injections.append([port["i"], port["j"], port["k"] + 1]) else: t_injections.append([port["i"], port["j"], port["k"]]) lasre["optional"]["t_injections"] = t_injections return lasre ================================================ FILE: glue/lattice_surgery/lassynth/rewrite_passes/color_z.py ================================================ """We do not have ColorZ from the SAT/SMT. Now we color the Z-pipes.""" from typing import Sequence, Mapping, Any, Union, Tuple def if_uncolorK(n_i: int, n_j: int, n_k: int, ExistK: Sequence[Sequence[Sequence[int]]], ColorKP: Sequence[Sequence[Sequence[int]]], ColorKM: Sequence[Sequence[Sequence[int]]]) -> bool: """return whether there are uncolored K-pipes""" for i in range(n_i): for j in range(n_j): for k in range(n_k): if ExistK[i][j][k] and (ColorKP[i][j][k] == -1 or ColorKM[i][j][k] == -1): return True return False def in_bound(n_i: int, n_j: int, n_k: int, i: int, j: int, k: int) -> bool: if i in range(n_i) and j in range(n_j) and k in range(n_k): return True return False def propogate_IJcolor(n_i: int, n_j: int, n_k: int, ExistI: Sequence[Sequence[Sequence[int]]], ExistJ: Sequence[Sequence[Sequence[int]]], ExistK: Sequence[Sequence[Sequence[int]]], ColorI: Sequence[Sequence[Sequence[int]]], ColorJ: Sequence[Sequence[Sequence[int]]], ColorKP: Sequence[Sequence[Sequence[int]]], ColorKM: Sequence[Sequence[Sequence[int]]]) -> None: """propagate the color of I- and J-pipes to their neighbor K-pipes.""" for i in range(n_i): for j in range(n_j): for k in range(n_k): if ExistK[i][j][k]: # 4 possible neighbor I/J pipe for the minus end of K-pipe if in_bound(n_i, n_j, n_k, i - 1, j, k) and ExistI[i - 1][j][k]: ColorKM[i][j][k] = 1 - ColorI[i - 1][j][k] if ExistI[i][j][k]: ColorKM[i][j][k] = 1 - ColorI[i][j][k] if in_bound(n_i, n_j, n_k, i, j - 1, k) and ExistJ[i][j - 1][k]: ColorKM[i][j][k] = 1 - ColorJ[i][j - 1][k] if ExistJ[i][j][k]: ColorKM[i][j][k] = 1 - ColorJ[i][j][k] # 4 possible neighbor I/J pipe for the plus end of K-pipe if (in_bound(n_i, n_j, n_k, i - 1, j, k + 1) and ExistI[i - 1][j][k + 1]): ColorKP[i][j][k] = 1 - ColorI[i - 1][j][k + 1] if in_bound(n_i, n_j, n_k, i, j, k + 1) and ExistI[i][j][k + 1]: ColorKP[i][j][k] = 1 - ColorI[i][j][k + 1] if (in_bound(n_i, n_j, n_k, i, j - 1, k + 1) and ExistJ[i][j - 1][k + 1]): ColorKP[i][j][k] = 1 - ColorJ[i][j - 1][k + 1] if in_bound(n_i, n_j, n_k, i, j, k + 1) and ExistJ[i][j][k + 1]: ColorKP[i][j][k] = 1 - ColorJ[i][j][k + 1] def propogate_Kcolor(n_i: int, n_j: int, n_k: int, ExistK: Sequence[Sequence[Sequence[int]]], ColorKP: Sequence[Sequence[Sequence[int]]], ColorKM: Sequence[Sequence[Sequence[int]]], NodeY: Sequence[Sequence[Sequence[int]]]) -> bool: """propagate color from colored K-pipes to uncolored K-pipes. If no new color can be assigned, return False; otherwise, return True.""" did_something = False for i in range(n_i): for j in range(n_j): for k in range(n_k): if ExistK[i][j][k]: # consider propagate color from below if in_bound( n_i, n_j, n_k, i, j, k - 1) and ExistK[i][j][k - 1] and NodeY[i][j][k - 1] == 0: if ColorKP[i][j][k - 1] > -1 and ColorKM[i][j][k] == -1: ColorKM[i][j][k] = ColorKP[i][j][k - 1] did_something = True # consider propagate color from above if in_bound( n_i, n_j, n_k, i, j, k + 1) and ExistK[i][j][k + 1] and NodeY[i][j][k + 1] == 0: if ColorKM[i][j][k + 1] > -1 and ColorKP[i][j][k] == -1: ColorKP[i][j][k] = ColorKM[i][j][k + 1] did_something = True # if K-pipe connects a Y Cube, two ends can be colored same if (NodeY[i][j][k] and ColorKM[i][j][k] == -1 and ColorKP[i][j][k] > -1): ColorKM[i][j][k] = ColorKP[i][j][k] did_something = True if (in_bound(n_i, n_j, n_k, i, j, k + 1) and NodeY[i][j][k + 1] and ColorKM[i][j][k] > -1 and ColorKP[i][j][k] == -1): ColorKP[i][j][k] = ColorKM[i][j][k] did_something = True return did_something def assign_Kcolor(n_i: int, n_j: int, n_k: int, ExistK: Sequence[Sequence[Sequence[int]]], ColorKP: Sequence[Sequence[Sequence[int]]], ColorKM: Sequence[Sequence[Sequence[int]]], NodeY: Sequence[Sequence[Sequence[int]]]) -> None: """when no color can be deducted by propagating from other K-pipes, we assign some color variables at will. Then, we can continue to propagate.""" # assign a color by letting the two ends of a K-pipe to be the same for i in range(n_i): for j in range(n_j): for k in range(n_k): if ExistK[i][j][k]: if ColorKM[i][j][k] > -1 and ColorKP[i][j][k] == -1: ColorKP[i][j][k] = ColorKM[i][j][k] break # For K-pipes that have no color at both ends and connects a Y-cube for i in range(n_i): for j in range(n_j): for k in range(n_k): if ExistK[i][j][k]: if NodeY[i][j][k] and ColorKM[i][j][k] == -1: ColorKM[i][j][k] = 0 break if (in_bound(n_i, n_j, n_k, i, j, k + 1) and NodeY[i][j][k + 1] and ColorKP[i][j][k] == -1): ColorKP[i][j][k] = 0 break def color_ports(ports: Sequence[Mapping[str, Union[str, int]]], ColorKP: Sequence[Sequence[Sequence[int]]], ColorKM: Sequence[Sequence[Sequence[int]]]) -> None: for port in ports: if port['d'] == 'K': if port['e'] == '+': ColorKP[port['i']][port['j']][port['k']] = port['c'] else: ColorKM[port['i']][port['j']][port['k']] = port['c'] def color_kp_km( n_i: int, n_j: int, n_k: int, ExistI: Sequence[Sequence[Sequence[int]]], ExistJ: Sequence[Sequence[Sequence[int]]], ExistK: Sequence[Sequence[Sequence[int]]], ColorI: Sequence[Sequence[Sequence[int]]], ColorJ: Sequence[Sequence[Sequence[int]]], ports: Sequence[Mapping[str, Union[str, int]]], NodeY: Sequence[Sequence[Sequence[int]]], ) -> Tuple[Sequence[Sequence[Sequence[int]]], Sequence[Sequence[Sequence[int]]]]: ColorKP = [[[-1 for _ in range(n_k)] for _ in range(n_j)] for _ in range(n_i)] ColorKM = [[[-1 for _ in range(n_k)] for _ in range(n_j)] for _ in range(n_i)] # at ports, the color follows from the port configuration color_ports(ports, ColorKP, ColorKM) # propogate the color of I-pipes and J-pipes to their neighboring K-pipes propogate_IJcolor(n_i, n_j, n_k, ExistI, ExistJ, ExistK, ColorI, ColorJ, ColorKP, ColorKM) # the rest of the K-pipes are only neighboring other K-pipes. Until all of # them are colored, we propagate colors of the existing K-pipes. If at one # point, nothing can be implied via propagation, we assign a color at will # and continue. Because of the domain wall operation, we can do this. while if_uncolorK(n_i, n_j, n_k, ExistK, ColorKP, ColorKM): if not propogate_Kcolor(n_i, n_j, n_k, ExistK, ColorKP, ColorKM, NodeY): assign_Kcolor(n_i, n_j, n_k, ExistK, ColorKP, ColorKM, NodeY) return ColorKP, ColorKM def color_z(lasre: Mapping[str, Any]) -> Mapping[str, Any]: n_i, n_j, n_k = ( lasre['n_i'], lasre['n_j'], lasre['n_k'], ) ExistI, ColorI, ExistJ, ColorJ, ExistK = ( lasre['ExistI'], lasre['ColorI'], lasre['ExistJ'], lasre['ColorJ'], lasre['ExistK'], ) NodeY = lasre['NodeY'] ports = lasre['ports'] # for a K-pipe (i,j,k)-(i,j,k+1), ColorKP (plus) is its color at (i,j,k+1) # and ColorKM (minus) is its color at (i,j,k) lasre['ColorKP'], lasre['ColorKM'] = color_kp_km(n_i, n_j, n_k, ExistI, ExistJ, ExistK, ColorI, ColorJ, ports, NodeY) return lasre ================================================ FILE: glue/lattice_surgery/lassynth/rewrite_passes/remove_unconnected.py ================================================ """In the generated LaS, there can be some 'floating donuts' not connecting to any ports. These objects won't affect the function of the LaS. We remove them. """ from typing import Mapping, Any, Sequence, Union, Tuple def check_cubes( n_i: int, n_j: int, n_k: int, ExistI: Sequence[Sequence[Sequence[int]]], ExistJ: Sequence[Sequence[Sequence[int]]], ExistK: Sequence[Sequence[Sequence[int]]], ports: Sequence[Mapping[str, Union[str, int]]], NodeY: Sequence[Sequence[Sequence[int]]] ) -> Sequence[Sequence[Sequence[int]]]: # we linearize the cubes, cube at (i,j,k) -> index i*n_j*n_k + j*n_k + k # construct adjancency list of the cubes from the pipes adj = [[] for _ in range(n_i * n_j * n_k)] for i in range(n_i): for j in range(n_j): for k in range(n_k): if ExistI[i][j][k] and i + 1 < n_i: adj[i * n_j * n_k + j * n_k + k].append((i + 1) * n_j * n_k + j * n_k + k) adj[(i + 1) * n_j * n_k + j * n_k + k].append(i * n_j * n_k + j * n_k + k) if ExistJ[i][j][k] and j + 1 < n_j: adj[i * n_j * n_k + j * n_k + k].append(i * n_j * n_k + (j + 1) * n_k + k) adj[i * n_j * n_k + (j + 1) * n_k + k].append(i * n_j * n_k + j * n_k + k) if ExistK[i][j][k] and k + 1 < n_k: adj[i * n_j * n_k + j * n_k + k].append(i * n_j * n_k + j * n_k + k + 1) adj[i * n_j * n_k + j * n_k + k + 1].append(i * n_j * n_k + j * n_k + k) # if a cube can reach any of the vips, i.e., open cube for a port vips = [p["i"] * n_j * n_k + p["j"] * n_k + p["k"] for p in ports] # first assume all cubes are nonconnected connected_cubes = [[[0 for _ in range(n_k)] for _ in range(n_j)] for _ in range(n_i)] # a Y cube is only effective if it is connected to a cube (i,j,k) that is # connected to ports. In this case, (i,j,k) will be in `connected_cubes` # and all pipes from (i,j,k) will be selected in `check_pipes`, so we can # assume all the Y cubes to be nonconnected for now. y_cubes = [ i * n_j * n_k + j * n_k + k for i in range(n_i) for j in range(n_j) for k in range(n_k) if NodeY[i][j][k] ] for i in range(n_i): for j in range(n_j): for k in range(n_k): # breadth first search for each cube queue = [ i * n_j * n_k + j * n_k + k, ] if i * n_j * n_k + j * n_k + k in y_cubes: continue visited = [0 for _ in range(n_i * n_j * n_k)] while len(queue) > 0: if queue[0] in vips: connected_cubes[i][j][k] = 1 break visited[queue[0]] = 1 for v in adj[queue[0]]: if not visited[v] and v not in y_cubes: queue.append(v) queue.pop(0) return connected_cubes def check_pipes( n_i: int, n_j: int, n_k: int, ExistI: Sequence[Sequence[Sequence[int]]], ExistJ: Sequence[Sequence[Sequence[int]]], ExistK: Sequence[Sequence[Sequence[int]]], connected_cubes: Sequence[Sequence[Sequence[int]]] ) -> Tuple[Sequence[Sequence[Sequence[int]]], Sequence[Sequence[Sequence[int]]], Sequence[Sequence[Sequence[int]]]]: EffectI = [[[0 for _ in range(n_k)] for _ in range(n_j)] for _ in range(n_i)] EffectJ = [[[0 for _ in range(n_k)] for _ in range(n_j)] for _ in range(n_i)] EffectK = [[[0 for _ in range(n_k)] for _ in range(n_j)] for _ in range(n_i)] for i in range(n_i): for j in range(n_j): for k in range(n_k): if ExistI[i][j][k] and (connected_cubes[i][j][k] or (i + 1 < n_i and connected_cubes[i + 1][j][k])): EffectI[i][j][k] = 1 if ExistJ[i][j][k] and (connected_cubes[i][j][k] or (j + 1 < n_j and connected_cubes[i][j + 1][k])): EffectJ[i][j][k] = 1 if ExistK[i][j][k] and (connected_cubes[i][j][k] or (k + 1 < n_k and connected_cubes[i][j][k + 1])): EffectK[i][j][k] = 1 return EffectI, EffectJ, EffectK def array3DAnd( arr0: Sequence[Sequence[Sequence[int]]], arr1: Sequence[Sequence[Sequence[int]]] ) -> Sequence[Sequence[Sequence[int]]]: """taking the AND of two arrays of bits""" a = len(arr0) b = len(arr0[0]) c = len(arr0[0][0]) arrAnd = [[[0 for _ in range(c)] for _ in range(b)] for _ in range(a)] for i in range(a): for j in range(b): for k in range(c): if arr0[i][j][k] == 1 and arr1[i][j][k] == 1: arrAnd[i][j][k] = 1 return arrAnd def remove_unconnected(lasre: Mapping[str, Any]) -> Mapping[str, Any]: n_i, n_j, n_k = ( lasre["n_i"], lasre["n_j"], lasre["n_k"], ) ExistI, ExistJ, ExistK, NodeY = ( lasre["ExistI"], lasre["ExistJ"], lasre["ExistK"], lasre["NodeY"], ) ports = lasre["ports"] connected_cubes = check_cubes(n_i, n_j, n_k, ExistI, ExistJ, ExistK, ports, NodeY) connectedI, connectedJ, connectedK = check_pipes(n_i, n_j, n_k, ExistI, ExistJ, ExistK, connected_cubes) maskedI, maskedJ, maskedK = ( array3DAnd(ExistI, connectedI), array3DAnd(ExistJ, connectedJ), array3DAnd(ExistK, connectedK), ) lasre["ExistI"], lasre["ExistJ"], lasre[ "ExistK"] = maskedI, maskedJ, maskedK return lasre ================================================ FILE: glue/lattice_surgery/lassynth/sat_synthesis/__init__.py ================================================ ================================================ FILE: glue/lattice_surgery/lassynth/sat_synthesis/lattice_surgery_sat.py ================================================ """LatticeSurgerySAT to encode the synthesis problem to SAT/SMT""" import os import subprocess import sys import tempfile import time from typing import Any, Mapping, Sequence, Union, Tuple, Optional import z3 def var_given( data: Mapping[str, Any], arr: str, i: int, j: int, k: int, l: Optional[int] = None, ) -> bool: """Check whether data[arr][i][j][k]([l]) is given. If the given indices are not found, return False; otherwise return True. Args: data (Mapping[str, Any]): contain arrays arr (str): ExistI, etc. i (int): first index j (int): second index k (int): third index l (int, optional): optional fourth index. Defaults to None. Returns: bool: whether the variable value is given Raises: ValueError: found value, but not 0 nor 1 nor -1. """ if arr not in data: return False if l is None: if data[arr][i][j][k] == -1: return False elif data[arr][i][j][k] != 0 and data[arr][i][j][k] != 1: raise ValueError(f"{arr}[{i}, {j}, {k}] is not 0 nor 1 nor -1.") return True # l is not None, then if data[arr][i][j][k][l] == -1: return False elif data[arr][i][j][k][l] != 0 and data[arr][i][j][k][l] != 1: raise ValueError(f"{arr}[{i}, {j}, {k}, {l}] is not 0 nor 1 nor -1.") return True def port_incident_pipes( port: Mapping[str, Union[str, int]], n_i: int, n_j: int, n_k: int) -> Tuple[Sequence[str], Sequence[Tuple[int, int, int]]]: """Compute the pipes incident to a port. A port is an pipe with a open end. The incident pipes of a port are the five other pipes connecting to that end. However, some of these pipes can be out of bound, we just want to compute those that are in bound. Args: port (Mapping[str, Union[str, int]]): the port to consider n_i (int): spatial bound on I direction n_j (int): spatial bound on J direction n_k (int): spatial bound on K direction Returns: Tuple[Sequence[str], Sequence[Tuple[int, int, int]]]: Two lists of the same length [0,6): (dirs, coords) dirs: the direction of the incident pipes, can be "I", "J", or "K" coords: the coordinates of the incident pipes, each one is (i,j,k) """ coords = [] dirs = [] # first, just consider adjancency without caring about out-of-bound if port["d"] == "I": adj_dirs = ["I", "J", "J", "K", "K"] if port["e"] == "-": # empty cube is (i,j,k) adj_coords = [ (port["i"] - 1, port["j"], port["k"]), # (i-1,j,k)---(i,j,k) (port["i"], port["j"] - 1, port["k"]), # (i,j-1,k)---(i,j,k) (port["i"], port["j"], port["k"]), # (i,j,k)---(i,j+1,k) (port["i"], port["j"], port["k"] - 1), # (i,j,k-1)---(i,j,k) (port["i"], port["j"], port["k"]), # (i,j,k)---(i,j,k+1) ] elif port["e"] == "+": # empty cube is (i+1,j,k) adj_coords = [ (port["i"] + 1, port["j"], port["k"]), # (i+1,j,k)---(i+2,j,k) (port["i"] + 1, port["j"] - 1, port["k"]), # (i+1,j-1,k)---(i+1,j,k) (port["i"] + 1, port["j"], port["k"]), # (i+1,j,k)---(i+1,j+1,k) (port["i"] + 1, port["j"], port["k"] - 1), # (i+1,j,k-1)---(i+1,j,k) (port["i"] + 1, port["j"], port["k"]), # (i+1,j,k)---(i+1,j,k+1) ] if port["d"] == "J": adj_dirs = ["J", "K", "K", "I", "I"] if port["e"] == "-": adj_coords = [ (port["i"], port["j"] - 1, port["k"]), (port["i"], port["j"], port["k"] - 1), (port["i"], port["j"], port["k"]), (port["i"] - 1, port["j"], port["k"]), (port["i"], port["j"], port["k"]), ] elif port["e"] == "+": adj_coords = [ (port["i"], port["j"] + 1, port["k"]), (port["i"], port["j"] + 1, port["k"] - 1), (port["i"], port["j"] + 1, port["k"]), (port["i"] - 1, port["j"] + 1, port["k"]), (port["i"], port["j"] + 1, port["k"]), ] if port["d"] == "K": adj_dirs = ["K", "I", "I", "J", "J"] if port["e"] == "-": adj_coords = [ (port["i"], port["j"], port["k"] - 1), (port["i"] - 1, port["j"], port["k"]), (port["i"], port["j"], port["k"]), (port["i"], port["j"] - 1, port["k"]), (port["i"], port["j"], port["k"]), ] elif port["e"] == "+": adj_coords = [ (port["i"], port["j"], port["k"] + 1), (port["i"] - 1, port["j"], port["k"] + 1), (port["i"], port["j"], port["k"] + 1), (port["i"], port["j"] - 1, port["k"] + 1), (port["i"], port["j"], port["k"] + 1), ] # only keep the pipes in bound for i, coord in enumerate(adj_coords): if ((coord[0] in range(n_i)) and (coord[1] in range(n_j)) and (coord[2] in range(n_k))): coords.append(adj_coords[i]) dirs.append(adj_dirs[i]) return dirs, coords def cnf_even_parity_upto4(eles: Sequence[Any]) -> Any: """Compute the CNF format of parity of up to four Z3 binary variables. Args: eles (Sequence[Any]): the binary variables. Returns: (Any) the Z3 constraint meaning the parity of the inputs is even. Raises: ValueError: number of elements is not 1, 2, 3, or 4. """ if len(eles) == 1: # 1 var even parity -> this var is false return z3.Not(eles[0]) elif len(eles) == 2: # 2 vars even pairty -> both True or both False return z3.Or(z3.And(z3.Not(eles[0]), z3.Not(eles[1])), z3.And(eles[0], eles[1])) elif len(eles) == 3: # 3 vars even parity -> all False, or 2 True and 1 False return z3.Or( z3.And(z3.Not(eles[0]), z3.Not(eles[1]), z3.Not(eles[2])), z3.And(eles[0], eles[1], z3.Not(eles[2])), z3.And(eles[0], z3.Not(eles[1]), eles[2]), z3.And(z3.Not(eles[0]), eles[1], eles[2]), ) elif len(eles) == 4: # 4 vars even parity -> 0, 2, or 4 vars are True return z3.Or( z3.And(z3.Not(eles[0]), z3.Not(eles[1]), z3.Not(eles[2]), z3.Not(eles[3])), z3.And(z3.Not(eles[0]), z3.Not(eles[1]), eles[2], eles[3]), z3.And(z3.Not(eles[0]), eles[1], z3.Not(eles[2]), eles[3]), z3.And(z3.Not(eles[0]), eles[1], eles[2], z3.Not(eles[3])), z3.And(eles[0], z3.Not(eles[1]), z3.Not(eles[2]), eles[3]), z3.And(eles[0], z3.Not(eles[1]), eles[2], z3.Not(eles[3])), z3.And(eles[0], eles[1], z3.Not(eles[2]), z3.Not(eles[3])), z3.And(eles[0], eles[1], eles[2], eles[3]), ) else: raise ValueError("This function only supports 1, 2, 3, or 4 vars.") class LatticeSurgerySAT: """class of synthesizing LaSRe using Z3 SMT solver and Kissat SAT solver. It encodes a lattice surgery synthesis problem to SAT/SMT and checks whether there is a solution. We are given certain spacetime volume, certain ports, and certain stabilizers. LatticeSurgerySAT encodes the constraints on LaSRe variables such that the resulting variable assignments consist of a valid lattice surgery subroutine with the correct functionality (satisfies all the given stabilizers). LatticeSurgerySAT finds the solution with a SAT/SMT solver. """ def __init__( self, input_dict: Mapping[str, Any], color_ij: bool = True, given_arrs: Optional[Mapping[str, Any]] = None, given_vals: Optional[Sequence[Mapping[str, Any]]] = None, ) -> None: """initialization of LatticeSurgerySAT. Args: input_dict (Mapping[str, Any]): specification of LaS. color_ij (bool, optional): if the color matching constraints of I and J pipes are imposed. Defaults to True. So far, we always impose these constraints. given_arrs (Mapping[str, Any], optional): Arrays of values to plug in. Defaults to None. given_vals (Sequence[Mapping[str, Any]], optional): Values to plug in. Defaults to None. These values will replace existing values if already set by given_arrs. """ self.input_dict = input_dict self.color_ij = color_ij self.goal = z3.Goal() self.process_input(input_dict) self.build_smt_model(given_arrs=given_arrs, given_vals=given_vals) def process_input(self, input_dict: Mapping[str, Any]) -> None: """read input specification, mainly translating the info at the ports. Args: input_dict (Mapping[str, Any]): LaS specification. Raises: ValueError: missing key in input specification. ValueError: some spatial bound <= 0. ValueError: more stabilizers than ports. ValueError: stabilizer length is not the same as the number of ports. ValueError: stabilizer contains things other than I, X, Y, or Z. ValueError: missing key in port. ValueError: port location is not a 3-tuple. ValueError: port direction is not 2-string. ValueError: port sign (which end is dangling) is not - or +. ValueError: port axis is not I, J, or K. ValueError: port location+direction is out of bound. ValueError: port Z basis direction is not I, J, or K, and the same with the pipe. ValueError: forbidden cube location is not a 3-tuple. ValueError: forbiddent cube location is out of bounds. """ data = input_dict for key in ["max_i", "max_j", "max_k", "ports", "stabilizers"]: if key not in data: raise ValueError(f"missing key {key} in input specification.") # load spatial bound, check > 0 self.n_i = data["max_i"] self.n_j = data["max_j"] self.n_k = data["max_k"] if min([self.n_i, self.n_j, self.n_k]) <= 0: raise ValueError("max_i or _j or _k <= 0.") self.n_p = len(data["ports"]) self.n_s = len(data["stabilizers"]) # there should be at most as many stabilizers as ports if self.n_s > self.n_p: raise ValueError( f"{self.n_s} stabilizers, too many for {self.n_p} ports.") # stabilizers should be paulistrings of length #ports self.paulistrings = [s.replace(".", "I") for s in data["stabilizers"]] for s in self.paulistrings: if len(s) != self.n_p: raise ValueError( f"len({s}) = {len(s)}, but there are {self.n_p} ports.") for i in range(len(s)): if s[i] not in ["I", "X", "Y", "Z"]: raise ValueError( f"{s} has invalid Pauli. I, X, Y, and Z are allowed.") # transform port data self.ports = [] for port in data["ports"]: for key in ["location", "direction", "z_basis_direction"]: if key not in port: raise ValueError(f"missing key {key} in port {port}") if len(port["location"]) != 3: raise ValueError(f"port location should be 3-tuple {port}.") if len(port["direction"]) != 2: raise ValueError( f"port direction should have 2 characters {port}.") if port["direction"][0] not in ["+", "-"]: raise ValueError(f"port direction with invalid sign {port}.") if port["direction"][1] not in ["I", "J", "K"]: raise ValueError(f"port direction with invalid axis {port}.") if port["direction"][0] == "-" and port["direction"][ 1] == "I" and (port["location"][0] not in range( 1, self.n_i + 1)): raise ValueError( f"{port['location']} with direction {port['direction']}" f" should be in range [1, f{self.n_i+1}).") if port["direction"][0] == "+" and port["direction"][ 1] == "I" and (port["location"][0] not in range( 0, self.n_i)): raise ValueError( f"{port['location']} with direction {port['direction']}" f" should be in range [0, f{self.n_i}).") if port["direction"][0] == "-" and port["direction"][ 1] == "J" and (port["location"][1] not in range( 1, self.n_j + 1)): raise ValueError( f"{port['location']} with direction {port['direction']}" f" should be in range [1, {self.n_j+1}).") if port["direction"][0] == "+" and port["direction"][ 1] == "J" and (port["location"][1] not in range( 0, self.n_j)): raise ValueError( f"{port['location']} with direction {port['direction']}" f" should be in range [0, f{self.n_j}).") if port["direction"][0] == "-" and port["direction"][ 1] == "K" and (port["location"][2] not in range( 1, self.n_k + 1)): raise ValueError( f"{port['location']} with direction {port['direction']}" f" should be in range [1, f{self.n_k+1}).") if port["direction"][0] == "+" and port["direction"][ 1] == "K" and (port["location"][2] not in range( 0, self.n_k)): raise ValueError( f"{port['location']} with direction {port['direction']}" f" should be in range [0, f{self.n_k}).") # internally, a port is an pipe. This is different from what we # expose to the user: in LaS specification, a port is a cube and # associated with a direction, e.g., cube [i,j,k] and direction # "-K". This means the port should be the pipe connecting (i,j,k) # downwards to the volume of LaS. Thus, that pipe is (i,j,k-1) -- # (i,j,k) which by convention is the K-pipe (i,j,k-1). # a port here has fields "i", "j", "k", "d", "e", "f", "c" my_port = {} # "i", "j", and "k" are the i,j,k of the pipe my_port["i"], my_port["j"], my_port["k"] = port["location"] if port["direction"][0] == "-": my_port[port["direction"][1].lower()] -= 1 # "d" is one of I, J, and K, corresponding to the port being an # I-pipe, J-pipe, or a K-pipe my_port["d"] = port["direction"][1] # "e" is the end of the pipe that is open. For example, if the port # is a K-pipe (i,j,k), then "e"="+" means the cube (i,j,k+1) is # open; otherwise, "e"="-" means cube (i,j,k) is open. my_port["e"] = "-" if port["direction"][0] == "+" else "+" # "c" is the color variable of the pipe corresponding to the port z_dir = port["z_basis_direction"] if z_dir not in ["I", "J", "K"] or z_dir == my_port["d"]: raise ValueError( f"port with invalid Z basis direction {port}.") if my_port["d"] == "I": my_port["c"] = 0 if z_dir == "J" else 1 if my_port["d"] == "J": my_port["c"] = 0 if z_dir == "K" else 1 if my_port["d"] == "K": my_port["c"] = 0 if z_dir == "I" else 1 # "f" is the function of the pipe, e.g., it can say this port is a # T injection. This field is not used in the SAT synthesis, but # we keep this info to use in later stages like gltf generation if "function" in port: my_port["f"] = port["function"] self.ports.append(my_port) # from paulistrings to correlation surfaces self.stabs = self.derive_corr_boundary(self.paulistrings) self.optional = {} self.forbidden_cubes = [] if "optional" in data: self.optional = data["optional"] if "forbidden_cubes" in data["optional"]: for cube in data["optional"]["forbidden_cubes"]: if len(cube) != 3: raise ValueError( f"forbid cube should be 3-tuple {cube}.") if (cube[0] not in range(self.n_i) or cube[1] not in range(self.n_j) or cube[2] not in range(self.n_k)): raise ValueError( f"forbidden {cube} out of range " f"(i,j,k) < ({self.n_i, self.n_j, self.n_k})") self.forbidden_cubes.append(cube) self.get_port_cubes() def get_port_cubes(self) -> None: """calculate which cubes are the open cube for the ports. Note that these are *** 3-tuples ***, not lists with 3 elements.""" self.port_cubes = [] for p in self.ports: # if e=-, (i,j,k); otherwise, +1 in the proper direction if p["e"] == "-": self.port_cubes.append((p["i"], p["j"], p["k"])) elif p["d"] == "I": self.port_cubes.append((p["i"] + 1, p["j"], p["k"])) elif p["d"] == "J": self.port_cubes.append((p["i"], p["j"] + 1, p["k"])) elif p["d"] == "K": self.port_cubes.append((p["i"], p["j"], p["k"] + 1)) def derive_corr_boundary( self, paulistrings: Sequence[str] ) -> Sequence[Sequence[Mapping[str, int]]]: """derive the boundary correlation surface variable values. From the color orientation of the ports and the stabilizers, we can derive which correlation surface variables evaluates to True and which to False at the ports for each stabilizer. Args: paulistrings (Sequence[str]): stabilizers as a list of Paulistrings Returns: Sequence[Sequence[Mapping[str, int]]]: Outer layer list is the list of stabilizers. Inner layer list is the situation at each port for one specifeic stabilizer. Each port is specified with a dictionary of 2 bits for the 2 correaltion surfaces. """ stabs = [] for paulistring in paulistrings: corr = [] for p in range(self.n_p): if paulistring[p] == "I": # I -> no corr surf should be present if self.ports[p]["d"] == "I": corr.append({"IJ": 0, "IK": 0}) if self.ports[p]["d"] == "J": corr.append({"JI": 0, "JK": 0}) if self.ports[p]["d"] == "K": corr.append({"KI": 0, "KJ": 0}) if paulistring[p] == "Y": # Y -> both corr surf should be present if self.ports[p]["d"] == "I": corr.append({"IJ": 1, "IK": 1}) if self.ports[p]["d"] == "J": corr.append({"JI": 1, "JK": 1}) if self.ports[p]["d"] == "K": corr.append({"KI": 1, "KJ": 1}) if paulistring[p] == "X": # X -> only corr surf touching red faces if self.ports[p]["d"] == "I": if self.ports[p]["c"]: corr.append({"IJ": 1, "IK": 0}) else: corr.append({"IJ": 0, "IK": 1}) if self.ports[p]["d"] == "J": if self.ports[p]["c"]: corr.append({"JI": 0, "JK": 1}) else: corr.append({"JI": 1, "JK": 0}) if self.ports[p]["d"] == "K": if self.ports[p]["c"]: corr.append({"KI": 1, "KJ": 0}) else: corr.append({"KI": 0, "KJ": 1}) if paulistring[p] == "Z": # Z -> only corr surf touching blue faces if self.ports[p]["d"] == "I": if not self.ports[p]["c"]: corr.append({"IJ": 1, "IK": 0}) else: corr.append({"IJ": 0, "IK": 1}) if self.ports[p]["d"] == "J": if not self.ports[p]["c"]: corr.append({"JI": 0, "JK": 1}) else: corr.append({"JI": 1, "JK": 0}) if self.ports[p]["d"] == "K": if not self.ports[p]["c"]: corr.append({"KI": 1, "KJ": 0}) else: corr.append({"KI": 0, "KJ": 1}) stabs.append(corr) return stabs def build_smt_model( self, given_arrs: Optional[Mapping[str, Any]] = None, given_vals: Optional[Sequence[Mapping[str, Any]]] = None, ) -> None: """build the SMT model with variables and constraints. Args: given_arrs (Mapping[str, Any], optional): Arrays of values to plug in. Defaults to None. given_vals (Sequence[Mapping[str, Any]], optional): Values to plug in. Defaults to None. These values will replace existing values if already set by given_arrs. """ self.define_vars() if given_arrs is not None: self.plugin_arrs(given_arrs) if given_vals is not None: self.plugin_vals(given_vals) # baseline order of constraint sets, '...' menas name in the paper # validity constraints that directly set variables values self.constraint_forbid_cube() self.constraint_port() # 'no fanouts' self.constraint_connect_outside() # 'no unexpected ports' # more complex validity constraints involving boolean logic self.constraint_timelike_y() # 'time-like Y cubes' self.constraint_no_deg1() # 'no degree-1 non-Y cubes' if self.color_ij: # 'matching colors at passthroughs' and '... at turns' self.constraint_ij_color() self.constraint_3d_corner() # 'no 3D corners' # simpler functionality constraints self.constraint_corr_ports() # 'stabilizer as boundary conditions' self.constraint_corr_y() # 'both or non at Y cubes' # more complex functionality constraints # 'all or no orthogonal surfaces at non-Y cubes: self.constraint_corr_perp() # 'even parity of parallel surfaces at non-Y cubes': self.constraint_corr_para() def define_vars(self) -> None: """define the variables in Z3 into self.vars.""" self.vars = { "ExistI": [[[z3.Bool(f"ExistI({i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)], "ExistJ": [[[z3.Bool(f"ExistJ({i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)], "ExistK": [[[z3.Bool(f"ExistK({i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)], "NodeY": [[[z3.Bool(f"NodeY({i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)], "CorrIJ": [[[[z3.Bool(f"CorrIJ({s},{i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)] for s in range(self.n_s)], "CorrIK": [[[[z3.Bool(f"CorrIK({s},{i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)] for s in range(self.n_s)], "CorrJK": [[[[z3.Bool(f"CorrJK({s},{i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)] for s in range(self.n_s)], "CorrJI": [[[[z3.Bool(f"CorrJI({s},{i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)] for s in range(self.n_s)], "CorrKI": [[[[z3.Bool(f"CorrKI({s},{i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)] for s in range(self.n_s)], "CorrKJ": [[[[z3.Bool(f"CorrKJ({s},{i},{j},{k})") for k in range(self.n_k)] for j in range(self.n_j)] for i in range(self.n_i)] for s in range(self.n_s)], } if self.color_ij: self.vars["ColorI"] = [[[ z3.Bool(f"ColorI({i},{j},{k})") for k in range(self.n_k) ] for j in range(self.n_j)] for i in range(self.n_i)] self.vars["ColorJ"] = [[[ z3.Bool(f"ColorJ({i},{j},{k})") for k in range(self.n_k) ] for j in range(self.n_j)] for i in range(self.n_i)] def plugin_arrs(self, data: Mapping[str, Any]) -> None: """plug in the given arrays of values. Args: data (Mapping[str, Any]): contains gieven values. Raises: ValueError: data contains an invalid array name. ValueError: array given has wrong dimensions. """ for key in data: if key in [ "NodeY", "ExistI", "ExistJ", "ExistK", "ColorI", "ColorJ", ]: if len(data[key]) != self.n_i: raise ValueError(f"dimension of {key} is wrong.") for tmp in data[key]: if len(tmp) != self.n_j: raise ValueError(f"dimension of {key} is wrong.") for tmptmp in tmp: if len(tmptmp) != self.n_k: raise ValueError(f"dimension of {key} is wrong.") elif key in [ "CorrIJ", "CorrIK", "CorrJI", "CorrJK", "CorrKI", "CorrKJ", ]: if len(data[key]) != self.n_s: raise ValueError(f"dimension of {key} is wrong.") for tmp in data[key]: if len(tmp) != self.n_i: raise ValueError(f"dimension of {key} is wrong.") for tmptmp in tmp: if len(tmptmp) != self.n_j: raise ValueError(f"dimension of {key} is wrong.") for tmptmptmp in tmptmp: if len(tmptmptmp) != self.n_k: raise ValueError( f"dimension of {key} is wrong.") else: raise ValueError(f"{key} is not a valid array name") arrs = [ "NodeY", "ExistI", "ExistJ", "ExistK", ] if self.color_ij: arrs += ["ColorI", "ColorJ"] for s in range(self.n_s): for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): if s == 0: # Exist, Node, and Color vars for arr in arrs: if var_given(data, arr, i, j, k): self.goal.add( self.vars[arr][i][j][k] if data[arr][i][j][k] == 1 else z3.Not(self.vars[arr][i][j][k])) # Corr vars for arr in [ "CorrIJ", "CorrIK", "CorrJI", "CorrJK", "CorrKI", "CorrKJ", ]: if var_given(data, arr, s, i, j, k): self.goal.add( self.vars[arr][s][i][j][k] if data[arr][s][i][j][k] == 1 else z3.Not(self.vars[arr][s][i][j][k])) def plugin_vals(self, data_set: Sequence[Mapping[str, Any]]): """plug in the given values Args: data (Sequence[Mapping[str, Any]]): given values as a sequence of dicts. Each one contains three fields: "array", the name of the array, e.g., "ExistI"; "indices", a sequence of the indices; and "value". Raises: ValueError: given_vals missing a field. ValueError: array name is not valid. ValueError: indices dimension for certain array is wrong. ValueError: index value out of bound. ValueError: given value is neither 0 nor 1. """ for data in data_set: for key in ["array", "indices", "value"]: if key not in data: raise ValueError(f"{key} is not in given val") if data["array"] not in [ "NodeY", "ExistI", "ExistJ", "ExistK", "ColorI", "ColorJ", "CorrIJ", "CorrIK", "CorrJI", "CorrJK", "CorrKI", "CorrKJ", ]: raise ValueError(f"{data['array']} is not a valid array.") if data["array"] in [ "NodeY", "ExistI", "ExistJ", "ExistK", "ColorI", "ColorJ", ]: if len(data["indices"] != 3): raise ValueError(f"Need 3 indices for {data['array']}.") if data["indices"][0] not in range(self.n_i): raise ValueError(f"i index out of range") if data["indices"][1] not in range(self.n_j): raise ValueError(f"j index out of range") if data["indices"][2] not in range(self.n_k): raise ValueError(f"k index out of range") if data["array"] in [ "CorrIJ", "CorrIK", "CorrJI", "CorrJK", "CorrKI", "CorrKJ", ]: if len(data["indices"] != 4): raise ValueError(f"Need 4 indices for {data['array']}.") if data["indices"][0] not in range(self.n_s): raise ValueError(f"s index out of range") if data["indices"][1] not in range(self.n_i): raise ValueError(f"i index out of range") if data["indices"][2] not in range(self.n_j): raise ValueError(f"j index out of range") if data["indices"][3] not in range(self.n_k): raise ValueError(f"k index out of range") if data["value"] not in [0, 1]: raise ValueError("Given value can only be 0 or 1.") (arr, idx) = data["array"], data["indices"] if arr.startswith("Corr"): s, i, j, k = idx if data["value"] == 1: self.goal.add(self.vars[arr][s][i][j][k]) else: self.goal.add(z3.Not(self.vars[arr][s][i][j][k])) else: i, j, k = idx if data["value"] == 1: self.goal.add(self.vars[arr][i][j][k]) else: self.goal.add(z3.Not(self.vars[arr][i][j][k])) def constraint_forbid_cube(self) -> None: """forbid a list of cubes.""" for cube in self.forbidden_cubes: (i, j, k) = cube[0], cube[1], cube[2] self.goal.add(z3.Not(self.vars["NodeY"][i][j][k])) if i > 0: self.goal.add(z3.Not(self.vars["ExistI"][i - 1][j][k])) self.goal.add(z3.Not(self.vars["ExistI"][i][j][k])) if j > 0: self.goal.add(z3.Not(self.vars["ExistJ"][i][j - 1][k])) self.goal.add(z3.Not(self.vars["ExistJ"][i][j][k])) if k > 0: self.goal.add(z3.Not(self.vars["ExistK"][i][j][k - 1])) self.goal.add(z3.Not(self.vars["ExistK"][i][j][k])) def constraint_port(self) -> None: """some pipes must exist and some must not depending on the ports.""" for port in self.ports: # the pipe specified by the port exists self.goal.add(self.vars[f"Exist{port['d']}"][port["i"]][port["j"]][ port["k"]]) # if I- or J-pipe exist, set the color value too to the given one if self.color_ij: if port["d"] != "K": if port["c"] == 1: self.goal.add(self.vars[f"Color{port['d']}"][port["i"]] [port["j"]][port["k"]]) else: self.goal.add( z3.Not(self.vars[f"Color{port['d']}"][port["i"]][ port["j"]][port["k"]])) # collect the pipes touching the port to forbid them dirs, coords = port_incident_pipes(port, self.n_i, self.n_j, self.n_k) for i, coord in enumerate(coords): self.goal.add( z3.Not(self.vars[f"Exist{dirs[i]}"][coord[0]][coord[1]][ coord[2]])) def constraint_connect_outside(self) -> None: """no pipe should cross the spatial bound except for ports.""" for i in range(self.n_i): for j in range(self.n_j): # consider K-pipes crossing K-bound and not a port if (i, j, self.n_k) not in self.port_cubes: self.goal.add( z3.Not(self.vars["ExistK"][i][j][self.n_k - 1])) for i in range(self.n_i): for k in range(self.n_k): if (i, self.n_j, k) not in self.port_cubes: self.goal.add( z3.Not(self.vars["ExistJ"][i][self.n_j - 1][k])) for j in range(self.n_j): for k in range(self.n_k): if (self.n_i, j, k) not in self.port_cubes: self.goal.add( z3.Not(self.vars["ExistI"][self.n_i - 1][j][k])) def constraint_timelike_y(self) -> None: """forbid all I- and J- pipes to Y cubes.""" for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): if (i, j, k) not in self.port_cubes: self.goal.add( z3.Implies( self.vars["NodeY"][i][j][k], z3.Not(self.vars["ExistI"][i][j][k]), )) self.goal.add( z3.Implies( self.vars["NodeY"][i][j][k], z3.Not(self.vars["ExistJ"][i][j][k]), )) if i - 1 >= 0: self.goal.add( z3.Implies( self.vars["NodeY"][i][j][k], z3.Not(self.vars["ExistI"][i - 1][j][k]), )) if j - 1 >= 0: self.goal.add( z3.Implies( self.vars["NodeY"][i][j][k], z3.Not(self.vars["ExistJ"][i][j - 1][k]), )) def constraint_ij_color(self) -> None: """color matching for I- and J-pipes.""" for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): if i >= 1 and j >= 1: # (i-1,j,k)-(i,j,k) and (i,j-1,k)-(i,j,k) self.goal.add( z3.Implies( z3.And( self.vars["ExistI"][i - 1][j][k], self.vars["ExistJ"][i][j - 1][k], ), z3.Or( z3.And( self.vars["ColorI"][i - 1][j][k], z3.Not(self.vars["ColorJ"][i][j - 1][k]), ), z3.And( z3.Not(self.vars["ColorI"][i - 1][j][k]), self.vars["ColorJ"][i][j - 1][k], ), ), )) if i >= 1: # (i-1,j,k)-(i,j,k) and (i,j,k)-(i,j+1,k) self.goal.add( z3.Implies( z3.And( self.vars["ExistI"][i - 1][j][k], self.vars["ExistJ"][i][j][k], ), z3.Or( z3.And( self.vars["ColorI"][i - 1][j][k], z3.Not(self.vars["ColorJ"][i][j][k]), ), z3.And( z3.Not(self.vars["ColorI"][i - 1][j][k]), self.vars["ColorJ"][i][j][k], ), ), )) # (i-1,j,k)-(i,j,k) and (i,j,k)-(i+1,j,k) self.goal.add( z3.Implies( z3.And( self.vars["ExistI"][i - 1][j][k], self.vars["ExistI"][i][j][k], ), z3.Or( z3.And( self.vars["ColorI"][i - 1][j][k], self.vars["ColorI"][i][j][k], ), z3.And( z3.Not(self.vars["ColorI"][i - 1][j][k]), z3.Not(self.vars["ColorI"][i][j][k]), ), ), )) if j >= 1: # (i,j,k)-(i+1,j,k) and (i,j-1,k)-(i,j,k) self.goal.add( z3.Implies( z3.And( self.vars["ExistI"][i][j][k], self.vars["ExistJ"][i][j - 1][k], ), z3.Or( z3.And( self.vars["ColorI"][i][j][k], z3.Not(self.vars["ColorJ"][i][j - 1][k]), ), z3.And( z3.Not(self.vars["ColorI"][i][j][k]), self.vars["ColorJ"][i][j - 1][k], ), ), )) # (i,j-1,k)-(i,j,k) and (i,j,k)-(i,j+1,k) self.goal.add( z3.Implies( z3.And( self.vars["ExistJ"][i][j - 1][k], self.vars["ExistJ"][i][j][k], ), z3.Or( z3.And( self.vars["ColorJ"][i][j - 1][k], self.vars["ColorJ"][i][j][k], ), z3.And( z3.Not(self.vars["ColorJ"][i][j - 1][k]), z3.Not(self.vars["ColorJ"][i][j][k]), ), ), )) # (i,j,k)-(i+1,j,k) and (i,j,k)-(i,j+1,k) self.goal.add( z3.Implies( z3.And(self.vars["ExistI"][i][j][k], self.vars["ExistJ"][i][j][k]), z3.Or( z3.And( self.vars["ColorI"][i][j][k], z3.Not(self.vars["ColorJ"][i][j][k]), ), z3.And( z3.Not(self.vars["ColorI"][i][j][k]), self.vars["ColorJ"][i][j][k], ), ), )) def constraint_3d_corner(self) -> None: """at least in one direction, both pipes nonexist.""" for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): i_pipes = [ self.vars["ExistI"][i][j][k], ] if i - 1 >= 0: i_pipes.append(self.vars["ExistI"][i - 1][j][k]) j_pipes = [ self.vars["ExistJ"][i][j][k], ] if j - 1 >= 0: j_pipes.append(self.vars["ExistJ"][i][j - 1][k]) k_pipes = [ self.vars["ExistK"][i][j][k], ] if k - 1 >= 0: k_pipes.append(self.vars["ExistK"][i][j][k - 1]) # at least one of the three terms is true. The first term # is that both I-pipes connecting to (i,j,k) do not exist. self.goal.add( z3.Or( z3.Not(z3.Or(i_pipes)), z3.Not(z3.Or(j_pipes)), z3.Not(z3.Or(k_pipes)), )) def constraint_no_deg1(self) -> None: """forbid degree-1 X or Z cubes by considering incident pipes.""" for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): for d in ["I", "J", "K"]: for e in ["-", "+"]: cube = {"I": i, "J": j, "K": k} cube[d] += 1 if e == "+" else 0 # construct fake ports to get incident pipes p0 = { "i": i, "j": j, "k": k, "d": d, "e": e, "c": 0 } found_p0 = False for port in self.ports: if (i == port["i"] and j == port["j"] and k == port["k"] and d == port["d"]): found_p0 = True # only non-port pipes need to consider if (not found_p0 and cube["I"] < self.n_i and cube["J"] < self.n_j and cube["K"] < self.n_k): # only cubes inside bound need to consider dirs, coords = port_incident_pipes( p0, self.n_i, self.n_j, self.n_k) pipes = [ self.vars[f"Exist{dirs[l]}"][coord[0]][ coord[1]][coord[2]] for l, coord in enumerate(coords) ] # if the cube is not Y and the pipe exist, then # at least one of its incident pipes exists. self.goal.add( z3.Implies( z3.And( z3.Not( self.vars["NodeY"][cube["I"]][ cube["J"]][cube["K"]]), self.vars[f"Exist{d}"][i][j][k], ), z3.Or(pipes), )) def constraint_corr_ports(self) -> None: """plug in the correlation surface values at the ports.""" for s, stab in enumerate(self.stabs): for p, corrs in enumerate(stab): for k, v in corrs.items(): if v == 1: self.goal.add( self.vars[f"Corr{k}"][s][self.ports[p]["i"]][ self.ports[p]["j"]][self.ports[p]["k"]]) else: self.goal.add( z3.Not(self.vars[f"Corr{k}"][s][self.ports[p]["i"]] [self.ports[p]["j"]][self.ports[p]["k"]])) def constraint_corr_y(self) -> None: """correlation surfaces at Y-cubes should both exist or nonexist.""" for s in range(self.n_s): for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): self.goal.add( z3.Or( z3.Not(self.vars["NodeY"][i][j][k]), z3.Or( z3.And( self.vars["CorrKI"][s][i][j][k], self.vars["CorrKJ"][s][i][j][k], ), z3.And( z3.Not( self.vars["CorrKI"][s][i][j][k]), z3.Not( self.vars["CorrKJ"][s][i][j][k]), ), ), )) if k - 1 >= 0: self.goal.add( z3.Or( z3.Not(self.vars["NodeY"][i][j][k]), z3.Or( z3.And( self.vars["CorrKI"][s][i][j][k - 1], self.vars["CorrKJ"][s][i][j][k - 1], ), z3.And( z3.Not(self.vars["CorrKI"][s][i][j] [k - 1]), z3.Not(self.vars["CorrKJ"][s][i][j] [k - 1]), ), ), )) def constraint_corr_perp(self) -> None: """for corr surf perpendicular to normal vector, all or none exists.""" for s in range(self.n_s): for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): if (i, j, k) not in self.port_cubes: # only consider X or Z spider # if normal is K meaning meaning both # (i,j,k)-(i,j,k+1) and (i,j,k)-(i,j,k-1) are # out of range, or in range but nonexistent normal = z3.And( z3.Not(self.vars["NodeY"][i][j][k]), z3.Not(self.vars["ExistK"][i][j][k]), ) if k - 1 >= 0: normal = z3.And( normal, z3.Not(self.vars["ExistK"][i][j][k - 1])) # for other pipes, we need to build an intermediate # expression for (i,j,k)-(i+1,j,k) and # (i,j,k)-(i,j+1,k), built expression meaning # the pipe is nonexistent or exist and has # the correlation surface perpendicular to # the normal vector in them. no_pipe_or_with_corr = [ z3.Or( z3.Not(self.vars["ExistI"][i][j][k]), self.vars["CorrIJ"][s][i][j][k], ), z3.Or( z3.Not(self.vars["ExistJ"][i][j][k]), self.vars["CorrJI"][s][i][j][k], ), ] # for (i,j,k)-(i+1,j,k) and (i,j,k)-(i,j+1,k), # build expression meaning the pipe is nonexistent # or exist and does not have the correlation # surface perpendicular to the normal vector. no_pipe_or_no_corr = [ z3.Or( z3.Not(self.vars["ExistI"][i][j][k]), z3.Not(self.vars["CorrIJ"][s][i][j][k]), ), z3.Or( z3.Not(self.vars["ExistJ"][i][j][k]), z3.Not(self.vars["CorrJI"][s][i][j][k]), ), ] if i - 1 >= 0: # add (i-1,j,k)-(i,j,k) to the expression no_pipe_or_with_corr.append( z3.Or( z3.Not(self.vars["ExistI"][i - 1][j][k]), self.vars["CorrIJ"][s][i - 1][j][k], )) no_pipe_or_no_corr.append( z3.Or( z3.Not(self.vars["ExistI"][i - 1][j][k]), z3.Not( self.vars["CorrIJ"][s][i - 1][j][k]), )) if j - 1 >= 0: # add (i,j-1,k)-(i,j,k) to the expression no_pipe_or_with_corr.append( z3.Or( z3.Not(self.vars["ExistJ"][i][j - 1][k]), self.vars["CorrJI"][s][i][j - 1][k], )) no_pipe_or_no_corr.append( z3.Or( z3.Not(self.vars["ExistJ"][i][j - 1][k]), z3.Not( self.vars["CorrJI"][s][i][j - 1][k]), )) # if normal vector is K, then in all its # incident pipes that exist all correlation surface # in I-J plane exist or nonexist self.goal.add( z3.Implies( normal, z3.Or( z3.And(no_pipe_or_with_corr), z3.And(no_pipe_or_no_corr), ), )) # if normal is I normal = z3.And( z3.Not(self.vars["NodeY"][i][j][k]), z3.Not(self.vars["ExistI"][i][j][k]), ) if i - 1 >= 0: normal = z3.And( normal, z3.Not(self.vars["ExistI"][i - 1][j][k])) no_pipe_or_with_corr = [ z3.Or( z3.Not(self.vars["ExistJ"][i][j][k]), self.vars["CorrJK"][s][i][j][k], ), z3.Or( z3.Not(self.vars["ExistK"][i][j][k]), self.vars["CorrKJ"][s][i][j][k], ), ] no_pipe_or_no_corr = [ z3.Or( z3.Not(self.vars["ExistJ"][i][j][k]), z3.Not(self.vars["CorrJK"][s][i][j][k]), ), z3.Or( z3.Not(self.vars["ExistK"][i][j][k]), z3.Not(self.vars["CorrKJ"][s][i][j][k]), ), ] if j - 1 >= 0: no_pipe_or_with_corr.append( z3.Or( z3.Not(self.vars["ExistJ"][i][j - 1][k]), self.vars["CorrJK"][s][i][j - 1][k], )) no_pipe_or_no_corr.append( z3.Or( z3.Not(self.vars["ExistJ"][i][j - 1][k]), z3.Not( self.vars["CorrJK"][s][i][j - 1][k]), )) if k - 1 >= 0: no_pipe_or_with_corr.append( z3.Or( z3.Not(self.vars["ExistK"][i][j][k - 1]), self.vars["CorrKJ"][s][i][j][k - 1], )) no_pipe_or_no_corr.append( z3.Or( z3.Not(self.vars["ExistK"][i][j][k - 1]), z3.Not( self.vars["CorrKJ"][s][i][j][k - 1]), )) self.goal.add( z3.Implies( normal, z3.Or( z3.And(no_pipe_or_with_corr), z3.And(no_pipe_or_no_corr), ), )) # if normal is J normal = z3.And( z3.Not(self.vars["NodeY"][i][j][k]), z3.Not(self.vars["ExistJ"][i][j][k]), ) if j - 1 >= 0: normal = z3.And( normal, z3.Not(self.vars["ExistJ"][i][j - 1][k])) no_pipe_or_with_corr = [ z3.Or( z3.Not(self.vars["ExistI"][i][j][k]), self.vars["CorrIK"][s][i][j][k], ), z3.Or( z3.Not(self.vars["ExistK"][i][j][k]), self.vars["CorrKI"][s][i][j][k], ), ] no_pipe_or_no_corr = [ z3.Or( z3.Not(self.vars["ExistI"][i][j][k]), z3.Not(self.vars["CorrIK"][s][i][j][k]), ), z3.Or( z3.Not(self.vars["ExistK"][i][j][k]), z3.Not(self.vars["CorrKI"][s][i][j][k]), ), ] if i - 1 >= 0: no_pipe_or_with_corr.append( z3.Or( z3.Not(self.vars["ExistI"][i - 1][j][k]), self.vars["CorrIK"][s][i - 1][j][k], )) no_pipe_or_no_corr.append( z3.Or( z3.Not(self.vars["ExistI"][i - 1][j][k]), z3.Not( self.vars["CorrIK"][s][i - 1][j][k]), )) if k - 1 >= 0: no_pipe_or_with_corr.append( z3.Or( z3.Not(self.vars["ExistK"][i][j][k - 1]), self.vars["CorrKI"][s][i][j][k - 1], )) no_pipe_or_no_corr.append( z3.Or( z3.Not(self.vars["ExistK"][i][j][k - 1]), z3.Not( self.vars["CorrKI"][s][i][j][k - 1]), )) self.goal.add( z3.Implies( normal, z3.Or( z3.And(no_pipe_or_with_corr), z3.And(no_pipe_or_no_corr), ), )) def constraint_corr_para(self) -> None: """for corr surf parallel to the normal , even number of them exist.""" for s in range(self.n_s): for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): if (i, j, k) not in self.port_cubes: # only X or Z spiders, if normal is K normal = z3.And( z3.Not(self.vars["NodeY"][i][j][k]), z3.Not(self.vars["ExistK"][i][j][k]), ) if k - 1 >= 0: normal = z3.And( normal, z3.Not(self.vars["ExistK"][i][j][k - 1])) # unlike in constraint_corr_perp, we only care # about the cases where the pipe exists and the # correlation surface parallel to K also is present # so we build intermediate expressions as below pipe_with_corr = [ z3.And( self.vars["ExistI"][i][j][k], self.vars["CorrIK"][s][i][j][k], ), z3.And( self.vars["ExistJ"][i][j][k], self.vars["CorrJK"][s][i][j][k], ), ] # add (i-1,j,k)-(i,j,k) to the expression if i - 1 >= 0: pipe_with_corr.append( z3.And( self.vars["ExistI"][i - 1][j][k], self.vars["CorrIK"][s][i - 1][j][k], )) # add (i,j-1,k)-(i,j,k) to the expression if j - 1 >= 0: pipe_with_corr.append( z3.And( self.vars["ExistJ"][i][j - 1][k], self.vars["CorrJK"][s][i][j - 1][k], )) # parity of the expressions must be even self.goal.add( z3.Implies( normal, cnf_even_parity_upto4(pipe_with_corr))) # if normal is I normal = z3.And( z3.Not(self.vars["NodeY"][i][j][k]), z3.Not(self.vars["ExistI"][i][j][k]), ) if i - 1 >= 0: normal = z3.And( normal, z3.Not(self.vars["ExistI"][i - 1][j][k])) pipe_with_corr = [ z3.And( self.vars["ExistJ"][i][j][k], self.vars["CorrJI"][s][i][j][k], ), z3.And( self.vars["ExistK"][i][j][k], self.vars["CorrKI"][s][i][j][k], ), ] if j - 1 >= 0: pipe_with_corr.append( z3.And( self.vars["ExistJ"][i][j - 1][k], self.vars["CorrJI"][s][i][j - 1][k], )) if k - 1 >= 0: pipe_with_corr.append( z3.And( self.vars["ExistK"][i][j][k - 1], self.vars["CorrKI"][s][i][j][k - 1], )) self.goal.add( z3.Implies( normal, cnf_even_parity_upto4(pipe_with_corr))) # if normal is J normal = z3.And( z3.Not(self.vars["NodeY"][i][j][k]), z3.Not(self.vars["ExistJ"][i][j][k]), ) if j - 1 >= 0: normal = z3.And( normal, z3.Not(self.vars["ExistJ"][i][j - 1][k])) pipe_with_corr = [ z3.And( self.vars["ExistI"][i][j][k], self.vars["CorrIJ"][s][i][j][k], ), z3.And( self.vars["ExistK"][i][j][k], self.vars["CorrKJ"][s][i][j][k], ), ] if i - 1 >= 0: pipe_with_corr.append( z3.And( self.vars["ExistI"][i - 1][j][k], self.vars["CorrIJ"][s][i - 1][j][k], )) if k - 1 >= 0: pipe_with_corr.append( z3.And( self.vars["ExistK"][i][j][k - 1], self.vars["CorrKJ"][s][i][j][k - 1], )) self.goal.add( z3.Implies( normal, cnf_even_parity_upto4(pipe_with_corr))) def check_z3(self, print_progress: bool = True) -> bool: """check whether the built goal in self.goal is satisfiable. Args: print_progress (bool, optional): if print out the progress made. Returns: bool: True if SAT, False if UNSAT """ if print_progress: print("Construct a Z3 SMT model and solve...") start_time = time.time() self.solver = z3.Solver() self.solver.add(self.goal) ifsat = self.solver.check() elapsed = time.time() - start_time if print_progress: print("elapsed time: {:2f}s".format(elapsed)) if ifsat == z3.sat: if print_progress: print("Z3 SAT") return True else: if print_progress: print("Z3 UNSAT") return False def check_kissat( self, kissat_dir: str, dimacs_file_name: Optional[str] = None, sat_log_file_name: Optional[str] = None, print_progress: bool = True, ) -> bool: """check whether there is a solution with Kissat Args: kissat_dir (str): directory containing an executable named kissat dimacs_file_name (str, optional): Defaults to None. Then, the dimacs file is in a tmp directory. If specified, the dimacs will be saved to that path. sat_log_file_name (str, optional): Defaults to None. Then, the sat log file is in a tmp directory. If specified, the sat log will be saved to that path. print_progress (bool, optional): whether print the SAT solving process on screen. Defaults to True. Raises: ValueError: kissat_dir is not a directory ValueError: there is no executable kissat in kissat_dir ValueError: the return code to kissat is neither SAT nor UNSAT Returns: bool: True if SAT, False if UNSAT """ if not os.path.isdir(kissat_dir): raise ValueError(f"{kissat_dir} is not a directory.") if kissat_dir.endswith("/"): solver_cmd = kissat_dir + "kissat" else: solver_cmd = kissat_dir + "/kissat" if not os.path.isfile(solver_cmd): raise ValueError(f"There is no kissat in {kissat_dir}.") if_solved = False with tempfile.TemporaryDirectory() as tmp_dir: # open a tmp directory as workspace # dimacs and sat log are either in the tmp dir, or as user specify tmp_dimacs_file_name = (dimacs_file_name + ".dimacs" if dimacs_file_name else tmp_dir + "/tmp.dimacs") tmp_sat_log_file_name = (sat_log_file_name + ".kissat" if sat_log_file_name else tmp_dir + "/tmp.sat") if self.write_cnf(tmp_dimacs_file_name, print_progress=print_progress): # continue if the CNF is non-trivial, i.e., write_cnf is True kissat_start_time = time.time() with open(tmp_sat_log_file_name, "w") as log: # use tmp_sat_log_file_name to record stdout of kissat kissat_return_code = -100 # -100 if the return code is not updated later on. import random with subprocess.Popen( [ solver_cmd, f'--seed={random.randrange(1000000)}', tmp_dimacs_file_name ], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, ) as run_kissat: for line in run_kissat.stdout: log.write(line) if print_progress: sys.stdout.write(line) get_return_code = run_kissat.communicate()[0] kissat_return_code = run_kissat.returncode if kissat_return_code == 10: # 10 means SAT in Kissat if_solved = True if print_progress: print( f"kissat runtime: {time.time()-kissat_start_time}") print("kissat SAT!") # we read the Kissat solution from the SAT log, then, plug # those into the Z3 model and solved inside Z3 again. # The reason is that Z3 did some simplification of the # problem so not every variable appear in the DIMACS given # to Kissat. We still need to know their value. result = self.read_kissat_result( tmp_dimacs_file_name, tmp_sat_log_file_name, ) self.plugin_arrs(result) self.check_z3(print_progress) elif kissat_return_code == 20: if print_progress: print(f"{solver_cmd} UNSAT") elif kissat_return_code == -100: print("Did not get Kissat return code.") else: raise ValueError( f"Kissat return code {kissat_return_code} is neither" " SAT nor UNSAT. Maybe you should add print_process=" "True to enable the Kissat printout message to see " "what is going on.") # closing the tmp directory, the files and itself are removed return if_solved def write_cnf(self, output_file_name: str, print_progress: bool = False) -> bool: """generate CNF for the problem. Args: output_file_name (str): file to write CNF. Returns: bool: False if the CNF is trivial, True otherwise """ cnf_start_time = time.time() simplified = z3.Tactic("simplify")(self.goal)[0] simplified = z3.Tactic("propagate-values")(simplified)[0] cnf = z3.Tactic("tseitin-cnf")(simplified)[0] dimacs = cnf.dimacs() if print_progress: print(f"CNF generation time: {time.time() - cnf_start_time}") with open(output_file_name, "w") as output_f: output_f.write(cnf.dimacs()) if dimacs.startswith("p cnf 1 1"): print("Generated CNF is trivial meaning z3 concludes the instance" " UNSAT during simplification.") return False else: return True def read_kissat_result(self, dimacs_file: str, result_file: str) -> Mapping[str, Any]: """read result from external SAT solver Args: dimacs_file (str): result_file (str): log from Kissat containing SAT assignments Raises: ValueError: in the dimacs file, the last lines are comments that records the mapping from SAT variable indices to the variable names in Z3. If the coordinates in this name is incorrect. Returns: Mapping[str, Any]: variable assignment in arrays. All the one with a corresponding SAT variable are read off from the SAT log. The others are left with -1. """ results = { "ExistI": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "ExistJ": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "ExistK": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "ColorI": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "ColorJ": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "NodeY": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "CorrIJ": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrIK": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrJK": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrJI": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrKI": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrKJ": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], } # in this file, the assigments are lines starting with "v" like # v -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 ... # the vars starts from 1 and - means it's False; otherwise, it's True # we scan through all these lines, and save the assignments to `sat` with open(result_file, "r") as f: sat_output = f.readlines() sat = {} for line in sat_output: if line.startswith("v"): assignments = line[1:].strip().split(" ") for assignment in assignments: tmp = int(assignment) if tmp < 0: sat[str(-tmp)] = 0 elif tmp > 0: sat[str(tmp)] = 1 # in the dimacs generated by Z3, there are lines starting with "c" like # c 8804 CorrIJ(1,0,3,4) or c 60053 k!44404 # which records the mapping from our variables to variables in dimacs # the ones starting with k! are added in the translation, we don"t care with open(dimacs_file, "r") as f: dimacs = f.readlines() for line in dimacs: if line.startswith("c"): _, index, name = line.strip().split(" ") if name.startswith(( "NodeY", "ExistI", "ExistJ", "ExistK", "ColorI", "ColorJ", "CorrIJ", "CorrIK", "CorrJI", "CorrJK", "CorrKI", "CorrKJ", )): arr, tmp = name[:-1].split("(") coords = [int(num) for num in tmp.split(",")] if len(coords) == 3: results[arr][coords[0]][coords[1]][ coords[2]] = sat[index] elif len(coords) == 4: results[arr][coords[0]][coords[1]][coords[2]][ coords[3]] = sat[index] else: raise ValueError("number of coord should be 3 or 4!") return results def get_result(self) -> Mapping[str, Any]: """get the variable values. Returns: Mapping[str, Any]: output in the LaSRe format """ model = self.solver.model() data = { "n_i": self.n_i, "n_j": self.n_j, "n_k": self.n_k, "n_p": self.n_p, "n_s": self.n_s, "ports": self.ports, "stabs": self.stabs, "port_cubes": self.port_cubes, "optional": self.optional, "ExistI": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "ExistJ": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "ExistK": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "ColorI": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "ColorJ": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "NodeY": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)], "CorrIJ": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrIK": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrJK": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrJI": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrKI": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], "CorrKJ": [[[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] for _ in range(self.n_i)] for _ in range(self.n_s)], } arrs = [ "NodeY", "ExistI", "ExistJ", "ExistK", ] if self.color_ij: arrs += [ "ColorI", "ColorJ", ] for s in range(self.n_s): for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): if s == 0: # Exist, Node, and Color vars for arr in arrs: data[arr][i][j][k] = ( 1 if model[self.vars[arr][i][j][k]] else 0) # Corr vars for arr in [ "CorrIJ", "CorrIK", "CorrJI", "CorrJK", "CorrKI", "CorrKJ", ]: data[arr][s][i][j][k] = ( 1 if model[self.vars[arr][s][i][j][k]] else 0) return data ================================================ FILE: glue/lattice_surgery/lassynth/tools/__init__.py ================================================ ================================================ FILE: glue/lattice_surgery/lassynth/tools/verify_stabilizers.py ================================================ from typing import Sequence import stim import stimzx def verify_stabilizers( specified_paulistrings: Sequence[str], result_networkx, print_stabilizers: bool = False, ) -> bool: result_stabilizers = [ stab.output for stab in stimzx.zx_graph_to_external_stabilizers(result_networkx) ] specified_stabilizers = [ stim.PauliString(paulistring) for paulistring in specified_paulistrings ] if print_stabilizers: print("specified:") for s in specified_stabilizers: print(s) print("==============================================================") print("resulting:") for s in result_stabilizers: print(s) print("==============================================================") for s in result_stabilizers: for ss in specified_stabilizers: if not ss.commutes(s): print(f"result stabilizer {s} not commuting with " f"specified stabilizer {ss}") if print_stabilizers: print("specified and resulting stabilizers not equivalent") return False if print_stabilizers: print("specified and resulting stabilizers are equivalent.") return True ================================================ FILE: glue/lattice_surgery/lassynth/translators/__init__.py ================================================ from lassynth.translators.zx_grid_graph import ZXGridGraph ================================================ FILE: glue/lattice_surgery/lassynth/translators/gltf_generator.py ================================================ """generating a 3D modelling file in gltf format from our LaSRe.""" import json from typing import Any, Mapping, Optional, Sequence, Tuple # constants SQ2 = 0.707106769085 # square root of 2 THICKNESS = 0.01 # half separation of front and back sides of each face AXESTHICKNESS = 0.1 def float_to_little_endian_hex(f): from struct import pack # Pack the float into a binary string using the little-endian format binary_data = pack(" Mapping[str, Any]: """generate basic gltf contents, i.e., independent from the LaS Args: tubelen (float, optional): ratio of the length of the pipe with respect to the length of a cube. Defaults to 2.0. Returns: Mapping[str, Any]: gltf with everything here. """ # floats as hex, little endian floats = { "0": "00000000", "1": "0000803F", "-1": "000080BF", "0.5": "0000003F", "0.45": "6666E63E", } floats["tube"] = float_to_little_endian_hex(tubelen) floats["+SQ2"] = float_to_little_endian_hex(SQ2) floats["-SQ2"] = float_to_little_endian_hex(-SQ2) floats["+T"] = float_to_little_endian_hex(THICKNESS) floats["-T"] = float_to_little_endian_hex(-THICKNESS) floats["1-T"] = float_to_little_endian_hex(1 - THICKNESS) floats["T-1"] = float_to_little_endian_hex(THICKNESS - 1) floats["0.5+T"] = float_to_little_endian_hex(0.5 + THICKNESS) floats["0.5-T"] = float_to_little_endian_hex(0.5 - THICKNESS) # integers as hex ints = ["0000", "0100", "0200", "0300", "0400", "0500", "0600", "0700"] gltf = { "asset": { "generator": "LaSRe CodeGen by Daniel Bochen Tan", "version": "2.0" }, "scene": 0, "scenes": [{ "name": "Scene", "nodes": [0] }], "nodes": [{ "name": "Lattice Surgery Subroutine", "children": [] }], } gltf["accessors"] = [] gltf["buffers"] = [] gltf["bufferViews"] = [] # materials are the colors. baseColorFactor is (R, G, B, alpha) gltf["materials"] = [ { "name": "0-blue", "pbrMetallicRoughness": { "baseColorFactor": [0, 0, 1, 1] }, "doubleSided": False, }, { "name": "1-red", "pbrMetallicRoughness": { "baseColorFactor": [1, 0, 0, 1] }, "doubleSided": False, }, { "name": "2-green", "pbrMetallicRoughness": { "baseColorFactor": [0, 1, 0, 1] }, "doubleSided": False, }, { "name": "3-gray", "pbrMetallicRoughness": { "baseColorFactor": [0.5, 0.5, 0.5, 1] }, "doubleSided": False, }, { "name": "4-cyan.3", "pbrMetallicRoughness": { "baseColorFactor": [0, 1, 1, 0.3] }, "doubleSided": False, "alphaMode": "BLEND", }, { "name": "5-black", "pbrMetallicRoughness": { "baseColorFactor": [0, 0, 0, 1] }, "doubleSided": False, }, { "name": "6-yellow", "pbrMetallicRoughness": { "baseColorFactor": [1, 1, 0, 1] }, "doubleSided": False, }, { "name": "7-white", "pbrMetallicRoughness": { "baseColorFactor": [1, 1, 1, 1] }, "doubleSided": False, }, ] # for a 3D coordinate (X,Y,Z), the convention of VEC3 in GLTF is (X,Z,-Y) # below are the data we store into the embedded binary in the GLTF. # For each data, we create a buffer, there is one and only one bufferView # for this buffer, and there is one and only one accessor for this # bufferView. This is for simplicity. So in what follows, we always gather # the string corresponding to the data, whether they're a list of floats or # a list of integers. Then, we append a buffer, a bufferView, and an # accessor to the GLTF. This part is quite machinary. # GLTF itself support doubleside color in materials, but this can lead to # problems when converting to other formats. So, for each face of a cube or # a pipe, we will make it two sides, front and back. The POSITION of # vertices of these two are shifted on the Z axis by 2*THICKNESS. Since we # need their color to both facing outside, the back side require opposite # NORMAL vectors, and the index order needs to be reversed. We begin with # definition for the front sides. # 0, positions of square: [(+T,+T,-T),(1-T,+T,-T),(+T,1-T,-T),(1-T,1-T,-T)] s = (floats["+T"] + floats["-T"] + floats["-T"] + floats["1-T"] + floats["-T"] + floats["-T"] + floats["+T"] + floats["-T"] + floats["T-1"] + floats["1-T"] + floats["-T"] + floats["T-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 0, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 0, "componentType": 5126, "type": "VEC3", "count": 4, "max": [1 - THICKNESS, -THICKNESS, -THICKNESS], "min": [THICKNESS, -THICKNESS, THICKNESS - 1], }) # 1, positions of rectangle: [(0,0,-T),(L,0,-T),(0,1,-T),(L,1,-T)] s = (floats["0"] + floats["-T"] + floats["0"] + floats["tube"] + floats["-T"] + floats["0"] + floats["0"] + floats["-T"] + floats["-1"] + floats["tube"] + floats["-T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 1, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 1, "componentType": 5126, "type": "VEC3", "count": 4, "max": [tubelen, -THICKNESS, 0], "min": [0, -THICKNESS, -1], }) # 2, normals of rect/sqr: (0,0,-1)*4 s = (floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 2, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 2, "componentType": 5126, "type": "VEC3", "count": 4 }) # 3, vertices of rect/sqr: [1,0,3, 3,0,2] s = ints[1] + ints[0] + ints[3] + ints[3] + ints[0] + ints[2] gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 3, "byteLength": 12, "byteOffset": 0, "target": 34963 }) gltf["accessors"].append({ "bufferView": 3, "componentType": 5123, "type": "SCALAR", "count": 6 }) # 4, positions of tilted rect: [(0,0,1/2+T),(1/2,0,+T),(0,1,1/2+T),(1/2,1,+T)] s = (floats["0"] + floats["0.5+T"] + floats["0"] + floats["0.5"] + floats["+T"] + floats["0"] + floats["0"] + floats["0.5+T"] + floats["-1"] + floats["0.5"] + floats["+T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 4, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 4, "componentType": 5126, "type": "VEC3", "count": 4, "max": [0.5, 0.5 + THICKNESS, 0], "min": [0, THICKNESS, -1], }) # 5, normals of tilted rect: (-sqrt(2)/2, 0, -sqrt(2)/2)*4 s = (floats["-SQ2"] + floats["-SQ2"] + floats["0"] + floats["-SQ2"] + floats["-SQ2"] + floats["0"] + floats["-SQ2"] + floats["-SQ2"] + floats["0"] + floats["-SQ2"] + floats["-SQ2"] + floats["0"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 5, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 5, "componentType": 5126, "type": "VEC3", "count": 4 }) # 6, positions of Hadamard rectangle: [(0,0,-T),(15/32L,0,-T),(15/32L,1,-T), # (15/32L,1,-T),(17/32L,0,-T),(17/32L,1,-T),(L,0,-T),(L,1,-T)] floats["left"] = float_to_little_endian_hex(tubelen * 15 / 32) floats["right"] = float_to_little_endian_hex(tubelen * 17 / 32) s = (floats["0"] + floats["-T"] + floats["0"] + floats["left"] + floats["-T"] + floats["0"] + floats["0"] + floats["-T"] + floats["-1"] + floats["left"] + floats["-T"] + floats["-1"] + floats["right"] + floats["-T"] + floats["0"] + floats["right"] + floats["-T"] + floats["-1"] + floats["tube"] + floats["-T"] + floats["0"] + floats["tube"] + floats["-T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 96, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 6, "byteLength": 96, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 6, "componentType": 5126, "type": "VEC3", "count": 8, "max": [tubelen, -THICKNESS, 0], "min": [0, -THICKNESS, -1], }) # 7, normals of Hadamard rect (0,0,-1)*8 s = (floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + floats["0"]) gltf["buffers"].append({"byteLength": 96, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 7, "byteLength": 96, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 7, "componentType": 5126, "type": "VEC3", "count": 8 }) # 8, vertices of middle rect in Hadamard rect: [4,1,5, 5,1,3] s = ints[4] + ints[1] + ints[5] + ints[5] + ints[1] + ints[3] gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 8, "byteLength": 12, "byteOffset": 0, "target": 34963 }) gltf["accessors"].append({ "bufferView": 8, "componentType": 5123, "type": "SCALAR", "count": 6 }) # 9, vertices of upper rect in Hadamard rect: [6,4,7, 7,4,5] s = ints[6] + ints[4] + ints[7] + ints[7] + ints[4] + ints[5] gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 9, "byteLength": 12, "byteOffset": 0, "target": 34963 }) gltf["accessors"].append({ "bufferView": 9, "componentType": 5123, "type": "SCALAR", "count": 6 }) # 10, vertices of lines around a face: [0,1, 0,2, 2,3, 3,1] # GLTF supports drawing lines, but there may be a problem converting to # other formats. We have thus not used these data. s = (ints[0] + ints[1] + ints[0] + ints[2] + ints[2] + ints[3] + ints[3] + ints[1]) gltf["buffers"].append({"byteLength": 16, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 10, "byteLength": 16, "byteOffset": 0, "target": 34963 }) gltf["accessors"].append({ "bufferView": 10, "componentType": 5123, "type": "SCALAR", "count": 8 }) # 11, positions of half-distance rectangle: [(0,0,-T),(0.45,0,-T),(0,1,-T),(0.45,1,-T)] s = (floats["0"] + floats["-T"] + floats["0"] + floats["0.45"] + floats["-T"] + floats["0"] + floats["0"] + floats["-T"] + floats["-1"] + floats["0.45"] + floats["-T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 11, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 11, "componentType": 5126, "type": "VEC3", "count": 4, "max": [0.45, -THICKNESS, 0], "min": [0, -THICKNESS, -1], }) # 12, backside, positions of square: [(+T,+T,+T),(1-T,+T,+T),(+T,1-T,+T),(1-T,1-T,+T)] s = (floats["+T"] + floats["+T"] + floats["-T"] + floats["1-T"] + floats["+T"] + floats["-T"] + floats["+T"] + floats["+T"] + floats["T-1"] + floats["1-T"] + floats["+T"] + floats["T-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 12, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 12, "componentType": 5126, "type": "VEC3", "count": 4, "max": [1 - THICKNESS, THICKNESS, -THICKNESS], "min": [THICKNESS, THICKNESS, THICKNESS - 1], }) # 13, backside, normals of rect/sqr: (0,0,1)*4 s = (floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 13, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 13, "componentType": 5126, "type": "VEC3", "count": 4 }) # For the cubes, we want to draw black lines around its boundaries to help # people identify them visually. However, drawing lines in GLTF may become # a problem when converting to other formats. Here, we define super thin # rectangles at the boundaries of squares which will be seen as lines. # 14, frontside, positions of edge 0: [(+T,0,-T),(1-T,0,-T),(+T,+T,-T),(1-T,+T,-T)] s = (floats["+T"] + floats["-T"] + floats["0"] + floats["1-T"] + floats["-T"] + floats["0"] + floats["+T"] + floats["-T"] + floats["-T"] + floats["1-T"] + floats["-T"] + floats["-T"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 14, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 14, "componentType": 5126, "type": "VEC3", "count": 4, "max": [1 - THICKNESS, -THICKNESS, 0], "min": [THICKNESS, -THICKNESS, -THICKNESS], }) # 15, frontside, positions of edge 1: [(1-T,+T,-T),(1,+T,-T),(1-T,1-T,-T),(1,1-T,-T)] s = (floats["1-T"] + floats["-T"] + floats["-T"] + floats["1"] + floats["-T"] + floats["-T"] + floats["1-T"] + floats["-T"] + floats["T-1"] + floats["1"] + floats["-T"] + floats["T-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 15, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 15, "componentType": 5126, "type": "VEC3", "count": 4, "max": [1, -THICKNESS, -THICKNESS], "min": [1 - THICKNESS, -THICKNESS, THICKNESS - 1], }) # 16, frontside, positions of edge 2: [(0,+T,-T),(+T,+T,-T),(0,1-T,-T),(+T,1-T,-T)] s = (floats["0"] + floats["-T"] + floats["-T"] + floats["+T"] + floats["-T"] + floats["-T"] + floats["0"] + floats["-T"] + floats["T-1"] + floats["+T"] + floats["-T"] + floats["T-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 16, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 16, "componentType": 5126, "type": "VEC3", "count": 4, "max": [THICKNESS, -THICKNESS, -THICKNESS], "min": [0, -THICKNESS, THICKNESS - 1], }) # 17, frontside, positions of edge 3: [(+T,1-T,-T),(1-T,1-T,-T),(+T,1,-T),(1-T,1,-T)] s = (floats["+T"] + floats["-T"] + floats["T-1"] + floats["1-T"] + floats["-T"] + floats["T-1"] + floats["+T"] + floats["-T"] + floats["-1"] + floats["1-T"] + floats["-T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 17, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 17, "componentType": 5126, "type": "VEC3", "count": 4, "max": [1 - THICKNESS, -THICKNESS, THICKNESS - 1], "min": [THICKNESS, -THICKNESS, -1], }) # 18, backside, positions of edge 0: [(+T,0,+T),(1-T,0,+T),(+T,+T,+T),(1-T,+T,+T)] s = (floats["+T"] + floats["+T"] + floats["0"] + floats["1-T"] + floats["+T"] + floats["0"] + floats["+T"] + floats["+T"] + floats["-T"] + floats["1-T"] + floats["+T"] + floats["-T"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 18, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 18, "componentType": 5126, "type": "VEC3", "count": 4, "max": [1 - THICKNESS, THICKNESS, 0], "min": [THICKNESS, THICKNESS, -THICKNESS], }) # 19, backside, positions of edge 1: [(1-T,+T,+T),(1,+T,+T),(1-T,1-T,+T),(1,1-T,+T)] s = (floats["1-T"] + floats["+T"] + floats["-T"] + floats["1"] + floats["+T"] + floats["-T"] + floats["1-T"] + floats["+T"] + floats["T-1"] + floats["1"] + floats["+T"] + floats["T-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 19, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 19, "componentType": 5126, "type": "VEC3", "count": 4, "max": [1, THICKNESS, -THICKNESS], "min": [1 - THICKNESS, THICKNESS, THICKNESS - 1], }) # 20, backside, positions of edge 2: [(0,+T,+T),(+T,+T,+T),(0,1-T,+T),(+T,1-T,+T)] s = (floats["0"] + floats["+T"] + floats["-T"] + floats["+T"] + floats["+T"] + floats["-T"] + floats["0"] + floats["+T"] + floats["T-1"] + floats["+T"] + floats["+T"] + floats["T-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 20, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 20, "componentType": 5126, "type": "VEC3", "count": 4, "max": [THICKNESS, THICKNESS, -THICKNESS], "min": [0, THICKNESS, THICKNESS - 1], }) # 21, backside, positions of edge 3: [(+T,1-T,+T),(1-T,1-T,+T),(+T,1,+T),(1-T,1,+T)] s = (floats["+T"] + floats["+T"] + floats["T-1"] + floats["1-T"] + floats["+T"] + floats["T-1"] + floats["+T"] + floats["+T"] + floats["-1"] + floats["1-T"] + floats["+T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 21, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 21, "componentType": 5126, "type": "VEC3", "count": 4, "max": [1 - THICKNESS, THICKNESS, THICKNESS - 1], "min": [THICKNESS, THICKNESS, -1], }) # 22, backside vertices of rect/sqr: [1,3,0, 3,2,0] s = ints[1] + ints[3] + ints[0] + ints[3] + ints[2] + ints[0] gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 22, "byteLength": 12, "byteOffset": 0, "target": 34963 }) gltf["accessors"].append({ "bufferView": 22, "componentType": 5123, "type": "SCALAR", "count": 6 }) # 23, backside, positions of rectangle: [(0,0,+T),(L,0,+T),(0,1,+T),(L,1,+T)] s = (floats["0"] + floats["+T"] + floats["0"] + floats["tube"] + floats["+T"] + floats["0"] + floats["0"] + floats["+T"] + floats["-1"] + floats["tube"] + floats["+T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 23, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 23, "componentType": 5126, "type": "VEC3", "count": 4, "max": [tubelen, THICKNESS, 0], "min": [0, THICKNESS, -1], }) # 24, backside, positions of half-distance rectangle: [(0,0,+T),(0.45,0,+T),(0,1,+T),(0.45,1,+T)] s = (floats["0"] + floats["+T"] + floats["0"] + floats["0.45"] + floats["+T"] + floats["0"] + floats["0"] + floats["+T"] + floats["-1"] + floats["0.45"] + floats["+T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 24, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 24, "componentType": 5126, "type": "VEC3", "count": 4, "max": [0.45, THICKNESS, 0], "min": [0, THICKNESS, -1], }) # 25, backside, positions of Hadamard rectangle: [(0,0,+T),(15/32L,0,+T),(15/32L,1,+T), # (15/32L,1,+T),(17/32L,0,+T),(17/32L,1,+T),(L,0,+T),(L,1,+T)] floats["left"] = float_to_little_endian_hex(tubelen * 15 / 32) floats["right"] = float_to_little_endian_hex(tubelen * 17 / 32) s = (floats["0"] + floats["+T"] + floats["0"] + floats["left"] + floats["+T"] + floats["0"] + floats["0"] + floats["+T"] + floats["-1"] + floats["left"] + floats["+T"] + floats["-1"] + floats["right"] + floats["+T"] + floats["0"] + floats["right"] + floats["+T"] + floats["-1"] + floats["tube"] + floats["+T"] + floats["0"] + floats["tube"] + floats["+T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 96, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 25, "byteLength": 96, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 25, "componentType": 5126, "type": "VEC3", "count": 8, "max": [tubelen, THICKNESS, 0], "min": [0, THICKNESS, -1], }) # 26, backside, normals of Hadamard rect (0,0,1)*8 s = (floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"]) gltf["buffers"].append({"byteLength": 96, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 26, "byteLength": 96, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 26, "componentType": 5126, "type": "VEC3", "count": 8 }) # 27, backside, vertices of middle rect in Hadamard rect: [4,5,1, 5,3,1] s = ints[4] + ints[5] + ints[1] + ints[5] + ints[3] + ints[1] gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 27, "byteLength": 12, "byteOffset": 0, "target": 34963 }) gltf["accessors"].append({ "bufferView": 27, "componentType": 5123, "type": "SCALAR", "count": 6 }) # 28, backside, vertices of upper rect in Hadamard rect: [6,7,4, 7,5,4] s = ints[6] + ints[7] + ints[4] + ints[7] + ints[5] + ints[4] gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 28, "byteLength": 12, "byteOffset": 0, "target": 34963 }) gltf["accessors"].append({ "bufferView": 28, "componentType": 5123, "type": "SCALAR", "count": 6 }) # 29, backside, positions of tilted rect: [(0,0,1/2+T),(1/2,0,+T),(0,1,1/2+T),(1/2,1,+T)] s = (floats["0"] + floats["0.5-T"] + floats["0"] + floats["0.5"] + floats["-T"] + floats["0"] + floats["0"] + floats["0.5-T"] + floats["-1"] + floats["0.5"] + floats["-T"] + floats["-1"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 29, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 29, "componentType": 5126, "type": "VEC3", "count": 4, "max": [0.5, 0.5 - THICKNESS, 0], "min": [0, -THICKNESS, -1], }) # 30, backside, normals of tilted rect: (sqrt(2)/2, 0, sqrt(2)/2)*4 s = (floats["+SQ2"] + floats["+SQ2"] + floats["0"] + floats["+SQ2"] + floats["+SQ2"] + floats["0"] + floats["+SQ2"] + floats["+SQ2"] + floats["0"] + floats["+SQ2"] + floats["+SQ2"] + floats["0"]) gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) gltf["bufferViews"].append({ "buffer": 30, "byteLength": 48, "byteOffset": 0, "target": 34962 }) gltf["accessors"].append({ "bufferView": 30, "componentType": 5126, "type": "VEC3", "count": 4 }) # finished creating the binary # Now we create meshes. These are the real constructors of the 3D diagram. # a mesh can contain multiple primitives. A primitive can be defined by a # set of vertices POSITION, their NORMAL vectors, the order of going around # these vertices, and the color (material) for the triangles defined by # going around the vertices. `mode:4` means color these triangles. gltf["meshes"] = [ { "name": "0-square-blue", "primitives": [ # front side { "attributes": { "NORMAL": 2, "POSITION": 0 }, "indices": 3, "material": 0, "mode": 4, }, # back side { "attributes": { "NORMAL": 13, "POSITION": 12 }, "indices": 22, "material": 0, "mode": 4, }, # front side edge 0 { "attributes": { "NORMAL": 2, "POSITION": 14 }, "indices": 3, "material": 5, "mode": 4, }, # front side edge 1 { "attributes": { "NORMAL": 2, "POSITION": 15 }, "indices": 3, "material": 5, "mode": 4, }, # front side edge 2 { "attributes": { "NORMAL": 2, "POSITION": 16 }, "indices": 3, "material": 5, "mode": 4, }, # front side edge 3 { "attributes": { "NORMAL": 2, "POSITION": 17 }, "indices": 3, "material": 5, "mode": 4, }, # back side edge 0 { "attributes": { "NORMAL": 13, "POSITION": 18 }, "indices": 22, "material": 5, "mode": 4, }, # back side edge 1 { "attributes": { "NORMAL": 13, "POSITION": 19 }, "indices": 22, "material": 5, "mode": 4, }, # back side edge 2 { "attributes": { "NORMAL": 13, "POSITION": 20 }, "indices": 22, "material": 5, "mode": 4, }, # back side edge 3 { "attributes": { "NORMAL": 13, "POSITION": 21 }, "indices": 22, "material": 5, "mode": 4, }, ], }, { "name": "1-square-red", "primitives": [ # front side { "attributes": { "NORMAL": 2, "POSITION": 0 }, "indices": 3, "material": 1, "mode": 4, }, # back side { "attributes": { "NORMAL": 13, "POSITION": 12 }, "indices": 22, "material": 1, "mode": 4, }, # front side edge 0 { "attributes": { "NORMAL": 2, "POSITION": 14 }, "indices": 3, "material": 5, "mode": 4, }, # front side edge 1 { "attributes": { "NORMAL": 2, "POSITION": 15 }, "indices": 3, "material": 5, "mode": 4, }, # front side edge 2 { "attributes": { "NORMAL": 2, "POSITION": 16 }, "indices": 3, "material": 5, "mode": 4, }, # front side edge 3 { "attributes": { "NORMAL": 2, "POSITION": 17 }, "indices": 3, "material": 5, "mode": 4, }, # back side edge 0 { "attributes": { "NORMAL": 13, "POSITION": 18 }, "indices": 22, "material": 5, "mode": 4, }, # back side edge 1 { "attributes": { "NORMAL": 13, "POSITION": 19 }, "indices": 22, "material": 5, "mode": 4, }, # back side edge 2 { "attributes": { "NORMAL": 13, "POSITION": 20 }, "indices": 22, "material": 5, "mode": 4, }, # back side edge 3 { "attributes": { "NORMAL": 13, "POSITION": 21 }, "indices": 22, "material": 5, "mode": 4, }, ], }, { "name": "2-square-gray", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 0 }, "indices": 3, "material": 3, "mode": 4, }, # back side { "attributes": { "NORMAL": 13, "POSITION": 12 }, "indices": 22, "material": 3, "mode": 4, }, ], }, { "name": "3-square-green", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 0 }, "indices": 3, "material": 2, "mode": 4, }, # back side { "attributes": { "NORMAL": 13, "POSITION": 12 }, "indices": 22, "material": 2, "mode": 4, }, ], }, { "name": "4-rectangle-blue", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 1 }, "indices": 3, "material": 0, "mode": 4, }, # backside { "attributes": { "NORMAL": 13, "POSITION": 23 }, "indices": 22, "material": 0, "mode": 4, } ], }, { "name": "5-rectangle-red", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 1 }, "indices": 3, "material": 1, "mode": 4, }, # backside { "attributes": { "NORMAL": 13, "POSITION": 23 }, "indices": 22, "material": 1, "mode": 4, } ], }, { "name": "6-rectangle-gray", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 1 }, "indices": 3, "material": 3, "mode": 4, }, # backside { "attributes": { "NORMAL": 13, "POSITION": 23 }, "indices": 22, "material": 3, "mode": 4, } ], }, { "name": "7-rectangle-red/yellow/blue", "primitives": [ { "attributes": { "NORMAL": 7, "POSITION": 6 }, "indices": 3, "material": 1, "mode": 4, }, { "attributes": { "NORMAL": 7, "POSITION": 6 }, "indices": 8, "material": 6, "mode": 4, }, { "attributes": { "NORMAL": 7, "POSITION": 6 }, "indices": 9, "material": 0, "mode": 4, }, # backside { "attributes": { "NORMAL": 26, "POSITION": 25 }, "indices": 22, "material": 1, "mode": 4, }, { "attributes": { "NORMAL": 26, "POSITION": 25 }, "indices": 27, "material": 6, "mode": 4, }, { "attributes": { "NORMAL": 26, "POSITION": 25 }, "indices": 28, "material": 0, "mode": 4, }, ], }, { "name": "8-rectangle-blue/yellow/red", "primitives": [ { "attributes": { "NORMAL": 7, "POSITION": 6 }, "indices": 3, "material": 0, "mode": 4, }, { "attributes": { "NORMAL": 7, "POSITION": 6 }, "indices": 8, "material": 6, "mode": 4, }, { "attributes": { "NORMAL": 7, "POSITION": 6 }, "indices": 9, "material": 1, "mode": 4, }, # backside { "attributes": { "NORMAL": 26, "POSITION": 25 }, "indices": 22, "material": 0, "mode": 4, }, { "attributes": { "NORMAL": 26, "POSITION": 25 }, "indices": 27, "material": 6, "mode": 4, }, { "attributes": { "NORMAL": 26, "POSITION": 25 }, "indices": 28, "material": 1, "mode": 4, }, ], }, { "name": "9-square-cyan.3", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 0 }, "indices": 3, "material": 4, "mode": 4, }, # back side { "attributes": { "NORMAL": 13, "POSITION": 12 }, "indices": 22, "material": 4, "mode": 4, }, ], }, { "name": "10-rectangle-cyan.3", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 1 }, "indices": 3, "material": 4, "mode": 4, }, # backside { "attributes": { "NORMAL": 13, "POSITION": 23 }, "indices": 22, "material": 4, "mode": 4, } ], }, { "name": "11-tilted-cyan.3", "primitives": [ { "attributes": { "NORMAL": 5, "POSITION": 4 }, "indices": 3, "material": 4, "mode": 4, }, # backside { "attributes": { "NORMAL": 30, "POSITION": 29 }, "indices": 22, "material": 4, "mode": 4, }, ], }, { "name": "12-half-distance-rectangle-green", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 11 }, "indices": 3, "material": 2, "mode": 4, }, # back side { "attributes": { "NORMAL": 13, "POSITION": 24 }, "indices": 22, "material": 2, "mode": 4, }, ], }, { "name": "13-square-black", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 0 }, "indices": 3, "material": 5, "mode": 4, }, # back side { "attributes": { "NORMAL": 13, "POSITION": 12 }, "indices": 22, "material": 5, "mode": 4, }, ], }, { "name": "14-half-distance-rectangle-black", "primitives": [ { "attributes": { "NORMAL": 2, "POSITION": 11 }, "indices": 3, "material": 5, "mode": 4, }, # back side { "attributes": { "NORMAL": 13, "POSITION": 24 }, "indices": 22, "material": 5, "mode": 4, }, ], }, ] return gltf def axes_gen(SEP: float, max_i: int, max_j: int, max_k: int) -> Sequence[Mapping[str, Any]]: rectangles = [] # I axis, red rectangles += [ { "name": f"axisI:-K", "mesh": 5, "translation": [-0.5, -0.5, 0.5], "scale": [SEP * max_i / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisI:+K", "mesh": 5, "translation": [-0.5, -0.5 + AXESTHICKNESS, 0.5], "scale": [SEP * max_i / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisI:-J", "mesh": 5, "translation": [-0.5, -0.5, 0.5], "rotation": [SQ2, 0, 0, SQ2], "scale": [SEP * max_i / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisI:+J", "mesh": 5, "translation": [-0.5, -0.5, 0.5 - AXESTHICKNESS], "rotation": [SQ2, 0, 0, SQ2], "scale": [SEP * max_i / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], }, ] # J axis, green rectangles += [ { "name": f"axisJ:-K", "rotation": [0, SQ2, 0, SQ2], "translation": [-0.5 + AXESTHICKNESS, -0.5, 0.5], "mesh": 3, "scale": [SEP * max_j, AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisJ:+K", "rotation": [0, SQ2, 0, SQ2], "translation": [ -0.5 + AXESTHICKNESS, -0.5 + AXESTHICKNESS, 0.5, ], "mesh": 3, "scale": [SEP * max_j, AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisJ:-I", "rotation": [0.5, 0.5, -0.5, 0.5], "translation": [-0.5, -0.5, 0.5], "mesh": 3, "scale": [SEP * max_j, AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisJ:+I", "rotation": [0.5, 0.5, -0.5, 0.5], "translation": [-0.5 + AXESTHICKNESS, -0.5, 0.5], "mesh": 3, "scale": [SEP * max_j, AXESTHICKNESS, AXESTHICKNESS], }, ] # K axis, blue rectangles += [ { "name": f"axisK:-I", "mesh": 4, "rotation": [0, 0, SQ2, SQ2], "translation": [-0.5, -0.5 + AXESTHICKNESS, 0.5], "scale": [SEP * max_k / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisK:+I", "mesh": 4, "rotation": [0, 0, SQ2, SQ2], "translation": [-0.5 + AXESTHICKNESS, -0.5 + AXESTHICKNESS, 0.5], "scale": [SEP * max_k / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisK:-J", "mesh": 4, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [-0.5 + AXESTHICKNESS, -0.5 + AXESTHICKNESS, 0.5], "scale": [SEP * max_k / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], }, { "name": f"axisK:+J", "mesh": 4, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [ -0.5 + AXESTHICKNESS, -0.5 + AXESTHICKNESS, 0.5 - AXESTHICKNESS, ], "scale": [SEP * max_k / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], }, ] return rectangles def tube_gen(SEP: float, loc: Tuple[int, int, int], dir: str, color: int, stabilizer: int, corr: Tuple[int, int], noColor: bool, rm_dir: str) -> Sequence[Mapping[str, Any]]: """compute the GLTF nodes for a pipe. This can include its four faces and correlation surface inside, minus the face to remove specified by rm_dir. Args: SEP (float): the distance, e.g., from I-pipe(i,j,k) to I-pipe(i+1,j,k). loc (Tuple[int, int, int]): 3D coordinate of the pipe. dir (str): direction of the pipe, "I", "J", or "K". color (int): color variable of the pipe, can be -1(unknown), 0, or 1. stabilizer (int): index of the stabilizer. corr (Tuple[int, int]): two bits for two possible corr surface inside. noColor (bool): K-pipe are not colored if this is True. rm_dir (str): the direction of face to remove. if a stabilier is shown. Returns: Sequence[Mapping[str, Any]]: list of constructed GLTF nodes, typically 4 or 5 contiguous nodes in the list corredpond to one pipe. """ rectangles = [] if dir == "I": rectangles = [ { "name": f"edgeI{loc}:-K", "mesh": 4 if color else 5, "translation": [1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1]], }, { "name": f"edgeI{loc}:+K", "mesh": 4 if color else 5, "translation": [1 + SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1]], }, { "name": f"edgeI{loc}:-J", "mesh": 5 if color else 4, "translation": [1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1]], "rotation": [SQ2, 0, 0, SQ2], }, { "name": f"edgeI{loc}:+J", "mesh": 5 if color else 4, "translation": [1 + SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1]], "rotation": [SQ2, 0, 0, SQ2], }, ] if corr[0]: rectangles.append({ "name": f"edgeI{loc}:CorrIJ", "mesh": 10, "translation": [ 1 + SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], }) if corr[1]: rectangles.append({ "name": f"edgeI{loc}:CorrIK", "mesh": 10, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [SQ2, 0, 0, SQ2], }) elif dir == "J": rectangles = [ { "name": f"edgeJ{loc}:-K", "rotation": [0, SQ2, 0, SQ2], "translation": [1 + SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1]], "mesh": 5 if color else 4, }, { "name": f"edgeJ{loc}:+K", "rotation": [0, SQ2, 0, SQ2], "translation": [ 1 + SEP * loc[0], 1 + SEP * loc[2], -1 - SEP * loc[1], ], "mesh": 5 if color else 4, }, { "name": f"edgeJ{loc}:-I", "rotation": [0.5, 0.5, -0.5, 0.5], "translation": [SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1]], "mesh": 4 if color else 5, }, { "name": f"edgeJ{loc}:+I", "rotation": [0.5, 0.5, -0.5, 0.5], "translation": [1 + SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1]], "mesh": 4 if color else 5, }, ] if corr[0]: rectangles.append({ "name": f"edgeJ{loc}:CorrJK", "mesh": 10, "rotation": [0.5, 0.5, -0.5, 0.5], "translation": [ 0.5 + SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1], ], }) if corr[1]: rectangles.append({ "name": f"edgeJ{loc}:CorrJI", "mesh": 10, "rotation": [0, SQ2, 0, SQ2], "translation": [ 1 + SEP * loc[0], 0.5 + SEP * loc[2], -1 - SEP * loc[1], ], }) elif dir == "K": colorKM = color // 7 colorKP = color % 7 rectangles = [ { "name": f"edgeJ{loc}:-I", "mesh": 6, "rotation": [0, 0, SQ2, SQ2], "translation": [SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1]], }, { "name": f"edgeJ{loc}:+I", "mesh": 6, "rotation": [0, 0, SQ2, SQ2], "translation": [1 + SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1]], }, { "name": f"edgeK{loc}:-J", "mesh": 6, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [1 + SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1]], }, { "name": f"edgeJ{loc}:+J", "mesh": 6, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [ 1 + SEP * loc[0], 1 + SEP * loc[2], -1 - SEP * loc[1], ], }, ] if not noColor: if colorKM == 0 and colorKP == 0: rectangles[0]["mesh"] = 4 rectangles[1]["mesh"] = 4 rectangles[2]["mesh"] = 5 rectangles[3]["mesh"] = 5 if colorKM == 1 and colorKP == 1: rectangles[0]["mesh"] = 5 rectangles[1]["mesh"] = 5 rectangles[2]["mesh"] = 4 rectangles[3]["mesh"] = 4 if colorKM == 1 and colorKP == 0: rectangles[0]["mesh"] = 7 rectangles[1]["mesh"] = 7 rectangles[2]["mesh"] = 8 rectangles[3]["mesh"] = 8 if colorKM == 0 and colorKP == 1: rectangles[0]["mesh"] = 8 rectangles[1]["mesh"] = 8 rectangles[2]["mesh"] = 7 rectangles[3]["mesh"] = 7 if corr[0]: rectangles.append({ "name": f"edgeK{loc}:CorrKI", "mesh": 10, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [ 1 + SEP * loc[0], 1 + SEP * loc[2], -0.5 - SEP * loc[1], ], }) if corr[1]: rectangles.append({ "name": f"edgeK{loc}:CorrKJ", "mesh": 10, "rotation": [0, 0, SQ2, SQ2], "translation": [ 0.5 + SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1], ], }) rectangles = [rect for rect in rectangles if rm_dir not in rect["name"]] if stabilizer == -1: rectangles = [ rect for rect in rectangles if "Corr" not in rect["name"] ] return rectangles def cube_gen( SEP: float, loc: Tuple[int, int, int], exists: Mapping[str, int], colors: Mapping[str, int], stabilizer: int, corr: Mapping[str, Tuple[int, int]], noColor: bool, rm_dir: str, ) -> Sequence[Mapping[str, Any]]: """compute the GLTF nodes for a cube. This can include its faces and correlation surface inside, minus the face to remove specified by rm_dir. Args: SEP (float): the distance, e.g., from cube(i,j,k) to cube(i+1,j,k). loc (Tuple[int, int, int]): 3D coordinate of the pipe. exists (Mapping[str, int]): whether there is a pipe in the 6 directions to this cube. (+|-)(I|J|K). colors (Mapping[str, int]): color variable of the pipe, can be -1(unknown), 0, or 1. stabilizer (int): index of the stabilizer. corr (Mapping[str, Tuple[int, int]]): two bits for two possible correlation surface inside a pipe. These info for all 6 pipes. noColor (bool): K-pipe are not colored if this is True. rm_dir (str): the direction of face to remove. if a stabilier is shown. Returns: Sequence[Mapping[str, Any]]: list of constructed GLTF nodes. """ squares = [] for face in ["-K", "+K"]: if exists[face] == 0: squares.append({ "name": f"spider{loc}:{face}", "mesh": 2, "translation": [ SEP * loc[0], (1 if face == "+K" else 0) + SEP * loc[2], -SEP * loc[1], ], }) for dir in ["+I", "-I", "+J", "-J"]: if exists[dir]: if dir == "+I" or dir == "-I": if colors[dir] == 1: squares[-1]["mesh"] = 0 else: squares[-1]["mesh"] = 1 else: if colors[dir] == 0: squares[-1]["mesh"] = 0 else: squares[-1]["mesh"] = 1 break for face in ["-I", "+I"]: if exists[face] == 0: squares.append({ "name": f"spider{loc}:{face}", "mesh": 2, "translation": [ (1 if face == "+I" else 0) + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) for dir in ["+J", "-J", "+K", "-K"]: if exists[dir]: if dir == "+J" or dir == "-J": if colors[dir] == 1: squares[-1]["mesh"] = 0 else: squares[-1]["mesh"] = 1 elif not noColor: if colors[dir] == 1: squares[-1]["mesh"] = 1 elif colors[dir] == 0: squares[-1]["mesh"] = 0 for face in ["-J", "+J"]: if exists[face] == 0: squares.append({ "name": f"spider{loc}:{face}", "mesh": 2, "translation": [ 1 + SEP * loc[0], SEP * loc[2], (-1 if face == "+J" else 0) - SEP * loc[1], ], "rotation": [0.5, 0.5, 0.5, 0.5], }) for dir in ["+I", "-I", "+K", "-K"]: if exists[dir]: if dir == "+I" or dir == "-I": if colors[dir] == 0: squares[-1]["mesh"] = 0 else: squares[-1]["mesh"] = 1 elif not noColor: if colors[dir] == 1: squares[-1]["mesh"] = 0 elif colors[dir] == 0: squares[-1]["mesh"] = 1 degree = sum([v for (k, v) in exists.items()]) normal = {"I": 0, "J": 0, "K": 0} if exists["-I"] == 0 and exists["+I"] == 0: normal["I"] = 1 if exists["-J"] == 0 and exists["+J"] == 0: normal["J"] = 1 if exists["-K"] == 0 and exists["+K"] == 0: normal["K"] = 1 if degree > 1: if (exists["-I"] and exists["+I"] and exists["-J"] == 0 and exists["+J"] == 0 and exists["-K"] == 0 and exists["+K"] == 0): if corr["-I"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], }) if corr["-I"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [0.5, 0.5, 0.5, 0.5], }) elif (exists["-I"] == 0 and exists["+I"] == 0 and exists["-J"] and exists["+J"] and exists["-K"] == 0 and exists["+K"] == 0): if corr["-J"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 0.5 + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) if corr["-J"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], }) elif (exists["-I"] == 0 and exists["+I"] == 0 and exists["-J"] == 0 and exists["+J"] == 0 and exists["-K"] and exists["+K"]): if corr["-K"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [0.5, 0.5, 0.5, 0.5], }) if corr["-K"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 0.5 + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) else: if normal["I"]: if corr["-J"][0] or corr["+J"][0] or corr["-K"][1] or corr[ "+K"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 0.5 + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) if corr["-J"][1] and corr["+J"][1] and corr["-K"][0] and corr[ "+K"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1], ], "rotation": [0, -SQ2, 0, SQ2], }) squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ SEP * loc[0], 0.5 + SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [0, -SQ2, 0, SQ2], }) elif corr["-J"][1] and corr["+J"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], }) elif corr["-K"][0] and corr["+K"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [0.5, 0.5, 0.5, 0.5], }) elif corr["-J"][1] and corr["-K"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, SQ2, 0, SQ2], }) elif corr["+J"][1] and corr["+K"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 1 + SEP * loc[0], 0.5 + SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [0, SQ2, 0, SQ2], }) elif corr["+J"][1] and corr["-K"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1], ], "rotation": [0, -SQ2, 0, SQ2], }) elif corr["-J"][1] and corr["+K"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ SEP * loc[0], 0.5 + SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [0, -SQ2, 0, SQ2], }) elif normal["J"]: if corr["-K"][0] or corr["+K"][0] or corr["-I"][1] or corr[ "+I"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [0.5, 0.5, 0.5, 0.5], }) if corr["-K"][1] and corr["+K"][1] and corr["-I"][0] and corr[ "+I"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 0.5 + SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) elif corr["-K"][1] and corr["+K"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 0.5 + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) elif corr["-I"][0] and corr["+I"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], }) elif corr["-K"][1] and corr["-I"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [SEP * loc[0], SEP * loc[2], -SEP * loc[1]], }) elif corr["+K"][1] and corr["+I"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 0.5 + SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], }) elif corr["+K"][1] and corr["-I"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 0.5 + SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) elif corr["-K"][1] and corr["+I"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) else: if corr["-I"][0] or corr["+I"][0] or corr["-J"][1] or corr[ "+J"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ SEP * loc[0], 0.5 + SEP * loc[2], -SEP * loc[1], ], }) if corr["-I"][1] and corr["+I"][1] and corr["-J"][0] and corr[ "+J"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 0.5 + SEP * loc[0], SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [SQ2, 0, 0, SQ2], }) squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1], ], "rotation": [SQ2, 0, 0, SQ2], }) elif corr["-I"][1] and corr["+I"][1]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 1 + SEP * loc[0], SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [0.5, 0.5, 0.5, 0.5], }) elif corr["-J"][0] and corr["+J"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 9, "translation": [ 0.5 + SEP * loc[0], SEP * loc[2], -SEP * loc[1], ], "rotation": [0, 0, SQ2, SQ2], }) elif corr["-I"][1] and corr["-J"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1], ], "rotation": [-SQ2, 0, 0, SQ2], }) elif corr["+I"][1] and corr["+J"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 0.5 + SEP * loc[0], 1 + SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [-SQ2, 0, 0, SQ2], }) elif corr["+I"][1] and corr["-J"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ 0.5 + SEP * loc[0], SEP * loc[2], -0.5 - SEP * loc[1], ], "rotation": [SQ2, 0, 0, SQ2], }) elif corr["-I"][1] and corr["+J"][0]: squares.append({ "name": f"spider{loc}:Corr", "mesh": 11, "translation": [ SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1], ], "rotation": [SQ2, 0, 0, SQ2], }) squares = [sqar for sqar in squares if rm_dir not in sqar["name"]] if stabilizer == -1: squares = [sqar for sqar in squares if "Corr" not in sqar["name"]] return squares def special_gen( SEP: float, loc: Tuple[int, int, int], exists: Mapping[str, int], type: str, stabilizer: int, rm_dir: str, ) -> Sequence[Mapping[str, Any]]: """compute the GLTF nodes for special cubes. Currently Ycube and Tinjection Args: SEP (float): the distance, e.g., from cube(i,j,k) to cube(i+1,j,k). loc (Tuple[int, int, int]): 3D coordinate of the pipe. exists (Mapping[str, int]): whether there is a pipe in the 6 directions to this cube. (+|-)(I|J|K). stabilizer (int): index of the stabilizer. noColor (bool): K-pipe are not colored if this is True. rm_dir (str): the direction of face to remove. if a stabilier is shown. Returns: Sequence[Mapping[str, Any]]: list of constructed GLTF nodes. """ if type == "Y": square_mesh = 3 half_dist_mesh = 12 elif type == "T": square_mesh = 13 half_dist_mesh = 14 else: square_mesh = -1 half_dist_mesh = -1 shapes = [] if exists["+K"]: # need connect to top shapes.append({ "name": f"spider{loc}:top:-K", "mesh": square_mesh, "translation": [ SEP * loc[0], 0.55 + SEP * loc[2], -SEP * loc[1], ], }) shapes.append({ "name": f"spider{loc}:top:-I", "mesh": half_dist_mesh, "rotation": [0, 0, SQ2, SQ2], "translation": [SEP * loc[0], 0.55 + SEP * loc[2], -SEP * loc[1]], }) shapes.append({ "name": f"spider{loc}:top:+I", "mesh": half_dist_mesh, "rotation": [0, 0, SQ2, SQ2], "translation": [1 + SEP * loc[0], 0.55 + SEP * loc[2], -SEP * loc[1]], }) shapes.append({ "name": f"spider{loc}:top:-J", "mesh": half_dist_mesh, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [1 + SEP * loc[0], 0.55 + SEP * loc[2], -SEP * loc[1]], }) shapes.append({ "name": f"spider{loc}:top:+J", "mesh": half_dist_mesh, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [ 1 + SEP * loc[0], 0.55 + SEP * loc[2], -SEP * loc[1] - 1, ], }) if exists["-K"]: # need connect to bottom shapes.append({ "name": f"spider{loc}:bottom:+K", "mesh": square_mesh, "translation": [ SEP * loc[0], 0.45 + SEP * loc[2], -SEP * loc[1], ], }) shapes.append({ "name": f"spider{loc}:bottom:-I", "mesh": half_dist_mesh, "rotation": [0, 0, SQ2, SQ2], "translation": [SEP * loc[0], SEP * loc[2], -SEP * loc[1]], }) shapes.append({ "name": f"spider{loc}:bottom:+I", "mesh": half_dist_mesh, "rotation": [0, 0, SQ2, SQ2], "translation": [1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1]], }) shapes.append({ "name": f"spider{loc}:bottom:-J", "mesh": half_dist_mesh, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1]], }) shapes.append({ "name": f"spider{loc}:bottom:+J", "mesh": half_dist_mesh, "rotation": [0.5, 0.5, 0.5, 0.5], "translation": [ 1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1] - 1, ], }) shapes = [shp for shp in shapes if rm_dir not in shp["name"]] return shapes def gltf_generator(lasre: Mapping[str, Any], stabilizer: int = -1, tube_len: float = 2.0, no_color_z: bool = False, attach_axes: bool = False, rm_dir: Optional[str] = None) -> Mapping[str, Any]: """generate gltf in a dict and write to a json file with extension .gltf Args: lasre (Mapping[str, Any]): LaSRe of the LaS. stabilizer (int, optional): index of the stabilizer. The correlation surfaces corresponding to it will be drawn. Defaults to -1, which means do not draw any correlation surfaces. tube_len (float, optional): ratio of the length of the pipes compared to the length of the cubes. Defaults to 2.0. no_color_z (bool, optional): do not color the Z-pipes. Defaults to False, which means by default Z-pipes are colored. attach_axes (bool, optional): attach an IJK axes. Defaults to False. rm_dir (str, optional): the direction of faces to remove to reveal the correlation surfaces. Defaults to None. Raises: ValueError: rm_dir is not any one of :(+|-)(I|J|K) ValueError: the index of stabilizer is not -1 nor in [0, n_stabilizer) Returns: Mapping[str, Any]: the constructed gltf in a dict. """ s, tubelen, noColor = ( stabilizer, tube_len, no_color_z, ) if rm_dir is None: rm_dir = ":II" elif rm_dir not in [":+I", ":-I", ":+J", ":-J", ":+K", ":-K"]: raise ValueError("rm_dir is not one of :+I, :-I, :+J, :-J, :+K, :-K") gltf = base_gen(tubelen) i_bound = lasre["n_i"] j_bound = lasre["n_j"] k_bound = lasre["n_k"] NodeY = lasre["NodeY"] ExistI = lasre["ExistI"] ColorI = lasre["ColorI"] ExistJ = lasre["ExistJ"] ColorJ = lasre["ColorJ"] ExistK = lasre["ExistK"] s_bound = None if "CorrIJ" in lasre: CorrIJ = lasre["CorrIJ"] CorrIK = lasre["CorrIK"] CorrJI = lasre["CorrJI"] CorrJK = lasre["CorrJK"] CorrKI = lasre["CorrKI"] CorrKJ = lasre["CorrKJ"] s_bound = len(CorrIJ) if "ColorKP" not in lasre: ColorKP = [[[-1 for _ in range(k_bound)] for _ in range(j_bound)] for _ in range(i_bound)] else: ColorKP = lasre["ColorKP"] if "ColorKM" not in lasre: ColorKM = [[[-1 for _ in range(k_bound)] for _ in range(j_bound)] for _ in range(i_bound)] else: ColorKM = lasre["ColorKM"] port_cubes = lasre["port_cubes"] t_injections = (lasre["optional"]["t_injections"] if "t_injections" in lasre["optional"] else []) if s < -1 or (s_bound is not None and s_bound > 0 and s not in range(-1, s_bound)): raise ValueError(f"No such stabilizer index {s}.") for i in range(i_bound): for j in range(j_bound): for k in range(k_bound): if ExistI[i][j][k]: gltf["nodes"] += tube_gen( tubelen + 1.0, (i, j, k), "I", ColorI[i][j][k], s, (CorrIJ[s][i][j][k], CorrIK[s][i][j][k]) if s_bound else (0, 0), noColor, rm_dir, ) if ExistJ[i][j][k]: gltf["nodes"] += tube_gen( tubelen + 1.0, (i, j, k), "J", ColorJ[i][j][k], s, (CorrJK[s][i][j][k], CorrJI[s][i][j][k]) if s_bound else (0, 0), noColor, rm_dir, ) if ExistK[i][j][k]: gltf["nodes"] += tube_gen( tubelen + 1.0, (i, j, k), "K", 7 * ColorKM[i][j][k] + ColorKP[i][j][k], s, (CorrKI[s][i][j][k], CorrKJ[s][i][j][k]) if s_bound else (0, 0), noColor, rm_dir, ) for i in range(i_bound): for j in range(j_bound): for k in range(k_bound): exists = {"-I": 0, "+I": 0, "-K": 0, "+K": 0, "-J": 0, "+J": 0} colors = {} corr = { "-I": (0, 0), "+I": (0, 0), "-J": (0, 0), "+J": (0, 0), "-K": (0, 0), "+K": (0, 0), } if i > 0 and ExistI[i - 1][j][k]: exists["-I"] = 1 colors["-I"] = ColorI[i - 1][j][k] corr["-I"] = (CorrIJ[s][i - 1][j][k], CorrIK[s][i - 1][j][k]) if s_bound else (0, 0) if ExistI[i][j][k]: exists["+I"] = 1 colors["+I"] = ColorI[i][j][k] corr["+I"] = (CorrIJ[s][i][j][k], CorrIK[s][i][j][k]) if s_bound else (0, 0) if j > 0 and ExistJ[i][j - 1][k]: exists["-J"] = 1 colors["-J"] = ColorJ[i][j - 1][k] corr["-J"] = (CorrJK[s][i][j - 1][k], CorrJI[s][i][j - 1][k]) if s_bound else (0, 0) if ExistJ[i][j][k]: exists["+J"] = 1 colors["+J"] = ColorJ[i][j][k] corr["+J"] = (CorrJK[s][i][j][k], CorrJI[s][i][j][k]) if s_bound else (0, 0) if k > 0 and ExistK[i][j][k - 1]: exists["-K"] = 1 colors["-K"] = ColorKP[i][j][k - 1] corr["-K"] = (CorrKI[s][i][j][k - 1], CorrKJ[s][i][j][k - 1]) if s_bound else (0, 0) if ExistK[i][j][k]: exists["+K"] = 1 colors["+K"] = ColorKM[i][j][k] corr["+K"] = (CorrKI[s][i][j][k], CorrKJ[s][i][j][k]) if s_bound else (0, 0) if sum([v for (k, v) in exists.items()]) > 0: if (i, j, k) not in port_cubes: if NodeY[i][j][k]: gltf["nodes"] += special_gen( tubelen + 1.0, (i, j, k), exists, "Y", s, rm_dir, ) else: gltf["nodes"] += cube_gen( tubelen + 1.0, (i, j, k), exists, colors, s, corr, noColor, rm_dir, ) elif [i, j, k] in t_injections: gltf["nodes"] += special_gen( tubelen + 1.0, (i, j, k), exists, "T", s, rm_dir, ) if attach_axes: gltf["nodes"] += axes_gen(tube_len + 1.0, i_bound, j_bound, k_bound) gltf["nodes"][0]["children"] = list(range(1, len(gltf["nodes"]))) return gltf ================================================ FILE: glue/lattice_surgery/lassynth/translators/networkx_generator.py ================================================ """generate a annotated networkx.Graph corresponding to the LaS.""" import networkx from lassynth.translators import ZXGridGraph import stimzx from typing import Mapping, Any def networkx_generator(lasre: Mapping[str, Any]) -> networkx.Graph: n_i, n_j, n_k = lasre["n_i"], lasre["n_j"], lasre["n_k"] port_cubes = lasre["port_cubes"] zxgridgraph = ZXGridGraph(lasre) edges = zxgridgraph.edges nodes = zxgridgraph.nodes zx_nx_graph = networkx.Graph() type_to_str = {"X": "X", "Z": "Z", "Pi": "in", "Po": "out", "I": "X"} cnt = 0 for (i, j, k) in port_cubes: node = nodes[i][j][k] zx_nx_graph.add_node(cnt, value=stimzx.ZxType(type_to_str[node.type])) node.node_id = cnt cnt += 1 for i in range(n_i + 1): for j in range(n_j + 1): for k in range(n_k + 1): node = nodes[i][j][k] if node.type not in ["N", "Po", "Pi"]: zx_nx_graph.add_node(cnt, value=stimzx.ZxType( type_to_str[node.type])) node.node_id = cnt cnt += 1 if node.y_tail_minus: zx_nx_graph.add_node(cnt, value=stimzx.ZxType("Z", 1)) zx_nx_graph.add_edge(node.node_id, cnt) cnt += 1 if node.y_tail_plus: zx_nx_graph.add_node(cnt, value=stimzx.ZxType("Z", 3)) zx_nx_graph.add_edge(node.node_id, cnt) cnt += 1 for edge in edges: if edge.type != "h": zx_nx_graph.add_edge(edge.node0.node_id, edge.node1.node_id) else: zx_nx_graph.add_node(cnt, value=stimzx.ZxType("H")) zx_nx_graph.add_edge(cnt, edge.node0.node_id) zx_nx_graph.add_edge(cnt, edge.node1.node_id) cnt += 1 return zx_nx_graph ================================================ FILE: glue/lattice_surgery/lassynth/translators/textfig_generator.py ================================================ """Generate text figures of 2D time slices of the LaS.""" from lassynth.translators import ZXGridGraph class TextLayer: pad_i = 1 pad_j = 1 sep_i = 4 sep_j = 4 def __init__(self, zx_graph: ZXGridGraph, k: int, if_middle: bool) -> None: self.n_i, self.n_j, self.n_k = ( zx_graph.n_i, zx_graph.n_j, zx_graph.n_k, ) self.chars = [[ " " for _ in range(2 * TextLayer.pad_i + (self.n_i - 1) * TextLayer.sep_i + 1) ] + ["\n"] for _ in range(2 * TextLayer.pad_j + (self.n_j - 1) * TextLayer.sep_j + 1)] if if_middle: self.compute_middle(zx_graph, k) else: self.compute_normal(zx_graph, k) def set_char(self, j: int, i: int, character): self.chars[j][i] = character def compute_normal(self, zx_graph: ZXGridGraph, k: int): """a normal layer corresponds to a layer of cubes in LaS, e.g., / / X X | / | |/ Z . / There are 2x2 tiles of surface codes. The bottom right one is not being used, represented by a `.`; the top right one is identity in because it has degree 2, but our convention is that these spiders have type `X` The top left one is like that, too. The bottom left is a Z-spider with three edges, which is non trivial. `-` and `|` I-pipes and J-pipes. `/` are K-pipes. The `/` on the bottom left corner of a spider connects to the previous moment. The `/` on the top right corner of a spider connects to the next moment. Args: zx_graph (ZXGridGraph): k (int): the height of this layer. """ for i in range(self.n_i): for j in range(self.n_j): spider = zx_graph.nodes[i][j][k] if spider.type in ["N", "Pi", "Po"]: self.set_char( TextLayer.pad_j + j * TextLayer.sep_j, TextLayer.pad_i + i * TextLayer.sep_i, ".", ) continue elif spider.type == "I": self.set_char( TextLayer.pad_j + j * TextLayer.sep_j, TextLayer.pad_i + i * TextLayer.sep_i, "X", ) else: self.set_char( TextLayer.pad_j + j * TextLayer.sep_j, TextLayer.pad_i + i * TextLayer.sep_i, spider.type, ) # I pipes if spider.exists["+I"]: for offset in range(1, TextLayer.sep_i): self.set_char( TextLayer.pad_j + j * TextLayer.sep_j, TextLayer.pad_i + i * TextLayer.sep_i + offset, "-", ) # J pipes if spider.exists["+J"]: for offset in range(1, TextLayer.sep_i): self.set_char( TextLayer.pad_j + j * TextLayer.sep_j + offset, TextLayer.pad_i + i * TextLayer.sep_i, "|", ) # K pipes if spider.exists["+K"]: self.set_char( TextLayer.pad_j + j * TextLayer.sep_j - 1, TextLayer.pad_i + i * TextLayer.sep_i + 1, "/", ) if spider.exists["-K"]: self.set_char( TextLayer.pad_j + j * TextLayer.sep_j + 1, TextLayer.pad_i + i * TextLayer.sep_i - 1, "/", ) def compute_middle(self, zx_graph: ZXGridGraph, k: int): """a middle layer is either a Hadmard edge or a normal edge, e.g., / . X / / H . / These layers cannot have `-` or `|`. It only has `/` which are K-pipes. The node is either `H` meaning the edge is a Hadamard edge, or `X` meaning the edge is a normal edge. We use `X` for identity here. Args: zx_graph (ZXGridGraph): k (int): the height of this layer. There is a middle layer after a normal layer. """ for i in range(self.n_i): for j in range(self.n_j): self.set_char( TextLayer.pad_j + j * TextLayer.sep_j, TextLayer.pad_i + i * TextLayer.sep_i, ".", ) spider = zx_graph.nodes[i][j][k] color_sum = -1 if k == self.n_k - 1: try: for port in zx_graph.lasre["ports"]: if (port["i"], port["j"], port["k"]) == (i, j, k): color_sum = port["c"] + spider.colors["+K"] break except ValueError: print( f"KPipe({i},{j},{k}) connect outside but not port." ) else: upper_spider = zx_graph.nodes[i][j][k + 1] if spider.exists["+K"] == 1 and upper_spider.exists[ "-K"] == 1: color_sum = spider.colors["+K"] + upper_spider.colors[ "-K"] if spider.exists["+K"] == 0 and upper_spider.exists[ "-K"] == 1: try: for port in zx_graph.lasre["ports"]: if (port["i"], port["j"], port["k"]) == (i, j, k): color_sum = port[ "c"] + upper_spider.colors["-K"] break except ValueError: print(f"KPipe({i},{j},{k})- should be a port.") if spider.exists["+K"] == 1 and upper_spider.exists[ "-K"] == 0: try: for port in zx_graph.lasre["ports"]: if (port["i"], port["j"], port["k"]) == (i, j, k + 1): color_sum = port["c"] + spider.colors["+K"] break except ValueError: print(f"KPipe({i},{j},{k + 1})- should be a port") if color_sum != -1: self.set_char( TextLayer.pad_j + j * TextLayer.sep_j - 1, TextLayer.pad_i + i * TextLayer.sep_i + 1, "/", ) self.set_char( TextLayer.pad_j + j * TextLayer.sep_j + 1, TextLayer.pad_i + i * TextLayer.sep_i - 1, "/", ) if color_sum == 1: self.set_char( TextLayer.pad_j + j * TextLayer.sep_j, TextLayer.pad_i + i * TextLayer.sep_i, "H", ) else: self.set_char( TextLayer.pad_j + j * TextLayer.sep_j, TextLayer.pad_i + i * TextLayer.sep_i, "X", ) def get_text(self): text = "" for j in range(2 * TextLayer.pad_j + (self.n_j - 1) * TextLayer.sep_j + 1): for i in range(2 * TextLayer.pad_i + (self.n_i - 1) * TextLayer.sep_i + 1): text += self.chars[j][i] text += "\n" return text def textfig_generator(lasre: dict): text = "======================================\n" zx_graph = ZXGridGraph(lasre) for k in range(lasre["n_k"] - 1, -1, -1): text += TextLayer(zx_graph, k, True).get_text() text += "======================================\n" text += TextLayer(zx_graph, k, False).get_text() text += "======================================\n" return text ================================================ FILE: glue/lattice_surgery/lassynth/translators/zx_grid_graph.py ================================================ """Classes ZXGridEdge, ZXGridSpider, and ZXGridGraph. ZXGridGraph is a graph where nodes are the cubes in LaS and edges are pipes in LaS. """ from typing import Any, Mapping, Sequence, Tuple, Optional class ZXGridNode: def __init__(self, coord3: Tuple[int, int, int], connectivity: Mapping[str, Mapping[str, int]]) -> None: """initialize ZXGridNode for a cube in the LaS. self.type: type of ZX spider, 'N': no spider, 'X'/'Z': X/Z-spider, 'S': Y cube, 'I': identity, 'Pi': input port, 'Po': output port. self.i/j/k: 3D corrdinates of the cube in the LaS. self.exists is a dictionary with six keys corresponding to whether a pipe exist in the six directions to a cube in the LaS. self.colors are the colors of these possible pipes. self.y_tail_plus: if this node connects a Y on the top. self.y_tail_minus: if this node connects a Y on the bottom. Args: coord3 (Tuple[int, int, int]): 3D coordinate of the cube. connectivity (Mapping[str, Mapping[str, int]]): contains exists and colors of the six possible pipes to a cube """ self.i, self.j, self.k = coord3 self.y_tail_plus = False self.y_tail_minus = False self.node_id = -1 self.exists = connectivity["exists"] self.colors = connectivity["colors"] self.compute_type() def compute_type(self) -> None: """decide the type of a ZXGridNoe Raises: ValueError: node has degree=1, which should be forbidden earlier. ValueError: node has degree>4, which should be forbidden earlier. """ deg = sum([v for (k, v) in self.exists.items()]) if deg == 0: self.type = "N" return elif deg == 1: raise ValueError("There should not be deg-1 Z or X spiders.") elif deg == 2: self.type = "I" elif deg >= 5: raise ValueError("deg > 4: 3D corner exists") else: # degree = 3 or 4 if self.exists["-I"] == 0 and self.exists["+I"] == 0: if self.exists["-J"]: if self.colors["-J"] == 0: self.type = "X" else: self.type = "Z" else: # must exist +J if self.colors["+J"] == 0: self.type = "X" else: self.type = "Z" if self.exists["-J"] == 0 and self.exists["+J"] == 0: if self.exists["-I"]: if self.colors["-I"] == 0: self.type = "Z" else: self.type = "X" else: # must exist +I if self.colors["+I"] == 0: self.type = "Z" else: self.type = "X" if self.exists["-K"] == 0 and self.exists["+K"] == 0: if self.exists["-I"]: if self.colors["-I"] == 0: self.type = "X" else: self.type = "Z" else: # must exist +I if self.colors["+I"] == 0: self.type = "X" else: self.type = "Z" def zigxag_xy(self, n_j: int) -> Tuple[int, int]: return (self.k * (n_j + 2) + self.j, -(n_j + 1) * self.i + self.j) def zigxag_str(self, n_j: int) -> str: zigxag_type = { 'Z': '@', 'X': 'O', 'S': 's', 'W': 'w', 'I': 'O', 'Pi': 'in', 'Po': 'out', } (x, y) = self.zigxag_xy(n_j) return str(-y) + ',' + str(-x) + ',' + str(zigxag_type[self.type]) class ZXGridEdge: def __init__(self, if_h: bool, node0: ZXGridNode, node1: ZXGridNode) -> None: """initialize ZXGridEdge for a pipe in the LaS. Args: if_h (bool): if this edge is a Hadamard edge. node0 (ZXGridNode): one end of the edge. node1 (ZXGridNode): the other end of the edge. Raises: ValueError: the two spiders are the same. ValueError: the two spiders are not neighbors. """ dist = abs(node0.i - node1.i) + abs(node0.j - node1.j) + abs(node0.k - node1.k) if dist == 0: raise ValueError(f"{node0} and {node1} are the same.") if dist > 1: raise ValueError(f"{node0} and {node1} are not neighbors.") self.node0, self.node1 = node0, node1 self.type = "h" if if_h else "-" def zigxag_str(self, n_j: int) -> str: (xa, ya) = self.node0.zigxag_xy(n_j) (xb, yb) = self.node1.zigxag_xy(n_j) return (str(-ya) + ',' + str(-xa) + ',' + str(-yb) + ',' + str(-xb) + ',' + self.type) class ZXGridGraph: def __init__(self, lasre: Mapping[str, Any]) -> None: self.lasre = lasre self.n_i, self.n_j, self.n_k = ( lasre["n_i"], lasre["n_j"], lasre["n_k"], ) self.nodes = [[[ ZXGridNode((i, j, k), self.gather_cube_connectivity(i, j, k)) for k in range(self.n_k + 1) ] for j in range(self.n_j + 1)] for i in range(self.n_i + 1)] for (i, j, k) in self.lasre["port_cubes"]: self.nodes[i][j][k].type = 'Po' self.append_y_tails() self.edges = [] self.derive_edges() def gather_cube_connectivity(self, i: int, j: int, k: int) -> Mapping[str, Mapping[str, int]]: # exists and colors for no cube exists = {"-I": 0, "+I": 0, "-K": 0, "+K": 0, "-J": 0, "+J": 0} colors = { "-I": -1, "+I": -1, "-K": -1, "+K": -1, "-J": -1, "+J": -1, } if i in range(self.n_i) and j in range(self.n_j) and k in range( self.n_k) and ((i, j, k) not in self.lasre["port_cubes"]) and ( self.lasre["NodeY"][i][j][k] == 0): if i > 0 and self.lasre["ExistI"][i - 1][j][k]: exists["-I"] = 1 colors["-I"] = self.lasre["ColorI"][i - 1][j][k] if self.lasre["ExistI"][i][j][k]: exists["+I"] = 1 colors["+I"] = self.lasre["ColorI"][i][j][k] if j > 0 and self.lasre["ExistJ"][i][j - 1][k]: exists["-J"] = 1 colors["-J"] = self.lasre["ColorJ"][i][j - 1][k] if self.lasre["ExistJ"][i][j][k]: exists["+J"] = 1 colors["+J"] = self.lasre["ColorJ"][i][j][k] if k > 0 and self.lasre["ExistK"][i][j][k - 1]: exists["-K"] = 1 colors["-K"] = self.lasre["ColorKP"][i][j][k - 1] if self.lasre["ExistK"][i][j][k]: exists["+K"] = 1 colors["+K"] = self.lasre["ColorKM"][i][j][k] return {"exists": exists, "colors": colors} def append_y_tails(self) -> None: for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): if self.lasre["NodeY"][i][j][k]: if (k - 1 >= 0 and self.lasre["ExistK"][i][j][k - 1] and (not self.lasre["NodeY"][i][j][k - 1])): self.nodes[i][j][k - 1].y_tail_plus = True if (k + 1 < self.n_k and self.lasre["ExistK"][i][j][k] and (not self.lasre["NodeY"][i][j][k + 1])): self.nodes[i][j][k + 1].y_tail_minus = True def derive_edges(self): valid_types = ["Z", "X", "S", "I", "Pi", "Po"] for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): if (self.lasre["ExistI"][i][j][k] == 1 and self.nodes[i][j][k].type in valid_types and self.nodes[i + 1][j][k].type in valid_types): self.edges.append( ZXGridEdge(0, self.nodes[i][j][k], self.nodes[i + 1][j][k])) if (self.lasre["ExistJ"][i][j][k] == 1 and self.nodes[i][j][k].type in valid_types and self.nodes[i][j + 1][k].type in valid_types): self.edges.append( ZXGridEdge(0, self.nodes[i][j][k], self.nodes[i][j + 1][k])) if (self.lasre["ExistK"][i][j][k] == 1 and self.nodes[i][j][k].type in valid_types and self.nodes[i][j][k + 1].type in valid_types): self.edges.append( ZXGridEdge( abs(self.lasre["ColorKM"][i][j][k] - self.lasre["ColorKP"][i][j][k]), self.nodes[i][j][k], self.nodes[i][j][k + 1], )) def to_zigxag_url(self, io_spec: Optional[Sequence[str]] = None) -> str: """generate a url for ZigXag Args: io_spec (Sequence[str], optional): specify whether each port is an input port or an output port. Raises: ValueError: len(io_spec) is not the same with the number of ports. Returns: str: zigxag url """ if io_spec is not None: if len(io_spec) != len(self.lasre["port_cubes"]): raise ValueError( f"io_spec has length {len(io_spec)} but there are " f"{len(self.lasre['port_cubes'])} ports.") for w, (i, j, k) in enumerate(self.lasre["port_cubes"]): self.nodes[i][j][k].type = io_spec[w] valid_types = ["Z", "X", "S", "W", "I", "Pi", "Po"] nodes_str = "" first = True for i in range(self.n_i + 1): for j in range(self.n_j + 1): for k in range(self.n_k + 1): if self.nodes[i][j][k].type in valid_types: if not first: nodes_str += ";" nodes_str += self.nodes[i][j][k].zigxag_str(self.n_j) first = False edges_str = "" for i, edge in enumerate(self.edges): if i > 0: edges_str += ";" edges_str += edge.zigxag_str(self.n_j) # add nodes and edges for Y cubes for i in range(self.n_i): for j in range(self.n_j): for k in range(self.n_k): (x, y) = self.nodes[i][j][k].zigxag_xy(self.n_j) if self.nodes[i][j][k].y_tail_plus: nodes_str += (";" + str(x + self.n_j - j) + "," + str(y) + ",s") edges_str += (";" + str(x + self.n_j - j) + "," + str(y) + "," + str(x) + "," + str(y) + ",-") if self.nodes[i][j][k].y_tail_minus: nodes_str += (";" + str(x - j - 1) + "," + str(y) + ",s") edges_str += (";" + str(x - j - 1) + "," + str(y) + "," + str(x) + "," + str(y) + ",-") zigxag_str = "https://algassert.com/zigxag#" + nodes_str + ":" + edges_str return zigxag_str ================================================ FILE: glue/lattice_surgery/setup.py ================================================ from setuptools import find_packages, setup with open('README.md', encoding='UTF-8') as f: long_description = f.read() __version__ = '0.1.0' setup( name='LaSsynth', version=__version__, author='', author_email='', url='', license='Apache 2', packages=find_packages(), description='Lattice Surgery Subroutine Synthesizer', long_description=long_description, long_description_content_type='text/markdown', python_requires='>=3.6.0', data_files=['README.md'], install_requires=[ 'z3-solver==4.12.1.0', 'stim', 'networkx', 'ipykernel', ], tests_require=['pytest', 'python3-distutils'], ) ================================================ FILE: glue/lattice_surgery/stimzx/__init__.py ================================================ __version__ = '1.12.dev0' from ._external_stabilizer import ( ExternalStabilizer, ) from ._text_diagram_parsing import ( text_diagram_to_networkx_graph, ) from ._zx_graph_solver import ( zx_graph_to_external_stabilizers, text_diagram_to_zx_graph, ZxType, ) ================================================ FILE: glue/lattice_surgery/stimzx/_external_stabilizer.py ================================================ from typing import List, Any import stim class ExternalStabilizer: """An input-to-output relationship enforced by a stabilizer circuit.""" def __init__(self, *, input: stim.PauliString, output: stim.PauliString): self.input = input self.output = output @staticmethod def from_dual(dual: stim.PauliString, num_inputs: int) -> 'ExternalStabilizer': sign = dual.sign # Transpose input. Ys get negated. for k in range(num_inputs): if dual[k] == 2: sign *= -1 return ExternalStabilizer( input=dual[:num_inputs], output=dual[num_inputs:], ) @staticmethod def canonicals_from_duals(duals: List[stim.PauliString], num_inputs: int) -> List['ExternalStabilizer']: if not duals: return [] duals = [e.copy() for e in duals] num_qubits = len(duals[0]) num_outputs = num_qubits - num_inputs id_out = stim.PauliString(num_outputs) # Pivot on output qubits, to potentially isolate input-only stabilizers. _eliminate_stabilizers(duals, range(num_inputs, num_qubits)) # Separate input-only stabilizers from the rest. input_only_stabilizers = [] output_using_stabilizers = [] for dual in duals: if dual[num_inputs:] == id_out: input_only_stabilizers.append(dual) else: output_using_stabilizers.append(dual) # Separately canonicalize the output-using and input-only stabilizers. _eliminate_stabilizers(output_using_stabilizers, range(num_qubits)) _eliminate_stabilizers(input_only_stabilizers, range(num_inputs)) duals = input_only_stabilizers + output_using_stabilizers return [ExternalStabilizer.from_dual(e, num_inputs) for e in duals] def __mul__(self, other: 'ExternalStabilizer') -> 'ExternalStabilizer': return ExternalStabilizer(input=other.input * self.input, output=self.output * other.output) def __str__(self) -> str: return str(self.input) + ' -> ' + str(self.output) def __eq__(self, other: Any) -> bool: if not isinstance(other, ExternalStabilizer): return NotImplemented return self.output == other.output and self.input == other.input def __ne__(self, other: Any) -> bool: return not self == other def __repr__(self): return f'stimzx.ExternalStabilizer(input={self.input!r}, output={self.output!r})' def _eliminate_stabilizers(stabilizers: List[stim.PauliString], elimination_indices: range): """Performs partial Gaussian elimination on the list of stabilizers.""" min_pivot = 0 for q in elimination_indices: for b in [1, 3]: for pivot in range(min_pivot, len(stabilizers)): p = stabilizers[pivot][q] if p == 2 or p == b: break else: continue for k, stabilizer in enumerate(stabilizers): p = stabilizer[q] if k != pivot and (p == 2 or p == b): stabilizer *= stabilizers[pivot] if min_pivot != pivot: stabilizers[min_pivot], stabilizers[pivot] = stabilizers[pivot], stabilizers[min_pivot] min_pivot += 1 ================================================ FILE: glue/lattice_surgery/stimzx/_external_stabilizer_test.py ================================================ import stim import stimzx def test_repr(): e = stimzx.ExternalStabilizer(input=stim.PauliString("XX"), output=stim.PauliString("Y")) assert eval(repr(e), {'stimzx': stimzx, 'stim': stim}) == e ================================================ FILE: glue/lattice_surgery/stimzx/_text_diagram_parsing.py ================================================ import re from typing import Dict, Tuple, TypeVar, List, Set, Callable import networkx as nx K = TypeVar("K") def text_diagram_to_networkx_graph(text_diagram: str, *, value_func: Callable[[str], K] = str) -> nx.MultiGraph: r"""Converts a text diagram into a networkx multi graph. Args: text_diagram: An ascii text diagram of the graph, linking nodes together with edges. Edges can be horizontal (-), vertical (|), diagonal (/\), crossing (+), or changing direction (*). Nodes can be alphanumeric with parentheses. It is assumed that all text is shown with a fixed-width font. value_func: An optional transformation to apply to the node text in order to get the node's value. Otherwise the node's value is just its text. Example: >>> import stimzx >>> import networkx as nx >>> actual = stimzx.text_diagram_to_networkx_graph(r''' ... ... A ... | ... NODE1--+--NODE2----------* ... | | / ... B | / ... *------NODE4 ... ... ''') >>> expected = nx.MultiGraph() >>> expected.add_node(0, value='A') >>> expected.add_node(1, value='NODE1') >>> expected.add_node(2, value='NODE2') >>> expected.add_node(3, value='B') >>> expected.add_node(4, value='NODE4') >>> _ = expected.add_edge(0, 3) >>> _ = expected.add_edge(1, 2) >>> _ = expected.add_edge(2, 4) >>> _ = expected.add_edge(2, 4) >>> nx.testing.assert_graphs_equal(actual, expected) Returns: A networkx multi graph containing the graph from the text diagram. Nodes in the graph are integers (the ordering of nodes is in the natural string ordering from left to right then top to bottom in the diagram), and have a "value" attribute containing either the node's string from the diagram or else a function of that string if value_func was specified. """ char_map = _text_to_char_map(text_diagram) node_ids, nodes = _find_nodes(char_map, value_func) edges = _find_all_edges(char_map, node_ids) result = nx.MultiGraph() for k, v in enumerate(nodes): result.add_node(k, value=v) for a, b in edges: result.add_edge(a, b) return result def _text_to_char_map(text: str) -> Dict[complex, str]: char_map = {} x = 0 y = 0 for c in text: if c == '\n': x = 0 y += 1 continue if c != ' ': char_map[x + 1j*y] = c x += 1 return char_map DIR_TO_CHARS = { -1 - 1j: '\\', 0 - 1j: '|+', 1 - 1j: '/', -1: '-+', 1: '-+', -1 + 1j: '/', 1j: '|+', 1 + 1j: '\\', } CHAR_TO_DIR = { '\\': 1 + 1j, '-': 1, '|': 1j, '/': -1 + 1j, } def _find_all_edges(char_map: Dict[complex, str], terminal_map: Dict[complex, K]) -> List[Tuple[K, K]]: edges = [] already_travelled = set() for xy, c in char_map.items(): x = int(xy.real) y = int(xy.imag) if xy in terminal_map or xy in already_travelled or c in '*+': continue already_travelled.add(xy) dxy = CHAR_TO_DIR.get(c) if dxy is None: raise ValueError(f"Character {x+1} ('{c}') in line {y+1} isn't part in a node or an edge") n1 = _find_end_of_edge(xy + dxy, dxy, char_map, terminal_map, already_travelled) n2 = _find_end_of_edge(xy - dxy, -dxy, char_map, terminal_map, already_travelled) edges.append((n2, n1)) return edges def _find_end_of_edge(xy: complex, dxy: complex, char_map: Dict[complex, str], terminal_map: Dict[complex, K], already_travelled: Set[complex]): while True: c = char_map[xy] if xy in terminal_map: return terminal_map[xy] if c != '+': if xy in already_travelled: raise ValueError("Edge used twice.") already_travelled.add(xy) next_deltas: List[complex] = [] if c == '*': for dx2 in [-1, 0, 1]: for dy2 in [-1, 0, 1]: dxy2 = dx2 + dy2 * 1j c2 = char_map.get(xy + dxy2) if dxy2 != 0 and dxy2 != -dxy and c2 is not None and c2 in DIR_TO_CHARS[dxy2]: next_deltas.append(dxy2) if len(next_deltas) != 1: raise ValueError(f"Edge junction ('*') at character {int(xy.real)+1}$ in line {int(xy.imag)+1} doesn't have exactly 2 legs.") dxy, = next_deltas else: expected = DIR_TO_CHARS[dxy] if c not in expected: raise ValueError(f"Dangling edge at character {int(xy.real)+1} in line {int(xy.imag)+1} travelling dx=${int(dxy.real)},dy={int(dxy.imag)}.") xy += dxy def _find_nodes(char_map: Dict[complex, str], value_func: Callable[[str], K]) -> Tuple[Dict[complex, int], List[K]]: node_ids = {} nodes = [] node_chars = re.compile("^[a-zA-Z0-9()]$") next_node_id = 0 for xy, lead_char in char_map.items(): if xy in node_ids: continue if not node_chars.match(lead_char): continue n = 0 nested = 0 full_name = '' while True: c = char_map.get(xy + n, ' ') if c == ' ' and nested > 0: raise ValueError("Label ended before ')' to go with '(' was found.") if nested == 0 and not node_chars.match(c): break full_name += c if c == '(': nested += 1 elif c == ')': nested -= 1 n += 1 nodes.append(value_func(full_name)) node_id = next_node_id next_node_id += 1 for k in range(n): node_ids[xy + k] = node_id return node_ids, nodes ================================================ FILE: glue/lattice_surgery/stimzx/_text_diagram_parsing_test.py ================================================ import networkx as nx import pytest from ._text_diagram_parsing import _find_nodes, _text_to_char_map, _find_end_of_edge, _find_all_edges, text_diagram_to_networkx_graph def test_text_to_char_map(): assert _text_to_char_map(""" ABC DEF G HI """) == { 0 + 1j: 'A', 1 + 1j: 'B', 2 + 1j: 'C', 4 + 1j: 'D', 5 + 1j: 'E', 6 + 1j: 'F', 0 + 2j: 'G', 1 + 3j: 'H', 2 + 3j: 'I', } def test_find_nodes(): assert _find_nodes(_text_to_char_map(''), lambda e: e) == ({}, []) with pytest.raises(ValueError, match="base 10"): _find_nodes(_text_to_char_map('NOTANINT'), int) with pytest.raises(ValueError, match=r"ended before '\)'"): _find_nodes(_text_to_char_map('X(run_off'), str) assert _find_nodes(_text_to_char_map('X'), str) == ( { 0j: 0, }, ['X'], ) assert _find_nodes(_text_to_char_map('\n X'), str) == ( { 3 + 1j: 0, }, ['X'], ) assert _find_nodes(_text_to_char_map('X(pi)'), str) == ( { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, }, ['X(pi)'], ) assert _find_nodes(_text_to_char_map('X--Z'), str) == ( { 0: 0, 3: 1, }, ['X', 'Z'], ) assert _find_nodes(_text_to_char_map(""" X--* / Z """), str) == ( { 1j: 0, 1 + 3j: 1, }, ['X', 'Z'], ) assert _find_nodes(_text_to_char_map(""" X(pi)--Z """), str) == ( { 0 + 1j: 0, 1 + 1j: 0, 2 + 1j: 0, 3 + 1j: 0, 4 + 1j: 0, 7 + 1j: 1, }, ["X(pi)", "Z"], ) def test_find_end_of_edge(): c = _text_to_char_map(r""" 1--------* \ 2 | 5 \ *--++-* *-----+-* |/ | | / 2 |/ * """) terminal = {1: 'ONE', 18 + 6j: 'TWO'} seen = set() assert _find_end_of_edge(1 + 1j, 1, c, terminal, seen) == 'TWO' assert len(seen) == 31 def test_find_all_edges(): c = _text_to_char_map(r""" X---Z H----X(pi/2) / Z(pi/2) """) node_ids, _ = _find_nodes(c, str) assert _find_all_edges(c, node_ids) == [ (0, 1), (2, 3), (2, 4), ] def test_from_text_diagram(): actual = text_diagram_to_networkx_graph(""" in---Z---H---------out | in---X---Z(-pi/2)---out """) expected = nx.MultiGraph() expected.add_node(0, value='in'), expected.add_node(1, value='Z'), expected.add_node(2, value='H'), expected.add_node(3, value='out'), expected.add_node(4, value='in'), expected.add_node(5, value='X'), expected.add_node(6, value='Z(-pi/2)'), expected.add_node(7, value='out'), expected.add_edge(0, 1) expected.add_edge(1, 2) expected.add_edge(2, 3) expected.add_edge(1, 5) expected.add_edge(4, 5) expected.add_edge(5, 6) expected.add_edge(6, 7) nx.testing.assert_graphs_equal(actual, expected) actual = text_diagram_to_networkx_graph(""" Z-* | | X-* """) expected = nx.MultiGraph() expected.add_node(0, value='Z') expected.add_node(1, value='X') expected.add_edge(0, 1) expected.add_edge(0, 1) nx.testing.assert_graphs_equal(actual, expected) ================================================ FILE: glue/lattice_surgery/stimzx/_zx_graph_solver.py ================================================ from typing import Dict, Tuple, List, Any, Union import stim import networkx as nx from ._text_diagram_parsing import text_diagram_to_networkx_graph from ._external_stabilizer import ExternalStabilizer class ZxType: """Data describing a ZX node.""" def __init__(self, kind: str, quarter_turns: int = 0): self.kind = kind self.quarter_turns = quarter_turns def __eq__(self, other): if not isinstance(other, ZxType): return NotImplemented return self.kind == other.kind and self.quarter_turns == other.quarter_turns def __ne__(self, other): return not self == other def __hash__(self): return hash((ZxType, self.kind, self.quarter_turns)) def __repr__(self): return f'ZxType(kind={self.kind!r}, quarter_turns={self.quarter_turns!r})' ZX_TYPES = { "X": ZxType("X"), "X(pi/2)": ZxType("X", 1), "X(pi)": ZxType("X", 2), "X(-pi/2)": ZxType("X", 3), "Z": ZxType("Z"), "Z(pi/2)": ZxType("Z", 1), "Z(pi)": ZxType("Z", 2), "Z(-pi/2)": ZxType("Z", 3), "H": ZxType("H"), "in": ZxType("in"), "out": ZxType("out"), } def text_diagram_to_zx_graph(text_diagram: str) -> nx.MultiGraph: """Converts an ASCII text diagram into a ZX graph (represented as a networkx MultiGraph). Supported node types: "X": X spider with angle set to 0. "Z": Z spider with angle set to 0. "X(pi/2)": X spider with angle set to pi/2. "X(pi)": X spider with angle set to pi. "X(-pi/2)": X spider with angle set to -pi/2. "Z(pi/2)": X spider with angle set to pi/2. "Z(pi)": X spider with angle set to pi. "Z(-pi/2)": X spider with angle set to -pi/2. "H": Hadamard node. Must have degree 2. "in": Input node. Must have degree 1. "out": Output node. Must have degree 1. Args: text_diagram: A text diagram containing ZX nodes (e.g. "X(pi)") and edges (e.g. "------") connecting them. Example: >>> import stimzx >>> import networkx >>> actual: networkx.MultiGraph = stimzx.text_diagram_to_zx_graph(r''' ... in----X------out ... | ... in---Z(pi)---out ... ''') >>> expected = networkx.MultiGraph() >>> expected.add_node(0, value=stimzx.ZxType("in")) >>> expected.add_node(1, value=stimzx.ZxType("X")) >>> expected.add_node(2, value=stimzx.ZxType("out")) >>> expected.add_node(3, value=stimzx.ZxType("in")) >>> expected.add_node(4, value=stimzx.ZxType("Z", quarter_turns=2)) >>> expected.add_node(5, value=stimzx.ZxType("out")) >>> _ = expected.add_edge(0, 1) >>> _ = expected.add_edge(1, 2) >>> _ = expected.add_edge(1, 4) >>> _ = expected.add_edge(3, 4) >>> _ = expected.add_edge(4, 5) >>> networkx.testing.assert_graphs_equal(actual, expected) Returns: A networkx MultiGraph containing the nodes and edges from the diagram. Nodes are numbered 0, 1, 2, etc in reading ordering from the diagram, and have a "value" attribute of type `stimzx.ZxType`. """ return text_diagram_to_networkx_graph(text_diagram, value_func=ZX_TYPES.__getitem__) def _reduced_zx_graph(graph: Union[nx.Graph, nx.MultiGraph]) -> nx.Graph: """Return an equivalent graph without self edges or repeated edges.""" reduced_graph = nx.Graph() odd_parity_edges = set() for n1, n2 in graph.edges(): if n1 == n2: continue odd_parity_edges ^= {frozenset([n1, n2])} for n, value in graph.nodes('value'): reduced_graph.add_node(n, value=value) for n1, n2 in odd_parity_edges: reduced_graph.add_edge(n1, n2) return reduced_graph def zx_graph_to_external_stabilizers(graph: Union[nx.Graph, nx.MultiGraph]) -> List[ExternalStabilizer]: """Computes the external stabilizers of a ZX graph; generators of Paulis that leave it unchanged including sign. Args: graph: A non-contradictory connected ZX graph with nodes annotated by 'type' and optionally by 'angle'. Allowed types are 'x', 'z', 'h', and 'out'. Allowed angles are multiples of `math.pi/2`. Only 'x' and 'z' node types can have angles. 'out' nodes must have degree 1. 'h' nodes must have degree 2. Returns: A list of canonicalized external stabilizer generators for the graph. """ graph = _reduced_zx_graph(graph) sim = stim.TableauSimulator() # Interpret each edge as a cup producing an EPR pair. # - The qubits of the EPR pair fly away from the center of the edge, towards their respective nodes. # - The qubit keyed by (a, b) is the qubit heading towards b from the edge between a and b. qubit_ids: Dict[Tuple[Any, Any], int] = {} for n1, n2 in graph.edges: qubit_ids[(n1, n2)] = len(qubit_ids) qubit_ids[(n2, n1)] = len(qubit_ids) sim.h(qubit_ids[(n1, n2)]) sim.cnot(qubit_ids[(n1, n2)], qubit_ids[(n2, n1)]) # Interpret each internal node as a family of post-selected parity measurements. for n, node_type in graph.nodes('value'): if node_type.kind in 'XZ': # Surround X type node with Hadamards so it can be handled as if it were Z type. if node_type.kind == 'X': for neighbor in graph.neighbors(n): sim.h(qubit_ids[(neighbor, n)]) elif node_type.kind == 'H': # Hadamard one input so the H node can be handled as if it were Z type. neighbor, _ = graph.neighbors(n) sim.h(qubit_ids[(neighbor, n)]) elif node_type.kind in ['out', 'in']: continue # Don't measure qubits leaving the system. else: raise ValueError(f"Unknown node type {node_type!r}") # Handle Z type node. # - Postselects the ZZ observable over each pair of incoming qubits. # - Postselects the (S**quarter_turns X S**-quarter_turns)XX..X observable over all incoming qubits. neighbors = [n2 for n2 in graph.neighbors(n) if n2 != n] center = qubit_ids[(neighbors[0], n)] # Pick one incoming qubit to be the common control for the others. # Handle node angle using a phasing operation. [id, sim.s, sim.z, sim.s_dag][node_type.quarter_turns](center) # Use multi-target CNOT and Hadamard to transform postselected observables into single-qubit Z observables. for n2 in neighbors[1:]: sim.cnot(center, qubit_ids[(n2, n)]) sim.h(center) # Postselect the observables. for n2 in neighbors: _pseudo_postselect(sim, qubit_ids[(n2, n)]) # Find output qubits. in_nodes = sorted(n for n, value in graph.nodes('value') if value.kind == 'in') out_nodes = sorted(n for n, value in graph.nodes('value') if value.kind == 'out') ext_nodes = in_nodes + out_nodes out_qubits = [] for out in ext_nodes: (neighbor,) = graph.neighbors(out) out_qubits.append(qubit_ids[(neighbor, out)]) # Remove qubits corresponding to non-external edges. for i, q in enumerate(out_qubits): sim.swap(q, len(qubit_ids) + i) for i, q in enumerate(out_qubits): sim.swap(i, len(qubit_ids) + i) sim.set_num_qubits(len(out_qubits)) # Stabilizers of the simulator state are the external stabilizers of the graph. dual_stabilizers = sim.canonical_stabilizers() return ExternalStabilizer.canonicals_from_duals(dual_stabilizers, len(in_nodes)) def _pseudo_postselect(sim: stim.TableauSimulator, target: int): """Pretend to postselect by using classical feedback to consistently get into the measurement-was-false state.""" measurement_result, kickback = sim.measure_kickback(target) if kickback is not None: for qubit, pauli in enumerate(kickback): feedback_op = [None, sim.cnot, sim.cy, sim.cz][pauli] if feedback_op is not None: feedback_op(stim.target_rec(-1), qubit) assert kickback is not None or not measurement_result, "Impossible postselection. Graph contained a contradiction." ================================================ FILE: glue/lattice_surgery/stimzx/_zx_graph_solver_test.py ================================================ from typing import List import stim from ._zx_graph_solver import zx_graph_to_external_stabilizers, text_diagram_to_zx_graph, ExternalStabilizer def test_disconnected(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X X---out """)) == [ ExternalStabilizer(input=stim.PauliString("Z"), output=stim.PauliString("_")), ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), ] assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z---out | X """)) == [ ExternalStabilizer(input=stim.PauliString("Z"), output=stim.PauliString("_")), ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), ] assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z---X---out | | *---* """)) == [ ExternalStabilizer(input=stim.PauliString("X"), output=stim.PauliString("_")), ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), ] def test_cnot(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X---out | in---Z---out """)) == external_stabilizers_of_circuit(stim.Circuit("CNOT 1 0")) assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z---out | in---X---out """)) == external_stabilizers_of_circuit(stim.Circuit("CNOT 0 1")) def test_cz(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z---out | H | in---Z---out """)) == external_stabilizers_of_circuit(stim.Circuit("CZ 0 1")) def test_s(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z(pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("S 0")) def test_s_dag(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z(-pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("S_DAG 0")) def test_sqrt_x(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X(pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("SQRT_X 0")) def test_sqrt_x_sqrt_x(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X(pi/2)---X(pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("X 0")) def test_sqrt_z_sqrt_z(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z(pi/2)---Z(pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("Z 0")) def test_sqrt_x_dag(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X(-pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("SQRT_X_DAG 0")) def test_x(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X(pi)---out """)) == external_stabilizers_of_circuit(stim.Circuit("X 0")) def test_z(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z(pi)---out """)) == external_stabilizers_of_circuit(stim.Circuit("Z 0")) def test_id(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X---Z---out """)) == external_stabilizers_of_circuit(stim.Circuit("I 0")) def test_s_state_distill(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(r""" * *---------------Z--------------------Z-------Z(pi/2) / \ | | | *-----* *------------Z---+---------------+---Z----------------+-------Z(pi/2) | | | | | | X---X---Z(pi/2) X---X---Z(pi/2) X---X---Z(pi/2) X---X---Z(pi/2) | | | | | | *---+------------------Z-------------------+--------------------+---Z---Z(pi/2) | | | in-------Z--------------------------------------Z-------------------Z(pi)--------out """)) == external_stabilizers_of_circuit(stim.Circuit("S 0")) def external_stabilizers_of_circuit(circuit: stim.Circuit) -> List[ExternalStabilizer]: n = circuit.num_qubits s = stim.TableauSimulator() s.do(circuit) t = s.current_inverse_tableau()**-1 stabilizers = [] for k in range(n): p = [0] * n p[k] = 1 stabilizers.append(stim.PauliString(p) + t.x_output(k)) p[k] = 3 stabilizers.append(stim.PauliString(p) + t.z_output(k)) return [ExternalStabilizer.from_dual(e, circuit.num_qubits) for e in stabilizers] ================================================ FILE: glue/python/README.md ================================================ # Stim Stim is a fast simulator for quantum stabilizer circuits. API references are available on the stim github wiki: https://github.com/quantumlib/stim/wiki Stim can be installed into a python 3 environment using pip: ```bash pip install stim ``` Once stim is installed, you can `import stim` and use it. There are three supported use cases: 1. Interactive simulation with `stim.TableauSimulator`. 2. High speed sampling with samplers compiled from `stim.Circuit`. 3. Independent exploration using `stim.Tableau` and `stim.PauliString`. ## Interactive Simulation Use `stim.TableauSimulator` to simulate operations one by one while inspecting the results: ```python import stim s = stim.TableauSimulator() # Create a GHZ state. s.h(0) s.cnot(0, 1) s.cnot(0, 2) # Look at the simulator state re-inverted to be forwards: t = s.current_inverse_tableau() print(t**-1) # prints: # +-xz-xz-xz- # | ++ ++ ++ # | ZX _Z _Z # | _X XZ __ # | _X __ XZ # Measure the GHZ state. print(s.measure_many(0, 1, 2)) # prints one of: # [True, True, True] # or: # [False, False, False] ``` ## High Speed Sampling By creating a `stim.Circuit` and compiling it into a sampler, samples can be generated very quickly: ```python import stim # Create a circuit that measures a large GHZ state. c = stim.Circuit() c.append("H", [0]) for k in range(1, 30): c.append("CNOT", [0, k]) c.append("M", range(30)) # Compile the circuit into a high performance sampler. sampler = c.compile_sampler() # Collect a batch of samples. # Note: the ideal batch size, in terms of speed per sample, is roughly 1024. # Smaller batches are slower because they are not sufficiently vectorized. # Bigger batches are slower because they use more memory. batch = sampler.sample(1024) print(type(batch)) # numpy.ndarray print(batch.dtype) # numpy.uint8 print(batch.shape) # (1024, 30) print(batch) # Prints something like: # [[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] # [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] # [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] # ... # [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] # [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] # [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] # [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]] ``` This also works on circuits that include noise: ```python import stim import numpy as np c = stim.Circuit(""" X_ERROR(0.1) 0 Y_ERROR(0.2) 1 Z_ERROR(0.3) 2 DEPOLARIZE1(0.4) 3 DEPOLARIZE2(0.5) 4 5 M 0 1 2 3 4 5 """) batch = c.compile_sampler().sample(2**20) print(np.mean(batch, axis=0).round(3)) # Prints something like: # [0.1 0.2 0. 0.267 0.267 0.266] ``` You can also sample annotated detection events using `stim.Circuit.compile_detector_sampler`. For a list of gates that can appear in a `stim.Circuit`, see the [latest readme on github](https://github.com/quantumlib/Stim#readme). ## Independent Exploration Stim provides data types `stim.PauliString` and `stim.Tableau`, which support a variety of fast operations. ```python import stim xx = stim.PauliString("XX") yy = stim.PauliString("YY") assert xx * yy == -stim.PauliString("ZZ") s = stim.Tableau.from_named_gate("S") print(repr(s)) # prints: # stim.Tableau.from_conjugated_generators( # xs=[ # stim.PauliString("+Y"), # ], # zs=[ # stim.PauliString("+Z"), # ], # ) s_dag = stim.Tableau.from_named_gate("S_DAG") assert s**-1 == s_dag assert s**1000000003 == s_dag cnot = stim.Tableau.from_named_gate("CNOT") cz = stim.Tableau.from_named_gate("CZ") h = stim.Tableau.from_named_gate("H") t = stim.Tableau(5) t.append(cnot, [1, 4]) t.append(h, [4]) t.append(cz, [1, 4]) t.prepend(h, [4]) assert t == stim.Tableau(5) ``` ================================================ FILE: glue/python/src/stim/__init__.py ================================================ """Stim: a fast quantum stabilizer circuit library.""" # This is the entrypoint when running `import stim`. # # It does runtime detection of CPU features, and based on that imports the fastest pre-built C++ extension that only uses # compatible instructions. Importing a different one can result in runtime segfaults that crash the python interpreter. import stim._detect_machine_architecture as _tmp _tmp = _tmp._UNSTABLE_detect_march() try: # NOTE: avx2 disabled until https://github.com/quantumlib/Stim/issues/432 is fixed # if _tmp == 'avx2': # from stim._stim_avx2 import _UNSTABLE_raw_format_data, __version__ # from stim._stim_avx2 import * if _tmp == 'avx2' or _tmp == 'sse2': from stim._stim_sse2 import _UNSTABLE_raw_format_data, __version__ from stim._stim_sse2 import * else: from stim._stim_polyfill import _UNSTABLE_raw_format_data, __version__ from stim._stim_polyfill import * except ModuleNotFoundError: from stim._stim_polyfill import _UNSTABLE_raw_format_data, __version__ from stim._stim_polyfill import * del _tmp def _pytest_pycharm_pybind_repr_bug_workaround(cls): f = cls.__repr__ cls.__repr__ = lambda e: f(e) cls.__repr__.__doc__ = f.__doc__ _pytest_pycharm_pybind_repr_bug_workaround(Circuit) _pytest_pycharm_pybind_repr_bug_workaround(CircuitErrorLocation) _pytest_pycharm_pybind_repr_bug_workaround(CircuitErrorLocationStackFrame) _pytest_pycharm_pybind_repr_bug_workaround(CircuitInstruction) _pytest_pycharm_pybind_repr_bug_workaround(CircuitRepeatBlock) _pytest_pycharm_pybind_repr_bug_workaround(CircuitTargetsInsideInstruction) _pytest_pycharm_pybind_repr_bug_workaround(CompiledDemSampler) _pytest_pycharm_pybind_repr_bug_workaround(CompiledDetectorSampler) _pytest_pycharm_pybind_repr_bug_workaround(CompiledMeasurementSampler) _pytest_pycharm_pybind_repr_bug_workaround(CompiledMeasurementsToDetectionEventsConverter) _pytest_pycharm_pybind_repr_bug_workaround(DemInstruction) _pytest_pycharm_pybind_repr_bug_workaround(DemRepeatBlock) _pytest_pycharm_pybind_repr_bug_workaround(DemTarget) _pytest_pycharm_pybind_repr_bug_workaround(DemTargetWithCoords) _pytest_pycharm_pybind_repr_bug_workaround(DetectorErrorModel) _pytest_pycharm_pybind_repr_bug_workaround(ExplainedError) _pytest_pycharm_pybind_repr_bug_workaround(FlipSimulator) _pytest_pycharm_pybind_repr_bug_workaround(FlippedMeasurement) _pytest_pycharm_pybind_repr_bug_workaround(Flow) _pytest_pycharm_pybind_repr_bug_workaround(GateData) _pytest_pycharm_pybind_repr_bug_workaround(GateTarget) _pytest_pycharm_pybind_repr_bug_workaround(GateTargetWithCoords) _pytest_pycharm_pybind_repr_bug_workaround(PauliString) _pytest_pycharm_pybind_repr_bug_workaround(PauliStringIterator) _pytest_pycharm_pybind_repr_bug_workaround(Tableau) _pytest_pycharm_pybind_repr_bug_workaround(TableauIterator) _pytest_pycharm_pybind_repr_bug_workaround(TableauSimulator) del _pytest_pycharm_pybind_repr_bug_workaround ================================================ FILE: glue/python/src/stim/__init__.pyi ================================================ """Stim (Development Version): a fast quantum stabilizer circuit library.""" # (This is a stubs file describing the classes and methods in stim.) from __future__ import annotations from typing import overload, TYPE_CHECKING, List, Dict, Tuple, Any, Union, Iterable, Optional, Sequence, Literal if TYPE_CHECKING: import io import pathlib import numpy as np import stim class Circuit: """A mutable stabilizer circuit. The stim.Circuit class is arguably the most important object in the entire library. It is the interface through which you explain a noisy quantum computation to Stim, in order to do fast bulk sampling or fast error analysis. For example, suppose you want to use a matching-based decoder on a new quantum error correction construction. Stim can help you do this but the very first step is to create a circuit implementing the construction. Once you have the circuit you can then use methods like stim.Circuit.detector_error_model() to create an object that can be used to configure the decoder, or like stim.Circuit.compile_detector_sampler() to produce problems for the decoder to solve, or like stim.Circuit.shortest_graphlike_error() to check for mistakes in the implementation of the code. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append("X", 0) >>> c.append("M", 0) >>> c.compile_sampler().sample(shots=1) array([[ True]]) >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''').compile_detector_sampler().sample(shots=1) array([[False]]) """ def __add__( self, second: stim.Circuit, ) -> stim.Circuit: """Creates a circuit by appending two circuits. Examples: >>> import stim >>> c1 = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c2 = stim.Circuit(''' ... M 0 1 2 ... ''') >>> c1 + c2 stim.Circuit(''' X 0 Y 1 2 M 0 1 2 ''') """ def __eq__( self, arg0: stim.Circuit, ) -> bool: """Determines if two circuits have identical contents. """ @overload def __getitem__( self, index_or_slice: int, ) -> Union[stim.CircuitInstruction, stim.CircuitRepeatBlock]: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.Circuit: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns copies of instructions from the circuit. Args: index_or_slice: An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a circuit. Returns: If the index was an integer, then an instruction from the circuit. If the index was a slice, then a circuit made up of the instructions in that slice. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X 0 ... X_ERROR(0.5) 2 ... REPEAT 100 { ... X 0 ... Y 1 2 ... } ... TICK ... M 0 ... DETECTOR rec[-1] ... ''') >>> circuit[1] stim.CircuitInstruction('X_ERROR', [stim.GateTarget(2)], [0.5]) >>> circuit[2] stim.CircuitRepeatBlock(100, stim.Circuit(''' X 0 Y 1 2 ''')) >>> circuit[1::2] stim.Circuit(''' X_ERROR(0.5) 2 TICK DETECTOR rec[-1] ''') """ def __iadd__( self, second: stim.Circuit, ) -> stim.Circuit: """Appends a circuit into the receiving circuit (mutating it). Examples: >>> import stim >>> c1 = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c2 = stim.Circuit(''' ... M 0 1 2 ... ''') >>> c1 += c2 >>> print(repr(c1)) stim.Circuit(''' X 0 Y 1 2 M 0 1 2 ''') """ def __imul__( self, repetitions: int, ) -> stim.Circuit: """Mutates the circuit by putting its contents into a REPEAT block. Special case: if the repetition count is 0, the circuit is cleared. Special case: if the repetition count is 1, nothing happens. Args: repetitions: The number of times the REPEAT block should repeat. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c *= 3 >>> print(repr(c)) stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ def __init__( self, stim_program_text: str = '', ) -> None: """Creates a stim.Circuit. Args: stim_program_text: Defaults to empty. Describes operations to append into the circuit. Examples: >>> import stim >>> empty = stim.Circuit() >>> not_empty = stim.Circuit(''' ... X 0 ... CNOT 0 1 ... M 1 ... ''') """ def __len__( self, ) -> int: """Returns the number of top-level instructions and blocks in the circuit. Instructions inside of blocks are not included in this count. Examples: >>> import stim >>> len(stim.Circuit()) 0 >>> len(stim.Circuit(''' ... X 0 ... X_ERROR(0.5) 1 2 ... TICK ... M 0 ... DETECTOR rec[-1] ... ''')) 5 >>> len(stim.Circuit(''' ... REPEAT 100 { ... X 0 ... Y 1 2 ... } ... ''')) 1 """ def __mul__( self, repetitions: int, ) -> stim.Circuit: """Repeats the circuit using a REPEAT block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the REPEAT block should repeat. Returns: repetitions=0: An empty circuit. repetitions=1: A copy of this circuit. repetitions>=2: A circuit with a single REPEAT block, where the contents of that repeat block are this circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c * 3 stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ def __ne__( self, arg0: stim.Circuit, ) -> bool: """Determines if two circuits have non-identical contents. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.Circuit`. """ def __rmul__( self, repetitions: int, ) -> stim.Circuit: """Repeats the circuit using a REPEAT block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the REPEAT block should repeat. Returns: repetitions=0: An empty circuit. repetitions=1: A copy of this circuit. repetitions>=2: A circuit with a single REPEAT block, where the contents of that repeat block are this circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> 3 * c stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') """ def __str__( self, ) -> str: """Returns stim instructions (that can be saved to a file and parsed by stim) for the current circuit. """ @overload def append( self, name: str, targets: Union[int, stim.GateTarget, stim.PauliString, Iterable[Union[int, stim.GateTarget, stim.PauliString]]], arg: Union[float, Iterable[float], None] = None, *, tag: str = "", ) -> None: pass @overload def append( self, name: Union[stim.CircuitInstruction, stim.CircuitRepeatBlock, stim.Circuit], ) -> None: pass def append( self, name: object, targets: object = (), arg: object = None, *, tag: str = '', ) -> None: """Appends an operation into the circuit. Note: `stim.Circuit.append_operation` is an alias of `stim.Circuit.append`. Args: name: The name of the operation's gate (e.g. "H" or "M" or "CNOT"). This argument can also be set to a `stim.CircuitInstruction` or `stim.CircuitInstructionBlock`, which results in the instruction or block being appended to the circuit. The other arguments (targets and arg) can't be specified when doing so. (The argument being called `name` is no longer quite right, but is being kept for backwards compatibility.) targets: The objects operated on by the gate. This can be either a single target or an iterable of multiple targets. Each target can be: An int: The index of a targeted qubit. A `stim.GateTarget`: Could be a variety of things. Methods like `stim.target_rec`, `stim.target_sweet`, `stim.target_x`, and `stim.CircuitInstruction.__getitem__` all return this type. A `stim.PauliString`: This will automatically be expanded into a product of pauli targets like `X1*Y2*Z3`. arg: The "parens arguments" for the gate, such as the probability for a noise operation. A double or list of doubles parameterizing the gate. Different gates take different parens arguments. For example, X_ERROR takes a probability, OBSERVABLE_INCLUDE takes an observable index, and PAULI_CHANNEL_1 takes three disjoint probabilities. Note: Defaults to no parens arguments. Except, for backwards compatibility reasons, `cirq.append_operation` (but not `cirq.append`) will default to a single 0.0 argument for gates that take exactly one argument. tag: A customizable string attached to the instruction. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append("X", 0) >>> c.append("H", [0, 1]) >>> c.append("M", [0, stim.target_inv(1)]) >>> c.append("CNOT", [stim.target_rec(-1), 0]) >>> c.append("X_ERROR", [0], 0.125) >>> c.append("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(2)], 0.25) >>> c.append("MPP", [stim.PauliString("X1*Y2"), stim.GateTarget("Z3")]) >>> print(repr(c)) stim.Circuit(''' X 0 H 0 1 M 0 !1 CX rec[-1] 0 X_ERROR(0.125) 0 E(0.25) X0 Y2 MPP X1*Y2 Z3 ''') """ def append_from_stim_program_text( self, stim_program_text: str, ) -> None: """Appends operations described by a STIM format program into the circuit. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append_from_stim_program_text(''' ... H 0 # comment ... CNOT 0 2 ... ... M 2 ... CNOT rec[-1] 1 ... ''') >>> print(c) H 0 CX 0 2 M 2 CX rec[-1] 1 Args: stim_program_text: The STIM program text containing the circuit operations to append. """ def append_operation( self, name: object, targets: object = (), arg: object = None, *, tag: str = '', ) -> None: """[DEPRECATED] use stim.Circuit.append instead """ def approx_equals( self, other: object, *, atol: float, ) -> bool: """Checks if a circuit is approximately equal to another circuit. Two circuits are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example, `X_ERROR(0.100) 0` is approximately equal to `X_ERROR(0.099)` within an absolute tolerance of 0.002. All other details of the circuits (such as the ordering of instructions and targets) must be exactly the same. Args: other: The circuit, or other object, to compare to this one. atol: The absolute error tolerance. The maximum amount each probability may have been perturbed by. Returns: True if the given object is a circuit approximately equal up to the receiving circuit up to the given tolerance, otherwise False. Examples: >>> import stim >>> base = stim.Circuit(''' ... X_ERROR(0.099) 0 1 2 ... M 0 1 2 ... ''') >>> base.approx_equals(base, atol=0) True >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0) False >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0.0001) False >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0.01) True >>> base.approx_equals(stim.Circuit(''' ... DEPOLARIZE1(0.099) 0 1 2 ... MRX 0 1 2 ... '''), atol=9999) False """ def clear( self, ) -> None: """Clears the contents of the circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c.clear() >>> c stim.Circuit() """ def compile_detector_sampler( self, *, seed: object = None, ) -> stim.CompiledDetectorSampler: """Returns an object that can batch sample detection events from the circuit. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[False]]) """ def compile_m2d_converter( self, *, skip_reference_sample: bool = False, ) -> stim.CompiledMeasurementsToDetectionEventsConverter: """Creates a measurement-to-detection-event converter for the given circuit. The converter can efficiently compute detection events and observable flips from raw measurement data. The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is. Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit. Args: skip_reference_sample: Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution). Returns: An initialized stim.CompiledMeasurementsToDetectionEventsConverter. Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> converter.convert( ... measurements=np.array([[0], [1]], dtype=np.bool_), ... append_observables=False, ... ) array([[ True], [False]]) """ def compile_sampler( self, *, skip_reference_sample: bool = False, seed: Optional[int] = None, reference_sample: Optional[np.ndarray] = None, ) -> stim.CompiledMeasurementSampler: """Returns an object that can quickly batch sample measurements from the circuit. Args: skip_reference_sample: Defaults to False. When set to True, the reference sample used by the sampler is initialized to all-zeroes instead of being collected from the circuit. This means that the results returned by the sampler are actually whether or not each measurement was *flipped*, instead of true measurement results. Forcing an all-zero reference sample is useful when you are only interested in error propagation and don't want to have to deal with the fact that some measurements want to be On when no errors occur. It is also useful when you know for sure that the all-zero result is actually a possible result from the circuit (under noiseless execution), meaning it is a valid reference sample as good as any other. Computing the reference sample is the most time consuming and memory intensive part of simulating the circuit, so promising that the simulator can safely skip that step is an effective optimization. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. reference_sample: The data to xor into the measurement flips produced by the frame simulator, in order to produce proper measurement results. This can either be specified as an `np.bool_` array or a bit packed `np.uint8` array (little endian). Under normal conditions, the reference sample should be a valid noiseless sample of the circuit, such as the one returned by `circuit.reference_sample()`. If this argument is not provided, the reference sample will be set to `circuit.reference_sample()`, unless `skip_reference_sample=True` is used, in which case it will be set to all-zeros. Raises: ValueError: skip_reference_sample is True and reference_sample is not None. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 2 ... M 0 1 2 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[False, False, True]]) """ def copy( self, ) -> stim.Circuit: """Returns a copy of the circuit. An independent circuit with the same contents. Examples: >>> import stim >>> c1 = stim.Circuit("H 0") >>> c2 = c1.copy() >>> c2 is c1 False >>> c2 == c1 True """ def count_determined_measurements( self, *, unknown_input: bool = False, ) -> int: """Counts the number of predictable measurements in the circuit. This method ignores any noise in the circuit. This method works by performing a tableau stabilizer simulation of the circuit and, before each measurement is simulated, checking if its expectation is non-zero. A measurement is predictable if its result can be predicted by using other measurements that have already been performed, assuming the circuit is executed without any noise. Note that, when multiple measurements occur at the same time, re-ordering the order they are resolved can change which specific measurements are predictable but won't change how many of them were predictable in total. The number of predictable measurements is a useful quantity because it's related to the number of detectors and observables that a circuit should declare. If circuit.num_detectors + circuit.num_observables is less than circuit.count_determined_measurements(), this is a warning sign that you've missed some detector declarations. The exact relationship between the number of determined measurements and the number of detectors and observables can differ from code to code. For example, the toric code has an extra redundant measurement compared to the surface code because in the toric code the last X stabilizer to be measured is equal to the product of all other X stabilizers even in the first round when initializing in the Z basis. Typically this relationship is not declared as a detector, because it's not local, or as an observable, because it doesn't store a qubit. Args: unknown_input: Defaults to False (inputs assumed to be in the |0> state). When set to True, the inputs are instead treated as being in unknown random states. For example, this means that Z-basis measurements at the very beginning of the circuit will be considered random rather than determined. Returns: The number of measurements that were predictable. Examples: >>> import stim >>> stim.Circuit(''' ... R 0 ... M 0 ... ''').count_determined_measurements() 1 >>> stim.Circuit(''' ... R 0 ... H 0 ... M 0 ... ''').count_determined_measurements() 0 >>> stim.Circuit(''' ... M 0 ... ''').count_determined_measurements() 1 >>> stim.Circuit(''' ... M 0 ... ''').count_determined_measurements(unknown_input=True) 0 >>> stim.Circuit(''' ... M 0 ... M 0 1 ... M 0 1 2 ... M 0 1 2 3 ... ''').count_determined_measurements(unknown_input=True) 6 >>> stim.Circuit(''' ... R 0 1 ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... ''').count_determined_measurements() 2 >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... distance=5, ... rounds=9, ... ) >>> circuit.count_determined_measurements() 217 >>> circuit.num_detectors + circuit.num_observables 217 """ def decomposed( self, ) -> stim.Circuit: """Recreates the circuit using (mostly) the {H,S,CX,M,R} gate set. The intent of this method is to simplify the circuit to use fewer gate types, so it's easier for other tools to consume. Currently, this method performs the following simplifications: - Single qubit cliffords are decomposed into {H,S}. - Multi-qubit cliffords are decomposed into {H,S,CX}. - Single qubit dissipative gates are decomposed into {H,S,M,R}. - Multi-qubit dissipative gates are decomposed into {H,S,CX,M,R}. Currently, the following types of gate *aren't* simplified, but they may be in the future: - Noise instructions (like X_ERROR, DEPOLARIZE2, and E). - Annotations (like TICK, DETECTOR, and SHIFT_COORDS). - The MPAD instruction. - Repeat blocks are not flattened. Returns: A `stim.Circuit` whose function is equivalent to the original circuit, but with most gates decomposed into the {H,S,CX,M,R} gate set. Examples: >>> import stim >>> stim.Circuit(''' ... SWAP 0 1 ... ''').decomposed() stim.Circuit(''' CX 0 1 1 0 0 1 ''') >>> stim.Circuit(''' ... ISWAP 0 1 2 1 ... TICK ... MPP !X1*Y2*Z3 ... ''').decomposed() stim.Circuit(''' H 0 CX 0 1 1 0 H 1 S 1 0 H 2 CX 2 1 1 2 H 1 S 1 2 TICK H 1 2 S 2 H 2 S 2 2 CX 2 1 3 1 M !1 CX 2 1 3 1 H 2 S 2 H 2 S 2 2 H 1 ''') """ def detecting_regions( self, *, targets: Optional[Iterable[stim.DemTarget | str | Iterable[float]]] = None, ticks: Optional[Iterable[int]] = None, ) -> Dict[stim.DemTarget, Dict[int, stim.PauliString]]: """Records where detectors and observables are sensitive to errors over time. The result of this method is a nested dictionary, mapping detectors/observables and ticks to Pauli sensitivities for that detector/observable at that time. For example, if observable 2 has Z-type sensitivity on qubits 5 and 6 during tick 3, then `result[stim.target_logical_observable_id(2)][3]` will be equal to `stim.PauliString("Z5*Z6")`. If you want sensitivities from more places in the circuit, besides just at the TICK instructions, you can work around this by making a version of the circuit with more TICKs. Args: targets: Defaults to everything (None). When specified, this should be an iterable of filters where items matching any one filter are included. A variety of filters are supported: stim.DemTarget: Includes the targeted detector or observable. Iterable[float]: Coordinate prefix match. Includes detectors whose coordinate data begins with the same floats. "D": Includes all detectors. "L": Includes all observables. "D#" (e.g. "D5"): Includes the detector with the specified index. "L#" (e.g. "L5"): Includes the observable with the specified index. ticks: Defaults to everything (None). When specified, this should be a list of integers corresponding to the tick indices to report sensitivities for. ignore_anticommutation_errors: Defaults to False. When set to False, invalid detecting regions that anticommute with a reset will cause the method to raise an exception. When set to True, the offending component will simply be silently dropped. This can result in broken detectors having apparently enormous detecting regions. Returns: Nested dictionaries keyed first by a `stim.DemTarget` identifying the detector or observable, then by the index of the tick, leading to a PauliString with that target's error sensitivity at that tick. Note you can use `stim.PauliString.pauli_indices` to quickly get to the non-identity terms in the sensitivity. Examples: >>> import stim >>> detecting_regions = stim.Circuit(''' ... R 0 ... TICK ... H 0 ... TICK ... CX 0 1 ... TICK ... MX 0 1 ... DETECTOR rec[-1] rec[-2] ... ''').detecting_regions() >>> for target, tick_regions in detecting_regions.items(): ... print("target", target) ... for tick, sensitivity in tick_regions.items(): ... print(" tick", tick, "=", sensitivity) target D0 tick 0 = +Z_ tick 1 = +X_ tick 2 = +XX >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... rounds=5, ... distance=4, ... ) >>> detecting_regions = circuit.detecting_regions( ... targets=["L0", (2, 4), stim.DemTarget.relative_detector_id(5)], ... ticks=range(5, 15), ... ) >>> for target, tick_regions in detecting_regions.items(): ... print("target", target) ... for tick, sensitivity in tick_regions.items(): ... print(" tick", tick, "=", sensitivity) target D1 tick 5 = +____________________X______________________ tick 6 = +____________________Z______________________ target D5 tick 5 = +______X____________________________________ tick 6 = +______Z____________________________________ target D14 tick 5 = +__________X_X______XXX_____________________ tick 6 = +__________X_X______XZX_____________________ tick 7 = +__________X_X______XZX_____________________ tick 8 = +__________X_X______XXX_____________________ tick 9 = +__________XXX_____XXX______________________ tick 10 = +__________XXX_______X______________________ tick 11 = +__________X_________X______________________ tick 12 = +____________________X______________________ tick 13 = +____________________Z______________________ target D29 tick 7 = +____________________Z______________________ tick 8 = +____________________X______________________ tick 9 = +____________________XX_____________________ tick 10 = +___________________XXX_______X_____________ tick 11 = +____________X______XXXX______X_____________ tick 12 = +__________X_X______XXX_____________________ tick 13 = +__________X_X______XZX_____________________ tick 14 = +__________X_X______XZX_____________________ target D44 tick 14 = +____________________Z______________________ target L0 tick 5 = +_X________X________X________X______________ tick 6 = +_X________X________X________X______________ tick 7 = +_X________X________X________X______________ tick 8 = +_X________X________X________X______________ tick 9 = +_X________X_______XX________X______________ tick 10 = +_X________X________X________X______________ tick 11 = +_X________XX_______X________XX_____________ tick 12 = +_X________X________X________X______________ tick 13 = +_X________X________X________X______________ tick 14 = +_X________X________X________X______________ """ def detector_error_model( self, *, decompose_errors: bool = False, flatten_loops: bool = False, allow_gauge_detectors: bool = False, approximate_disjoint_errors: float = False, ignore_decomposition_failures: bool = False, block_decomposition_from_introducing_remnant_edges: bool = False, ) -> stim.DetectorErrorModel: """Returns a stim.DetectorErrorModel describing the error processes in the circuit. Args: decompose_errors: Defaults to false. When set to true, the error analysis attempts to decompose the components of composite error mechanisms (such as depolarization errors) into simpler errors, and suggest this decomposition via `stim.target_separator()` between the components. For example, in an XZ surface code, single qubit depolarization has a Y error term which can be decomposed into simpler X and Z error terms. Decomposition fails (causing this method to throw) if it's not possible to decompose large errors into simple errors that affect at most two detectors. flatten_loops: Defaults to false. When set to True, the output will not contain any `repeat` blocks. When set to False, the error analysis watches for loops in the circuit reaching a periodic steady state with respect to the detectors being introduced, the error mechanisms that affect them, and the locations of the logical observables. When it identifies such a steady state, it outputs a repeat block. This is massively more efficient than flattening for circuits that contain loops, but creates a more complex output. allow_gauge_detectors: Defaults to false. When set to false, the error analysis verifies that detectors in the circuit are actually deterministic under noiseless execution of the circuit. When set to True, these detectors are instead considered to be part of degrees freedom that can be removed from the error model. For example, if detectors D1 and D3 both anti-commute with a reset, then the error model has a gauge `error(0.5) D1 D3`. When gauges are identified, one of the involved detectors is removed from the system using Gaussian elimination. Note that logical observables are still verified to be deterministic, even if this option is set. approximate_disjoint_errors: Defaults to false. When set to false, composite error mechanisms with disjoint components (such as `PAULI_CHANNEL_1(0.1, 0.2, 0.0)`) can cause the error analysis to throw exceptions (because detector error models can only contain independent error mechanisms). When set to true, the probabilities of the disjoint cases are instead assumed to be independent probabilities. For example, a `PAULI_CHANNEL_1(0.1, 0.2, 0.0)` becomes equivalent to an `X_ERROR(0.1)` followed by a `Y_ERROR(0.2)`. This assumption is an approximation, but it is a good approximation for small probabilities. This argument can also be set to a probability between 0 and 1, setting a threshold below which the approximation is acceptable. Any error mechanisms that have a component probability above the threshold will cause an exception to be thrown. ignore_decomposition_failures: Defaults to False. When this is set to True, circuit errors that fail to decompose into graphlike detector error model errors no longer cause the conversion process to abort. Instead, the undecomposed error is inserted into the output. Whatever tool the detector error model is then given to is responsible for dealing with the undecomposed errors (e.g. a tool may choose to simply ignore them). Irrelevant unless decompose_errors=True. block_decomposition_from_introducing_remnant_edges: Defaults to False. Requires that both A B and C D be present elsewhere in the detector error model in order to decompose A B C D into A B ^ C D. Normally, only one of A B or C D needs to appear to allow this decomposition. Remnant edges can be a useful feature for ensuring decomposition succeeds, but they can also reduce the effective code distance by giving the decoder single edges that actually represent multiple errors in the circuit (resulting in the decoder making misinformed choices when decoding). Irrelevant unless decompose_errors=True. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model() stim.DetectorErrorModel(''' error(0.125) D0 error(0.375) D0 D1 error(0.25) D1 ''') """ def diagram( self, type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "interactive", "interactive-html"] = 'timeline-text', *, tick: Union[None, int, range] = None, filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), rows: int | None = None, ) -> 'stim._DiagramHelper': """Returns a diagram of the circuit, from a variety of options. Args: type: The type of diagram. Available types are: "timeline-text" (default): An ASCII diagram of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-svg": An SVG image of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-svg-html": A resizable SVG image viewer of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-3d": A 3d model, in GLTF format, of the operations applied by the circuit over time. "timeline-3d-html": Same 3d model as 'timeline-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. "detslice-text": An ASCII diagram of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. "detslice-svg": An SVG image of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. For example, a detector slice diagram of a CSS surface code circuit during the TICK between a measurement layer and a reset layer will produce the usual diagram of a surface code. Uses the Pauli color convention XYZ=RGB. "detslice-svg-html": Same as detslice-svg but the SVG image is inside a resizable HTML iframe. "matchgraph-svg": An SVG image of the match graph extracted from the circuit by stim.Circuit.detector_error_model. "matchgraph-svg-html": Same as matchgraph-svg but the SVG image is inside a resizable HTML iframe. "matchgraph-3d": An 3D model of the match graph extracted from the circuit by stim.Circuit.detector_error_model. "matchgraph-3d-html": Same 3d model as 'match-graph-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. "timeslice-svg": An SVG image of the operations applied between two TICK instructions in the circuit, with the operations laid out in 2d. "timeslice-svg-html": Same as timeslice-svg but the SVG image is inside a resizable HTML iframe. "detslice-with-ops-svg": A combination of timeslice-svg and detslice-svg, with the operations overlaid over the detector slices taken from the TICK after the operations were applied. "detslice-with-ops-svg-html": Same as detslice-with-ops-svg but the SVG image is inside a resizable HTML iframe. "interactive" or "interactive-html": An HTML web page containing Crumble (an interactive editor for 2D stabilizer circuits) initialized with the given circuit as its default contents. tick: Required for detector and time slice diagrams. Specifies which TICK instruction, or range of TICK instructions, to slice at. Note that the first TICK instruction in the circuit corresponds tick=1. The value tick=0 refers to the very start of the circuit. Passing `range(A, B)` for a detector slice will show the slices for ticks A through B including A but excluding B. Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. rows: In diagrams that have multiple separate pieces, such as timeslice diagrams and detslice diagrams, this controls how many rows of pieces there will be. If not specified, a number of rows that creates a roughly square layout will be chosen. filter_coords: A list of things to include in the diagram. Different effects depending on the diagram. For detslice diagrams, the filter defaults to showing all detectors and no observables. When specified, each list entry can be a collection of floats (detectors whose coordinates start with the same numbers will be included), a stim.DemTarget (specifying a detector or observable to include), a string like "D5" or "L0" specifying a detector or observable to include. Returns: An object whose `__str__` method returns the diagram, so that writing the diagram to a file works correctly. The returned object may also define methods such as `_repr_html_`, so that ipython notebooks recognize it can be shown using a specialized viewer instead of as raw text. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... CNOT 0 1 1 2 ... ''') >>> print(circuit.diagram()) q0: -H-@--- | q1: ---X-@- | q2: -----X- >>> circuit = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... TICK ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.diagram("detslice-text", tick=1)) q0: -Z:D0- | q1: -Z:D0- """ def explain_detector_error_model_errors( self, *, dem_filter: object = None, reduce_to_one_representative_error: bool = False, ) -> List[stim.ExplainedError]: """Explains how detector error model errors are produced by circuit errors. Args: dem_filter: Defaults to None (unused). When used, the output will only contain detector error model errors that appear in the given `stim.DetectorErrorModel`. Any error mechanisms from the detector error model that can't be reproduced using one error from the circuit will also be included in the result, but with an empty list of associated circuit error mechanisms. reduce_to_one_representative_error: Defaults to False. When True, the items in the result will contain at most one circuit error mechanism. Returns: A `List[stim.ExplainedError]` (see `stim.ExplainedError` for more information). Each item in the list describes how a detector error model error can be produced by individual circuit errors. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... # Create Bell pair. ... H 0 ... CNOT 0 1 ... ... # Noise. ... DEPOLARIZE1(0.01) 0 ... ... # Bell basis measurement. ... CNOT 0 1 ... H 0 ... M 0 1 ... ... # Both measurements should be False under noiseless execution. ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... ''') >>> explained_errors = circuit.explain_detector_error_model_errors( ... dem_filter=stim.DetectorErrorModel('error(1) D0 D1'), ... reduce_to_one_representative_error=True, ... ) >>> print(explained_errors[0].circuit_error_locations[0]) CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 0 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.01) 0 } """ def flattened( self, ) -> stim.Circuit: """Creates an equivalent circuit without REPEAT or SHIFT_COORDS. Returns: A `stim.Circuit` with the same instructions in the same order, but with loops flattened into repeated instructions and with all coordinate shifts inlined. Examples: >>> import stim >>> stim.Circuit(''' ... REPEAT 5 { ... MR 0 1 ... DETECTOR(0, 0) rec[-2] ... DETECTOR(1, 0) rec[-1] ... SHIFT_COORDS(0, 1) ... } ... ''').flattened() stim.Circuit(''' MR 0 1 DETECTOR(0, 0) rec[-2] DETECTOR(1, 0) rec[-1] MR 0 1 DETECTOR(0, 1) rec[-2] DETECTOR(1, 1) rec[-1] MR 0 1 DETECTOR(0, 2) rec[-2] DETECTOR(1, 2) rec[-1] MR 0 1 DETECTOR(0, 3) rec[-2] DETECTOR(1, 3) rec[-1] MR 0 1 DETECTOR(0, 4) rec[-2] DETECTOR(1, 4) rec[-1] ''') """ def flattened_operations( self, ) -> list: """[DEPRECATED] Returns a list of tuples encoding the contents of the circuit. Instead of this method, use `for instruction in circuit` or, to avoid REPEAT blocks, `for instruction in circuit.flattened()`. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... X_ERROR(0.125) 1 ... M 0 !1 ... ''').flattened_operations() [('H', [0], 0), ('X_ERROR', [1], 0.125), ('M', [0, ('inv', 1)], 0)] >>> stim.Circuit(''' ... REPEAT 2 { ... H 6 ... } ... ''').flattened_operations() [('H', [6], 0), ('H', [6], 0)] """ def flow_generators( self, ) -> List[stim.Flow]: """Returns a list of flows that generate all of the circuit's flows. Every stabilizer flow that the circuit implements is a product of some subset of the returned generators. Every returned flow will be a flow of the circuit. Returns: A list of flow generators for the circuit. Examples: >>> import stim >>> stim.Circuit("H 0").flow_generators() [stim.Flow("X -> Z"), stim.Flow("Z -> X")] >>> stim.Circuit("M 0").flow_generators() [stim.Flow("1 -> Z xor rec[0]"), stim.Flow("Z -> rec[0]")] >>> stim.Circuit("RX 0").flow_generators() [stim.Flow("1 -> X")] >>> for flow in stim.Circuit("MXX 0 1").flow_generators(): ... print(flow) 1 -> XX xor rec[0] _X -> _X X_ -> _X xor rec[0] ZZ -> ZZ >>> for flow in stim.Circuit.generated( ... "repetition_code:memory", ... rounds=2, ... distance=3, ... after_clifford_depolarization=1e-3, ... ).flow_generators(): ... print(flow) 1 -> rec[0] 1 -> rec[1] 1 -> rec[2] 1 -> rec[3] 1 -> rec[4] 1 -> rec[5] 1 -> rec[6] 1 -> ____Z 1 -> ___Z_ 1 -> __Z__ 1 -> _Z___ 1 -> Z____ """ @staticmethod def from_file( file: Union[io.TextIOBase, str, pathlib.Path], ) -> stim.Circuit: """Reads a stim circuit from a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md Args: file: A file path or open file object to read from. Returns: The circuit parsed from the file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('H 5', file=f) ... circuit = stim.Circuit.from_file(path) >>> circuit stim.Circuit(''' H 5 ''') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('CNOT 4 5', file=f) ... with open(path) as f: ... circuit = stim.Circuit.from_file(f) >>> circuit stim.Circuit(''' CX 4 5 ''') """ @staticmethod def generated( code_task: str, *, distance: int, rounds: int, after_clifford_depolarization: float = 0.0, before_round_data_depolarization: float = 0.0, before_measure_flip_probability: float = 0.0, after_reset_flip_probability: float = 0.0, ) -> stim.Circuit: """Generates common circuits. The generated circuits can include configurable noise. The generated circuits include DETECTOR and OBSERVABLE_INCLUDE annotations so that their detection events and logical observables can be sampled. The generated circuits include TICK annotations to mark the progression of time. (E.g. so that converting them using `stimcirq.stim_circuit_to_cirq_circuit` will produce a `cirq.Circuit` with the intended moment structure.) Args: code_task: A string identifying the type of circuit to generate. Available code tasks are: - "repetition_code:memory" - "surface_code:rotated_memory_x" - "surface_code:rotated_memory_z" - "surface_code:unrotated_memory_x" - "surface_code:unrotated_memory_z" - "color_code:memory_xyz" distance: The desired code distance of the generated circuit. The code distance is the minimum number of physical errors needed to cause a logical error. This parameter indirectly determines how many qubits the generated circuit uses. rounds: How many times the measurement qubits in the generated circuit will be measured. Indirectly determines the duration of the generated circuit. after_clifford_depolarization: Defaults to 0. The probability (p) of `DEPOLARIZE1(p)` operations to add after every single-qubit Clifford operation and `DEPOLARIZE2(p)` operations to add after every two-qubit Clifford operation. The after-Clifford depolarizing operations are only included if this probability is not 0. before_round_data_depolarization: Defaults to 0. The probability (p) of `DEPOLARIZE1(p)` operations to apply to every data qubit at the start of a round of stabilizer measurements. The start-of-round depolarizing operations are only included if this probability is not 0. before_measure_flip_probability: Defaults to 0. The probability (p) of `X_ERROR(p)` operations applied to qubits before each measurement (X basis measurements use `Z_ERROR(p)` instead). The before-measurement flips are only included if this probability is not 0. after_reset_flip_probability: Defaults to 0. The probability (p) of `X_ERROR(p)` operations applied to qubits after each reset (X basis resets use `Z_ERROR(p)` instead). The after-reset flips are only included if this probability is not 0. Returns: The generated circuit. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... distance=4, ... rounds=10000, ... after_clifford_depolarization=0.0125) >>> print(circuit) R 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.0125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.0125) 2 1 4 3 6 5 TICK MR 1 3 5 DETECTOR(1, 0) rec[-3] DETECTOR(3, 0) rec[-2] DETECTOR(5, 0) rec[-1] REPEAT 9999 { TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.0125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.0125) 2 1 4 3 6 5 TICK MR 1 3 5 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-3] rec[-6] DETECTOR(3, 0) rec[-2] rec[-5] DETECTOR(5, 0) rec[-1] rec[-4] } M 0 2 4 6 DETECTOR(1, 1) rec[-3] rec[-4] rec[-7] DETECTOR(3, 1) rec[-2] rec[-3] rec[-6] DETECTOR(5, 1) rec[-1] rec[-2] rec[-5] OBSERVABLE_INCLUDE(0) rec[-1] """ def get_detector_coordinates( self, only: object = None, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of detectors in the circuit. Args: only: Defaults to None (meaning include all detectors). A list of detector indices to include in the result. Detector indices beyond the end of the detector error model of the circuit cause an error. Returns: A dictionary mapping integers (detector indices) to lists of floats (coordinates). Detectors with no specified coordinate data are mapped to an empty tuple. If `only` is specified, then `set(result.keys()) == set(only)`. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... M 0 ... DETECTOR rec[-1] ... DETECTOR(1, 2, 3) rec[-1] ... REPEAT 3 { ... DETECTOR(42) rec[-1] ... SHIFT_COORDS(100) ... } ... ''') >>> circuit.get_detector_coordinates() {0: [], 1: [1.0, 2.0, 3.0], 2: [42.0], 3: [142.0], 4: [242.0]} >>> circuit.get_detector_coordinates(only=[1]) {1: [1.0, 2.0, 3.0]} """ def get_final_qubit_coordinates( self, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of qubits in the circuit. If a qubit's coordinates are specified multiple times, only the last specified coordinates are returned. Returns: A dictionary mapping qubit indices (integers) to coordinates (lists of floats). Qubits that never had their coordinates specified are not included in the result. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... QUBIT_COORDS(1, 2, 3) 1 ... ''') >>> circuit.get_final_qubit_coordinates() {1: [1.0, 2.0, 3.0]} """ def has_all_flows( self, flows: Iterable[stim.Flow], *, unsigned: bool = False, ) -> bool: """Determines if the circuit has all the given stabilizer flow or not. This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster because, behind the scenes, the circuit can be iterated once instead of once per flow. This method ignores any noise in the circuit. Args: flows: An iterable of `stim.Flow` instances representing the flows to check. unsigned: Defaults to False. When False, the flows must be correct including the sign of the Pauli strings. When True, only the Pauli terms need to be correct; the signs are permitted to be inverted. In effect, this requires the circuit to be correct up to Pauli gates. Returns: True if the circuit has the given flow; False otherwise. Examples: >>> import stim >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> Y'), ... stim.Flow('Z -> X'), ... ]) False >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> -Y'), ... stim.Flow('Z -> X'), ... ]) True >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> Y'), ... stim.Flow('Z -> X'), ... ], unsigned=True) True Caveats: Currently, the unsigned=False version of this method is implemented by performing 256 randomized tests. Each test has a 50% chance of a false positive, and a 0% chance of a false negative. So, when the method returns True, there is technically still a 2^-256 chance the circuit doesn't have the flow. This is lower than the chance of a cosmic ray flipping the result. """ def has_flow( self, flow: stim.Flow, *, unsigned: bool = False, ) -> bool: """Determines if the circuit has the given stabilizer flow or not. A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer P at the start of the circuit to the instantaneous stabilizer Q at the end of the circuit. The flow may be mediated by certain measurements. For example, a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and the CNOT flows implemented by the circuit involve these measurements. A flow like P -> Q means the circuit transforms P into Q. A flow like 1 -> P means the circuit prepares P. A flow like P -> 1 means the circuit measures P. A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). This method ignores any noise in the circuit. Args: flow: The flow to check for. unsigned: Defaults to False. When False, the flows must be correct including the sign of the Pauli strings. When True, only the Pauli terms need to be correct; the signs are permitted to be inverted. In effect, this requires the circuit to be correct up to Pauli gates. Returns: True if the circuit has the given flow; False otherwise. Examples: >>> import stim >>> m = stim.Circuit('M 0') >>> m.has_flow(stim.Flow('Z -> Z')) True >>> m.has_flow(stim.Flow('X -> X')) False >>> m.has_flow(stim.Flow('Z -> I')) False >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) True >>> m.has_flow(stim.Flow('Z -> rec[-1]')) True >>> cx58 = stim.Circuit('CX 5 8') >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) True >>> cx58.has_flow(stim.Flow('X_ -> XX')) False >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) True >>> stim.Circuit(''' ... RY 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("Y"), ... )) True >>> stim.Circuit(''' ... RY 0 ... X_ERROR(0.1) 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("Y"), ... )) True >>> stim.Circuit(''' ... RY 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("X"), ... )) False >>> stim.Circuit(''' ... CX 0 1 ... ''').has_flow(stim.Flow( ... input=stim.PauliString("+X_"), ... output=stim.PauliString("+XX"), ... )) True >>> stim.Circuit(''' ... # Lattice surgery CNOT ... R 1 ... MXX 0 1 ... MZZ 1 2 ... MX 1 ... ''').has_flow(stim.Flow( ... input=stim.PauliString("+X_X"), ... output=stim.PauliString("+__X"), ... measurements=[0, 2], ... )) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( ... stim.Flow("Y -> Y"), ... unsigned=True, ... ) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( ... stim.Flow("Y -> Y"), ... unsigned=False, ... ) False Caveats: Currently, the unsigned=False version of this method is implemented by performing 256 randomized tests. Each test has a 50% chance of a false positive, and a 0% chance of a false negative. So, when the method returns True, there is technically still a 2^-256 chance the circuit doesn't have the flow. This is lower than the chance of a cosmic ray flipping the result. """ def insert( self, index: int, operation: Union[stim.CircuitInstruction, stim.Circuit], ) -> None: """Inserts an operation at the given index, pushing existing operations forward. Beware that inserted operations are automatically fused with the preceding and following operations, if possible. This can make it complex to reason about how the indices of operations change in response to insertions. Args: index: The index to insert at. Must satisfy -len(circuit) <= index < len(circuit). Negative indices are made non-negative by adding len(circuit) to them, so they refer to indices relative to the end of the circuit instead of the start. Instructions before the index are not shifted. Instructions that were at or after the index are shifted forwards as needed. operation: The object to insert. This can be a single stim.CircuitInstruction or an entire stim.Circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... S 1 ... X 2 ... ''') >>> c.insert(1, stim.CircuitInstruction("Y", [3, 4, 5])) >>> c stim.Circuit(''' H 0 Y 3 4 5 S 1 X 2 ''') >>> c.insert(-1, stim.Circuit("S 999\nCX 0 1\nCZ 2 3")) >>> c stim.Circuit(''' H 0 Y 3 4 5 S 1 999 CX 0 1 CZ 2 3 X 2 ''') """ def inverse( self, ) -> stim.Circuit: """Returns a circuit that applies the same operations but inverted and in reverse. If circuit starts with QUBIT_COORDS instructions, the returned circuit will still have the same QUBIT_COORDS instructions in the same order at the start. Returns: A `stim.Circuit` that applies inverted operations in the reverse order. Raises: ValueError: The circuit contains operations that don't have an inverse, such as measurements. There are also some unsupported operations such as SHIFT_COORDS. Examples: >>> import stim >>> stim.Circuit(''' ... S 0 1 ... ISWAP 0 1 1 2 ... ''').inverse() stim.Circuit(''' ISWAP_DAG 1 2 0 1 S_DAG 1 0 ''') >>> stim.Circuit(''' ... QUBIT_COORDS(1, 2) 0 ... QUBIT_COORDS(4, 3) 1 ... QUBIT_COORDS(9, 5) 2 ... H 0 1 ... REPEAT 100 { ... CX 0 1 1 2 ... TICK ... S 1 2 ... } ... ''').inverse() stim.Circuit(''' QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(4, 3) 1 QUBIT_COORDS(9, 5) 2 REPEAT 100 { S_DAG 2 1 TICK CX 1 2 0 1 } H 1 0 ''') """ def likeliest_error_sat_problem( self, *, quantization: int = 100, format: str = 'WDIMACS', ) -> str: """Makes a maxSAT problem for the circuit's likeliest undetectable logical error. The output is a string describing the maxSAT problem in WDIMACS format (see https://jix.github.io/varisat/manual/0.2.0/formats/dimacs.html). The optimal solution to the problem is the highest likelihood set of error mechanisms that combine to flip any logical observable while producing no detection events). If there are any errors with probability p > 0.5, they are inverted so that the resulting weight ends up being positive. If there are errors with weight close or equal to 0.5, they can end up with 0 weight meaning that they can be included or not in the solution with no affect on the likelihood. There are many tools that can solve maxSAT problems in WDIMACS format. One quick way to get started is to install pysat by running this BASH terminal command: pip install python-sat Afterwards, you can run the included maxSAT solver "RC2" with this Python code: from pysat.examples.rc2 import RC2 from pysat.formula import WCNF wcnf = WCNF(from_string="p wcnf 1 2 3\n3 -1 0\n3 1 0\n") with RC2(wcnf) as rc2: print(rc2.compute()) print(rc2.cost) Much faster solvers are available online. For example, you can download one of the entries in the 2023 maxSAT competition (see https://maxsat-evaluations.github.io/2023) and run it on your problem by running these BASH terminal commands: wget https://maxsat-evaluations.github.io/2023/mse23-solver-src/exact/CASHWMaxSAT-CorePlus.zip unzip CASHWMaxSAT-CorePlus.zip ./CASHWMaxSAT-CorePlus/bin/cashwmaxsatcoreplus -bm -m your_problem.wcnf Args: format: Defaults to "WDIMACS", corresponding to WDIMACS format which is described here: http://www.maxhs.org/docs/wdimacs.html quantization: Defaults to 10. Error probabilities are converted to log-odds and scaled/rounded to be positive integers at most this large. Setting this argument to a larger number results in more accurate quantization such that the returned error set should have a likelihood closer to the true most likely solution. This comes at the cost of making some maxSAT solvers slower. Returns: A string corresponding to the contents of a maxSAT problem file in the requested format. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X_ERROR(0.1) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... X_ERROR(0.4) 0 ... M 0 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.likeliest_error_sat_problem( ... quantization=1000 ... ), end='') p wcnf 2 4 4001 185 -1 0 1000 -2 0 4001 -1 0 4001 2 0 """ def missing_detectors( self, *, unknown_input: bool = False, ) -> int: """Finds deterministic measurements independent of declared detectors/observables. This method is useful for debugging missing detectors in a circuit, because it identifies generators for uncovered degrees of freedom. It's not recommended to use this method to solve for the detectors of a circuit. The returned detectors are not guaranteed to be stable across versions, and aren't optimized to be "good" (e.g. form a low weight basis or be matchable if possible). It will also identify things that are technically determined but that the user may not want to use as a detector, such as the fact that in the first round after transversal Z basis initialization of a toric code the product of all X stabilizer measurements is deterministic even though the individual measurements are all random. Args: unknown_input: Defaults to False (inputs assumed to be in the |0> state). When set to True, the inputs are instead treated as being in unknown random states. For example, this means that Z-basis measurements at the very beginning of the circuit will be considered random rather than determined. Returns: A circuit containing DETECTOR instructions that specify the uncovered degrees of freedom in the deterministic measurement sets of the input circuit. The returned circuit can be appended to the input circuit to get a circuit with no missing detectors. Examples: >>> import stim >>> stim.Circuit(''' ... R 0 ... M 0 ... ''').missing_detectors() stim.Circuit(''' DETECTOR rec[-1] ''') >>> stim.Circuit(''' ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... DEPOLARIZE1(0.1) 0 1 ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... DETECTOR rec[-1] rec[-4] ... DETECTOR rec[-2] rec[-5] ... DETECTOR rec[-3] rec[-6] ... ''').missing_detectors(unknown_input=True) stim.Circuit(''' DETECTOR rec[-3] rec[-2] rec[-1] ''') """ @property def num_detectors( self, ) -> int: """Counts the number of bits produced when sampling the circuit's detectors. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... DETECTOR rec[-1] ... REPEAT 100 { ... M 0 1 2 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... } ... ''') >>> c.num_detectors 201 """ @property def num_measurements( self, ) -> int: """Counts the number of bits produced when sampling the circuit's measurements. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... REPEAT 100 { ... M 0 1 ... } ... ''') >>> c.num_measurements 201 """ @property def num_observables( self, ) -> int: """Counts the number of logical observables defined by the circuit. This is one more than the largest index that appears as an argument to an OBSERVABLE_INCLUDE instruction. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... OBSERVABLE_INCLUDE(2) rec[-1] ... OBSERVABLE_INCLUDE(5) rec[-1] ... ''') >>> c.num_observables 6 """ @property def num_qubits( self, ) -> int: """Counts the number of qubits used when simulating the circuit. This is always one more than the largest qubit index used by the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... X 0 ... M 0 1 ... ''').num_qubits 2 >>> stim.Circuit(''' ... X 0 ... M 0 1 ... H 100 ... ''').num_qubits 101 """ @property def num_sweep_bits( self, ) -> int: """Returns the number of sweep bits needed to completely configure the circuit. This is always one more than the largest sweep bit index used by the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... CX sweep[2] 0 ... ''').num_sweep_bits 3 >>> stim.Circuit(''' ... CZ sweep[5] 0 ... CX sweep[2] 0 ... ''').num_sweep_bits 6 """ @property def num_ticks( self, ) -> int: """Counts the number of TICK instructions executed when running the circuit. TICKs in loops are counted once per iteration. Returns: The number of ticks executed by the circuit. Examples: >>> import stim >>> stim.Circuit().num_ticks 0 >>> stim.Circuit(''' ... TICK ... ''').num_ticks 1 >>> stim.Circuit(''' ... H 0 ... TICK ... CX 0 1 ... TICK ... ''').num_ticks 2 >>> stim.Circuit(''' ... H 0 ... TICK ... REPEAT 100 { ... CX 0 1 ... TICK ... } ... ''').num_ticks 101 """ def pop( self, index: int = -1, ) -> Union[stim.CircuitInstruction, stim.CircuitRepeatBlock]: """Pops an operation from the end of the circuit, or at the given index. Args: index: Defaults to -1 (end of circuit). The index to pop from. Returns: The popped instruction. Raises: IndexError: The given index is outside the bounds of the circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... S 1 ... X 2 ... Y 3 ... ''') >>> c.pop() stim.CircuitInstruction('Y', [stim.GateTarget(3)], []) >>> c.pop(1) stim.CircuitInstruction('S', [stim.GateTarget(1)], []) >>> c stim.Circuit(''' H 0 X 2 ''') """ def reference_detector_and_observable_signs( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: """Determines noiseless parities of the measurement sets of detectors/observables. BEWARE: the returned values are NOT the "expected value of the detector/observable". Stim consistently defines the value of a detector/observable as whether or not it flipped, so the expected value of a detector/observable is vacuously always 0 (not flipped). This method instead returns the "sign"; the expected parity of the measurement set declared by the detector/observable. The sign is the baseline used to determine if a flip occurred. A detector/observable's value is whether its sign disagrees with the measured parity of its measurement set. Note that this method doesn't account for sweep bits. It will effectively ignore instructions like `CX sweep[0] 0`. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: A (det, obs) tuple with numpy arrays containing the reference parities. if bit_packed: det.shape == (math.ceil(num_detectors / 8),) det.dtype == np.uint8 obs.shape == (math.ceil(num_observables / 8),) obs.dtype == np.uint8 else: det.shape == (num_detectors,) det.dtype == np.bool_ obs.shape == (num_observables,) obs.dtype == np.bool_ Examples: >>> import stim >>> stim.Circuit(''' ... X 1 ... M 0 1 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... OBSERVABLE_INCLUDE(3) rec[-1] rec[-2] ... ''').reference_detector_and_observable_signs() (array([ True, False]), array([False, False, False, True])) """ def reference_sample( self, *, bit_packed: bool = False, ) -> np.ndarray: """Samples the given circuit in a deterministic fashion. Discards all noisy operations, and biases all collapse events towards +Z instead of randomly +Z/-Z. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: A numpy array containing the reference sample. if bit_packed: shape == (math.ceil(num_measurements / 8),) dtype == np.uint8 else: shape == (num_measurements,) dtype == np.bool_ Examples: >>> import stim >>> stim.Circuit(''' ... X 1 ... M 0 1 ... ''').reference_sample() array([False, True]) """ def search_for_undetectable_logical_errors( self, *, dont_explore_detection_event_sets_with_size_above: int, dont_explore_edges_with_degree_above: int, dont_explore_edges_increasing_symptom_degree: bool, canonicalize_circuit_errors: bool = False, ) -> List[stim.ExplainedError]: """Searches for small sets of errors that form an undetectable logical error. THIS IS A HEURISTIC METHOD. It does not guarantee that it will find errors of particular sizes, or with particular properties. The errors it finds are a tangled combination of the truncation parameters you specify, internal optimizations which are correct when not truncating, and minutia of the circuit being considered. If you want a well behaved method that does provide guarantees of finding errors of a particular type, use `stim.Circuit.shortest_graphlike_error`. This method is more thorough than that (assuming you don't truncate so hard you omit graphlike edges), but exactly how thorough is difficult to describe. It's also not guaranteed that the behavior of this method will not be changed in the future in a way that permutes which logical errors are found and which are missed. This search method considers hyper errors, so it has worst case exponential runtime. It is important to carefully consider the arguments you are providing, which truncate the search space and trade cost for quality. The search progresses by starting from each error that crosses a logical observable, noting which detection events each error produces, and then iteratively adding in errors touching those detection events attempting to cancel out the detection event with the lowest index. Beware that the choice of logical observable can interact with the truncation options. Using different observables can change whether or not the search succeeds, even if those observables are equal modulo the stabilizers of the code. This is because the edges crossing logical observables are used as starting points for the search, and starting from different places along a path will result in different numbers of symptoms in intermediate states as the search progresses. For example, if the logical observable is next to a boundary, then the starting edges are likely boundary edges (degree 1) with 'room to grow', whereas if the observable was running through the bulk then the starting edges will have degree at least 2. Args: dont_explore_detection_event_sets_with_size_above: Truncates the search space by refusing to cross an edge (i.e. add an error) when doing so would produce an intermediate state that has more detection events than this limit. dont_explore_edges_with_degree_above: Truncates the search space by refusing to consider errors that cause a lot of detection events. For example, you may only want to consider graphlike errors which have two or fewer detection events. dont_explore_edges_increasing_symptom_degree: Truncates the search space by refusing to cross an edge (i.e. add an error) when doing so would produce an intermediate state that has more detection events that the previous intermediate state. This massively improves the efficiency of the search because instead of, for example, exploring all n^4 possible detection event sets with 4 symptoms, the search will attempt to cancel out symptoms one by one. canonicalize_circuit_errors: Whether or not to use one representative for equal-symptom circuit errors. False (default): Each DEM error lists every possible circuit error that single handedly produces those symptoms as a potential match. This is verbose but gives complete information. True: Each DEM error is matched with one possible circuit error that single handedly produces those symptoms, with a preference towards errors that are simpler (e.g. apply Paulis to fewer qubits). This discards mostly-redundant information about different ways to produce the same symptoms in order to give a succinct result. Returns: A list of error mechanisms that cause an undetected logical error. Each entry in the list is a `stim.ExplainedError` detailing the location and effects of a single physical error. The effects of the entire list combine to produce a logical frame change without any detection events. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... rounds=5, ... distance=5, ... after_clifford_depolarization=0.001) >>> print(len(circuit.search_for_undetectable_logical_errors( ... dont_explore_detection_event_sets_with_size_above=4, ... dont_explore_edges_with_degree_above=4, ... dont_explore_edges_increasing_symptom_degree=True, ... ))) 5 """ def shortest_error_sat_problem( self, *, format: str = 'WDIMACS', ) -> str: """Makes a maxSAT problem of the circuit's distance, that other tools can solve. The output is a string describing the maxSAT problem in WDIMACS format (see https://jix.github.io/varisat/manual/0.2.0/formats/dimacs.html). The optimal solution to the problem is the fault distance of the circuit (the minimum number of error mechanisms that combine to flip any logical observable while producing no detection events). This method ignores the probabilities of the error mechanisms since it only cares about minimizing the number of errors triggered. There are many tools that can solve maxSAT problems in WDIMACS format. One quick way to get started is to install pysat by running this BASH terminal command: pip install python-sat Afterwards, you can run the included maxSAT solver "RC2" with this Python code: from pysat.examples.rc2 import RC2 from pysat.formula import WCNF wcnf = WCNF(from_string="p wcnf 1 2 3\n3 -1 0\n3 1 0\n") with RC2(wcnf) as rc2: print(rc2.compute()) print(rc2.cost) Much faster solvers are available online. For example, you can download one of the entries in the 2023 maxSAT competition (see https://maxsat-evaluations.github.io/2023) and run it on your problem by running these BASH terminal commands: wget https://maxsat-evaluations.github.io/2023/mse23-solver-src/exact/CASHWMaxSAT-CorePlus.zip unzip CASHWMaxSAT-CorePlus.zip ./CASHWMaxSAT-CorePlus/bin/cashwmaxsatcoreplus -bm -m your_problem.wcnf Args: format: Defaults to "WDIMACS", corresponding to WDIMACS format which is described here: http://www.maxhs.org/docs/wdimacs.html Returns: A string corresponding to the contents of a maxSAT problem file in the requested format. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X_ERROR(0.1) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... X_ERROR(0.4) 0 ... M 0 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.shortest_error_sat_problem(), end='') p wcnf 2 4 5 1 -1 0 1 -2 0 5 -1 0 5 2 0 """ def shortest_graphlike_error( self, *, ignore_ungraphlike_errors: bool = True, canonicalize_circuit_errors: bool = False, ) -> List[stim.ExplainedError]: """Finds a minimum set of graphlike errors to produce an undetected logical error. A "graphlike error" is an error that creates at most two detection events (causes a change in the parity of the measurement sets of at most two DETECTOR annotations). Note that this method does not pay attention to error probabilities (other than ignoring errors with probability 0). It searches for a logical error with the minimum *number* of physical errors, not the maximum probability of those physical errors all occurring. This method works by converting the circuit into a `stim.DetectorErrorModel` using `circuit.detector_error_model(...)`, computing the shortest graphlike error of the error model, and then converting the physical errors making up that logical error back into representative circuit errors. Args: ignore_ungraphlike_errors: False: Attempt to decompose any ungraphlike errors in the circuit into graphlike parts. If this fails, raise an exception instead of continuing. Note: in some cases, graphlike errors only appear as parts of decomposed ungraphlike errors. This can produce a result that lists DEM errors with zero matching circuit errors, because the only way to achieve those errors is by combining a decomposed error with a graphlike error. As a result, when using this option it is NOT guaranteed that the length of the result is an upper bound on the true code distance. That is only the case if every item in the result lists at least one matching circuit error. True (default): Ungraphlike errors are simply skipped as if they weren't present, even if they could become graphlike if decomposed. This guarantees the length of the result is an upper bound on the true code distance. canonicalize_circuit_errors: Whether or not to use one representative for equal-symptom circuit errors. False (default): Each DEM error lists every possible circuit error that single handedly produces those symptoms as a potential match. This is verbose but gives complete information. True: Each DEM error is matched with one possible circuit error that single handedly produces those symptoms, with a preference towards errors that are simpler (e.g. apply Paulis to fewer qubits). This discards mostly-redundant information about different ways to produce the same symptoms in order to give a succinct result. Returns: A list of error mechanisms that cause an undetected logical error. Each entry in the list is a `stim.ExplainedError` detailing the location and effects of a single physical error. The effects of the entire list combine to produce a logical frame change without any detection events. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... before_round_data_depolarization=0.01) >>> len(circuit.shortest_graphlike_error()) 7 """ def solve_flow_measurements( self, flows: List[stim.Flow], ) -> List[Optional[List[int]]]: """Finds measurements to explain the starts/ends of the given flows, ignoring sign. CAUTION: it's not guaranteed that the solutions returned by this method are minimal. It may use 20 measurements when only 2 are needed. The method applies some simple heuristics that attempt to reduce the size, but these heuristics aren't perfect and don't make any strong guarantees. The recommended way to use this method is on small parts of a circuit, such as a single surface code round. The ideal use case is when there is exactly one solution for each flow, because then the method behaves predictably and consistently. When there are multiple solutions, the method has no real way to pick out a "good" solution rather than a "cataclysmic trash fire of a" solution. For example, if you have a multi-round surface code circuit with open time boundaries and solve the flow 1 -> Z1*Z2*Z3*Z4, then there's a good solution (the Z1*Z2*Z3*Z4 measurement from the last round), various mediocre solutions (a Z1*Z2*Z3*Z4 measurement from a different round), and lots of terrible solutions (a combination of multiple Z1*Z2*Z3*Z4 measurements from an odd number of rounds, times a random combination of unrelated detectors). The method is permitted to return any of those solutions. Args: flows: A list of flows, each of which to be solved. Measurements and signs are entirely ignored. An error is raised if one of the given flows has an identity pauli string as its input and as its output, despite the fact that this case has a vacuous solution (no measurements). This error is only present as a safety check that catches some possible bugs in the calling code, such as accidentally applying this method to detector flows. This error may be removed in the future, so that the vacuous case succeeds vacuously. Returns: A list of solutions for each given flow. If no solution exists for flows[k], then solutions[k] is None. Otherwise, solutions[k] is a list of measurement indices for flows[k]. When solutions[k] is not None, it's guaranteed that circuit.has_flow(stim.Flow( input=flows[k].input, output=flows[k].output, measurements=solutions[k], ), unsigned=True) Raises: ValueError: A flow had an empty input and output. Examples: >>> import stim >>> stim.Circuit(''' ... M 2 ... ''').solve_flow_measurements([ ... stim.Flow("Z2 -> 1"), ... ]) [[0]] >>> stim.Circuit(''' ... M 2 ... ''').solve_flow_measurements([ ... stim.Flow("X2 -> X2"), ... ]) [None] >>> stim.Circuit(''' ... MXX 0 1 ... ''').solve_flow_measurements([ ... stim.Flow("YY -> ZZ"), ... ]) [[0]] >>> # Rep code cycle >>> stim.Circuit(''' ... R 1 3 ... CX 0 1 2 3 ... CX 4 3 2 1 ... M 1 3 ... ''').solve_flow_measurements([ ... stim.Flow("1 -> Z0*Z4"), ... stim.Flow("Z0 -> Z2"), ... stim.Flow("X0*X2*X4 -> X0*X2*X4"), ... stim.Flow("Y0 -> Y0"), ... ]) [[0, 1], [0], [], None] """ def time_reversed_for_flows( self, flows: Iterable[stim.Flow], *, dont_turn_measurements_into_resets: bool = False, ) -> Tuple[stim.Circuit, List[stim.Flow]]: """Time-reverses the circuit while preserving error correction structure. This method returns a circuit that has the same internal detecting regions as the given circuit, as well as the same internal-to-external flows given in the `flows` argument, except they are all time-reversed. For example, if you pass a fault tolerant preparation circuit into this method (1 -> Z), the result will be a fault tolerant *measurement* circuit (Z -> 1). Or, if you pass a fault tolerant C_XYZ circuit into this method (X->Y, Y->Z, and Z->X), the result will be a fault tolerant C_ZYX circuit (X->Z, Y->X, and Z->Y). Note that this method doesn't guarantee that it will preserve the *sign* of the detecting regions or stabilizer flows. For example, inverting a memory circuit that preserves a logical observable (X->X and Z->Z) may produce a memory circuit that always bit flips the logical observable (X->X and Z->-Z) or that dynamically adjusts the logical observable in response to measurements (like "X -> X xor rec[-1]" and "Z -> Z xor rec[-2]"). This method will turn time-reversed resets into measurements, and attempts to turn time-reversed measurements into resets. A measurement will time-reverse into a reset if some annotated detectors, annotated observables, or given flows have detecting regions with sensitivity just before the measurement but none have detecting regions with sensitivity after the measurement. In some cases this method will have to introduce new operations. In particular, when a measurement-reset operation has a noisy result, time-reversing this measurement noise produces reset noise. But the measure-reset operations don't have built-in reset noise, so the reset noise is specified by adding an X_ERROR or Z_ERROR noise instruction after the time-reversed measure-reset operation. Args: flows: Flows you care about, that reach past the start/end of the given circuit. The result will contain an inverted flow for each of these given flows. You need this information because it reveals the measurements needed to produce the inverted flows that you care about. An exception will be raised if the circuit doesn't have all these flows. The inverted circuit will have the inverses of these flows (ignoring sign). dont_turn_measurements_into_resets: Defaults to False. When set to True, measurements will time-reverse into measurements even if nothing is sensitive to the measured qubit after the measurement completes. This guarantees the output circuit has *all* flows that the input circuit has (up to sign and feedback), even ones that aren't annotated. Returns: An (inverted_circuit, inverted_flows) tuple. inverted_circuit is the qec inverse of the given circuit. inverted_flows is a list of flows, matching up by index with the flows given as arguments to the method. The input, output, and sign fields of these flows are boring. The useful field is measurement_indices, because it's difficult to predict which measurements are needed for the inverted flow due to effects such as implicitly-included resets inverting into explicitly-included measurements. Caveats: Currently, this method doesn't compute the sign of the inverted flows. It unconditionally sets the sign to False. Examples: >>> import stim >>> inv_circuit, inv_flows = stim.Circuit(''' ... R 0 ... H 0 ... S 0 ... MY 0 ... DETECTOR rec[-1] ... ''').time_reversed_for_flows([]) >>> inv_circuit stim.Circuit(''' RY 0 S_DAG 0 H 0 M 0 DETECTOR rec[-1] ''') >>> inv_flows [] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows([ ... stim.Flow("Z -> rec[-1]"), ... ]) >>> inv_circuit stim.Circuit(''' R 0 ''') >>> inv_flows [stim.Flow("1 -> Z")] >>> inv_circuit.has_all_flows(inv_flows, unsigned=True) True >>> inv_circuit, inv_flows = stim.Circuit(''' ... R 0 ... ''').time_reversed_for_flows([ ... stim.Flow("1 -> Z"), ... ]) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("Z -> rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows([ ... stim.Flow("1 -> Z xor rec[-1]"), ... ]) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("Z -> rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows( ... flows=[stim.Flow("Z -> rec[-1]")], ... dont_turn_measurements_into_resets=True, ... ) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("1 -> Z xor rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... MR(0.125) 0 ... ''').time_reversed_for_flows([]) >>> inv_circuit stim.Circuit(''' MR 0 X_ERROR(0.125) 0 ''') >>> inv_flows [] >>> inv_circuit, inv_flows = stim.Circuit(''' ... MXX 0 1 ... H 0 ... ''').time_reversed_for_flows([ ... stim.Flow("ZZ -> YY xor rec[-1]"), ... stim.Flow("ZZ -> XZ"), ... ]) >>> inv_circuit stim.Circuit(''' H 0 MXX 0 1 ''') >>> inv_flows [stim.Flow("YY -> ZZ xor rec[-1]"), stim.Flow("XZ -> ZZ")] >>> stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... distance=2, ... rounds=1, ... ).time_reversed_for_flows([])[0] stim.Circuit(''' QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 1) 3 QUBIT_COORDS(1, 3) 6 QUBIT_COORDS(2, 2) 7 QUBIT_COORDS(3, 3) 8 QUBIT_COORDS(2, 4) 12 RX 8 6 3 1 MR 12 7 2 TICK H 12 2 TICK CX 1 7 12 6 TICK CX 6 7 12 8 TICK CX 3 7 2 1 TICK CX 8 7 2 3 TICK H 12 2 TICK M 12 7 2 DETECTOR(2, 0, 1) rec[-1] DETECTOR(2, 4, 1) rec[-3] MX 8 6 3 1 DETECTOR(2, 0, 0) rec[-5] rec[-2] rec[-1] DETECTOR(2, 4, 0) rec[-7] rec[-4] rec[-3] OBSERVABLE_INCLUDE(0) rec[-3] rec[-1] ''') """ def to_crumble_url( self, *, skip_detectors: bool = False, mark: Optional[Dict[int, List[stim.ExplainedError]]] = None, ) -> str: """Returns a URL that opens up crumble and loads this circuit into it. Crumble is a tool for editing stabilizer circuits, and visualizing their stabilizer flows. Its source code is in the `glue/crumble` directory of the stim code repository on github. A prebuilt version is made available at https://algassert.com/crumble, which is what the URL returned by this method will point to. Args: skip_detectors: Defaults to False. If set to True, detectors from the circuit aren't included in the crumble URL. This can reduce visual clutter in crumble, and improve its performance, since it doesn't need to indicate or track the sensitivity regions of detectors. mark: Defaults to None (no marks). If set to a dictionary from int to errors, such as `mark={1: circuit.shortest_graphlike_error()}`, then the errors will be highlighted and tracked forward by crumble. Returns: A URL that can be opened in a web browser. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... S 1 ... ''').to_crumble_url() 'https://algassert.com/crumble#circuit=H_0;CX_0_1;S_1_' >>> circuit = stim.Circuit(''' ... M(0.25) 0 1 2 ... DETECTOR rec[-1] rec[-2] ... DETECTOR rec[-2] rec[-3] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''') >>> err = circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) >>> circuit.to_crumble_url(skip_detectors=True, mark={1: err}) 'https://algassert.com/crumble#circuit=;TICK;MARKX(1)1;MARKX(1)2;MARKX(1)0;TICK;M(0.25)0_1_2;OI(0)rec[-1]_' """ def to_file( self, file: Union[io.TextIOBase, str, pathlib.Path], ) -> None: """Writes the stim circuit to a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md Args: file: A file path or an open file to write to. Examples: >>> import stim >>> import tempfile >>> c = stim.Circuit('H 5\nX 0') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... c.to_file(f) ... with open(path) as f: ... contents = f.read() >>> contents 'H 5\nX 0\n' >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... c.to_file(path) ... with open(path) as f: ... contents = f.read() >>> contents 'H 5\nX 0\n' """ def to_qasm( self, *, open_qasm_version: int, skip_dets_and_obs: bool = False, ) -> str: """Creates an equivalent OpenQASM implementation of the circuit. Args: open_qasm_version: The version of OpenQASM to target. This should be set to 2 or to 3. Differences between the versions are: - Support for operations on classical bits (only version 3). This means DETECTOR and OBSERVABLE_INCLUDE only work with version 3. - Support for feedback operations (only version 3). - Support for subroutines (only version 3). Without subroutines, non-standard dissipative gates like MR and RX need to decompose inline every single time they're used. - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc). skip_dets_and_obs: Defaults to False. When set to False, the output will include a `dets` register and an `obs` register (assuming the circuit has detectors and observables). These registers will be computed as part of running the circuit. This requires performing a simulation of the circuit, in order to correctly account for the expected value of measurements. When set to True, the `dets` and `obs` registers are not included in the output, and no simulation of the circuit is performed. Returns: The OpenQASM code as a string. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... R 0 1 ... X 1 ... H 0 ... CX 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... '''); >>> qasm = circuit.to_qasm(open_qasm_version=3); >>> print(qasm.strip().replace('\n\n', '\n')) OPENQASM 3.0; include "stdgates.inc"; qreg q[2]; creg rec[2]; creg dets[1]; reset q[0]; reset q[1]; x q[1]; h q[0]; cx q[0], q[1]; measure q[0] -> rec[0]; measure q[1] -> rec[1]; dets[0] = rec[1] ^ rec[0] ^ 1; """ def to_quirk_url( self, ) -> str: """Returns a URL that opens up quirk and loads this circuit into it. Quirk is an open source drag and drop circuit editor with support for up to 16 qubits. Its source code is available at https://github.com/strilanc/quirk and a prebuilt version is available at https://algassert.com/quirk, which is what the URL returned by this method will point to. Quirk doesn't support features like noise, feedback, or detectors. This method will simply drop any unsupported operations from the circuit when producing the URL. Returns: A URL that can be opened in a web browser. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... S 1 ... ''').to_quirk_url() 'https://algassert.com/quirk#circuit={"cols":[["H"],["•","X"],[1,"Z^½"]]}' """ def to_tableau( self, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False, ) -> stim.Tableau: """Converts the circuit into an equivalent stabilizer tableau. Args: ignore_noise: Defaults to False. When False, any noise operations in the circuit will cause the conversion to fail with an exception. When True, noise operations are skipped over as if they weren't even present in the circuit. ignore_measurement: Defaults to False. When False, any measurement operations in the circuit will cause the conversion to fail with an exception. When True, measurement operations are skipped over as if they weren't even present in the circuit. ignore_reset: Defaults to False. When False, any reset operations in the circuit will cause the conversion to fail with an exception. When True, reset operations are skipped over as if they weren't even present in the circuit. Returns: A tableau equivalent to the circuit (up to global phase). Raises: ValueError: The circuit contains noise operations but ignore_noise=False. OR The circuit contains measurement operations but ignore_measurement=False. OR The circuit contains reset operations but ignore_reset=False. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''').to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ def with_inlined_feedback( self, ) -> stim.Circuit: """Returns a circuit without feedback with rewritten detectors/observables. When a feedback operation affects the expected parity of a detector or observable, the measurement controlling that feedback operation is implicitly part of the measurement set that defines the detector or observable. This method removes all feedback, but avoids changing the meaning of detectors or observables by turning these implicit measurement dependencies into explicit measurement dependencies added to the observable or detector. This method guarantees that the detector error model derived from the original circuit, and the transformed circuit, will be equivalent (modulo floating point rounding errors and variations in where loops are placed). Specifically, the following should be true for any circuit: dem1 = circuit.flattened().detector_error_model() dem2 = circuit.with_inlined_feedback().flattened().detector_error_model() assert dem1.approx_equals(dem2, 1e-5) Returns: A `stim.Circuit` with feedback operations removed, with rewritten DETECTOR instructions (as needed to avoid changing the meaning of each detector), and with additional OBSERVABLE_INCLUDE instructions (as needed to avoid changing the meaning of each observable). The circuit's function is permitted to differ from the original in that any feedback operation can be pushed to the end of the circuit and discarded. All non-feedback operations must stay where they are, preserving the structure of the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... CX 0 1 # copy to measure qubit ... M 1 # measure first time ... CX rec[-1] 1 # use feedback to reset measurement qubit ... CX 0 1 # copy to measure qubit ... M 1 # measure second time ... DETECTOR rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').with_inlined_feedback() stim.Circuit(''' CX 0 1 M 1 OBSERVABLE_INCLUDE(0) rec[-1] CX 0 1 M 1 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''') """ def without_noise( self, ) -> stim.Circuit: """Returns a copy of the circuit with all noise processes removed. Pure noise instructions, such as X_ERROR and DEPOLARIZE2, are not included in the result. Noisy measurement instructions, like `M(0.001)`, have their noise parameter removed. Returns: A `stim.Circuit` with the same instructions except all noise processes have been removed. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.25) 0 ... CNOT 0 1 ... M(0.125) 0 ... ''').without_noise() stim.Circuit(''' CX 0 1 M 0 ''') """ def without_tags( self, ) -> stim.Circuit: """Returns a copy of the circuit with all tags removed. Returns: A `stim.Circuit` with the same instructions except all tags have been removed. Examples: >>> import stim >>> stim.Circuit(''' ... X[test-tag] 0 ... M[test-tag-2](0.125) 0 ... ''').without_tags() stim.Circuit(''' X 0 M(0.125) 0 ''') """ class CircuitErrorLocation: """Describes the location of an error mechanism from a stim circuit. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... distance=5, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ) >>> logical_error = circuit.shortest_graphlike_error() >>> error_location = logical_error[0].circuit_error_locations[0] >>> print(error_location) CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } """ def __init__( self, *, tick_offset: int, flipped_pauli_product: List[stim.GateTargetWithCoords], flipped_measurement: object, instruction_targets: stim.CircuitTargetsInsideInstruction, stack_frames: List[stim.CircuitErrorLocationStackFrame], noise_tag: str = '', ) -> None: """Creates a stim.CircuitErrorLocation. Examples: >>> import stim >>> err = stim.CircuitErrorLocation( ... tick_offset=1, ... flipped_pauli_product=( ... stim.GateTargetWithCoords( ... gate_target=stim.target_x(0), ... coords=[], ... ), ... ), ... flipped_measurement=stim.FlippedMeasurement( ... record_index=None, ... observable=(), ... ), ... instruction_targets=stim.CircuitTargetsInsideInstruction( ... gate='DEPOLARIZE1', ... args=[0.001], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=(stim.GateTargetWithCoords( ... gate_target=0, ... coords=[], ... ),) ... ), ... stack_frames=( ... stim.CircuitErrorLocationStackFrame( ... instruction_offset=2, ... iteration_index=0, ... instruction_repetitions_arg=0, ... ), ... ), ... noise_tag='test-tag', ... ) >>> print(err) CircuitErrorLocation { noise_tag: test-tag flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } """ @property def flipped_measurement( self, ) -> Optional[stim.FlippedMeasurement]: """The measurement that was flipped by the error mechanism. If the error isn't a measurement error, this will be None. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... M(0.125) 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement stim.FlippedMeasurement( record_index=0, observable=(stim.GateTargetWithCoords(stim.target_z(0), []),), ) """ @property def flipped_pauli_product( self, ) -> List[stim.GateTargetWithCoords]: """The Pauli errors that the error mechanism applied to qubits. When the error is a measurement error, this will be an empty list. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_pauli_product [stim.GateTargetWithCoords(stim.target_y(0), [])] """ @property def instruction_targets( self, ) -> stim.CircuitTargetsInsideInstruction: """Within the error instruction, which may have hundreds of targets, which specific targets were being executed to produce the error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> targets = err[0].circuit_error_locations[0].instruction_targets >>> targets == stim.CircuitTargetsInsideInstruction( ... gate='Y_ERROR', ... args=[0.125], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=(stim.GateTargetWithCoords(0, []),), ... ) True """ @property def noise_tag( self, ) -> str: """The tag on the noise instruction that caused the error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... Y_ERROR[test-tag](0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].noise_tag 'test-tag' """ @property def stack_frames( self, ) -> List[stim.CircuitErrorLocationStackFrame]: """Describes where in the circuit's execution the error happened. Multiple frames are needed because the error may occur within a loop, or a loop nested inside a loop, or etc. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames [stim.CircuitErrorLocationStackFrame( instruction_offset=2, iteration_index=0, instruction_repetitions_arg=0, )] """ @property def tick_offset( self, ) -> int: """The number of TICKs that executed before the error happened. This counts TICKs occurring multiple times during loops. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... TICK ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].tick_offset 3 """ class CircuitErrorLocationStackFrame: """Describes the location of an instruction being executed within a circuit or loop, distinguishing between separate loop iterations. The full location of an instruction is a list of these frames, drilling down from the top level circuit to the inner-most loop that the instruction is within. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames[0] stim.CircuitErrorLocationStackFrame( instruction_offset=0, iteration_index=0, instruction_repetitions_arg=5, ) >>> err[0].circuit_error_locations[0].stack_frames[1] stim.CircuitErrorLocationStackFrame( instruction_offset=1, iteration_index=4, instruction_repetitions_arg=0, ) """ def __init__( self, *, instruction_offset: int, iteration_index: int, instruction_repetitions_arg: int, ) -> None: """Creates a stim.CircuitErrorLocationStackFrame. Examples: >>> import stim >>> frame = stim.CircuitErrorLocationStackFrame( ... instruction_offset=1, ... iteration_index=2, ... instruction_repetitions_arg=3, ... ) """ @property def instruction_offset( self, ) -> int: """The index of the instruction within the circuit, or within the instruction's parent REPEAT block. This is slightly different from the line number, because blank lines and commented lines don't count and also because the offset of the first instruction is 0 instead of 1. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames[0].instruction_offset 2 """ @property def instruction_repetitions_arg( self, ) -> int: """If the instruction being referred to is a REPEAT block, this is the repetition count of that REPEAT block. Otherwise this field defaults to 0. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> full = err[0].circuit_error_locations[0].stack_frames[0] >>> loop = err[0].circuit_error_locations[0].stack_frames[1] >>> full.instruction_repetitions_arg 5 >>> loop.instruction_repetitions_arg 0 """ @property def iteration_index( self, ) -> int: """Disambiguates which iteration of the loop containing this instruction is being referred to. If the instruction isn't in a REPEAT block, this field defaults to 0. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> full = err[0].circuit_error_locations[0].stack_frames[0] >>> loop = err[0].circuit_error_locations[0].stack_frames[1] >>> full.iteration_index 0 >>> loop.iteration_index 4 """ class CircuitInstruction: """An instruction, like `H 0 1` or `CNOT rec[-1] 5`, from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... M 0 1 ... X_ERROR(0.125) 5 ... ''') >>> circuit[0] stim.CircuitInstruction('H', [stim.GateTarget(0)], []) >>> circuit[1] stim.CircuitInstruction('M', [stim.GateTarget(0), stim.GateTarget(1)], []) >>> circuit[2] stim.CircuitInstruction('X_ERROR', [stim.GateTarget(5)], [0.125]) """ def __eq__( self, arg0: stim.CircuitInstruction, ) -> bool: """Determines if two `stim.CircuitInstruction`s are identical. """ def __init__( self, name: str, targets: Optional[Iterable[Union[int, stim.GateTarget]]] = None, gate_args: Optional[Iterable[float]] = None, *, tag: str = "", ) -> None: """Creates or parses a `stim.CircuitInstruction`. Args: name: The name of the instruction being applied. If `targets` and `gate_args` aren't specified, this can be a full instruction line from a stim Circuit file, like "CX 0 1". targets: The targets the instruction is being applied to. These can be raw values like `0` and `stim.target_rec(-1)`, or instances of `stim.GateTarget`. gate_args: The sequence of numeric arguments parameterizing a gate. For noise gates this is their probabilities. For `OBSERVABLE_INCLUDE` instructions it's the index of the logical observable to affect. tag: Defaults to "". A custom string attached to the instruction. For example, for a TICK instruction, this could a string specifying an amount of time which is used by custom code for adding noise to a circuit. In general, stim will attempt to propagate tags across circuit transformations but will otherwise completely ignore them. Examples: >>> import stim >>> print(stim.CircuitInstruction('DEPOLARIZE1', [5], [0.25])) DEPOLARIZE1(0.25) 5 >>> stim.CircuitInstruction('CX rec[-1] 5 # comment') stim.CircuitInstruction('CX', [stim.target_rec(-1), stim.GateTarget(5)], []) >>> print(stim.CircuitInstruction('I', [2], tag='100ns')) I[100ns] 2 """ def __ne__( self, arg0: stim.CircuitInstruction, ) -> bool: """Determines if two `stim.CircuitInstruction`s are different. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CircuitInstruction`. """ def __str__( self, ) -> str: """Returns a text description of the instruction as a stim circuit file line. """ def gate_args_copy( self, ) -> List[float]: """Returns the gate's arguments (numbers parameterizing the instruction). For noisy gates this typically a list of probabilities. For OBSERVABLE_INCLUDE it's a singleton list containing the logical observable index. Examples: >>> import stim >>> instruction = stim.CircuitInstruction('X_ERROR', [2, 3], [0.125]) >>> instruction.gate_args_copy() [0.125] >>> instruction.gate_args_copy() == instruction.gate_args_copy() True >>> instruction.gate_args_copy() is instruction.gate_args_copy() False """ @property def name( self, ) -> str: """The name of the instruction (e.g. `H` or `X_ERROR` or `DETECTOR`). """ @property def num_measurements( self, ) -> int: """Returns the number of bits produced when running this instruction. Examples: >>> import stim >>> stim.CircuitInstruction('H', [0]).num_measurements 0 >>> stim.CircuitInstruction('M', [0]).num_measurements 1 >>> stim.CircuitInstruction('M', [2, 3, 5, 7, 11]).num_measurements 5 >>> stim.CircuitInstruction('MXX', [0, 1, 4, 5, 11, 13]).num_measurements 3 >>> stim.Circuit('MPP X0*X1 X0*Z1*Y2')[0].num_measurements 2 >>> stim.CircuitInstruction('HERALDED_ERASE', [0], [0.25]).num_measurements 1 """ @property def tag( self, ) -> str: """The custom tag attached to the instruction. The tag is an arbitrary string. The default tag, when none is specified, is the empty string. Examples: >>> import stim >>> stim.Circuit("H[test] 0")[0].tag 'test' >>> stim.Circuit("H 0")[0].tag '' """ def target_groups( self, ) -> List[List[stim.GateTarget]]: """Splits the instruction's targets into groups depending on the type of gate. Single qubit gates like H get one group per target. Two qubit gates like CX get one group per pair of targets. Pauli product gates like MPP get one group per combined product. Returns: A list of groups of targets. Examples: >>> import stim >>> for g in stim.Circuit('H 0 1 2')[0].target_groups(): ... print(repr(g)) [stim.GateTarget(0)] [stim.GateTarget(1)] [stim.GateTarget(2)] >>> for g in stim.Circuit('CX 0 1 2 3')[0].target_groups(): ... print(repr(g)) [stim.GateTarget(0), stim.GateTarget(1)] [stim.GateTarget(2), stim.GateTarget(3)] >>> for g in stim.Circuit('MPP X0*Y1*Z2 X5*X6')[0].target_groups(): ... print(repr(g)) [stim.target_x(0), stim.target_y(1), stim.target_z(2)] [stim.target_x(5), stim.target_x(6)] >>> for g in stim.Circuit('DETECTOR rec[-1] rec[-2]')[0].target_groups(): ... print(repr(g)) [stim.target_rec(-1)] [stim.target_rec(-2)] >>> for g in stim.Circuit('CORRELATED_ERROR(0.1) X0 Y1')[0].target_groups(): ... print(repr(g)) [stim.target_x(0), stim.target_y(1)] """ def targets_copy( self, ) -> List[stim.GateTarget]: """Returns a copy of the targets of the instruction. Examples: >>> import stim >>> instruction = stim.CircuitInstruction('X_ERROR', [2, 3], [0.125]) >>> instruction.targets_copy() [stim.GateTarget(2), stim.GateTarget(3)] >>> instruction.targets_copy() == instruction.targets_copy() True >>> instruction.targets_copy() is instruction.targets_copy() False """ class CircuitRepeatBlock: """A REPEAT block from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 >>> repeat_block.body_copy() stim.Circuit(''' CX 0 1 CZ 1 2 ''') """ def __eq__( self, arg0: stim.CircuitRepeatBlock, ) -> bool: """Determines if two `stim.CircuitRepeatBlock`s are identical. """ def __init__( self, repeat_count: int, body: stim.Circuit, *, tag: str = '', ) -> None: """Initializes a `stim.CircuitRepeatBlock`. Args: repeat_count: The number of times to repeat the block. body: The body of the block, as a circuit. tag: Defaults to empty. A custom string attached to the REPEAT instruction. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append(stim.CircuitRepeatBlock(100, stim.Circuit("M 0"))) >>> c stim.Circuit(''' REPEAT 100 { M 0 } ''') """ def __ne__( self, arg0: stim.CircuitRepeatBlock, ) -> bool: """Determines if two `stim.CircuitRepeatBlock`s are different. """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.CircuitRepeatBlock`. """ def body_copy( self, ) -> stim.Circuit: """Returns a copy of the body of the repeat block. (Making a copy is enforced to make it clear that editing the result won't change the block's body.) Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.body_copy() stim.Circuit(''' CX 0 1 CZ 1 2 ''') """ @property def name( self, ) -> str: """Returns the name "REPEAT". This is a duck-typing convenience method. It exists so that code that doesn't know whether it has a `stim.CircuitInstruction` or a `stim.CircuitRepeatBlock` can check the object's name without having to do an `instanceof` check first. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 1 2 ... } ... S 1 ... ''') >>> [instruction.name for instruction in circuit] ['H', 'REPEAT', 'S'] """ @property def num_measurements( self, ) -> int: """Returns the number of bits produced when running this loop. Examples: >>> import stim >>> stim.CircuitRepeatBlock( ... body=stim.Circuit("M 0 1"), ... repeat_count=25, ... ).num_measurements 50 """ @property def repeat_count( self, ) -> int: """The repetition count of the repeat block. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 """ @property def tag( self, ) -> str: """The custom tag attached to the REPEAT instruction. The tag is an arbitrary string. The default tag, when none is specified, is the empty string. Examples: >>> import stim >>> stim.Circuit(''' ... REPEAT[test] 5 { ... H 0 ... } ... ''')[0].tag 'test' >>> stim.Circuit(''' ... REPEAT 5 { ... H 0 ... } ... ''')[0].tag '' """ class CircuitTargetsInsideInstruction: """Describes a range of targets within a circuit instruction. """ def __init__( self, *, gate: str, tag: str = '', args: List[float], target_range_start: int, target_range_end: int, targets_in_range: List[stim.GateTargetWithCoords], ) -> None: """Creates a stim.CircuitTargetsInsideInstruction. Examples: >>> import stim >>> val = stim.CircuitTargetsInsideInstruction( ... gate='X_ERROR', ... tag='', ... args=[0.25], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=[stim.GateTargetWithCoords(0, [])], ... ) """ @property def args( self, ) -> List[float]: """Returns parens arguments of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.args [0.25] """ @property def gate( self, ) -> Optional[str]: """Returns the name of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.gate 'X_ERROR' """ @property def tag( self, ) -> str: """Returns the tag of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR[look-at-me-imma-tag](0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.tag 'look-at-me-imma-tag' """ @property def target_range_end( self, ) -> int: """Returns the exclusive end of the range of targets that were executing within the gate / instruction. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.target_range_start 0 >>> loc.instruction_targets.target_range_end 1 """ @property def target_range_start( self, ) -> int: """Returns the inclusive start of the range of targets that were executing within the gate / instruction. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.target_range_start 0 >>> loc.instruction_targets.target_range_end 1 """ @property def targets_in_range( self, ) -> List[stim.GateTargetWithCoords]: """Returns the subset of targets of the gate/instruction that were being executed. Includes coordinate data with the targets. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.targets_in_range [stim.GateTargetWithCoords(0, [])] """ class CliffordString: """A tensor product of single qubit Clifford gates (e.g. "H \u2297 X \u2297 S"). Represents a collection of Clifford operations applied pairwise to a collection of qubits. Ignores global phase. Examples: >>> import stim >>> stim.CliffordString("H,S,C_XYZ") * stim.CliffordString("H,H,H") stim.CliffordString("I,C_ZYX,SQRT_X_DAG") """ def __add__( self, rhs: stim.CliffordString, ) -> stim.CliffordString: """Concatenates two CliffordStrings. Args: rhs: The suffix of the concatenation. Returns: The concatenated Clifford string. Examples: >>> import stim >>> stim.CliffordString("I,X,H") + stim.CliffordString("Y,S") stim.CliffordString("I,X,H,Y,S") """ def __eq__( self, arg0: stim.CliffordString, ) -> bool: """Determines if two Clifford strings have identical contents. """ @overload def __getitem__( self, index_or_slice: int, ) -> stim.GateData: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.CliffordString: pass def __getitem__( self, index_or_slice: Union[int, slice], ) -> Union[stim.GateData, stim.CliffordString]: """Returns a Clifford or substring from the CliffordString. Args: index_or_slice: The index of the Clifford to return, or the slice corresponding to the sub CliffordString to return. Returns: The indexed Clifford (as a stim.GateData instance) or the sliced CliffordString. Examples: >>> import stim >>> s = stim.CliffordString("I,X,Y,Z,H") >>> s[2] stim.gate_data('Y') >>> s[-1] stim.gate_data('H') >>> s[:-1] stim.CliffordString("I,X,Y,Z") >>> s[::2] stim.CliffordString("I,Y,H") """ def __iadd__( self, rhs: stim.CliffordString, ) -> stim.CliffordString: """Mutates the CliffordString by concatenating onto it. Args: rhs: The suffix to concatenate onto the target CliffordString. Returns: The mutated Clifford string. Examples: >>> import stim >>> c = stim.CliffordString("I,X,H") >>> alias = c >>> alias += stim.CliffordString("Y,S") >>> c stim.CliffordString("I,X,H,Y,S") """ def __imul__( self, rhs: Union[stim.CliffordString, int], ) -> stim.CliffordString: """Inplace CliffordString multiplication. Mutates the CliffordString into itself multiplied by another CliffordString (via pairwise Clifford multipliation) or by an integer (via repeating the contents). Args: rhs: Either a stim.CliffordString or an int. If rhs is a stim.CliffordString, then the Cliffords from each string are multiplied pairwise. If rhs is an int, it is the number of times to repeat the Clifford string's contents. Returns: The mutated Clifford string. Examples: >>> import stim >>> c = stim.CliffordString("S,X,X") >>> alias = c >>> alias *= stim.CliffordString("S,Z,H,Z") >>> c stim.CliffordString("Z,Y,SQRT_Y,Z") >>> c = stim.CliffordString("I,X,H") >>> alias = c >>> alias *= 2 >>> c stim.CliffordString("I,X,H,I,X,H") """ def __init__( self, arg: Union[int, str, stim.CliffordString, stim.PauliString, stim.Circuit], /, ) -> None: """Initializes a stim.CliffordString from the given argument. Args: arg [position-only]: This can be a variety of types, including: int: initializes an identity Clifford string of the given length. str: initializes by parsing a comma-separated list of gate names. stim.CliffordString: initializes by copying the given Clifford string. stim.PauliString: initializes by copying from the given Pauli string (ignores the sign of the Pauli string). stim.Circuit: initializes a CliffordString equivalent to the action of the circuit (as long as the circuit only contains single qubit unitary operations and annotations). Iterable: initializes by interpreting each item as a Clifford. Each item can be a single-qubit Clifford gate name (like "SQRT_X") or stim.GateData instance. Examples: >>> import stim >>> stim.CliffordString(5) stim.CliffordString("I,I,I,I,I") >>> stim.CliffordString("X,Y,Z,SQRT_X") stim.CliffordString("X,Y,Z,SQRT_X") >>> stim.CliffordString(["H", stim.gate_data("S")]) stim.CliffordString("H,S") >>> stim.CliffordString(stim.PauliString("XYZ")) stim.CliffordString("X,Y,Z") >>> stim.CliffordString(stim.CliffordString("X,Y,Z")) stim.CliffordString("X,Y,Z") >>> stim.CliffordString(stim.Circuit(''' ... H 0 1 2 ... S 2 3 ... TICK ... S 3 ... I 6 ... ''')) stim.CliffordString("H,H,C_ZYX,Z,I,I,I") """ def __ipow__( self, num_qubits: int, ) -> object: """Mutates the CliffordString into itself raised to a power. Args: power: The power to raise the CliffordString's Cliffords to. This value can be negative (e.g. -1 inverts the string). Returns: The mutated Clifford string. Examples: >>> import stim >>> p = stim.CliffordString("I,X,H,S,C_XYZ") >>> p **= 3 >>> p stim.CliffordString("I,X,H,S_DAG,I") >>> p **= 2 >>> p stim.CliffordString("I,I,I,Z,I") >>> alias = p >>> alias **= 2 >>> p stim.CliffordString("I,I,I,I,I") """ def __len__( self, ) -> int: """Returns the number of Clifford operations in the string. Examples: >>> import stim >>> len(stim.CliffordString("I,X,Y,Z,H")) 5 """ def __mul__( self, rhs: Union[stim.CliffordString, int], ) -> stim.CliffordString: """CliffordString multiplication. Args: rhs: Either a stim.CliffordString or an int. If rhs is a stim.CliffordString, then the Cliffords from each string are multiplied pairwise. If rhs is an int, it is the number of times to repeat the Clifford string's contents. Examples: >>> import stim >>> stim.CliffordString("S,X,X") * stim.CliffordString("S,Z,H,Z") stim.CliffordString("Z,Y,SQRT_Y,Z") >>> stim.CliffordString("I,X,H") * 3 stim.CliffordString("I,X,H,I,X,H,I,X,H") """ def __ne__( self, arg0: stim.CliffordString, ) -> bool: """Determines if two Clifford strings have non-identical contents. """ def __pow__( self, power: int, ) -> stim.CliffordString: """Returns the CliffordString raised to a power. Args: power: The power to raise the CliffordString's Cliffords to. This value can be negative (e.g. -1 returns the inverse string). Returns: The Clifford string raised to the power. Examples: >>> import stim >>> p = stim.CliffordString("I,X,H,S,C_XYZ") >>> p**0 stim.CliffordString("I,I,I,I,I") >>> p**1 stim.CliffordString("I,X,H,S,C_XYZ") >>> p**12000001 stim.CliffordString("I,X,H,S,C_XYZ") >>> p**2 stim.CliffordString("I,I,I,Z,C_ZYX") >>> p**3 stim.CliffordString("I,X,H,S_DAG,I") >>> p**-1 stim.CliffordString("I,X,H,S_DAG,C_ZYX") """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CliffordString`. """ def __rmul__( self, lhs: int, ) -> stim.CliffordString: """CliffordString left-multiplication. Args: lhs: The number of times to repeat the Clifford string's contents. Returns: The repeated Clifford string. Examples: >>> import stim >>> 2 * stim.CliffordString("I,X,H") stim.CliffordString("I,X,H,I,X,H") >>> 0 * stim.CliffordString("I,X,H") stim.CliffordString("") >>> 5 * stim.CliffordString("I") stim.CliffordString("I,I,I,I,I") """ def __setitem__( self, index_or_slice: Union[int, slice], new_value: Union[str, stim.GateData, stim.CliffordString, stim.PauliString, stim.Tableau], ) -> None: """Overwrites an indexed Clifford, or slice of Cliffords, with the given value. Args: index_or_slice: The index of the Clifford to overwrite, or the slice of Cliffords to overwrite. new_value: Specifies the value to write into the Clifford string. This can be set to a few different types of values: - str: Name of the single qubit Clifford gate to write to the index or broadcast over the slice. - stim.GateData: The single qubit Clifford gate to write to the index or broadcast over the slice. - stim.Tableau: Must be a single qubit tableau. Specifies the single qubit Clifford gate to write to the index or broadcast over the slice. - stim.CliffordString: String of Cliffords to write into the slice. Examples: >>> import stim >>> s = stim.CliffordString("I,I,I,I,I") >>> s[1] = 'H' >>> s stim.CliffordString("I,H,I,I,I") >>> s[2:] = 'SQRT_X' >>> s stim.CliffordString("I,H,SQRT_X,SQRT_X,SQRT_X") >>> s[0] = stim.gate_data('S_DAG').inverse >>> s stim.CliffordString("S,H,SQRT_X,SQRT_X,SQRT_X") >>> s[:] = 'I' >>> s stim.CliffordString("I,I,I,I,I") >>> s[::2] = stim.CliffordString("X,Y,Z") >>> s stim.CliffordString("X,I,Y,I,Z") >>> s[0] = stim.Tableau.from_named_gate("H") >>> s stim.CliffordString("H,I,Y,I,Z") >>> s[:] = stim.Tableau.from_named_gate("S") >>> s stim.CliffordString("S,S,S,S,S") >>> s[:4] = stim.PauliString("IXYZ") >>> s stim.CliffordString("I,X,Y,Z,S") """ def __str__( self, ) -> str: """Returns a string representation of the CliffordString's operations. """ @staticmethod def all_cliffords_string( ) -> stim.CliffordString: """Returns a stim.CliffordString containing each single qubit Clifford once. Useful for things like testing that a method works on every single Clifford. Examples: >>> import stim >>> cliffords = stim.CliffordString.all_cliffords_string() >>> len(cliffords) 24 >>> print(cliffords[:8]) I,X,Y,Z,H_XY,S,S_DAG,H_NXY >>> print(cliffords[8:16]) H,SQRT_Y_DAG,H_NXZ,SQRT_Y,H_YZ,H_NYZ,SQRT_X,SQRT_X_DAG >>> print(cliffords[16:]) C_XYZ,C_XYNZ,C_NXYZ,C_XNYZ,C_ZYX,C_ZNYX,C_NZYX,C_ZYNX """ def copy( self, ) -> stim.CliffordString: """Returns a copy of the CliffordString. Returns: The copy. Examples: >>> import stim >>> c = stim.CliffordString("H,X") >>> alias = c >>> copy = c.copy() >>> c *= 5 >>> alias stim.CliffordString("H,X,H,X,H,X,H,X,H,X") >>> copy stim.CliffordString("H,X") """ @staticmethod def random( num_qubits: int, ) -> stim.CliffordString: """Samples a uniformly random CliffordString. Args: num_qubits: The number of qubits the CliffordString should act upon. Examples: >>> import stim >>> p = stim.CliffordString.random(5) >>> len(p) 5 Returns: The sampled Clifford string. """ def x_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates an X input into. For example, H conjugates X into +Z and S_DAG conjugates X into -Y. Combined with `z_outputs`, the results of this method completely specify the single qubit Clifford applied to each qubit. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> x_paulis, x_signs = stim.CliffordString("I,Y,H,S").x_outputs() >>> x_paulis stim.PauliString("+XXZY") >>> x_signs array([False, True, False, False]) >>> stim.CliffordString("I,Y,H,S").x_outputs(bit_packed_signs=True)[1] array([2], dtype=uint8) """ def y_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates a Y input into. For example, H conjugates Y into -Y and S_DAG conjugates Y into +X. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> y_paulis, y_signs = stim.CliffordString("I,X,H,S").y_outputs() >>> y_paulis stim.PauliString("+YYYX") >>> y_signs array([False, True, True, True]) >>> stim.CliffordString("I,X,H,S").y_outputs(bit_packed_signs=True)[1] array([14], dtype=uint8) """ def z_outputs( self, *, bit_packed_signs: bool = False, ) -> Tuple[stim.PauliString, np.ndarray]: """Returns what each Clifford in the CliffordString conjugates a Z input into. For example, H conjugates Z into +X and SQRT_X conjugates Z into -Y. Combined with `x_outputs`, the results of this method completely specify the single qubit Clifford applied to each qubit. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> z_paulis, z_signs = stim.CliffordString("I,Y,H,S").z_outputs() >>> z_paulis stim.PauliString("+ZZXZ") >>> z_signs array([False, True, False, False]) >>> stim.CliffordString("I,Y,H,S").z_outputs(bit_packed_signs=True)[1] array([2], dtype=uint8) """ class CompiledDemSampler: """A helper class for efficiently sampler from a detector error model. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) """ def sample( self, shots: int, *, bit_packed: bool = False, return_errors: bool = False, recorded_errors_to_replay: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray]]: """Samples the detector error model's error mechanisms to produce sample data. Args: shots: The number of times to sample from the model. bit_packed: Defaults to false. False: the returned numpy arrays have dtype=np.bool_. True: the returned numpy arrays have dtype=np.uint8 and pack 8 bits into each byte. Setting this to True is equivalent to running `np.packbits(data, bitorder='little', axis=1)` on each output value, but has the performance benefit of the data never being expanded into an unpacked form. return_errors: Defaults to False. False: the third entry of the returned tuple is None. True: the third entry of the returned tuple is a numpy array recording which errors were sampled. recorded_errors_to_replay: Defaults to None, meaning sample errors randomly. If not None, this is expected to be a 2d numpy array specifying which errors to apply (e.g. one returned from a previous call to the sample method). The array must have dtype=np.bool_ and shape=(num_shots, num_errors) or dtype=np.uint8 and shape=(num_shots, math.ceil(num_errors / 8)). Returns: A tuple (detector_data, obs_data, error_data). Assuming bit_packed is False and return_errors is True: - If error_data[s, k] is True, then the error with index k fired in the shot with index s. - If detector_data[s, k] is True, then the detector with index k ended up flipped in the shot with index s. - If obs_data[s, k] is True, then the observable with index k ended up flipped in the shot with index s. The dtype and shape of the data depends on the arguments: if bit_packed: detector_data.shape == (num_shots, math.ceil(num_detectors / 8)) detector_data.dtype == np.uint8 obs_data.shape == (num_shots, math.ceil(num_observables / 8)) obs_data.dtype == np.uint8 if return_errors: error_data.shape = (num_shots, math.ceil(num_errors / 8)) error_data.dtype = np.uint8 else: error_data is None else: detector_data.shape == (num_shots, num_detectors) detector_data.dtype == np.bool_ obs_data.shape == (num_shots, num_observables) obs_data.dtype == np.bool_ if return_errors: error_data.shape = (num_shots, num_errors) error_data.dtype = np.bool_ else: error_data is None Note that bit packing is done using little endian order on the last axis (i.e. like `np.packbits(data, bitorder='little', axis=1)`). Examples: >>> import stim >>> import numpy as np >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> # Taking samples. >>> det_data, obs_data, err_data_not_requested = sampler.sample(shots=4) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data_not_requested is None True >>> # Recording errors. >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) >>> # Bit packing. >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True, ... bit_packed=True) >>> det_data array([[6], [6], [6], [6]], dtype=uint8) >>> obs_data array([[1], [1], [1], [1]], dtype=uint8) >>> err_data array([[2], [2], [2], [2]], dtype=uint8) >>> # Recording and replaying errors. >>> noisy_dem = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.25) D1 ... ''') >>> noisy_sampler = noisy_dem.compile_sampler() >>> det_data, obs_data, err_data = noisy_sampler.sample( ... shots=100, ... return_errors=True) >>> replay_det_data, replay_obs_data, _ = noisy_sampler.sample( ... shots=100, ... recorded_errors_to_replay=err_data) >>> np.array_equal(det_data, replay_det_data) True >>> np.array_equal(obs_data, replay_obs_data) True """ def sample_write( self, shots: int, *, det_out_file: Union[None, str, pathlib.Path], det_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', obs_out_file: Union[None, str, pathlib.Path], obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', err_out_file: Union[None, str, pathlib.Path] = None, err_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', replay_err_in_file: Union[None, str, pathlib.Path] = None, replay_err_in_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Samples the detector error model and writes the results to disk. Args: shots: The number of times to sample from the model. det_out_file: Where to write detection event data. If None: detection event data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase det_out_format: The format to write the detection event data in (e.g. "01" or "b8"). obs_out_file: Where to write observable flip data. If None: observable flip data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase obs_out_format: The format to write the observable flip data in (e.g. "01" or "b8"). err_out_file: Where to write errors-that-occurred data. If None: errors-that-occurred data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase err_out_format: The format to write the errors-that-occurred data in (e.g. "01" or "b8"). replay_err_in_file: If this is specified, errors are replayed from data instead of generated randomly. The following types are supported: - None: errors are generated randomly according to the probabilities in the detector error model. - str or pathlib.Path: the file at the given path is opened and errors-to-apply data is read from there. - io.IOBase: NOT IMPLEMENTED replay_err_in_format: The format to write the errors-that-occurred data in (e.g. "01" or "b8"). Returns: Nothing. Results are written to disk. Examples: >>> import stim >>> import tempfile >>> import pathlib >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(0) D1 ... error(0) D0 ... error(1) D1 D2 L0 ... error(0) D0 ... ''') >>> sampler = dem.compile_sampler() >>> with tempfile.TemporaryDirectory() as d: ... d = pathlib.Path(d) ... sampler.sample_write( ... shots=1, ... det_out_file=d / 'dets.01', ... det_out_format='01', ... obs_out_file=d / 'obs.01', ... obs_out_format='01', ... err_out_file=d / 'err.hits', ... err_out_format='hits', ... ) ... with open(d / 'dets.01') as f: ... assert f.read() == "011\n" ... with open(d / 'obs.01') as f: ... assert f.read() == "1\n" ... with open(d / 'err.hits') as f: ... assert f.read() == "3\n" """ class CompiledDetectorSampler: """An analyzed stabilizer circuit whose detection events can be sampled quickly. """ def __init__( self, circuit: stim.Circuit, *, seed: object = None, ) -> None: """Creates an object that can sample the detection events from a circuit. Args: circuit: The circuit to sample from. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Returns: An initialized stim.CompiledDetectorSampler. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(1.0) 0 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[ True]]) """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.CompiledDetectorSampler`. """ def sample( self, shots: int, *, prepend_observables: bool = False, append_observables: bool = False, separate_observables: bool = False, bit_packed: bool = False, dets_out: Optional[np.ndarray] = None, obs_out: Optional[np.ndarray] = None, ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: """Returns a numpy array containing a batch of detector samples from the circuit. The circuit must define the detectors using DETECTOR instructions. Observables defined by OBSERVABLE_INCLUDE instructions can also be included in the results as honorary detectors. Args: shots: The number of times to sample every detector in the circuit. separate_observables: Defaults to False. When set to True, the return value is a (detection_events, observable_flips) tuple instead of a flat detection_events array. prepend_observables: Defaults to false. When set, observables are included with the detectors and are placed at the start of the results. append_observables: Defaults to false. When set, observables are included with the detectors and are placed at the end of the results. bit_packed: Returns a uint8 numpy array with 8 bits per byte, instead of a bool_ numpy array with 1 bit per byte. Uses little endian packing. dets_out: Defaults to None. Specifies a pre-allocated numpy array to write the detection event data into. This array must have the correct shape and dtype. obs_out: Defaults to None. Specifies a pre-allocated numpy array to write the observable flip data into. This array must have the correct shape and dtype. Returns: A numpy array or tuple of numpy arrays containing the samples. if separate_observables=False and bit_packed=False: A single numpy array. dtype=bool_ shape=( shots, num_detectors + num_observables * ( append_observables + prepend_observables), ) The bit for detection event `m` in shot `s` is at result[s, m] if separate_observables=False and bit_packed=True: A single numpy array. dtype=uint8 shape=( shots, math.ceil((num_detectors + num_observables * ( append_observables + prepend_observables)) / 8), ) The bit for detection event `m` in shot `s` is at (result[s, m // 8] >> (m % 8)) & 1 if separate_observables=True and bit_packed=False: A (dets, obs) tuple. dets.dtype=bool_ dets.shape=(shots, num_detectors) obs.dtype=bool_ obs.shape=(shots, num_observables) The bit for detection event `m` in shot `s` is at dets[s, m] The bit for observable `m` in shot `s` is at obs[s, m] if separate_observables=True and bit_packed=True: A (dets, obs) tuple. dets.dtype=uint8 dets.shape=(shots, math.ceil(num_detectors / 8)) obs.dtype=uint8 obs.shape=(shots, math.ceil(num_observables / 8)) The bit for detection event `m` in shot `s` is at (dets[s, m // 8] >> (m % 8)) & 1 The bit for observable `m` in shot `s` is at (obs[s, m // 8] >> (m % 8)) & 1 Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(1.0) 0 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[ True]]) """ def sample_bit_packed( self, shots: int, *, prepend_observables: bool = False, append_observables: bool = False, ) -> object: """[DEPRECATED] Use sampler.sample(..., bit_packed=True) instead. Returns a numpy array containing bit packed detector samples from the circuit. The circuit must define the detectors using DETECTOR instructions. Observables defined by OBSERVABLE_INCLUDE instructions can also be included in the results as honorary detectors. Args: shots: The number of times to sample every detector in the circuit. prepend_observables: Defaults to false. When set, observables are included with the detectors and are placed at the start of the results. append_observables: Defaults to false. When set, observables are included with the detectors and are placed at the end of the results. Returns: A numpy array with `dtype=uint8` and `shape=(shots, n)` where `n` is `num_detectors + num_observables*(append_observables+prepend_observables)`. The bit for detection event `m` in shot `s` is at `result[s, (m // 8)] & 2**(m % 8)`. """ def sample_write( self, shots: int, *, filepath: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', obs_out_filepath: Optional[Union[str, pathlib.Path]] = None, obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', prepend_observables: bool = False, append_observables: bool = False, ) -> None: """Samples detection events from the circuit and writes them to a file. Args: shots: The number of times to sample every measurement in the circuit. filepath: The file to write the results to. format: The output format to write the results with. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". obs_out_filepath: Sample observables as part of each shot, and write them to this file. This keeps the observable data separate from the detector data. obs_out_format: If writing the observables to a file, this is the format to write them in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". prepend_observables: Sample observables as part of each shot, and put them at the start of the detector data. append_observables: Sample observables as part of each shot, and put them at the end of the detector data. Returns: None. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f"{d}/tmp.dat" ... c = stim.Circuit(''' ... X_ERROR(1) 0 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''') ... c.compile_detector_sampler().sample_write( ... shots=3, ... filepath=path, ... format="dets") ... with open(path) as f: ... print(f.read(), end='') shot D0 shot D0 shot D0 """ class CompiledMeasurementSampler: """An analyzed stabilizer circuit whose measurements can be sampled quickly. """ def __init__( self, circuit: stim.Circuit, *, skip_reference_sample: bool = False, seed: object = None, reference_sample: object = None, ) -> None: """Creates a measurement sampler for the given circuit. The sampler uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the sampler, as a baseline for deriving more samples using an error propagation simulator. Args: circuit: The stim circuit to sample from. skip_reference_sample: Defaults to False. When set to True, the reference sample used by the sampler is initialized to all-zeroes instead of being collected from the circuit. This means that the results returned by the sampler are actually whether or not each measurement was *flipped*, instead of true measurement results. Forcing an all-zero reference sample is useful when you are only interested in error propagation and don't want to have to deal with the fact that some measurements want to be On when no errors occur. It is also useful when you know for sure that the all-zero result is actually a possible result from the circuit (under noiseless execution), meaning it is a valid reference sample as good as any other. Computing the reference sample is the most time consuming and memory intensive part of simulating the circuit, so promising that the simulator can safely skip that step is an effective optimization. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. reference_sample: The data to xor into the measurement flips produced by the frame simulator, in order to produce proper measurement results. This can either be specified as an `np.bool_` array or a bit packed `np.uint8` array (little endian). Under normal conditions, the reference sample should be a valid noiseless sample of the circuit, such as the one returned by `circuit.reference_sample()`. If this argument is not provided, the reference sample will be set to `circuit.reference_sample()`, unless `skip_reference_sample=True` is used, in which case it will be set to all-zeros. Returns: An initialized stim.CompiledMeasurementSampler. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[ True, False, True, True]]) """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CompiledMeasurementSampler`. """ def sample( self, shots: int, *, bit_packed: bool = False, ) -> np.ndarray: """Samples a batch of measurement samples from the circuit. Args: shots: The number of times to sample every measurement in the circuit. bit_packed: Returns a uint8 numpy array with 8 bits per byte, instead of a bool_ numpy array with 1 bit per byte. Uses little endian packing. Returns: A numpy array containing the samples. If bit_packed=False: dtype=bool_ shape=(shots, circuit.num_measurements) The bit for measurement `m` in shot `s` is at result[s, m] If bit_packed=True: dtype=uint8 shape=(shots, math.ceil(circuit.num_measurements / 8)) The bit for measurement `m` in shot `s` is at (result[s, m // 8] >> (m % 8)) & 1 Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[ True, False, True, True]]) """ def sample_bit_packed( self, shots: int, ) -> np.ndarray: """[DEPRECATED] Use sampler.sample(..., bit_packed=True) instead. Samples a bit packed batch of measurement samples from the circuit. Args: shots: The number of times to sample every measurement in the circuit. Returns: A numpy array with `dtype=uint8` and `shape=(shots, (num_measurements + 7) // 8)`. The bit for measurement `m` in shot `s` is at `result[s, (m // 8)] & 2**(m % 8)`. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 1 2 3 4 5 6 7 10 ... M 0 1 2 3 4 5 6 7 8 9 10 ... ''') >>> s = c.compile_sampler() >>> s.sample_bit_packed(shots=1) array([[255, 4]], dtype=uint8) """ def sample_write( self, shots: int, *, filepath: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Samples measurements from the circuit and writes them to a file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f"{d}/tmp.dat" ... c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') ... c.compile_sampler().sample_write(5, filepath=path, format="01") ... with open(path) as f: ... print(f.read(), end='') 1011 1011 1011 1011 1011 Args: shots: The number of times to sample every measurement in the circuit. filepath: The file to write the results to. format: The output format to write the results with. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". Returns: None. """ class CompiledMeasurementsToDetectionEventsConverter: """A tool for quickly converting measurements from an analyzed stabilizer circuit into detection events. """ def __init__( self, circuit: stim.Circuit, *, skip_reference_sample: bool = False, ) -> None: """Creates a measurement-to-detection-events converter for the given circuit. The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is. Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit. Args: circuit: The stim circuit to use for conversions. skip_reference_sample: Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution). Returns: An initialized stim.CompiledMeasurementsToDetectionEventsConverter. Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> converter.convert( ... measurements=np.array([[0], [1]], dtype=np.bool_), ... append_observables=False, ... ) array([[ True], [False]]) """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.CompiledMeasurementsToDetectionEventsConverter`. """ @overload def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, append_observables: bool = False, bit_packed: bool = False, ) -> np.ndarray: pass @overload def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, separate_observables: Literal[True], append_observables: bool = False, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: pass def convert( self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, separate_observables: bool = False, append_observables: bool = False, bit_packed: bool = False, ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: """Converts measurement data into detection event data. Args: measurements: A numpy array containing measurement data. The dtype of the array is used to determine if it is bit packed or not. dtype=np.bool_ (unpacked data): shape=(num_shots, circuit.num_measurements) dtype=np.uint8 (bit packed data): shape=(num_shots, math.ceil(circuit.num_measurements / 8)) sweep_bits: Optional. A numpy array containing sweep data for the `sweep[k]` controls in the circuit. The dtype of the array is used to determine if it is bit packed or not. dtype=np.bool_ (unpacked data): shape=(num_shots, circuit.num_sweep_bits) dtype=np.uint8 (bit packed data): shape=(num_shots, math.ceil(circuit.num_sweep_bits / 8)) separate_observables: Defaults to False. When set to True, two numpy arrays are returned instead of one, with the second array containing the observable flip data. append_observables: Defaults to False. When set to True, the observables in the circuit are treated as if they were additional detectors. Their results are appended to the end of the detection event data. bit_packed: Defaults to False. When set to True, the returned numpy array contains bit packed data (dtype=np.uint8 with 8 bits per item) instead of unpacked data (dtype=np.bool_). Returns: The detection event data and (optionally) observable data. The result is a single numpy array if separate_observables is false, otherwise it's a tuple of two numpy arrays. When returning two numpy arrays, the first array is the detection event data and the second is the observable flip data. The dtype of the returned arrays is np.bool_ if bit_packed is false, otherwise they're np.uint8 arrays. shape[0] of the array(s) is the number of shots. shape[1] of the array(s) is the number of bits per shot (divided by 8 if bit packed) (e.g. for just detection event data it would be circuit.num_detectors). Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 1 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-2] ... ''').compile_m2d_converter() >>> dets, obs = converter.convert( ... measurements=np.array([[1, 0], ... [1, 0], ... [1, 0], ... [0, 0], ... [1, 0]], dtype=np.bool_), ... separate_observables=True, ... ) >>> dets array([[False, False], [False, False], [False, False], [False, True], [False, False]]) >>> obs array([[False], [False], [False], [ True], [False]]) """ def convert_file( self, *, measurements_filepath: Union[str, pathlib.Path], measurements_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', sweep_bits_filepath: Optional[Union[str, pathlib.Path]] = None, sweep_bits_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', detection_events_filepath: Union[str, pathlib.Path], detection_events_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', append_observables: bool = False, obs_out_filepath: Optional[Union[str, pathlib.Path]] = None, obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', ) -> None: """Reads measurement data from a file and writes detection events to another file. Args: measurements_filepath: A file containing measurement data to be converted. measurements_format: The format the measurement data is stored in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". detection_events_filepath: Where to save detection event data to. detection_events_format: The format to save the detection event data in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". sweep_bits_filepath: Defaults to None. A file containing sweep data, or None. When specified, sweep data (used for `sweep[k]` controls in the circuit, which can vary from shot to shot) will be read from the given file. When not specified, all sweep bits default to False and no sweep-controlled operations occur. sweep_bits_format: The format the sweep data is stored in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". obs_out_filepath: Sample observables as part of each shot, and write them to this file. This keeps the observable data separate from the detector data. obs_out_format: If writing the observables to a file, this is the format to write them in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". append_observables: When True, the observables in the circuit are included as part of the detection event data. Specifically, they are treated as if they were additional detectors at the end of the circuit. When False, observable data is not output. Examples: >>> import stim >>> import tempfile >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> with tempfile.TemporaryDirectory() as d: ... with open(f"{d}/measurements.01", "w") as f: ... print("0", file=f) ... print("1", file=f) ... converter.convert_file( ... measurements_filepath=f"{d}/measurements.01", ... detection_events_filepath=f"{d}/detections.01", ... append_observables=False, ... ) ... with open(f"{d}/detections.01") as f: ... print(f.read(), end="") 1 0 """ class DemInstruction: """An instruction from a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 L0 ... error(0.125) D1 D2 ... error(0.125) D2 D3 ... error(0.125) D3 ... ''') >>> instruction = model[0] >>> instruction stim.DemInstruction('error', [0.125], [stim.target_relative_detector_id(0)]) """ def __eq__( self, arg0: stim.DemInstruction, ) -> bool: """Determines if two instructions have identical contents. """ def __init__( self, type: str, args: Optional[Iterable[float]] = None, targets: Optional[Iterable[stim.DemTarget]] = None, *, tag: str = "", ) -> None: """Creates or parses a stim.DemInstruction. Args: type: The name of the instruction type (e.g. "error" or "shift_detectors"). If `args` and `targets` aren't specified, this can also be set to a full line of text from a dem file, like "error(0.25) D0". args: Numeric values parameterizing the instruction (e.g. the 0.1 in "error(0.1)"). targets: The objects the instruction involves (e.g. the "D0" and "L1" in "error(0.1) D0 L1"). tag: An arbitrary piece of text attached to the instruction. Examples: >>> import stim >>> instruction = stim.DemInstruction( ... 'error', ... [0.125], ... [stim.target_relative_detector_id(5)], ... tag='test-tag', ... ) >>> print(instruction) error[test-tag](0.125) D5 >>> print(stim.DemInstruction('error(0.125) D5 L6 ^ D4 # comment')) error(0.125) D5 L6 ^ D4 """ def __ne__( self, arg0: stim.DemInstruction, ) -> bool: """Determines if two instructions have non-identical contents. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.DetectorErrorModel`. """ def __str__( self, ) -> str: """Returns detector error model (.dem) instructions (that can be parsed by stim) for the model. """ def args_copy( self, ) -> List[float]: """Returns a copy of the list of numbers parameterizing the instruction. For example, this would be coordinates of a detector instruction or the probability of an error instruction. The result is a copy, meaning that editing it won't change the instruction's targets or future copies. Examples: >>> import stim >>> instruction = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''')[0] >>> instruction.args_copy() [0.125] >>> instruction.args_copy() == instruction.args_copy() True >>> instruction.args_copy() is instruction.args_copy() False """ @property def tag( self, ) -> str: """Returns the arbitrary text tag attached to the instruction. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error[test-tag](0.125) D0 ... error(0.125) D0 ... ''') >>> dem[0].tag 'test-tag' >>> dem[1].tag '' """ def target_groups( self, ) -> List[List[stim.DemTarget]]: """Returns a copy of the instruction's targets, split by target separators. When a detector error model instruction contains a suggested decomposition, its targets contain separators (`stim.DemTarget("^")`). This method splits the targets into groups based the separators, similar to how `str.split` works. Returns: A list of groups of targets. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.01) D0 D1 ^ D2 ... error(0.01) D0 L0 ... error(0.01) ... ''') >>> dem[0].target_groups() [[stim.DemTarget('D0'), stim.DemTarget('D1')], [stim.DemTarget('D2')]] >>> dem[1].target_groups() [[stim.DemTarget('D0'), stim.DemTarget('L0')]] >>> dem[2].target_groups() [[]] """ def targets_copy( self, ) -> List[Union[int, stim.DemTarget]]: """Returns a copy of the instruction's targets. The result is a copy, meaning that editing it won't change the instruction's targets or future copies. Examples: >>> import stim >>> instruction = stim.DetectorErrorModel(''' ... error(0.125) D0 L2 ... ''')[0] >>> instruction.targets_copy() [stim.DemTarget('D0'), stim.DemTarget('L2')] >>> instruction.targets_copy() == instruction.targets_copy() True >>> instruction.targets_copy() is instruction.targets_copy() False """ @property def type( self, ) -> str: """The name of the instruction type (e.g. "error" or "shift_detectors"). """ class DemRepeatBlock: """A repeat block from a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... repeat 100 { ... error(0.125) D0 D1 ... shift_detectors 1 ... } ... ''') >>> model[0] stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' error(0.125) D0 D1 shift_detectors 1 ''')) """ def __eq__( self, arg0: stim.DemRepeatBlock, ) -> bool: """Determines if two repeat blocks are identical. """ def __init__( self, repeat_count: int, block: stim.DetectorErrorModel, ) -> None: """Creates a stim.DemRepeatBlock. Args: repeat_count: The number of times the repeat block's body is supposed to execute. block: The body of the repeat block as a DetectorErrorModel containing the instructions to repeat. Examples: >>> import stim >>> repeat_block = stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' ... error(0.125) D0 D1 ... shift_detectors 1 ... ''')) """ def __ne__( self, arg0: stim.DemRepeatBlock, ) -> bool: """Determines if two repeat blocks are different. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.DemRepeatBlock`. """ def body_copy( self, ) -> stim.DetectorErrorModel: """Returns a copy of the block's body, as a stim.DetectorErrorModel. Examples: >>> import stim >>> body = stim.DetectorErrorModel(''' ... error(0.125) D0 D1 ... shift_detectors 1 ... ''') >>> repeat_block = stim.DemRepeatBlock(100, body) >>> repeat_block.body_copy() == body True >>> repeat_block.body_copy() is repeat_block.body_copy() False """ @property def repeat_count( self, ) -> int: """The number of times the repeat block's body is supposed to execute. """ @property def type( self, ) -> object: """Returns the type name "repeat". This is a duck-typing convenience method. It exists so that code that doesn't know whether it has a `stim.DemInstruction` or a `stim.DemRepeatBlock` can check the type field without having to do an `instanceof` check first. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.1) D0 L0 ... repeat 5 { ... error(0.1) D0 D1 ... shift_detectors 1 ... } ... logical_observable L0 ... ''') >>> [instruction.type for instruction in dem] ['error', 'repeat', 'logical_observable'] """ class DemTarget: """An instruction target from a detector error model (.dem) file. """ def __eq__( self, arg0: stim.DemTarget, ) -> bool: """Determines if two `stim.DemTarget`s are identical. """ def __init__( self, arg: object, /, ) -> None: """Creates a stim.DemTarget from the given object. Args: arg: A string to parse as a stim.DemTarget, or some other object to convert into a stim.DemTarget. Examples: >>> import stim >>> stim.DemTarget("D5") == stim.target_relative_detector_id(5) True >>> stim.DemTarget("L2") == stim.target_logical_observable_id(2) True >>> stim.DemTarget("^") == stim.target_separator() True """ def __ne__( self, arg0: stim.DemTarget, ) -> bool: """Determines if two `stim.DemTarget`s are different. """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.DemTarget`. """ def __str__( self, ) -> str: """Returns a text description of the detector error model target. """ def is_logical_observable_id( self, ) -> bool: """Determines if the detector error model target is a logical observable id target. In a detector error model file, observable targets are prefixed by `L`. For example, in `error(0.25) D0 L1` the `L1` is an observable target. Examples: >>> import stim >>> stim.DemTarget("L2").is_logical_observable_id() True >>> stim.DemTarget("D3").is_logical_observable_id() False >>> stim.DemTarget("^").is_logical_observable_id() False """ def is_relative_detector_id( self, ) -> bool: """Determines if the detector error model target is a relative detector id target. In a detector error model file, detectors are prefixed by `D`. For example, in `error(0.25) D0 L1` the `D0` is a relative detector target. Examples: >>> import stim >>> stim.DemTarget("L2").is_relative_detector_id() False >>> stim.DemTarget("D3").is_relative_detector_id() True >>> stim.DemTarget("^").is_relative_detector_id() False """ def is_separator( self, ) -> bool: """Determines if the detector error model target is a separator. Separates separate the components of a suggested decompositions within an error. For example, the `^` in `error(0.25) D1 D2 ^ D3 D4` is the separator. Examples: >>> import stim >>> stim.DemTarget("L2").is_separator() False >>> stim.DemTarget("D3").is_separator() False >>> stim.DemTarget("^").is_separator() True """ @staticmethod def logical_observable_id( index: int, ) -> stim.DemTarget: """Returns a logical observable id identifying a frame change. Args: index: The index of the observable. Returns: The logical observable target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.logical_observable_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) L13 ''') """ @staticmethod def relative_detector_id( index: int, ) -> stim.DemTarget: """Returns a relative detector id (e.g. "D5" in a .dem file). Args: index: The index of the detector, relative to the current detector offset. Returns: The relative detector target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D13 ''') """ @staticmethod def separator( ) -> stim.DemTarget: """Returns a target separator (e.g. "^" in a .dem file). Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(1), ... stim.DemTarget.separator(), ... stim.DemTarget.relative_detector_id(2), ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D1 ^ D2 ''') """ @property def val( self, ) -> int: """Returns the target's integer value. Example: >>> import stim >>> stim.DemTarget("D5").val 5 >>> stim.DemTarget("L6").val 6 """ class DemTargetWithCoords: """A detector error model instruction target with associated coords. It is also guaranteed that, if the type of the DEM target is a relative detector id, it is actually absolute (i.e. relative to 0). For example, if the DEM target is a detector from a circuit with coordinate arguments given to detectors, the coords field will contain the coordinate data for the detector. This is helpful information to have available when debugging a problem in a circuit, instead of having to constantly manually look up the coordinates of a detector index in order to understand what is happening. Examples: >>> import stim >>> t = stim.DemTargetWithCoords(stim.DemTarget("D1"), [1.5, 2.0]) >>> t.dem_target stim.DemTarget('D1') >>> t.coords [1.5, 2.0] """ def __init__( self, dem_target: stim.DemTarget, coords: List[float], ) -> None: """Creates a stim.DemTargetWithCoords. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0] stim.DemTargetWithCoords(dem_target=stim.DemTarget('D0'), coords=[2, 3]) """ @property def coords( self, ) -> List[float]: """Returns the associated coordinate information as a list of floats. If there is no coordinate information, returns an empty list. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0].coords [2.0, 3.0] """ @property def dem_target( self, ) -> stim.DemTarget: """Returns the actual DEM target as a `stim.DemTarget`. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0].dem_target stim.DemTarget('D0') """ class DetectorErrorModel: """An error model built out of independent error mechanics. This class is one of the most important classes in Stim, because it is the mechanism used to explain circuits to decoders. A typical workflow would look something like: 1. Create a quantum error correction circuit annotated with detectors and observables. 2. Fail at configuring your favorite decoder using the circuit, because it's a pain to convert circuit error mechanisms into a format understood by the decoder. 2a. Call circuit.detector_error_model(), with decompose_errors=True if working with a matching-based code. This converts the circuit errors into a straightforward list of independent "with probability p these detectors and observables get flipped" terms. 3. Write tedious but straightforward glue code to create whatever graph-like object the decoder needs from the detector error model. 3a. Actually, ideally, someone has already done that for you. For example, pymatching can take detector error models directly and sinter knows how to explain a detector error model to fusion_blossom. 4. Get samples using circuit.compile_detector_sampler(), feed them to the decoder, and compare its observable flip predictions to the actual flips recorded in the samples. 4a. Actually, sinter will basically handle steps 2 through 4 for you. So you should probably have just generated your circuits, called `sinter collect` on them, then `sinter plot` on the results. 5. Write the paper. Error mechanisms are described in terms of the visible detection events and the hidden observable frame changes that they causes. Error mechanisms can also suggest decompositions of their effects into components, which can be helpful for decoders that want to work with a simpler decomposed error model instead of the full error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 L0 ... error(0.125) D1 D2 ... error(0.125) D2 D3 ... error(0.125) D3 ... ''') >>> len(model) 5 >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model() stim.DetectorErrorModel(''' error(0.125) D0 error(0.375) D0 D1 error(0.25) D1 ''') """ def __add__( self, second: stim.DetectorErrorModel, ) -> stim.DetectorErrorModel: """Creates a detector error model by appending two models. Examples: >>> import stim >>> m1 = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''') >>> m2 = stim.DetectorErrorModel(''' ... error(0.25) D1 ... ''') >>> m1 + m2 stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D1 ''') """ def __eq__( self, arg0: stim.DetectorErrorModel, ) -> bool: """Determines if two detector error models have identical contents. """ @overload def __getitem__( self, index_or_slice: int, ) -> Union[stim.DemInstruction, stim.DemRepeatBlock]: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.DetectorErrorModel: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns copies of instructions from the detector error model. Args: index_or_slice: An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D1 L1 ... repeat 100 { ... error(0.125) D1 D2 ... shift_detectors 1 ... } ... error(0.125) D2 ... logical_observable L0 ... detector D5 ... ''') >>> model[0] stim.DemInstruction('error', [0.125], [stim.target_relative_detector_id(0)]) >>> model[2] stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' error(0.125) D1 D2 shift_detectors 1 ''')) >>> model[1::2] stim.DetectorErrorModel(''' error(0.125) D1 L1 error(0.125) D2 detector D5 ''') """ def __iadd__( self, second: stim.DetectorErrorModel, ) -> stim.DetectorErrorModel: """Appends a detector error model into the receiving model (mutating it). Examples: >>> import stim >>> m1 = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''') >>> m2 = stim.DetectorErrorModel(''' ... error(0.25) D1 ... ''') >>> m1 += m2 >>> print(repr(m1)) stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D1 ''') """ def __imul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Mutates the detector error model by putting its contents into a repeat block. Special case: if the repetition count is 0, the model is cleared. Special case: if the repetition count is 1, nothing happens. Args: repetitions: The number of times the repeat block should repeat. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> m *= 3 >>> print(m) repeat 3 { error(0.25) D0 shift_detectors 1 } """ def __init__( self, detector_error_model_text: str = '', ) -> None: """Creates a stim.DetectorErrorModel. Args: detector_error_model_text: Defaults to empty. Describes instructions to append into the circuit in the detector error model (.dem) format. Examples: >>> import stim >>> empty = stim.DetectorErrorModel() >>> not_empty = stim.DetectorErrorModel(''' ... error(0.125) D0 L0 ... ''') """ def __len__( self, ) -> int: """Returns the number of top-level instructions/blocks in the detector error model. Instructions inside of blocks are not included in this count. Examples: >>> import stim >>> len(stim.DetectorErrorModel()) 0 >>> len(stim.DetectorErrorModel(''' ... error(0.1) D0 D1 ... shift_detectors 100 ... logical_observable L5 ... ''')) 3 >>> len(stim.DetectorErrorModel(''' ... repeat 100 { ... error(0.1) D0 D1 ... error(0.1) D1 D2 ... } ... ''')) 1 """ def __mul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Repeats the detector error model using a repeat block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the repeat block should repeat. Returns: repetitions=0: An empty detector error model. repetitions=1: A copy of this detector error model. repetitions>=2: A detector error model with a single repeat block, where the contents of that repeat block are this detector error model. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> m * 3 stim.DetectorErrorModel(''' repeat 3 { error(0.25) D0 shift_detectors 1 } ''') """ def __ne__( self, arg0: stim.DetectorErrorModel, ) -> bool: """Determines if two detector error models have non-identical contents. """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.DetectorErrorModel`. """ def __rmul__( self, repetitions: int, ) -> stim.DetectorErrorModel: """Repeats the detector error model using a repeat block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the repeat block should repeat. Returns: repetitions=0: An empty detector error model. repetitions=1: A copy of this detector error model. repetitions>=2: A detector error model with a single repeat block, where the contents of that repeat block are this detector error model. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> 3 * m stim.DetectorErrorModel(''' repeat 3 { error(0.25) D0 shift_detectors 1 } ''') """ def __str__( self, ) -> str: """Returns the contents of a detector error model file (.dem) encoding the model. """ def append( self, instruction: object, parens_arguments: object = None, targets: List[object] = (), *, tag: str = '', ) -> None: """Appends an instruction to the detector error model. Args: instruction: Either the name of an instruction, a stim.DemInstruction, a stim.DemRepeatBlock. or a stim.DetectorErrorModel. The `parens_arguments`, `targets`, and 'tag' arguments should be given iff the instruction is a name. parens_arguments: Numeric values parameterizing the instruction. The numbers inside parentheses in a detector error model file (eg. the `0.25` in `error(0.25) D0`). This argument can be given either a list of doubles, or a single double (which will be implicitly wrapped into a list). targets: The instruction targets, such as the `D0` in `error(0.25) D0`. tag: An arbitrary piece of text attached to the repeat instruction. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.125, [ ... stim.DemTarget.relative_detector_id(1), ... ]) >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(1), ... stim.DemTarget.separator(), ... stim.DemTarget.relative_detector_id(2), ... stim.DemTarget.logical_observable_id(3), ... ], tag='test-tag') >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 ''') >>> m.append("shift_detectors", (1, 2, 3), [5]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 ''') >>> m += m * 3 >>> m.append(m[0]) >>> m.append(m[-2]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 repeat 3 { error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } error(0.125) D1 repeat 3 { error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } ''') """ def approx_equals( self, other: object, *, atol: float, ) -> bool: """Checks if detector error models are approximately equal. Two detector error model are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example `error(0.100) D0` is approximately equal to `error(0.099) D0` within an absolute tolerance of 0.002. All other details of the models (such as the ordering of errors and their targets) must be exactly the same. Args: other: The detector error model, or other object, to compare to this one. atol: The absolute error tolerance. The maximum amount each probability may have been perturbed by. Returns: True if the given object is a detector error model approximately equal up to the receiving circuit up to the given tolerance, otherwise False. Examples: >>> import stim >>> base = stim.DetectorErrorModel(''' ... error(0.099) D0 D1 ... ''') >>> base.approx_equals(base, atol=0) True >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0) False >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0.0001) False >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0.01) True >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.099) D0 D1 L0 L1 L2 L3 L4 ... '''), atol=9999) False """ def clear( self, ) -> None: """Clears the contents of the detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.1) D0 D1 ... ''') >>> model.clear() >>> model stim.DetectorErrorModel() """ def compile_sampler( self, *, seed: object = None, ) -> stim.CompiledDemSampler: """Returns a CompiledDemSampler that can batch sample from detector error models. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Returns: A seeded stim.CompiledDemSampler for the given detector error model. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) """ def copy( self, ) -> stim.DetectorErrorModel: """Returns a copy of the detector error model. The copy is an independent detector error model with the same contents. Examples: >>> import stim >>> c1 = stim.DetectorErrorModel("error(0.1) D0 D1") >>> c2 = c1.copy() >>> c2 is c1 False >>> c2 == c1 True """ def diagram( self, type: Literal["matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html"] = 'matchgraph-svg', ) -> Any: """Returns a diagram of the circuit, from a variety of options. Args: type: The type of diagram. Available types are: "matchgraph-svg": An image of the decoding graph of the detector error model. Red lines are errors crossing a logical observable. Blue lines are undecomposed hyper errors. "matchgraph-svg-html": Same as matchgraph-svg but with the SVG wrapped in a resizable HTML iframe. "matchgraph-3d": A 3d model of the decoding graph of the detector error model. Red lines are errors crossing a logical observable. Blue lines are undecomposed hyper errors. GLTF files can be opened with a variety of programs, or opened online in viewers such as https://gltf-viewer.donmccurdy.com/ . Red lines are errors crossing a logical observable. "matchgraph-3d-html": Same 3d model as 'match-graph-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. Returns: An object whose `__str__` method returns the diagram, so that writing the diagram to a file works correctly. The returned object also defines a `_repr_html_` method, so that ipython notebooks recognize it can be shown using a specialized viewer instead of as raw text. Examples: >>> import stim >>> import tempfile >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... after_clifford_depolarization=0.01) >>> dem = circuit.detector_error_model(decompose_errors=True) >>> with tempfile.TemporaryDirectory() as d: ... diagram = circuit.diagram("match-graph-svg") ... with open(f"{d}/dem_image.svg", "w") as f: ... print(diagram, file=f) >>> with tempfile.TemporaryDirectory() as d: ... diagram = circuit.diagram("match-graph-3d") ... with open(f"{d}/dem_3d_model.gltf", "w") as f: ... print(diagram, file=f) """ def flattened( self, ) -> stim.DetectorErrorModel: """Returns the detector error model without repeat or detector_shift instructions. Returns: A `stim.DetectorErrorModel` with the same errors in the same order, but with repeat loops flattened into actually repeated instructions and with all coordinate/index shifts inlined. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... REPEAT 5 { ... error(0.25) D0 D1 ... shift_detectors 1 ... } ... error(0.125) D0 L0 ... ''').flattened() stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D0 D1 error(0.25) D1 D2 error(0.25) D2 D3 error(0.25) D3 D4 error(0.25) D4 D5 error(0.125) D5 L0 ''') """ @staticmethod def from_file( file: Union[io.TextIOBase, str, pathlib.Path], ) -> stim.DetectorErrorModel: """Reads a detector error model from a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md Args: file: A file path or open file object to read from. Returns: The circuit parsed from the file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('error(0.25) D2 D3', file=f) ... circuit = stim.DetectorErrorModel.from_file(path) >>> circuit stim.DetectorErrorModel(''' error(0.25) D2 D3 ''') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('error(0.25) D2 D3', file=f) ... with open(path) as f: ... circuit = stim.DetectorErrorModel.from_file(f) >>> circuit stim.DetectorErrorModel(''' error(0.25) D2 D3 ''') """ def get_detector_coordinates( self, only: object = None, ) -> Dict[int, List[float]]: """Returns the coordinate metadata of detectors in the detector error model. Args: only: Defaults to None (meaning include all detectors). A list of detector indices to include in the result. Detector indices beyond the end of the detector error model cause an error. Returns: A dictionary mapping integers (detector indices) to lists of floats (coordinates). Detectors with no specified coordinate data are mapped to an empty tuple. If `only` is specified, then `set(result.keys()) == set(only)`. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.25) D0 D1 ... detector(1, 2, 3) D1 ... shift_detectors(5) 1 ... detector(1, 2) D2 ... ''') >>> dem.get_detector_coordinates() {0: [], 1: [1.0, 2.0, 3.0], 2: [], 3: [6.0, 2.0]} >>> dem.get_detector_coordinates(only=[1]) {1: [1.0, 2.0, 3.0]} """ @property def num_detectors( self, ) -> int: """Counts the number of detectors (e.g. `D2`) in the error model. Detector indices are assumed to be contiguous from 0 up to whatever the maximum detector id is. If the largest detector's absolute id is n-1, then the number of detectors is n. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model().num_detectors 2 >>> stim.DetectorErrorModel(''' ... error(0.1) D0 D199 ... ''').num_detectors 200 >>> stim.DetectorErrorModel(''' ... shift_detectors 1000 ... error(0.1) D0 D199 ... ''').num_detectors 1200 """ @property def num_errors( self, ) -> int: """Counts the number of errors (e.g. `error(0.1) D0`) in the error model. Error instructions inside repeat blocks count once per repetition. Redundant errors with the same targets count as separate errors. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... repeat 100 { ... repeat 5 { ... error(0.25) D1 ... } ... } ... ''').num_errors 501 """ @property def num_observables( self, ) -> int: """Counts the number of frame changes (e.g. `L2`) in the error model. Observable indices are assumed to be contiguous from 0 up to whatever the maximum observable id is. If the largest observable's id is n-1, then the number of observables is n. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(99) rec[-1] ... ''').detector_error_model().num_observables 100 >>> stim.DetectorErrorModel(''' ... error(0.1) L399 ... ''').num_observables 400 """ def rounded( self, arg0: int, ) -> stim.DetectorErrorModel: """Creates an equivalent detector error model but with rounded error probabilities. Args: digits: The number of digits to round to. Returns: A `stim.DetectorErrorModel` with the same instructions in the same order, but with the parens arguments of error instructions rounded to the given precision. Instructions whose error probability was rounded to zero are still included in the output. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.019499) D0 ... error(0.000001) D0 D1 ... ''') >>> dem.rounded(2) stim.DetectorErrorModel(''' error(0.02) D0 error(0) D0 D1 ''') >>> dem.rounded(3) stim.DetectorErrorModel(''' error(0.019) D0 error(0) D0 D1 ''') """ def shortest_graphlike_error( self, ignore_ungraphlike_errors: bool = True, ) -> stim.DetectorErrorModel: """Finds a minimum set of graphlike errors to produce an undetected logical error. Note that this method does not pay attention to error probabilities (other than ignoring errors with probability 0). It searches for a logical error with the minimum *number* of physical errors, not the maximum probability of those physical errors all occurring. This method works by looking for errors that have frame changes (eg. "error(0.1) D0 D1 L5" flips the frame of observable 5). These errors are converted into one or two symptoms and a net frame change. The symptoms can then be moved around by following errors touching that symptom. Each symptom is moved until it disappears into a boundary or cancels against another remaining symptom, while leaving the other symptoms alone (ensuring only one symptom is allowed to move significantly reduces waste in the search space). Eventually a path or cycle of errors is found that cancels out the symptoms, and if there is still a frame change at that point then that path or cycle is a logical error (otherwise all that was found was a stabilizer of the system; a dead end). The search process advances like a breadth first search, seeded from all the frame-change errors and branching them outward in tandem, until one of them wins the race to find a solution. Args: ignore_ungraphlike_errors: Defaults to True. When False, an exception is raised if there are any errors in the model that are not graphlike. When True, those errors are skipped as if they weren't present. A graphlike error is an error with less than two symptoms. For the purposes of this method, errors are also considered graphlike if they are decomposed into graphlike components: graphlike: error(0.1) D0 error(0.1) D0 D1 error(0.1) D0 D1 L0 not graphlike but decomposed into graphlike components: error(0.1) D0 D1 ^ D2 not graphlike, not decomposed into graphlike components: error(0.1) D0 D1 D2 error(0.1) D0 D1 D2 ^ D3 Returns: A detector error model containing just the error instructions corresponding to an undetectable logical error. There will be no other kinds of instructions (no `repeat`s, no `shift_detectors`, etc). The error probabilities will all be set to 1. The `len` of the returned model is the graphlike code distance of the circuit. But beware that in general the true code distance may be smaller. For example, in the XZ surface code with twists, the true minimum sized logical error is likely to use Y errors. But each Y error decomposes into two graphlike components (the X part and the Z part). As a result, the graphlike code distance in that context is likely to be nearly twice as large as the true code distance. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 ... error(0.125) D1 L55 ... error(0.125) D1 ... ''').shortest_graphlike_error() stim.DetectorErrorModel(''' error(1) D1 error(1) D1 L55 ''') >>> stim.DetectorErrorModel(''' ... error(0.125) D0 D1 D2 ... error(0.125) L0 ... ''').shortest_graphlike_error(ignore_ungraphlike_errors=True) stim.DetectorErrorModel(''' error(1) L0 ''') >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... before_round_data_depolarization=0.01) >>> model = circuit.detector_error_model(decompose_errors=True) >>> len(model.shortest_graphlike_error()) 7 """ def to_file( self, file: Union[io.TextIOBase, str, pathlib.Path], ) -> None: """Writes the detector error model to a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md Args: file: A file path or an open file to write to. Examples: >>> import stim >>> import tempfile >>> c = stim.DetectorErrorModel('error(0.25) D2 D3') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... c.to_file(f) ... with open(path) as f: ... contents = f.read() >>> contents 'error(0.25) D2 D3\n' >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... c.to_file(path) ... with open(path) as f: ... contents = f.read() >>> contents 'error(0.25) D2 D3\n' """ def without_tags( self, ) -> stim.DetectorErrorModel: """Returns a copy of the detector error model with all tags removed. Returns: A `stim.DetectorErrorModel` with the same instructions except all tags have been removed. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error[test-tag](0.25) D0 ... ''').without_tags() stim.DetectorErrorModel(''' error(0.25) D0 ''') """ class ExplainedError: """Describes the location of an error mechanism from a stim circuit. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0]) ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } } """ def __init__( self, *, dem_error_terms: List[stim.DemTargetWithCoords], circuit_error_locations: List[stim.CircuitErrorLocation], ) -> None: """Creates a stim.ExplainedError. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0]) ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } } """ @property def circuit_error_locations( self, ) -> List[stim.CircuitErrorLocation]: """The locations of circuit errors that produce the symptoms in dem_error_terms. Note: if this list contains a single entry, it may be because a result with a single representative error was requested (as opposed to all possible errors). Note: if this list is empty, it may be because there was a DEM error decomposed into parts where one of the parts is impossible to make on its own from a single circuit error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0].circuit_error_locations[0]) CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } """ @property def dem_error_terms( self, ) -> List[stim.DemTargetWithCoords]: """The detectors and observables flipped by this error mechanism. """ class FlipSimulator: """A simulator that tracks whether things are flipped, instead of what they are. Tracking flips is significantly cheaper than tracking actual values, requiring O(1) work per gate (compared to O(n) for unitary operations and O(n^2) for collapsing operations in the tableau simulator, where n is the qubit count). Supports interactive usage, where gates and measurements are applied on demand. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) """ def __init__( self, *, batch_size: int, disable_stabilizer_randomization: bool = False, num_qubits: int = 0, seed: Optional[int] = None, ) -> None: """Initializes a stim.FlipSimulator. Args: batch_size: For speed, the flip simulator simulates many instances in parallel. This argument determines the number of parallel instances. It's recommended to use a multiple of 256, because internally the state of the instances is striped across SSE (128 bit) or AVX (256 bit) words with one bit in the word belonging to each instance. The result is that, even if you only ask for 1 instance, probably the same amount of work is being done as if you'd asked for 256 instances. The extra results just aren't being used, creating waste. disable_stabilizer_randomization: Determines whether or not the flip simulator uses stabilizer randomization. Defaults to False (stabilizer randomization used). Set to True to disable stabilizer randomization. Stabilizer randomization means that, when a qubit is initialized or measured in the Z basis, a Z error is added to the qubit with 50% probability. More generally, anytime a stabilizer is introduced into the system by any means, an error equal to that stabilizer is applied with 50% probability. This ensures that observables anticommuting with stabilizers of the system must be maximally uncertain. In other words, this feature enforces Heisenberg's uncertainty principle. This is a safety feature that you should not turn off unless you have a reason to do so. Stabilizer randomization is turned on by default because it catches mistakes. For example, suppose you are trying to create a stabilizer code but you accidentally have the code measure two anticommuting stabilizers. With stabilizer randomization turned off, it will look like this code works. With stabilizer randomization turned on, the two measurements will correctly randomize each other revealing that the code doesn't work. In some use cases, stabilizer randomization is a hindrance instead of helpful. For example, if you are using the flip simulator to understand how an error propagates through the system, the stabilizer randomization will be introducing error terms that you don't want. num_qubits: Sets the initial number of qubits tracked by the simulation. The simulator will still automatically resize as needed when qubits beyond this limit are touched. This parameter exists as a way to hint at the desired size of the simulator's state for performance, and to ensure methods that peek at the size have the expected size from the start instead of only after the relevant qubits have been touched. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: An initialized stim.FlipSimulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) """ def append_measurement_flips( self, measurement_flip_data: np.ndarray, ) -> None: """Appends measurement flip data to the simulator's measurement record. Args: measurement_flip_data: The flip data to append. The following shape/dtype combinations are supported. Single measurement without bit packing: shape=(self.batch_size,) dtype=np.bool_ Single measurement with bit packing: shape=(math.ceil(self.batch_size / 8),) dtype=np.uint8 Multiple measurements without bit packing: shape=(num_measurements, self.batch_size) dtype=np.bool_ Multiple measurements with bit packing: shape=(num_measurements, math.ceil(self.batch_size / 8)) dtype=np.uint8 Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.append_measurement_flips(np.array( ... [0, 1, 0, 0, 1, 0, 0, 1, 1], ... dtype=np.bool_, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True]]) >>> sim.append_measurement_flips(np.array( ... [0b11001001, 0], ... dtype=np.uint8, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False]]) >>> sim.append_measurement_flips(np.array( ... [[0b11111111, 0b1], [0b00000000, 0b0], [0b11111111, 0b1]], ... dtype=np.uint8, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False], [ True, True, True, True, True, True, True, True, True], [False, False, False, False, False, False, False, False, False], [ True, True, True, True, True, True, True, True, True]]) >>> sim.append_measurement_flips(np.array( ... [[1, 0, 1, 0, 1, 0, 1, 0, 1], [0, 1, 0, 1, 0, 1, 0, 1, 0]], ... dtype=np.bool_, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False], [ True, True, True, True, True, True, True, True, True], [False, False, False, False, False, False, False, False, False], [ True, True, True, True, True, True, True, True, True], [ True, False, True, False, True, False, True, False, True], [False, True, False, True, False, True, False, True, False]]) """ @property def batch_size( self, ) -> int: """Returns the number of instances being simulated by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.batch_size 256 >>> sim = stim.FlipSimulator(batch_size=42) >>> sim.batch_size 42 """ def broadcast_pauli_errors( self, *, pauli: Union[str, int], mask: np.ndarray, p: float = 1, ) -> None: """Applies a pauli error to all qubits in all instances, filtered by a mask. Args: pauli: The pauli, specified as an integer or string. Uses the convention 0=I, 1=X, 2=Y, 3=Z. Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. mask: A 2d numpy array specifying where to apply errors. The first axis is qubits, the second axis is simulation instances. The first axis can have a length less than the current number of qubits (or more, which adds qubits to the simulation). The length of the second axis must match the simulator's `batch_size`. The array must satisfy mask.dtype == np.bool_ len(mask.shape) == 2 mask.shape[1] == flip_sim.batch_size The error is only applied to qubit q in instance k when mask[q, k] == True. p: Defaults to 1 (no effect). When specified, the error is applied probabilistically instead of deterministically to each (instance, qubit) pair matching the mask. This argument specifies the probability. Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator( ... batch_size=2, ... num_qubits=3, ... disable_stabilizer_randomization=True, ... ) >>> sim.broadcast_pauli_errors( ... pauli='X', ... mask=np.asarray([[True, False],[False, False],[True, True]]), ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+X_X"), stim.PauliString("+__X")] >>> sim.broadcast_pauli_errors( ... pauli='Z', ... mask=np.asarray([[False, True],[False, False],[True, True]]), ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] """ def clear( self, ) -> None: """Clears the simulator's state, so it can be reused for another simulation. This clears the measurement flip history, clears the detector flip history, and zeroes the observable flip state. It also resets all qubits to |0>. If stabilizer randomization is disabled, this zeros all pauli flip data. Otherwise it randomizes all pauli flips to be I or Z with equal probability. Behind the scenes, this doesn't free memory or resize the simulator. So, repeating the same simulation with calls to `clear` in between will be faster than allocating a new simulator each time (by avoiding re-allocations). Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.do(stim.Circuit("M(0.1) 9")) >>> sim.num_qubits 10 >>> sim.get_measurement_flips().shape (1, 256) >>> sim.clear() >>> sim.num_qubits 10 >>> sim.get_measurement_flips().shape (0, 256) """ def copy( self, *, copy_rng: bool = False, seed: Optional[int] = None, ) -> stim.FlipSimulator: """Returns a simulator with the same internal state, except perhaps its prng. Args: copy_rng: Defaults to False. When False, the copy's pseudo random number generator is reinitialized with a random seed instead of being a copy of the original simulator's pseudo random number generator. This causes the copy and the original to sample independent randomness, instead of identical randomness, for future random operations. When set to true, the copy will have the exact same pseudo random number generator state as the original, and so will produce identical results if told to do the same noisy operations. This argument is incompatible with the `seed` argument. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng state is either copied from the original simulator or reseeded from system entropy, depending on the copy_rng argument. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: The copy of the simulator. Examples: >>> import stim >>> import numpy as np >>> s1 = stim.FlipSimulator(batch_size=256) >>> s1.set_pauli_flip('X', qubit_index=2, instance_index=3) >>> s2 = s1.copy() >>> s2 is s1 False >>> s2.peek_pauli_flips() == s1.peek_pauli_flips() True >>> s1 = stim.FlipSimulator(batch_size=256) >>> s2 = s1.copy(copy_rng=True) >>> s1.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) >>> s2.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) >>> np.array_equal(s1.get_measurement_flips(), s2.get_measurement_flips()) True """ def do( self, obj: Union[stim.Circuit, stim.CircuitInstruction, stim.CircuitRepeatBlock], ) -> None: """Applies a circuit or circuit instruction to the simulator's state. The results of any measurements performed can be retrieved using the `get_measurement_flips` method. Args: obj: The circuit or instruction to apply to the simulator's state. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=1, ... disable_stabilizer_randomization=True, ... ) >>> circuit = stim.Circuit(''' ... X_ERROR(1) 0 1 3 ... REPEAT 5 { ... H 0 ... C_XYZ 1 ... } ... ''') >>> sim.do(circuit) >>> sim.peek_pauli_flips() [stim.PauliString("+ZZ_X")] >>> sim.do(circuit[0]) >>> sim.peek_pauli_flips() [stim.PauliString("+YY__")] >>> sim.do(circuit[1]) >>> sim.peek_pauli_flips() [stim.PauliString("+YX__")] """ def generate_bernoulli_samples( self, num_samples: int, *, p: float, bit_packed: bool = False, out: Optional[np.ndarray] = None, ) -> np.ndarray: """Uses the simulator's random number generator to produce biased coin flips. This method has best performance when specifying `bit_packed=True` and when specifying an `out=` parameter pointing to a numpy array that has contiguous data aligned to a 64 bit boundary. (If `out` isn't specified, the returned numpy array will have this property.) Args: num_samples: The number of samples to produce. p: The probability of each sample being True instead of False. bit_packed: Defaults to False (no bit packing). When True, the result has type np.uint8 instead of np.bool_ and 8 samples are packed into each byte as if by np.packbits(bitorder='little'). (The bit order is relevant when producing a number of samples that isn't a multiple of 8.) out: Defaults to None (allocate new). A numpy array to write the samples into. Must have the correct size and dtype. Returns: A numpy array containing the samples. The shape and dtype depends on the bit_packed argument: if not bit_packed: shape = (num_samples,) dtype = np.bool_ elif not transpose and bit_packed: shape = (math.ceil(num_samples / 8),) dtype = np.uint8 Raises: ValueError: The given `out` argument had a shape or dtype inconsistent with the requested data. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> r = sim.generate_bernoulli_samples(1001, p=0.25) >>> r.dtype dtype('bool') >>> r.shape (1001,) >>> r = sim.generate_bernoulli_samples(53, p=0.1, bit_packed=True) >>> r.dtype dtype('uint8') >>> r.shape (7,) >>> r[6] & 0b1110_0000 # zero'd padding bits np.uint8(0) >>> r2 = sim.generate_bernoulli_samples(53, p=0.2, bit_packed=True, out=r) >>> r is r2 # Check request to reuse r worked. True """ def get_detector_flips( self, *, detector_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves detector flip data from the simulator's detection event record. Args: record_index: Identifies a detector to read results from. Setting this to None (default) returns results from all detectors. Otherwise this should be an integer in range(0, self.num_detectors). instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_detectors, self.batch_size), where the first index is the detector_index and the second index is the instance_index and the dtype is np.bool_. Specifying detector_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a detector_index (or a 0d array, a boolean, if detector_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit(''' ... M 0 0 0 ... DETECTOR rec[-2] rec[-3] ... DETECTOR rec[-1] rec[-2] ... ''')) >>> sim.get_detector_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_detector_flips(bit_packed=True) array([[0, 0], [0, 0]], dtype=uint8) >>> sim.get_detector_flips(instance_index=2) array([False, False]) >>> sim.get_detector_flips(detector_index=1) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_detector_flips(instance_index=2, detector_index=1) array(False) """ def get_measurement_flips( self, *, record_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves measurement flip data from the simulator's measurement record. Args: record_index: Identifies a measurement to read results from. Setting this to None (default) returns results from all measurements. Setting this to a non-negative integer indexes measurements by the order they occurred. For example, record index 0 is the first measurement. Setting this to a negative integer indexes measurements by recency. For example, recording index -1 is the most recent measurement. instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be set to an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_measurements, self.batch_size), where the first index is the measurement_index and the second index is the instance_index and the dtype is np.bool_. Specifying record_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a measurement_index (or a 0d array, a boolean, if record_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit('M 0 1 2')) >>> sim.get_measurement_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_measurement_flips(bit_packed=True) array([[0, 0], [0, 0], [0, 0]], dtype=uint8) >>> sim.get_measurement_flips(instance_index=1) array([False, False, False]) >>> sim.get_measurement_flips(record_index=2) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_measurement_flips(instance_index=1, record_index=2) array(False) """ def get_observable_flips( self, *, observable_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False, ) -> np.ndarray: """Retrieves observable flip data from the simulator's detection event record. Args: record_index: Identifies a observable to read results from. Setting this to None (default) returns results from all observables. Otherwise this should be an integer in range(0, self.num_observables). instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_observables, self.batch_size), where the first index is the observable_index and the second index is the instance_index and the dtype is np.bool_. Specifying observable_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a observable_index (or a 0d array, a boolean, if observable_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit(''' ... M 0 0 0 ... OBSERVABLE_INCLUDE(0) rec[-2] ... OBSERVABLE_INCLUDE(1) rec[-1] ... ''')) >>> sim.get_observable_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_observable_flips(bit_packed=True) array([[0, 0], [0, 0]], dtype=uint8) >>> sim.get_observable_flips(instance_index=2) array([False, False]) >>> sim.get_observable_flips(observable_index=1) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_observable_flips(instance_index=2, observable_index=1) array(False) """ @property def num_detectors( self, ) -> int: """Returns the number of detectors that have been simulated and stored. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_detectors 0 >>> sim.do(stim.Circuit(''' ... M 0 0 ... DETECTOR rec[-1] rec[-2] ... ''')) >>> sim.num_detectors 1 """ @property def num_measurements( self, ) -> int: """Returns the number of measurements that have been simulated and stored. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_measurements 0 >>> sim.do(stim.Circuit('M 3 5')) >>> sim.num_measurements 2 """ @property def num_observables( self, ) -> int: """Returns the number of observables currently tracked by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_observables 0 >>> sim.do(stim.Circuit(''' ... M 0 ... OBSERVABLE_INCLUDE(4) rec[-1] ... ''')) >>> sim.num_observables 5 """ @property def num_qubits( self, ) -> int: """Returns the number of qubits currently tracked by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_qubits 0 >>> sim = stim.FlipSimulator(batch_size=256, num_qubits=4) >>> sim.num_qubits 4 >>> sim.do(stim.Circuit('H 5')) >>> sim.num_qubits 6 """ @overload def peek_pauli_flips( self, ) -> List[stim.PauliString]: pass @overload def peek_pauli_flips( self, *, instance_index: int, ) -> stim.PauliString: pass def peek_pauli_flips( self, *, instance_index: Optional[int] = None, ) -> Union[stim.PauliString, List[stim.PauliString]]: """Returns the current pauli errors packed into stim.PauliString instances. Args: instance_index: Defaults to None. When set to None, the pauli errors from all instances are returned as a list of `stim.PauliString`. When set to an integer, a single `stim.PauliString` is returned containing the errors for the indexed instance. Returns: if instance_index is None: A list of stim.PauliString, with the k'th entry being the errors from the k'th simulation instance. else: A stim.PauliString with the errors from the k'th simulation instance. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=2, ... disable_stabilizer_randomization=True, ... num_qubits=10, ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+__________"), stim.PauliString("+__________")] >>> sim.peek_pauli_flips(instance_index=0) stim.PauliString("+__________") >>> sim.do(stim.Circuit(''' ... X_ERROR(1) 0 3 5 ... Z_ERROR(1) 3 6 ... ''')) >>> sim.peek_pauli_flips() [stim.PauliString("+X__Y_XZ___"), stim.PauliString("+X__Y_XZ___")] >>> sim = stim.FlipSimulator( ... batch_size=1, ... num_qubits=100, ... ) >>> flips: stim.PauliString = sim.peek_pauli_flips(instance_index=0) >>> sorted(set(str(flips))) # Should have Zs from stabilizer randomization ['+', 'Z', '_'] """ def set_pauli_flip( self, pauli: Union[str, int], *, qubit_index: int, instance_index: int, ) -> None: """Sets the pauli flip on a given qubit in a given simulation instance. Args: pauli: The pauli, specified as an integer or string. Uses the convention 0=I, 1=X, 2=Y, 3=Z. Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. qubit_index: The qubit to put the error on. Must be non-negative. The state will automatically expand as needed to store the error. instance_index: The simulation index to put the error inside. Use negative indices to index from the end of the list. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=2, ... num_qubits=3, ... disable_stabilizer_randomization=True, ... ) >>> sim.set_pauli_flip('X', qubit_index=2, instance_index=1) >>> sim.peek_pauli_flips() [stim.PauliString("+___"), stim.PauliString("+__X")] """ def to_numpy( self, *, bit_packed: bool = False, transpose: bool = False, output_xs: Union[bool, np.ndarray] = False, output_zs: Union[bool, np.ndarray] = False, output_measure_flips: Union[bool, np.ndarray] = False, output_detector_flips: Union[bool, np.ndarray] = False, output_observable_flips: Union[bool, np.ndarray] = False, ) -> Optional[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]]: """Writes the simulator state into numpy arrays. Args: bit_packed: Whether or not the result is bit packed, storing 8 bits per byte instead of 1 bit per byte. Bit packing always applies to the second index of the result. Bits are packed in little endian order (as if by `np.packbits(X, axis=1, order='little')`). transpose: Defaults to False. When set to False, the second index of the returned array (the index affected by bit packing) is the shot index (meaning the first index is the qubit index or measurement index or etc). When set to True, results are transposed so that the first index is the shot index. output_xs: Defaults to False. When set to False, the X flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the X flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_zs: Defaults to False. When set to False, the Z flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the Z flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_measure_flips: Defaults to False. When set to False, the measure flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the measure flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_detector_flips: Defaults to False. When set to False, the detector flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the detector flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_observable_flips: Defaults to False. When set to False, the obs flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the obs flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). Returns: A tuple (xs, zs, ms, ds, os) of numpy arrays. The xs and zs arrays are the pauli flip data specified using XZ encoding (00=I, 10=X, 11=Y, 01=Z). The ms array is the measure flip data, the ds array is the detector flip data, and the os array is the obs flip data. The arrays default to `None` when the corresponding `output_*` argument was left False. The shape and dtype of the data depends on arguments given to the function. The following specifies each array's shape and dtype for each case: if not transpose and not bit_packed: xs.shape = (sim.batch_size, sim.num_qubits) zs.shape = (sim.batch_size, sim.num_qubits) ms.shape = (sim.batch_size, sim.num_measurements) ds.shape = (sim.batch_size, sim.num_detectors) os.shape = (sim.batch_size, sim.num_observables) xs.dtype = np.bool_ zs.dtype = np.bool_ ms.dtype = np.bool_ ds.dtype = np.bool_ os.dtype = np.bool_ elif not transpose and bit_packed: xs.shape = (sim.batch_size, math.ceil(sim.num_qubits / 8)) zs.shape = (sim.batch_size, math.ceil(sim.num_qubits / 8)) ms.shape = (sim.batch_size, math.ceil(sim.num_measurements / 8)) ds.shape = (sim.batch_size, math.ceil(sim.num_detectors / 8)) os.shape = (sim.batch_size, math.ceil(sim.num_observables / 8)) xs.dtype = np.uint8 zs.dtype = np.uint8 ms.dtype = np.uint8 ds.dtype = np.uint8 os.dtype = np.uint8 elif transpose and not bit_packed: xs.shape = (sim.num_qubits, sim.batch_size) zs.shape = (sim.num_qubits, sim.batch_size) ms.shape = (sim.num_measurements, sim.batch_size) ds.shape = (sim.num_detectors, sim.batch_size) os.shape = (sim.num_observables, sim.batch_size) xs.dtype = np.bool_ zs.dtype = np.bool_ ms.dtype = np.bool_ ds.dtype = np.bool_ os.dtype = np.bool_ elif transpose and bit_packed: xs.shape = (sim.num_qubits, math.ceil(sim.batch_size / 8)) zs.shape = (sim.num_qubits, math.ceil(sim.batch_size / 8)) ms.shape = (sim.num_measurements, math.ceil(sim.batch_size / 8)) ds.shape = (sim.num_detectors, math.ceil(sim.batch_size / 8)) os.shape = (sim.num_observables, math.ceil(sim.batch_size / 8)) xs.dtype = np.uint8 zs.dtype = np.uint8 ms.dtype = np.uint8 ds.dtype = np.uint8 os.dtype = np.uint8 Raises: ValueError: All the `output_*` arguments were False, or an `output_*` argument had a shape or dtype inconsistent with the requested data. Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit('M(1) 0 1 2')) >>> ms_buf = np.empty(shape=(9, 1), dtype=np.uint8) >>> xs, zs, ms, ds, os = sim.to_numpy( ... transpose=True, ... bit_packed=True, ... output_xs=True, ... output_measure_flips=ms_buf, ... ) >>> assert ms is ms_buf >>> xs array([[0], [0], [0], [0], [0], [0], [0], [0], [0]], dtype=uint8) >>> zs >>> ms array([[7], [7], [7], [7], [7], [7], [7], [7], [7]], dtype=uint8) >>> ds >>> os """ class FlippedMeasurement: """Describes a measurement that was flipped. Gives the measurement's index in the measurement record, and also the observable of the measurement. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement stim.FlippedMeasurement( record_index=1, observable=(stim.GateTargetWithCoords(stim.target_z(10), []),), ) """ def __init__( self, measurement_record_index: Optional[int], measured_observable: Iterable[stim.GateTargetWithCoords], ): """Creates a stim.FlippedMeasurement. Examples: >>> import stim >>> print(stim.FlippedMeasurement( ... record_index=5, ... observable=[], ... )) stim.FlippedMeasurement( record_index=5, observable=(), ) """ @property def observable( self, ) -> List[stim.GateTargetWithCoords]: """Returns the observable of the flipped measurement. For example, an `MX 5` measurement will have the observable X5. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement.observable [stim.GateTargetWithCoords(stim.target_z(10), [])] """ @property def record_index( self, ) -> int: """The measurement record index of the flipped measurement. For example, the fifth measurement in a circuit has a measurement record index of 4. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement.record_index 1 """ class Flow: """A stabilizer flow (e.g. "XI -> XX xor rec[-1]"). Stabilizer circuits implement, and can be defined by, how they turn input stabilizers into output stabilizers mediated by measurements. These relationships are called stabilizer flows, and `stim.Flow` is a representation of such a flow. For example, a `stim.Flow` can be given to `stim.Circuit.has_flow` to verify that a circuit implements the flow. A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer P at the start of the circuit to the instantaneous stabilizer Q at the end of the circuit. The flow may be mediated by certain measurements. For example, a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and the CNOT flows implemented by the circuit involve these measurements. A flow like P -> Q means the circuit transforms P into Q. A flow like 1 -> P means the circuit prepares P. A flow like P -> 1 means the circuit measures P. A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). References: Stim's gate documentation includes the stabilizer flows of each gate. Appendix A of https://arxiv.org/abs/2302.02192 describes how flows are defined and provides a circuit construction for experimentally verifying their presence. Examples: >>> import stim >>> c = stim.Circuit("CNOT 2 4") >>> c.has_flow(stim.Flow("__X__ -> __X_X")) True >>> c.has_flow(stim.Flow("X2*X4 -> X2")) True >>> c.has_flow(stim.Flow("Z4 -> Z4")) False """ def __eq__( self, arg0: stim.Flow, ) -> bool: """Determines if two flows have identical contents. """ def __init__( self, arg: Union[None, str, stim.Flow] = None, /, *, input: Optional[stim.PauliString] = None, output: Optional[stim.PauliString] = None, measurements: Optional[Iterable[Union[int, GateTarget]]] = None, included_observables: Optional[Iterable[int]] = None, ) -> None: """Initializes a stim.Flow. When given a string, the string is parsed as flow shorthand. For example, the string "X_ -> ZZ xor rec[-1]" will result in a flow with input pauli string "X_", output pauli string "ZZ", and measurement indices [-1]. Args: arg [position-only]: Defaults to None. Must be specified by itself if used. str: Initializes a flow by parsing the given shorthand text. stim.Flow: Initializes a copy of the given flow. None (default): Initializes an empty flow. input: Defaults to None. Can be set to a stim.PauliString to directly specify the flow's input stabilizer. output: Defaults to None. Can be set to a stim.PauliString to directly specify the flow's output stabilizer. measurements: Defaults to None. Can be set to a list of integers or gate targets like `stim.target_rec(-1)`, to specify the measurements that mediate the flow. Negative and positive measurement indices are allowed. Indexes follow the python convention where -1 is the last measurement in a circuit and 0 is the first measurement in a circuit. included_observables: Defaults to None. `OBSERVABLE_INCLUDE` instructions that target an observable index from this list will be implicitly included in the flow. This allows flows to refer to observables. For example, the flow "X5 -> obs[3]" says "At the start of the circuit, observable 3 should be an X term on qubit 5. By the end of the circuit it will be measured. The `OBSERVABLE_INCLUDE(3)` instructions in the circuit should explain how this happened.". Examples: >>> import stim >>> stim.Flow("X2 -> -Y2*Z4 xor rec[-1]") stim.Flow("__X -> -__Y_Z xor rec[-1]") >>> stim.Flow("Z -> 1 xor rec[-1]") stim.Flow("Z -> rec[-1]") >>> stim.Flow( ... input=stim.PauliString("XX"), ... output=stim.PauliString("_X"), ... measurements=[], ... ) stim.Flow("XX -> _X") >>> # Identical terms cancel. >>> stim.Flow("X2 -> Y2*Y2 xor rec[-2] xor rec[-2]") stim.Flow("__X -> ___") >>> stim.Flow("X -> Y xor obs[3] xor obs[3] xor obs[3]") stim.Flow("X -> Y xor obs[3]") """ def __mul__( self, rhs: stim.Flow, ) -> stim.Flow: """Computes the product of two flows. Args: rhs: The right hand side of the multiplication. Returns: The product of the two flows. Raises: ValueError: The inputs anti-commute (their product would be anti-Hermitian). For example, 1 -> X times 1 -> Y fails because it would give 1 -> iZ. Examples: >>> import stim >>> stim.Flow("X -> X") * stim.Flow("Z -> Z") stim.Flow("Y -> Y") >>> stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ") stim.Flow("1 -> -YY") >>> stim.Flow("X -> rec[-1]") * stim.Flow("X -> rec[-2]") stim.Flow("_ -> rec[-2] xor rec[-1]") """ def __ne__( self, arg0: stim.Flow, ) -> bool: """Determines if two flows have non-identical contents. """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.Flow`. """ def __str__( self, ) -> str: """Returns a shorthand description of the flow. """ def included_observables_copy( self, ) -> List[int]: """Returns a copy of the flow's included observable indices. When an observable is included in a flow, the flow implicitly includes all measurements and pauli terms from `OBSERVABLE_INCLUDE` instructions targeting that observable index. Examples: >>> import stim >>> f = stim.Flow(included_observables=[3, 2]) >>> f.included_observables_copy() [2, 3] >>> f.included_observables_copy() is f.included_observables_copy() False >>> f = stim.Flow("X2 -> obs[3]") >>> f.included_observables_copy() [3] >>> stim.Circuit("OBSERVABLE_INCLUDE(3) X2").has_flow(f) True """ def input_copy( self, ) -> stim.PauliString: """Returns a copy of the flow's input stabilizer. Examples: >>> import stim >>> f = stim.Flow(input=stim.PauliString('XX')) >>> f.input_copy() stim.PauliString("+XX") >>> f.input_copy() is f.input_copy() False """ def measurements_copy( self, ) -> List[int]: """Returns a copy of the flow's measurement indices. Examples: >>> import stim >>> f = stim.Flow(measurements=[-1, 2]) >>> f.measurements_copy() [-1, 2] >>> f.measurements_copy() is f.measurements_copy() False """ def output_copy( self, ) -> stim.PauliString: """Returns a copy of the flow's output stabilizer. Examples: >>> import stim >>> f = stim.Flow(output=stim.PauliString('XX')) >>> f.output_copy() stim.PauliString("+XX") >>> f.output_copy() is f.output_copy() False """ class GateData: """Details about a gate supported by stim. Examples: >>> import stim >>> stim.gate_data('h').name 'H' >>> stim.gate_data('h').is_unitary True >>> stim.gate_data('h').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) """ def __eq__( self, arg0: stim.GateData, ) -> bool: """Determines if two GateData instances are identical. """ def __init__( self, name: str, ) -> None: """Finds gate data for the named gate. Examples: >>> import stim >>> stim.GateData('H').is_unitary True """ def __ne__( self, arg0: stim.GateData, ) -> bool: """Determines if two GateData instances are not identical. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.GateData`. """ def __str__( self, ) -> str: """Returns text describing the gate data. """ @property def aliases( self, ) -> List[str]: """Returns all aliases that can be used to name the gate. Although gates can be referred to by lower case and mixed case named, the result only includes upper cased aliases. Examples: >>> import stim >>> stim.gate_data('H').aliases ['H', 'H_XZ'] >>> stim.gate_data('cnot').aliases ['CNOT', 'CX', 'ZCX'] """ @property def flows( self, ) -> Optional[List[stim.Flow]]: """Returns stabilizer flow generators for the gate, or else None. A stabilizer flow describes an input-output relationship that the gate satisfies, where an input pauli string is transformed into an output pauli string mediated by certain measurement results. Caution: this method returns None for variable-target-count gates like MPP. Not because MPP has no stabilizer flows, but because its stabilizer flows depend on how many qubits it targets and what basis it targets them in. Returns: A list of stim.Flow instances representing the generators. Examples: >>> import stim >>> stim.gate_data('H').flows [stim.Flow("X -> Z"), stim.Flow("Z -> X")] >>> for e in stim.gate_data('ISWAP').flows: ... print(e) X_ -> ZY Z_ -> _Z _X -> YZ _Z -> Z_ >>> for e in stim.gate_data('MXX').flows: ... print(e) X_ -> X_ _X -> _X ZZ -> ZZ XX -> rec[-1] """ @property def generalized_inverse( self, ) -> stim.GateData: """The closest-thing-to-an-inverse for the gate, if forced to pick something. The generalized inverse of a unitary gate U is its actual inverse U^-1. The generalized inverse of a reset or measurement gate U is a gate V such that, for every stabilizer flow that U has, V has the time reverse of that flow (up to Pauli feedback, with potentially more flows). For example, the time-reverse of R is MR because R has the single flow 1 -> Z and MR has the time reversed flow Z -> rec[-1]. The generalized inverse of noise like X_ERROR is just the same noise. The generalized inverse of an annotation like TICK is just the same annotation. Examples: >>> import stim >>> stim.gate_data('H').generalized_inverse stim.gate_data('H') >>> stim.gate_data('CXSWAP').generalized_inverse stim.gate_data('SWAPCX') >>> stim.gate_data('X_ERROR').generalized_inverse stim.gate_data('X_ERROR') >>> stim.gate_data('MX').generalized_inverse stim.gate_data('MX') >>> stim.gate_data('MRY').generalized_inverse stim.gate_data('MRY') >>> stim.gate_data('R').generalized_inverse stim.gate_data('M') >>> stim.gate_data('DETECTOR').generalized_inverse stim.gate_data('DETECTOR') >>> stim.gate_data('TICK').generalized_inverse stim.gate_data('TICK') """ def hadamard_conjugated( self, *, unsigned: bool = False, ) -> Optional[stim.GateData]: """Returns a stim gate equivalent to this gate conjugated by Hadamard gates. The Hadamard conjugate can be thought of as the XZ dual of the gate; the gate you get by exchanging the X and Z bases. For example, a SQRT_X will become a SQRT_Z and a CX gate will switch directions into an XCZ. If stim doesn't define a gate equivalent to conjugating this gate by Hadamards, the value `None` is returned. Args: unsigned: Defaults to False. When False, the returned gate must be *exactly* the Hadamard conjugation of this gate. When True, the returned gate must have the same flows but the sign of the flows can be different (i.e. the returned gate must be the Hadamard conjugate up to Pauli gate differences). Returns: A stim.GateData instance of the Hadamard conjugate, if it exists in stim. None, if stim doesn't define a gate equal to the Hadamard conjugate. Examples: >>> import stim >>> stim.gate_data('X').hadamard_conjugated() stim.gate_data('Z') >>> stim.gate_data('CX').hadamard_conjugated() stim.gate_data('XCZ') >>> stim.gate_data('RY').hadamard_conjugated() is None True >>> stim.gate_data('RY').hadamard_conjugated(unsigned=True) stim.gate_data('RY') >>> stim.gate_data('ISWAP').hadamard_conjugated(unsigned=True) is None True >>> stim.gate_data('SWAP').hadamard_conjugated() stim.gate_data('SWAP') >>> stim.gate_data('CXSWAP').hadamard_conjugated() stim.gate_data('SWAPCX') >>> stim.gate_data('MXX').hadamard_conjugated() stim.gate_data('MZZ') >>> stim.gate_data('DEPOLARIZE1').hadamard_conjugated() stim.gate_data('DEPOLARIZE1') >>> stim.gate_data('X_ERROR').hadamard_conjugated() stim.gate_data('Z_ERROR') >>> stim.gate_data('H_XY').hadamard_conjugated() stim.gate_data('H_NYZ') >>> stim.gate_data('DETECTOR').hadamard_conjugated(unsigned=True) stim.gate_data('DETECTOR') """ @property def inverse( self, ) -> Optional[stim.GateData]: """The inverse of the gate, or None if it has no inverse. The inverse V of a gate U must have the property that V undoes the effects of U and that U undoes the effects of V. In particular, the circuit U 0 1 V 0 1 should be equivalent to doing nothing at all. Examples: >>> import stim >>> stim.gate_data('H').inverse stim.gate_data('H') >>> stim.gate_data('CX').inverse stim.gate_data('CX') >>> stim.gate_data('S').inverse stim.gate_data('S_DAG') >>> stim.gate_data('CXSWAP').inverse stim.gate_data('SWAPCX') >>> stim.gate_data('X_ERROR').inverse is None True >>> stim.gate_data('M').inverse is None True >>> stim.gate_data('R').inverse is None True >>> stim.gate_data('DETECTOR').inverse is None True >>> stim.gate_data('TICK').inverse is None True """ @property def is_noisy_gate( self, ) -> bool: """Returns whether or not the gate can produce noise. Note that measurement operations are considered noisy, because for example `M(0.001) 2 3 5` will include noise that flips its result 0.1% of the time. Examples: >>> import stim >>> stim.gate_data('M').is_noisy_gate True >>> stim.gate_data('MXX').is_noisy_gate True >>> stim.gate_data('X_ERROR').is_noisy_gate True >>> stim.gate_data('CORRELATED_ERROR').is_noisy_gate True >>> stim.gate_data('MPP').is_noisy_gate True >>> stim.gate_data('H').is_noisy_gate False >>> stim.gate_data('CX').is_noisy_gate False >>> stim.gate_data('R').is_noisy_gate False >>> stim.gate_data('DETECTOR').is_noisy_gate False """ @property def is_reset( self, ) -> bool: """Returns whether or not the gate resets qubits in any basis. Examples: >>> import stim >>> stim.gate_data('R').is_reset True >>> stim.gate_data('RX').is_reset True >>> stim.gate_data('MR').is_reset True >>> stim.gate_data('M').is_reset False >>> stim.gate_data('MXX').is_reset False >>> stim.gate_data('MPP').is_reset False >>> stim.gate_data('H').is_reset False >>> stim.gate_data('CX').is_reset False >>> stim.gate_data('HERALDED_ERASE').is_reset False >>> stim.gate_data('DEPOLARIZE2').is_reset False >>> stim.gate_data('X_ERROR').is_reset False >>> stim.gate_data('CORRELATED_ERROR').is_reset False >>> stim.gate_data('DETECTOR').is_reset False """ @property def is_single_qubit_gate( self, ) -> bool: """Returns whether or not the gate is a single qubit gate. Single qubit gates apply separately to each of their targets. Variable-qubit gates like CORRELATED_ERROR and MPP are not considered single qubit gates. Examples: >>> import stim >>> stim.gate_data('H').is_single_qubit_gate True >>> stim.gate_data('R').is_single_qubit_gate True >>> stim.gate_data('M').is_single_qubit_gate True >>> stim.gate_data('X_ERROR').is_single_qubit_gate True >>> stim.gate_data('CX').is_single_qubit_gate False >>> stim.gate_data('MXX').is_single_qubit_gate False >>> stim.gate_data('CORRELATED_ERROR').is_single_qubit_gate False >>> stim.gate_data('MPP').is_single_qubit_gate False >>> stim.gate_data('DETECTOR').is_single_qubit_gate False >>> stim.gate_data('TICK').is_single_qubit_gate False >>> stim.gate_data('REPEAT').is_single_qubit_gate False """ @property def is_symmetric_gate( self, ) -> bool: """Returns whether or not the gate is the same when its targets are swapped. A two qubit gate is symmetric if it doesn't matter if you swap its targets. It is unaffected when conjugated by the SWAP gate. Single qubit gates are vacuously symmetric. A multi-qubit gate is symmetric if swapping any two of its targets has no effect. Note that this method is for symmetry *without broadcasting*. For example, SWAP is symmetric even though SWAP 1 2 3 4 isn't equal to SWAP 1 3 2 4. Returns: True if the gate is symmetric. False if the gate isn't symmetric. Examples: >>> import stim >>> stim.gate_data('CX').is_symmetric_gate False >>> stim.gate_data('CZ').is_symmetric_gate True >>> stim.gate_data('ISWAP').is_symmetric_gate True >>> stim.gate_data('CXSWAP').is_symmetric_gate False >>> stim.gate_data('MXX').is_symmetric_gate True >>> stim.gate_data('DEPOLARIZE2').is_symmetric_gate True >>> stim.gate_data('PAULI_CHANNEL_2').is_symmetric_gate False >>> stim.gate_data('H').is_symmetric_gate True >>> stim.gate_data('R').is_symmetric_gate True >>> stim.gate_data('X_ERROR').is_symmetric_gate True >>> stim.gate_data('CORRELATED_ERROR').is_symmetric_gate False >>> stim.gate_data('MPP').is_symmetric_gate False >>> stim.gate_data('DETECTOR').is_symmetric_gate False """ @property def is_two_qubit_gate( self, ) -> bool: """Returns whether or not the gate is a two qubit gate. Two qubit gates must be given an even number of targets. Variable-qubit gates like CORRELATED_ERROR and MPP are not considered two qubit gates. Returns: True if the gate is a two qubit gate. False if the gate isn't a two qubit gate. Examples: >>> import stim >>> stim.gate_data('CX').is_two_qubit_gate True >>> stim.gate_data('MXX').is_two_qubit_gate True >>> stim.gate_data('H').is_two_qubit_gate False >>> stim.gate_data('R').is_two_qubit_gate False >>> stim.gate_data('M').is_two_qubit_gate False >>> stim.gate_data('X_ERROR').is_two_qubit_gate False >>> stim.gate_data('CORRELATED_ERROR').is_two_qubit_gate False >>> stim.gate_data('MPP').is_two_qubit_gate False >>> stim.gate_data('DETECTOR').is_two_qubit_gate False """ @property def is_unitary( self, ) -> bool: """Returns whether or not the gate is a unitary gate. Examples: >>> import stim >>> stim.gate_data('H').is_unitary True >>> stim.gate_data('CX').is_unitary True >>> stim.gate_data('R').is_unitary False >>> stim.gate_data('M').is_unitary False >>> stim.gate_data('MXX').is_unitary False >>> stim.gate_data('X_ERROR').is_unitary False >>> stim.gate_data('CORRELATED_ERROR').is_unitary False >>> stim.gate_data('MPP').is_unitary False >>> stim.gate_data('DETECTOR').is_unitary False """ @property def name( self, ) -> str: """Returns the canonical name of the gate. Examples: >>> import stim >>> stim.gate_data('H').name 'H' >>> stim.gate_data('cnot').name 'CX' """ @property def num_parens_arguments_range( self, ) -> range: """Returns the min/max parens arguments taken by the gate, as a python range. Examples: >>> import stim >>> stim.gate_data('M').num_parens_arguments_range range(0, 2) >>> list(stim.gate_data('M').num_parens_arguments_range) [0, 1] >>> list(stim.gate_data('R').num_parens_arguments_range) [0] >>> list(stim.gate_data('H').num_parens_arguments_range) [0] >>> list(stim.gate_data('X_ERROR').num_parens_arguments_range) [1] >>> list(stim.gate_data('PAULI_CHANNEL_1').num_parens_arguments_range) [3] >>> list(stim.gate_data('PAULI_CHANNEL_2').num_parens_arguments_range) [15] >>> stim.gate_data('DETECTOR').num_parens_arguments_range range(0, 256) >>> list(stim.gate_data('OBSERVABLE_INCLUDE').num_parens_arguments_range) [1] """ @property def produces_measurements( self, ) -> bool: """Returns whether or not the gate produces measurement results. Examples: >>> import stim >>> stim.gate_data('M').produces_measurements True >>> stim.gate_data('MRY').produces_measurements True >>> stim.gate_data('MXX').produces_measurements True >>> stim.gate_data('MPP').produces_measurements True >>> stim.gate_data('HERALDED_ERASE').produces_measurements True >>> stim.gate_data('H').produces_measurements False >>> stim.gate_data('CX').produces_measurements False >>> stim.gate_data('R').produces_measurements False >>> stim.gate_data('X_ERROR').produces_measurements False >>> stim.gate_data('CORRELATED_ERROR').produces_measurements False >>> stim.gate_data('DETECTOR').produces_measurements False """ @property def tableau( self, ) -> Optional[stim.Tableau]: """Returns the gate's tableau, or None if the gate has no tableau. Examples: >>> import stim >>> print(stim.gate_data('M').tableau) None >>> stim.gate_data('H').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> stim.gate_data('ISWAP').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZY"), stim.PauliString("+YZ"), ], zs=[ stim.PauliString("+_Z"), stim.PauliString("+Z_"), ], ) """ @property def takes_measurement_record_targets( self, ) -> bool: """Returns whether or not the gate can accept rec targets. For example, `CX` can take a measurement record target like `CX rec[-1] 1`. Examples: >>> import stim >>> stim.gate_data('CX').takes_measurement_record_targets True >>> stim.gate_data('DETECTOR').takes_measurement_record_targets True >>> stim.gate_data('H').takes_measurement_record_targets False >>> stim.gate_data('SWAP').takes_measurement_record_targets False >>> stim.gate_data('R').takes_measurement_record_targets False >>> stim.gate_data('M').takes_measurement_record_targets False >>> stim.gate_data('MRY').takes_measurement_record_targets False >>> stim.gate_data('MXX').takes_measurement_record_targets False >>> stim.gate_data('X_ERROR').takes_measurement_record_targets False >>> stim.gate_data('CORRELATED_ERROR').takes_measurement_record_targets False >>> stim.gate_data('MPP').takes_measurement_record_targets False """ @property def takes_pauli_targets( self, ) -> bool: """Returns whether or not the gate expects pauli targets. For example, `CORRELATED_ERROR` takes targets like `X0` and `Y1` instead of `0` or `1`. Examples: >>> import stim >>> stim.gate_data('CORRELATED_ERROR').takes_pauli_targets True >>> stim.gate_data('MPP').takes_pauli_targets True >>> stim.gate_data('H').takes_pauli_targets False >>> stim.gate_data('CX').takes_pauli_targets False >>> stim.gate_data('R').takes_pauli_targets False >>> stim.gate_data('M').takes_pauli_targets False >>> stim.gate_data('MRY').takes_pauli_targets False >>> stim.gate_data('MXX').takes_pauli_targets False >>> stim.gate_data('X_ERROR').takes_pauli_targets False >>> stim.gate_data('DETECTOR').takes_pauli_targets False """ @property def unitary_matrix( self, ) -> Optional[np.ndarray]: """Returns the gate's unitary matrix, or None if the gate isn't unitary. Examples: >>> import stim >>> print(stim.gate_data('M').unitary_matrix) None >>> stim.gate_data('X').unitary_matrix array([[0.+0.j, 1.+0.j], [1.+0.j, 0.+0.j]], dtype=complex64) >>> stim.gate_data('ISWAP').unitary_matrix array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j], [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]], dtype=complex64) """ class GateTarget: """Represents a gate target, like `0` or `rec[-1]`, from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... M 0 !1 ... ''') >>> circuit[0].targets_copy()[0] stim.GateTarget(0) >>> circuit[0].targets_copy()[1] stim.target_inv(1) """ def __eq__( self, arg0: stim.GateTarget, ) -> bool: """Determines if two `stim.GateTarget`s are identical. """ def __init__( self, value: object, ) -> None: """Initializes a `stim.GateTarget`. Args: value: A value to convert into a gate target, like an integer to interpret as a qubit target or a string to parse. Examples: >>> import stim >>> stim.GateTarget(stim.GateTarget(5)) stim.GateTarget(5) >>> stim.GateTarget("X7") stim.target_x(7) >>> stim.GateTarget("rec[-3]") stim.target_rec(-3) >>> stim.GateTarget("!Z7") stim.target_z(7, invert=True) >>> stim.GateTarget("*") stim.GateTarget.combiner() """ def __ne__( self, arg0: stim.GateTarget, ) -> bool: """Determines if two `stim.GateTarget`s are different. """ def __repr__( self, ) -> str: """Returns text that is a valid python expression evaluating to an equivalent `stim.GateTarget`. """ @property def is_combiner( self, ) -> bool: """Returns whether or not this is a combiner target like `*`. Examples: >>> import stim >>> stim.GateTarget(6).is_combiner False >>> stim.target_inv(7).is_combiner False >>> stim.target_x(8).is_combiner False >>> stim.target_y(2).is_combiner False >>> stim.target_z(3).is_combiner False >>> stim.target_sweep_bit(9).is_combiner False >>> stim.target_rec(-5).is_combiner False >>> stim.target_combiner().is_combiner True """ @property def is_inverted_result_target( self, ) -> bool: """Returns whether or not this is an inverted target like `!5` or `!X4`. Examples: >>> import stim >>> stim.GateTarget(6).is_inverted_result_target False >>> stim.target_inv(7).is_inverted_result_target True >>> stim.target_x(8).is_inverted_result_target False >>> stim.target_x(8, invert=True).is_inverted_result_target True >>> stim.target_y(2).is_inverted_result_target False >>> stim.target_z(3).is_inverted_result_target False >>> stim.target_sweep_bit(9).is_inverted_result_target False >>> stim.target_rec(-5).is_inverted_result_target False """ @property def is_measurement_record_target( self, ) -> bool: """Returns whether or not this is a measurement record target like `rec[-5]`. Examples: >>> import stim >>> stim.GateTarget(6).is_measurement_record_target False >>> stim.target_inv(7).is_measurement_record_target False >>> stim.target_x(8).is_measurement_record_target False >>> stim.target_y(2).is_measurement_record_target False >>> stim.target_z(3).is_measurement_record_target False >>> stim.target_sweep_bit(9).is_measurement_record_target False >>> stim.target_rec(-5).is_measurement_record_target True """ @property def is_qubit_target( self, ) -> bool: """Returns whether or not this is a qubit target like `5` or `!6`. Examples: >>> import stim >>> stim.GateTarget(6).is_qubit_target True >>> stim.target_inv(7).is_qubit_target True >>> stim.target_x(8).is_qubit_target False >>> stim.target_y(2).is_qubit_target False >>> stim.target_z(3).is_qubit_target False >>> stim.target_sweep_bit(9).is_qubit_target False >>> stim.target_rec(-5).is_qubit_target False """ @property def is_sweep_bit_target( self, ) -> bool: """Returns whether or not this is a sweep bit target like `sweep[4]`. Examples: >>> import stim >>> stim.GateTarget(6).is_sweep_bit_target False >>> stim.target_inv(7).is_sweep_bit_target False >>> stim.target_x(8).is_sweep_bit_target False >>> stim.target_y(2).is_sweep_bit_target False >>> stim.target_z(3).is_sweep_bit_target False >>> stim.target_sweep_bit(9).is_sweep_bit_target True >>> stim.target_rec(-5).is_sweep_bit_target False """ @property def is_x_target( self, ) -> bool: """Returns whether or not this is an X pauli target like `X2` or `!X7`. Examples: >>> import stim >>> stim.GateTarget(6).is_x_target False >>> stim.target_inv(7).is_x_target False >>> stim.target_x(8).is_x_target True >>> stim.target_y(2).is_x_target False >>> stim.target_z(3).is_x_target False >>> stim.target_sweep_bit(9).is_x_target False >>> stim.target_rec(-5).is_x_target False """ @property def is_y_target( self, ) -> bool: """Returns whether or not this is a Y pauli target like `Y2` or `!Y7`. Examples: >>> import stim >>> stim.GateTarget(6).is_y_target False >>> stim.target_inv(7).is_y_target False >>> stim.target_x(8).is_y_target False >>> stim.target_y(2).is_y_target True >>> stim.target_z(3).is_y_target False >>> stim.target_sweep_bit(9).is_y_target False >>> stim.target_rec(-5).is_y_target False """ @property def is_z_target( self, ) -> bool: """Returns whether or not this is a Z pauli target like `Z2` or `!Z7`. Examples: >>> import stim >>> stim.GateTarget(6).is_z_target False >>> stim.target_inv(7).is_z_target False >>> stim.target_x(8).is_z_target False >>> stim.target_y(2).is_z_target False >>> stim.target_z(3).is_z_target True >>> stim.target_sweep_bit(9).is_z_target False >>> stim.target_rec(-5).is_z_target False """ @property def pauli_type( self, ) -> str: """Returns whether this is an 'X', 'Y', or 'Z' target. For non-pauli targets, this property evaluates to 'I'. Examples: >>> import stim >>> stim.GateTarget(6).pauli_type 'I' >>> stim.target_inv(7).pauli_type 'I' >>> stim.target_x(8).pauli_type 'X' >>> stim.target_y(2).pauli_type 'Y' >>> stim.target_z(3).pauli_type 'Z' >>> stim.target_sweep_bit(9).pauli_type 'I' >>> stim.target_rec(-5).pauli_type 'I' """ @property def qubit_value( self, ) -> Optional[int]: """Returns the integer value of the targeted qubit, or else None. Examples: >>> import stim >>> stim.GateTarget(6).qubit_value 6 >>> stim.target_inv(7).qubit_value 7 >>> stim.target_x(8).qubit_value 8 >>> stim.target_y(2).qubit_value 2 >>> stim.target_z(3).qubit_value 3 >>> print(stim.target_sweep_bit(9).qubit_value) None >>> print(stim.target_rec(-5).qubit_value) None """ @property def value( self, ) -> int: """The numeric part of the target. This is non-negative integer for qubit targets, and a negative integer for measurement record targets. Examples: >>> import stim >>> stim.GateTarget(6).value 6 >>> stim.target_inv(7).value 7 >>> stim.target_x(8).value 8 >>> stim.target_y(2).value 2 >>> stim.target_z(3).value 3 >>> stim.target_sweep_bit(9).value 9 >>> stim.target_rec(-5).value -5 """ class GateTargetWithCoords: """A gate target with associated coordinate information. For example, if the gate target is a qubit from a circuit with QUBIT_COORDS instructions, the coords field will contain the coordinate data from the QUBIT_COORDS instruction for the qubit. This is helpful information to have available when debugging a problem in a circuit, instead of having to constantly manually look up the coordinates of a qubit index in order to understand what is happening. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) >>> t.coords [1.5, 2.0] """ def __init__( self, gate_target: object, coords: List[float], ) -> None: """Creates a stim.GateTargetWithCoords. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) >>> t.coords [1.5, 2.0] """ @property def coords( self, ) -> List[float]: """Returns the associated coordinate information as a list of floats. If there is no coordinate information, returns an empty list. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.coords [1.5, 2.0] """ @property def gate_target( self, ) -> stim.GateTarget: """Returns the actual gate target as a `stim.GateTarget`. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) """ class PauliString: """A signed Pauli tensor product (e.g. "+X \u2297 X \u2297 X" or "-Y \u2297 Z". Represents a collection of Pauli operations (I, X, Y, Z) applied pairwise to a collection of qubits. Examples: >>> import stim >>> stim.PauliString("XX") * stim.PauliString("YY") stim.PauliString("-ZZ") >>> print(stim.PauliString(5)) +_____ """ def __add__( self, rhs: stim.PauliString, ) -> stim.PauliString: """Returns the tensor product of two Pauli strings. Concatenates the Pauli strings and multiplies their signs. Args: rhs: A second stim.PauliString. Examples: >>> import stim >>> stim.PauliString("X") + stim.PauliString("YZ") stim.PauliString("+XYZ") >>> stim.PauliString("iX") + stim.PauliString("-X") stim.PauliString("-iXX") Returns: The tensor product. """ def __eq__( self, arg0: stim.PauliString, ) -> bool: """Determines if two Pauli strings have identical contents. """ @overload def __getitem__( self, index_or_slice: int, ) -> int: pass @overload def __getitem__( self, index_or_slice: slice, ) -> stim.PauliString: pass def __getitem__( self, index_or_slice: object, ) -> object: """Returns an individual Pauli or Pauli string slice from the pauli string. Individual Paulis are returned as an int using the encoding 0=I, 1=X, 2=Y, 3=Z. Slices are returned as a stim.PauliString (always with positive sign). Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p[2] 2 >>> p[-1] 3 >>> p[:2] stim.PauliString("+_X") >>> p[::-1] stim.PauliString("+ZYX_") Args: index_or_slice: The index of the pauli to return, or the slice of paulis to return. Returns: 0: Identity. 1: Pauli X. 2: Pauli Y. 3: Pauli Z. """ def __iadd__( self, rhs: stim.PauliString, ) -> stim.PauliString: """Performs an inplace tensor product. Concatenates the given Pauli string onto the receiving string and multiplies their signs. Args: rhs: A second stim.PauliString. Examples: >>> import stim >>> p = stim.PauliString("iX") >>> alias = p >>> p += stim.PauliString("-YY") >>> p stim.PauliString("-iXYY") >>> alias is p True Returns: The mutated pauli string. """ def __imul__( self, rhs: object, ) -> stim.PauliString: """Inplace right-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: rhs: The right hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term into the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply into the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> p = stim.PauliString("X") >>> p *= 1j >>> p stim.PauliString("+iX") >>> p = stim.PauliString("iXY_") >>> p *= 3 >>> p stim.PauliString("-iXY_XY_XY_") >>> p = stim.PauliString("X") >>> alias = p >>> p *= stim.PauliString("Y") >>> alias stim.PauliString("+iZ") >>> p = stim.PauliString("X") >>> p *= stim.PauliString("_YY") >>> p stim.PauliString("+XYY") Returns: The mutated Pauli string. """ def __init__( self, arg: Union[None, int, str, stim.PauliString, Iterable[Union[int, Literal["_", "I", "X", "Y", "Z"]]]] = None, /, ) -> None: """Initializes a stim.PauliString from the given argument. When given a string, the string is parsed as a pauli string. The string can optionally start with a sign ('+', '-', 'i', '+i', or '-i'). The rest of the string should be either a dense pauli string or a sparse pauli string. A dense pauli string is made up of characters from '_IXYZ' where '_' and 'I' mean identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z. A sparse pauli string is a series of integers seperated by '*' and prefixed by 'I', 'X', 'Y', or 'Z'. Args: arg [position-only]: This can be a variety of types, including: None (default): initializes an empty Pauli string. int: initializes an identity Pauli string of the given length. str: initializes by parsing the given text. stim.PauliString: initializes a copy of the given Pauli string. Iterable: initializes by interpreting each item as a Pauli. Each item can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Dict[int, Union[int, str]]: initializes by interpreting keys as the qubit index and values as the Pauli for that index. Each value can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Dict[Union[int, str], Iterable[int]]: initializes by interpreting keys as Pauli operators and values as the qubit indices for that Pauli. Each key can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Examples: >>> import stim >>> stim.PauliString("-XYZ") stim.PauliString("-XYZ") >>> stim.PauliString() stim.PauliString("+") >>> stim.PauliString(5) stim.PauliString("+_____") >>> stim.PauliString(stim.PauliString("XX")) stim.PauliString("+XX") >>> stim.PauliString([0, 1, 3, 2]) stim.PauliString("+_XZY") >>> stim.PauliString("X" for _ in range(4)) stim.PauliString("+XXXX") >>> stim.PauliString("-X2*Y6") stim.PauliString("-__X___Y") >>> stim.PauliString("X6*Y6") stim.PauliString("+i______Z") >>> stim.PauliString({0: "X", 2: "Y", 3: "X"}) stim.PauliString("+X_YX") >>> stim.PauliString({0: "X", 2: 2, 3: 1}) stim.PauliString("+X_YX") >>> stim.PauliString({"X": [1], 2: [4], "Z": [0, 3]}) stim.PauliString("+ZX_ZY") """ def __itruediv__( self, rhs: complex, ) -> stim.PauliString: """Inplace divides the Pauli string by a complex unit. Args: rhs: The divisor. Can be 1, -1, 1j, or -1j. Examples: >>> import stim >>> p = stim.PauliString("X") >>> p /= 1j >>> p stim.PauliString("-iX") Returns: The mutated Pauli string. Raises: ValueError: The divisor isn't 1, -1, 1j, or -1j. """ def __len__( self, ) -> int: """Returns the length the pauli string; the number of qubits it operates on. Examples: >>> import stim >>> len(stim.PauliString("XY_ZZ")) 5 >>> len(stim.PauliString("X0*Z99")) 100 """ def __mul__( self, rhs: object, ) -> stim.PauliString: """Right-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: rhs: The right hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term with the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply with the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> stim.PauliString("X") * 1 stim.PauliString("+X") >>> stim.PauliString("X") * -1 stim.PauliString("-X") >>> stim.PauliString("X") * 1j stim.PauliString("+iX") >>> stim.PauliString("X") * 2 stim.PauliString("+XX") >>> stim.PauliString("-X") * 2 stim.PauliString("+XX") >>> stim.PauliString("iX") * 2 stim.PauliString("-XX") >>> stim.PauliString("X") * 3 stim.PauliString("+XXX") >>> stim.PauliString("iX") * 3 stim.PauliString("-iXXX") >>> stim.PauliString("X") * stim.PauliString("Y") stim.PauliString("+iZ") >>> stim.PauliString("X") * stim.PauliString("XX_") stim.PauliString("+_X_") >>> stim.PauliString("XXXX") * stim.PauliString("_XYZ") stim.PauliString("+X_ZY") Returns: The product or tensor power. Raises: TypeError: The right hand side isn't a stim.PauliString, a non-negative integer, or a complex unit (1, -1, 1j, or -1j). """ def __ne__( self, arg0: stim.PauliString, ) -> bool: """Determines if two Pauli strings have non-identical contents. """ def __neg__( self, ) -> stim.PauliString: """Returns the negation of the pauli string. Examples: >>> import stim >>> -stim.PauliString("X") stim.PauliString("-X") >>> -stim.PauliString("-Y") stim.PauliString("+Y") >>> -stim.PauliString("iZZZ") stim.PauliString("-iZZZ") """ def __pos__( self, ) -> stim.PauliString: """Returns a pauli string with the same contents. Examples: >>> import stim >>> +stim.PauliString("+X") stim.PauliString("+X") >>> +stim.PauliString("-YY") stim.PauliString("-YY") >>> +stim.PauliString("iZZZ") stim.PauliString("+iZZZ") """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equivalent `stim.PauliString`. """ def __rmul__( self, lhs: object, ) -> stim.PauliString: """Left-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: lhs: The left hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term with the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply with the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> 1 * stim.PauliString("X") stim.PauliString("+X") >>> -1 * stim.PauliString("X") stim.PauliString("-X") >>> 1j * stim.PauliString("X") stim.PauliString("+iX") >>> 2 * stim.PauliString("X") stim.PauliString("+XX") >>> 2 * stim.PauliString("-X") stim.PauliString("+XX") >>> 2 * stim.PauliString("iX") stim.PauliString("-XX") >>> 3 * stim.PauliString("X") stim.PauliString("+XXX") >>> 3 * stim.PauliString("iX") stim.PauliString("-iXXX") >>> stim.PauliString("X") * stim.PauliString("Y") stim.PauliString("+iZ") >>> stim.PauliString("X") * stim.PauliString("XX_") stim.PauliString("+_X_") >>> stim.PauliString("XXXX") * stim.PauliString("_XYZ") stim.PauliString("+X_ZY") Returns: The product. Raises: ValueError: The scalar phase factor isn't 1, -1, 1j, or -1j. """ def __setitem__( self, index: int, new_pauli: object, ) -> None: """Mutates an entry in the pauli string using the encoding 0=I, 1=X, 2=Y, 3=Z. Args: index: The index of the pauli to overwrite. new_pauli: Either a character from '_IXYZ' or an integer from range(4). Examples: >>> import stim >>> p = stim.PauliString(4) >>> p[2] = 1 >>> print(p) +__X_ >>> p[0] = 3 >>> p[1] = 2 >>> p[3] = 0 >>> print(p) +ZYX_ >>> p[0] = 'I' >>> p[1] = 'X' >>> p[2] = 'Y' >>> p[3] = 'Z' >>> print(p) +_XYZ >>> p[-1] = 'Y' >>> print(p) +_XYY """ def __str__( self, ) -> str: """Returns a text description. """ def __truediv__( self, rhs: complex, ) -> stim.PauliString: """Divides the Pauli string by a complex unit. Args: rhs: The divisor. Can be 1, -1, 1j, or -1j. Examples: >>> import stim >>> stim.PauliString("X") / 1j stim.PauliString("-iX") Returns: The quotient. Raises: ValueError: The divisor isn't 1, -1, 1j, or -1j. """ @overload def after( self, operation: Union[stim.Circuit, stim.CircuitInstruction], ) -> stim.PauliString: pass @overload def after( self, operation: stim.Tableau, targets: Iterable[int], ) -> stim.PauliString: pass def after( self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None, ) -> stim.PauliString: """Returns the result of conjugating the Pauli string by an operation. Args: operation: A circuit, tableau, or circuit instruction to conjugate the Pauli string by. Must be Clifford (e.g. if it's a circuit, the circuit can't have noise or measurements). targets: Required if and only if the operation is a tableau. Specifies which qubits to target. Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p.after(stim.CircuitInstruction("H", [1])) stim.PauliString("+_ZYZ") >>> p.after(stim.Circuit(''' ... C_XYZ 1 2 3 ... ''')) stim.PauliString("+_YZX") >>> p.after(stim.Tableau.from_named_gate('CZ'), targets=[0, 1]) stim.PauliString("+ZXYZ") Returns: The conjugated Pauli string. The Pauli string after the operation that is exactly equivalent to the given Pauli string before the operation. """ @overload def before( self, operation: Union[stim.Circuit, stim.CircuitInstruction], ) -> stim.PauliString: pass @overload def before( self, operation: stim.Tableau, targets: Iterable[int], ) -> stim.PauliString: pass def before( self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None, ) -> stim.PauliString: """Returns the result of conjugating the Pauli string by an operation. Args: operation: A circuit, tableau, or circuit instruction to anti-conjugate the Pauli string by. Must be Clifford (e.g. if it's a circuit, the circuit can't have noise or measurements). targets: Required if and only if the operation is a tableau. Specifies which qubits to target. Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p.before(stim.CircuitInstruction("H", [1])) stim.PauliString("+_ZYZ") >>> p.before(stim.Circuit(''' ... C_XYZ 1 2 3 ... ''')) stim.PauliString("+_ZXY") >>> p.before(stim.Tableau.from_named_gate('CZ'), targets=[0, 1]) stim.PauliString("+ZXYZ") Returns: The conjugated Pauli string. The Pauli string before the operation that is exactly equivalent to the given Pauli string after the operation. """ def commutes( self, other: stim.PauliString, ) -> bool: """Determines if two Pauli strings commute or not. Two Pauli strings commute if they have an even number of matched non-equal non-identity Pauli terms. Otherwise they anticommute. Args: other: The other Pauli string. Examples: >>> import stim >>> xx = stim.PauliString("XX") >>> xx.commutes(stim.PauliString("X_")) True >>> xx.commutes(stim.PauliString("XX")) True >>> xx.commutes(stim.PauliString("XY")) False >>> xx.commutes(stim.PauliString("XZ")) False >>> xx.commutes(stim.PauliString("ZZ")) True >>> xx.commutes(stim.PauliString("X_Y__")) True >>> xx.commutes(stim.PauliString("")) True Returns: True if the Pauli strings commute, False if they anti-commute. """ def copy( self, ) -> stim.PauliString: """Returns a copy of the pauli string. The copy is an independent pauli string with the same contents. Examples: >>> import stim >>> p1 = stim.PauliString.random(2) >>> p2 = p1.copy() >>> p2 is p1 False >>> p2 == p1 True """ def extended_product( self, other: stim.PauliString, ) -> Tuple[complex, stim.PauliString]: """[DEPRECATED] Use multiplication (__mul__ or *) instead. """ @staticmethod def from_numpy( *, xs: np.ndarray, zs: np.ndarray, sign: Union[int, float, complex] = +1, num_qubits: Optional[int] = None, ) -> stim.PauliString: """Creates a pauli string from X bit and Z bit numpy arrays, using the encoding: x=0 and z=0 -> P=I x=1 and z=0 -> P=X x=1 and z=1 -> P=Y x=0 and z=1 -> P=Z Args: xs: The X bits of the pauli string. This array can either be a 1-dimensional numpy array with dtype=np.bool_, or a bit packed 1-dimensional numpy array with dtype=np.uint8. If the dtype is np.uint8 then the array is assumed to be bit packed in little endian order and the "num_qubits" argument must be specified. When bit packed, the x bit with offset k is stored at (xs[k // 8] >> (k % 8)) & 1. zs: The Z bits of the pauli string. This array can either be a 1-dimensional numpy array with dtype=np.bool_, or a bit packed 1-dimensional numpy array with dtype=np.uint8. If the dtype is np.uint8 then the array is assumed to be bit packed in little endian order and the "num_qubits" argument must be specified. When bit packed, the x bit with offset k is stored at (xs[k // 8] >> (k % 8)) & 1. sign: Defaults to +1. Set to +1, -1, 1j, or -1j to control the sign of the returned Pauli string. num_qubits: Must be specified if xs or zs is a bit packed array. Specifies the expected length of the Pauli string. Returns: The created pauli string. Examples: >>> import stim >>> import numpy as np >>> xs = np.array([1, 1, 1, 1, 1, 1, 1, 0, 0], dtype=np.bool_) >>> zs = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1], dtype=np.bool_) >>> stim.PauliString.from_numpy(xs=xs, zs=zs, sign=-1) stim.PauliString("-XXXXYYYZZ") >>> xs = np.array([127, 0], dtype=np.uint8) >>> zs = np.array([240, 1], dtype=np.uint8) >>> stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=9) stim.PauliString("+XXXXYYYZZ") """ @staticmethod def from_unitary_matrix( matrix: Iterable[Iterable[Union[int, float, complex]]], *, endian: Literal["little", "big"] = 'little', unsigned: bool = False, ) -> stim.PauliString: """Creates a stim.PauliString from the unitary matrix of a Pauli group member. Args: matrix: A unitary matrix specified as an iterable of rows, with each row is an iterable of amplitudes. The unitary matrix must correspond to a Pauli string, including global phase. endian: "little": matrix entries are in little endian order, where higher index qubits correspond to larger changes in row/col indices. "big": matrix entries are in big endian order, where higher index qubits correspond to smaller changes in row/col indices. unsigned: When False, the input must only contain the values [0, 1, -1, 1j, -1j] and the output will have the correct global phase. When True, the input is permitted to be scaled by an arbitrary unit complex value and the output will always have positive sign. False is stricter but provides more information, while True is more flexible but provides less information. Returns: The pauli string equal to the given unitary matrix. Raises: ValueError: The given matrix isn't the unitary matrix of a Pauli string. Examples: >>> import stim >>> stim.PauliString.from_unitary_matrix([ ... [1j, 0], ... [0, -1j], ... ], endian='little') stim.PauliString("+iZ") >>> stim.PauliString.from_unitary_matrix([ ... [1j**0.1, 0], ... [0, -(1j**0.1)], ... ], endian='little', unsigned=True) stim.PauliString("+Z") >>> stim.PauliString.from_unitary_matrix([ ... [0, 1, 0, 0], ... [1, 0, 0, 0], ... [0, 0, 0, -1], ... [0, 0, -1, 0], ... ], endian='little') stim.PauliString("+XZ") """ @staticmethod def iter_all( num_qubits: int, *, min_weight: int = 0, max_weight: object = None, allowed_paulis: str = 'XYZ', ) -> stim.PauliStringIterator: """Returns an iterator that iterates over all matching pauli strings. Args: num_qubits: The desired number of qubits in the pauli strings. min_weight: Defaults to 0. The minimum number of non-identity terms that must be present in each yielded pauli string. max_weight: Defaults to None (unused). The maximum number of non-identity terms that must be present in each yielded pauli string. allowed_paulis: Defaults to "XYZ". Set this to a string containing the non-identity paulis that are allowed to appear in each yielded pauli string. This argument must be a string made up of only "X", "Y", and "Z" characters. A non-identity Pauli is allowed if it appears in the string, and not allowed if it doesn't. Identity Paulis are always allowed. Returns: An Iterable[stim.PauliString] that yields the requested pauli strings. Examples: >>> import stim >>> pauli_string_iterator = stim.PauliString.iter_all( ... num_qubits=3, ... min_weight=1, ... max_weight=2, ... allowed_paulis="XZ", ... ) >>> for p in pauli_string_iterator: ... print(p) +X__ +Z__ +_X_ +_Z_ +__X +__Z +XX_ +XZ_ +ZX_ +ZZ_ +X_X +X_Z +Z_X +Z_Z +_XX +_XZ +_ZX +_ZZ """ def pauli_indices( self, included_paulis: str = "XYZ", ) -> List[int]: """Returns the indices of non-identity Paulis, or of specified Paulis. Args: include: A string containing the Pauli types to include. X type Pauli indices are included if "X" or "x" is in the string. Y type Pauli indices are included if "Y" or "y" is in the string. Z type Pauli indices are included if "Z" or "z" is in the string. I type Pauli indices are included if "I" or "_" is in the string. An exception is thrown if other characters are in the string. Returns: A list containing the ascending indices of matching Pauli terms. Examples: >>> import stim >>> stim.PauliString("_____X___Y____Z___").pauli_indices() [5, 9, 14] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("XZ") [5, 14] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("X") [5] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("Y") [9] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("IY") [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17] >>> stim.PauliString("-X103*Y100").pauli_indices() [100, 103] """ @staticmethod def random( num_qubits: int, *, allow_imaginary: bool = False, ) -> stim.PauliString: """Samples a uniformly random Hermitian Pauli string. Args: num_qubits: The number of qubits the Pauli string should act on. allow_imaginary: Defaults to False. If True, the sign of the result may be 1j or -1j in addition to +1 or -1. In other words, setting this to True allows the result to be non-Hermitian. Examples: >>> import stim >>> p = stim.PauliString.random(5) >>> len(p) 5 >>> p.sign in [-1, +1] True >>> p2 = stim.PauliString.random(3, allow_imaginary=True) >>> len(p2) 3 >>> p2.sign in [-1, +1, 1j, -1j] True Returns: The sampled Pauli string. """ @property def sign( self, ) -> complex: """The sign of the Pauli string. Can be +1, -1, 1j, or -1j. Examples: >>> import stim >>> stim.PauliString("X").sign (1+0j) >>> stim.PauliString("-X").sign (-1+0j) >>> stim.PauliString("iX").sign 1j >>> stim.PauliString("-iX").sign (-0-1j) """ @sign.setter def sign(self, value: complex): pass def to_numpy( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: """Decomposes the contents of the pauli string into X bit and Z bit numpy arrays. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: An (xs, zs) tuple encoding the paulis from the string. The k'th Pauli from the string is encoded into k'th bit of xs and the k'th bit of zs using the "xz" encoding: P=I -> x=0 and z=0 P=X -> x=1 and z=0 P=Y -> x=1 and z=1 P=Z -> x=0 and z=1 The dtype and shape of the result depends on the bit_packed argument. If bit_packed=False: Each bit gets its own byte. xs.dtype = zs.dtype = np.bool_ xs.shape = zs.shape = len(self) xs_k = xs[k] zs_k = zs[k] If bit_packed=True: Equivalent to applying np.packbits(bitorder='little') to the result. xs.dtype = zs.dtype = np.uint8 xs.shape = zs.shape = math.ceil(len(self) / 8) xs_k = (xs[k // 8] >> (k % 8)) & 1 zs_k = (zs[k // 8] >> (k % 8)) & 1 Examples: >>> import stim >>> xs, zs = stim.PauliString("XXXXYYYZZ").to_numpy() >>> xs array([ True, True, True, True, True, True, True, False, False]) >>> zs array([False, False, False, False, True, True, True, True, True]) >>> xs, zs = stim.PauliString("XXXXYYYZZ").to_numpy(bit_packed=True) >>> xs array([127, 0], dtype=uint8) >>> zs array([240, 1], dtype=uint8) """ def to_tableau( self, ) -> stim.Tableau: """Creates a Tableau equivalent to this Pauli string. The tableau represents a Clifford operation that multiplies qubits by the corresponding Pauli operations from this Pauli string. The global phase of the pauli operation is lost in the conversion. Returns: The created tableau. Examples: >>> import stim >>> p = stim.PauliString("ZZ") >>> p.to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-X_"), stim.PauliString("-_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+_Z"), ], ) >>> q = stim.PauliString("YX_Z") >>> q.to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-X___"), stim.PauliString("+_X__"), stim.PauliString("+__X_"), stim.PauliString("-___X"), ], zs=[ stim.PauliString("-Z___"), stim.PauliString("-_Z__"), stim.PauliString("+__Z_"), stim.PauliString("+___Z"), ], ) """ def to_unitary_matrix( self, *, endian: Literal["little", "big"], ) -> np.ndarray[np.complex64]: """Converts the pauli string into a unitary matrix. Args: endian: "little": The first qubit is the least significant (corresponds to an offset of 1 in the matrix). "big": The first qubit is the most significant (corresponds to an offset of 2**(n - 1) in the matrix). Returns: A numpy array with dtype=np.complex64 and shape=(1 << len(pauli_string), 1 << len(pauli_string)). Example: >>> import stim >>> stim.PauliString("-YZ").to_unitary_matrix(endian="little") array([[0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.-1.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 0.-1.j], [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j]], dtype=complex64) """ @property def weight( self, ) -> int: """Returns the number of non-identity pauli terms in the pauli string. Examples: >>> import stim >>> stim.PauliString("+___").weight 0 >>> stim.PauliString("+__X").weight 1 >>> stim.PauliString("+XYZ").weight 3 >>> stim.PauliString("-XXX___XXYZ").weight 7 """ class PauliStringIterator: """Iterates over all pauli strings matching specified patterns. Examples: >>> import stim >>> pauli_string_iterator = stim.PauliString.iter_all( ... 2, ... min_weight=1, ... max_weight=1, ... allowed_paulis="XZ", ... ) >>> for p in pauli_string_iterator: ... print(p) +X_ +Z_ +_X +_Z """ def __iter__( self, ) -> stim.PauliStringIterator: """Returns an independent copy of the pauli string iterator. Since for-loops and loop-comprehensions call `iter` on things they iterate, this effectively allows the iterator to be iterated multiple times. """ def __next__( self, ) -> stim.PauliString: """Returns the next iterated pauli string. """ class Tableau: """A stabilizer tableau. Represents a Clifford operation by explicitly storing how that operation conjugates a list of Pauli group generators into composite Pauli products. Examples: >>> import stim >>> stim.Tableau.from_named_gate("H") stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> t = stim.Tableau.random(5) >>> t_inv = t**-1 >>> print(t * t_inv) +-xz-xz-xz-xz-xz- | ++ ++ ++ ++ ++ | XZ __ __ __ __ | __ XZ __ __ __ | __ __ XZ __ __ | __ __ __ XZ __ | __ __ __ __ XZ >>> x2z3 = t.x_output(2) * t.z_output(3) >>> t_inv(x2z3) stim.PauliString("+__XZ_") """ def __add__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Returns the direct sum (diagonal concatenation) of two Tableaus. Args: rhs: A second stim.Tableau. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> cz = stim.Tableau.from_named_gate("CZ") >>> print(s + cz) +-xz-xz-xz- | ++ ++ ++ | YZ __ __ | __ XZ Z_ | __ Z_ XZ Returns: The direct sum. """ def __call__( self, pauli_string: stim.PauliString, ) -> stim.PauliString: """Returns the equivalent PauliString after the Tableau's Clifford operation. If P is a Pauli product before a Clifford operation C, then this method returns Q = C * P * C**-1 (the conjugation of P by C). Q is the equivalent Pauli product after C. This works because: C*P = C*P * I = C*P * (C**-1 * C) = (C*P*C**-1) * C = Q*C (Keep in mind that A*B means first B is applied, then A is applied.) Args: pauli_string: The pauli string to conjugate. Returns: The new conjugated pauli string. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> p = stim.PauliString("XX") >>> result = t(p) >>> print(result) +X_ """ def __eq__( self, arg0: stim.Tableau, ) -> bool: """Determines if two tableaus have identical contents. """ def __iadd__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Performs an inplace direct sum (diagonal concatenation). Args: rhs: A second stim.Tableau. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> cz = stim.Tableau.from_named_gate("CZ") >>> alias = s >>> s += cz >>> alias is s True >>> print(s) +-xz-xz-xz- | ++ ++ ++ | YZ __ __ | __ XZ Z_ | __ Z_ XZ Returns: The mutated tableau. """ def __init__( self, num_qubits: int, ) -> None: """Creates an identity tableau over the given number of qubits. Examples: >>> import stim >>> t = stim.Tableau(3) >>> print(t) +-xz-xz-xz- | ++ ++ ++ | XZ __ __ | __ XZ __ | __ __ XZ Args: num_qubits: The number of qubits the tableau's operation acts on. """ def __len__( self, ) -> int: """Returns the number of qubits operated on by the tableau. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> len(t) 2 """ def __mul__( self, rhs: stim.Tableau, ) -> stim.Tableau: """Returns the product of two tableaus. If the tableau T1 represents the Clifford operation with unitary C1, and the tableau T2 represents the Clifford operation with unitary C2, then the tableau T1*T2 represents the Clifford operation with unitary C1*C2. Args: rhs: The tableau on the right hand side of the multiplication. Examples: >>> import stim >>> t1 = stim.Tableau.random(4) >>> t2 = stim.Tableau.random(4) >>> t3 = t2 * t1 >>> p = stim.PauliString.random(4) >>> t3(p) == t2(t1(p)) True """ def __ne__( self, arg0: stim.Tableau, ) -> bool: """Determines if two tableaus have non-identical contents. """ def __pow__( self, exponent: int, ) -> stim.Tableau: """Raises the tableau to an integer power. Large powers are reached efficiently using repeated squaring. Negative powers are reached by inverting the tableau. Args: exponent: The power to raise to. Can be negative, zero, or positive. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> s**0 == stim.Tableau(1) True >>> s**1 == s True >>> s**2 == stim.Tableau.from_named_gate("Z") True >>> s**-1 == s**3 == stim.Tableau.from_named_gate("S_DAG") True >>> s**5 == s True >>> s**(400000000 + 1) == s True >>> s**(-400000000 + 1) == s True """ def __repr__( self, ) -> str: """Returns valid python code evaluating to an equal `stim.Tableau`. """ def __str__( self, ) -> str: """Returns a text description. """ def append( self, gate: stim.Tableau, targets: Sequence[int], ) -> None: """Appends an operation's effect into this tableau, mutating this tableau. Time cost is O(n*m*m) where n=len(self) and m=len(gate). Args: gate: The tableau of the operation being appended into this tableau. targets: The qubits being targeted by the gate. Examples: >>> import stim >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> t = stim.Tableau(2) >>> t.append(cnot, [0, 1]) >>> t.append(cnot, [1, 0]) >>> t.append(cnot, [0, 1]) >>> t == stim.Tableau.from_named_gate("SWAP") True """ def copy( self, ) -> stim.Tableau: """Returns a copy of the tableau. An independent tableau with the same contents. Examples: >>> import stim >>> t1 = stim.Tableau.random(2) >>> t2 = t1.copy() >>> t2 is t1 False >>> t2 == t1 True """ @staticmethod def from_circuit( circuit: stim.Circuit, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False, ) -> stim.Tableau: """Converts a circuit into an equivalent stabilizer tableau. Args: circuit: The circuit to compile into a tableau. ignore_noise: Defaults to False. When False, any noise operations in the circuit will cause the conversion to fail with an exception. When True, noise operations are skipped over as if they weren't even present in the circuit. ignore_measurement: Defaults to False. When False, any measurement operations in the circuit will cause the conversion to fail with an exception. When True, measurement operations are skipped over as if they weren't even present in the circuit. ignore_reset: Defaults to False. When False, any reset operations in the circuit will cause the conversion to fail with an exception. When True, reset operations are skipped over as if they weren't even present in the circuit. Returns: The tableau equivalent to the given circuit (up to global phase). Raises: ValueError: The circuit contains noise operations but ignore_noise=False. OR The circuit contains measurement operations but ignore_measurement=False. OR The circuit contains reset operations but ignore_reset=False. Examples: >>> import stim >>> stim.Tableau.from_circuit(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''')) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ @staticmethod def from_conjugated_generators( *, xs: List[stim.PauliString], zs: List[stim.PauliString], ) -> stim.Tableau: """Creates a tableau from the given outputs for each generator. Verifies that the tableau is well formed. Args: xs: A List[stim.PauliString] with the results of conjugating X0, X1, etc. zs: A List[stim.PauliString] with the results of conjugating Z0, Z1, etc. Returns: The created tableau. Raises: ValueError: The given outputs are malformed. Their lengths are inconsistent, or they don't satisfy the required commutation relationships. Examples: >>> import stim >>> identity3 = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("X__"), ... stim.PauliString("_X_"), ... stim.PauliString("__X"), ... ], ... zs=[ ... stim.PauliString("Z__"), ... stim.PauliString("_Z_"), ... stim.PauliString("__Z"), ... ], ... ) >>> identity3 == stim.Tableau(3) True """ @staticmethod def from_named_gate( name: str, ) -> stim.Tableau: """Returns the tableau of a named Clifford gate. Args: name: The name of the Clifford gate. Returns: The gate's tableau. Examples: >>> import stim >>> print(stim.Tableau.from_named_gate("H")) +-xz- | ++ | ZX >>> print(stim.Tableau.from_named_gate("CNOT")) +-xz-xz- | ++ ++ | XZ _Z | X_ XZ >>> print(stim.Tableau.from_named_gate("S")) +-xz- | ++ | YZ """ def from_numpy( self, *, x2x: np.ndarray, x2z: np.ndarray, z2x: np.ndarray, z2z: np.ndarray, x_signs: Optional[np.ndarray] = None, z_signs: Optional[np.ndarray] = None, ) -> stim.Tableau: """Creates a tableau from numpy arrays x2x, x2z, z2x, z2z, x_signs, and z_signs. The x2x, x2z, z2x, z2z arrays are the four quadrants of the table defined in Aaronson and Gottesman's "Improved Simulation of Stabilizer Circuits" ( https://arxiv.org/abs/quant-ph/0406196 ). Args: x2x: A 2d numpy array containing the x-to-x coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.x_output_pauli(i, j) in [1, 2] == x2x[i, j]. Bit packing must be in little endian order and only applies to the second axis. x2z: A 2d numpy array containing the x-to-z coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.x_output_pauli(i, j) in [2, 3] == x2z[i, j]. Bit packing must be in little endian order and only applies to the second axis. z2x: A 2d numpy array containing the z-to-x coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.z_output_pauli(i, j) in [1, 2] == z2x[i, j]. Bit packing must be in little endian order and only applies to the second axis. z2z: A 2d numpy array containing the z-to-z coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.z_output_pauli(i, j) in [2, 3] == z2z[i, j]. Bit packing must be in little endian order and only applies to the second axis. x_signs: Defaults to all-positive if not specified. A 1d numpy array containing the sign bits for the X generator outputs. False means positive and True means negative. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). Bit packing must be in little endian order. z_signs: Defaults to all-positive if not specified. A 1d numpy array containing the sign bits for the Z generator outputs. False means positive and True means negative. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). Bit packing must be in little endian order. Returns: The tableau created from the numpy data. Examples: >>> import stim >>> import numpy as np >>> tableau = stim.Tableau.from_numpy( ... x2x=np.array([[1, 1], [0, 1]], dtype=np.bool_), ... z2x=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... x2z=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... z2z=np.array([[1, 0], [1, 1]], dtype=np.bool_), ... ) >>> tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+ZZ"), ], ) >>> tableau == stim.Tableau.from_named_gate("CNOT") True >>> stim.Tableau.from_numpy( ... x2x=np.array([[9], [5], [7], [6]], dtype=np.uint8), ... x2z=np.array([[13], [13], [0], [3]], dtype=np.uint8), ... z2x=np.array([[8], [5], [9], [15]], dtype=np.uint8), ... z2z=np.array([[6], [11], [2], [3]], dtype=np.uint8), ... x_signs=np.array([7], dtype=np.uint8), ... z_signs=np.array([9], dtype=np.uint8), ... ) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-Y_ZY"), stim.PauliString("-Y_YZ"), stim.PauliString("-XXX_"), stim.PauliString("+ZYX_"), ], zs=[ stim.PauliString("-_ZZX"), stim.PauliString("+YZXZ"), stim.PauliString("+XZ_X"), stim.PauliString("-YYXX"), ], ) """ @staticmethod def from_stabilizers( stabilizers: Iterable[stim.PauliString], *, allow_redundant: bool = False, allow_underconstrained: bool = False, ) -> stim.Tableau: """Creates a tableau representing a state with the given stabilizers. Args: stabilizers: A list of `stim.PauliString`s specifying the stabilizers that the state must have. It is permitted for stabilizers to have different lengths. All stabilizers are padded up to the length of the longest stabilizer by appending identity terms. allow_redundant: Defaults to False. If set to False, then the given stabilizers must all be independent. If any one of them is a product of the others (including the empty product), an exception will be raised. If set to True, then redundant stabilizers are simply ignored. allow_underconstrained: Defaults to False. If set to False, then the given stabilizers must form a complete set of generators. They must exactly specify the desired stabilizer state, with no degrees of freedom left over. For an n-qubit state there must be n independent stabilizers. If set to True, then there can be leftover degrees of freedom which can be set arbitrarily. Returns: A tableau which, when applied to the all-zeroes state, produces a state with the given stabilizers. Guarantees that result.z_output(k) will be equal to the k'th independent stabilizer from the `stabilizers` argument. Raises: ValueError: A stabilizer is redundant but allow_redundant=True wasn't set. OR The given stabilizers are contradictory (e.g. "+Z" and "-Z" both specified). OR The given stabilizers anticommute (e.g. "+Z" and "+X" both specified). OR The stabilizers left behind a degree of freedom but allow_underconstrained=True wasn't set. OR A stabilizer has an imaginary sign (i or -i). Examples: >>> import stim >>> stim.Tableau.from_stabilizers([ ... stim.PauliString("XX"), ... stim.PauliString("ZZ"), ... ]) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) >>> stim.Tableau.from_stabilizers([ ... stim.PauliString("XX_"), ... stim.PauliString("ZZ_"), ... stim.PauliString("-YY_"), ... stim.PauliString(""), ... ], allow_underconstrained=True, allow_redundant=True) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z__"), stim.PauliString("+_X_"), stim.PauliString("+__X"), ], zs=[ stim.PauliString("+XX_"), stim.PauliString("+ZZ_"), stim.PauliString("+__Z"), ], ) """ @staticmethod def from_state_vector( state_vector: Iterable[float], *, endian: Literal["little", "big"], ) -> stim.Tableau: """Creates a tableau representing the stabilizer state of the given state vector. Args: state_vector: A list of complex amplitudes specifying a superposition. The vector must correspond to a state that is reachable using Clifford operations, and can be unnormalized. endian: "little": state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A tableau which, when applied to the all-zeroes state, produces a state with the given state vector. Raises: ValueError: The given state vector isn't a list of complex values specifying a stabilizer state. OR The given endian value isn't 'little' or 'big'. Examples: >>> import stim >>> stim.Tableau.from_state_vector([ ... 0.5**0.5, ... 0.5**0.5 * 1j, ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+Y"), ], ) >>> stim.Tableau.from_state_vector([ ... 0.5**0.5, ... 0, ... 0, ... 0.5**0.5, ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ @staticmethod def from_unitary_matrix( matrix: Iterable[Iterable[float]], *, endian: Literal["little", "big"] = 'little', ) -> stim.Tableau: """Creates a tableau from the unitary matrix of a Clifford operation. Args: matrix: A unitary matrix specified as an iterable of rows, with each row is an iterable of amplitudes. The unitary matrix must correspond to a Clifford operation. endian: "little": matrix entries are in little endian order, where higher index qubits correspond to larger changes in row/col indices. "big": matrix entries are in big endian order, where higher index qubits correspond to smaller changes in row/col indices. Returns: The tableau equivalent to the given unitary matrix (up to global phase). Raises: ValueError: The given matrix isn't the unitary matrix of a Clifford operation. Examples: >>> import stim >>> stim.Tableau.from_unitary_matrix([ ... [1, 0], ... [0, 1j], ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Y"), ], zs=[ stim.PauliString("+Z"), ], ) >>> stim.Tableau.from_unitary_matrix([ ... [1, 0, 0, 0], ... [0, 1, 0, 0], ... [0, 0, 0, -1j], ... [0, 0, 1j, 0], ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XZ"), stim.PauliString("+YX"), ], zs=[ stim.PauliString("+ZZ"), stim.PauliString("+_Z"), ], ) """ def inverse( self, *, unsigned: bool = False, ) -> stim.Tableau: """Computes the inverse of the tableau. The inverse T^-1 of a tableau T is the unique tableau with the property that T * T^-1 = T^-1 * T = I where I is the identity tableau. Args: unsigned: Defaults to false. When set to true, skips computing the signs of the output observables and instead just set them all to be positive. This is beneficial because computing the signs takes O(n^3) time and the rest of the inverse computation is O(n^2) where n is the number of qubits in the tableau. So, if you only need the Pauli terms (not the signs), it is significantly cheaper. Returns: The inverse tableau. Examples: >>> import stim >>> # Check that the inverse agrees with hard-coded tableaus. >>> s = stim.Tableau.from_named_gate("S") >>> s_dag = stim.Tableau.from_named_gate("S_DAG") >>> s.inverse() == s_dag True >>> z = stim.Tableau.from_named_gate("Z") >>> z.inverse() == z True >>> # Check that multiplying by the inverse produces the identity. >>> t = stim.Tableau.random(10) >>> t_inv = t.inverse() >>> identity = stim.Tableau(10) >>> t * t_inv == t_inv * t == identity True >>> # Check a manual case. >>> t = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("-__Z"), ... stim.PauliString("+XZ_"), ... stim.PauliString("+_ZZ"), ... ], ... zs=[ ... stim.PauliString("-YYY"), ... stim.PauliString("+Z_Z"), ... stim.PauliString("-ZYZ") ... ], ... ) >>> print(t.inverse()) +-xz-xz-xz- | -- +- -- | XX XX YX | XZ Z_ X_ | X_ YX Y_ >>> print(t.inverse(unsigned=True)) +-xz-xz-xz- | ++ ++ ++ | XX XX YX | XZ Z_ X_ | X_ YX Y_ """ def inverse_x_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit X Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).x_output(input_index)`. Args: input_index: Identifies the column (the qubit of the X generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating an X generator by the inverse of the tableau. Examples: >>> import stim # Check equivalence with the inverse's x_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().x_output(0) >>> t.inverse_x_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_x_output(0, unsigned=True) == expected True """ def inverse_x_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().x_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input X generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_x_output_pauli(0, 0) 2 >>> t_inv.inverse_x_output_pauli(0, 1) 0 >>> t_inv.inverse_x_output_pauli(1, 0) 2 >>> t_inv.inverse_x_output_pauli(1, 1) 3 """ def inverse_y_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit Y Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).y_output(input_index)`. Args: input_index: Identifies the column (the qubit of the Y generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating a Y generator by the inverse of the tableau. Examples: >>> import stim # Check equivalence with the inverse's y_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().y_output(0) >>> t.inverse_y_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_y_output(0, unsigned=True) == expected True """ def inverse_y_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().y_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input Y generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_y_output_pauli(0, 0) 1 >>> t_inv.inverse_y_output_pauli(0, 1) 2 >>> t_inv.inverse_y_output_pauli(1, 0) 0 >>> t_inv.inverse_y_output_pauli(1, 1) 2 """ def inverse_z_output( self, input_index: int, *, unsigned: bool = False, ) -> stim.PauliString: """Conjugates a single-qubit Z Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).z_output(input_index)`. Args: input_index: Identifies the column (the qubit of the Z generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating a Z generator by the inverse of the tableau. Examples: >>> import stim >>> import stim # Check equivalence with the inverse's z_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().z_output(0) >>> t.inverse_z_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_z_output(0, unsigned=True) == expected True """ def inverse_z_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.inverse().z_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input Z generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_z_output_pauli(0, 0) 3 >>> t_inv.inverse_z_output_pauli(0, 1) 2 >>> t_inv.inverse_z_output_pauli(1, 0) 2 >>> t_inv.inverse_z_output_pauli(1, 1) 1 """ @staticmethod def iter_all( num_qubits: int, *, unsigned: bool = False, ) -> stim.TableauIterator: """Returns an iterator that iterates over all Tableaus of a given size. Args: num_qubits: The size of tableau to iterate over. unsigned: Defaults to False. If set to True, only tableaus where all columns have positive sign are yielded by the iterator. This substantially reduces the total number of tableaus to iterate over. Returns: An Iterable[stim.Tableau] that yields the requested tableaus. Examples: >>> import stim >>> single_qubit_gate_reprs = set() >>> for t in stim.Tableau.iter_all(1): ... single_qubit_gate_reprs.add(repr(t)) >>> len(single_qubit_gate_reprs) 24 >>> num_2q_gates_mod_paulis = 0 >>> for _ in stim.Tableau.iter_all(2, unsigned=True): ... num_2q_gates_mod_paulis += 1 >>> num_2q_gates_mod_paulis 720 """ def prepend( self, gate: stim.Tableau, targets: Sequence[int], ) -> None: """Prepends an operation's effect into this tableau, mutating this tableau. Time cost is O(n*m*m) where n=len(self) and m=len(gate). Args: gate: The tableau of the operation being prepended into this tableau. targets: The qubits being targeted by the gate. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("H") >>> t.prepend(stim.Tableau.from_named_gate("X"), [0]) >>> t == stim.Tableau.from_named_gate("SQRT_Y_DAG") True """ @staticmethod def random( num_qubits: int, ) -> stim.Tableau: """Samples a uniformly random Clifford operation and returns its tableau. Args: num_qubits: The number of qubits the tableau should act on. Returns: The sampled tableau. Examples: >>> import stim >>> t = stim.Tableau.random(42) References: "Hadamard-free circuits expose the structure of the Clifford group" Sergey Bravyi, Dmitri Maslov https://arxiv.org/abs/2003.09412 """ def then( self, second: stim.Tableau, ) -> stim.Tableau: """Returns the result of composing two tableaus. If the tableau T1 represents the Clifford operation with unitary C1, and the tableau T2 represents the Clifford operation with unitary C2, then the tableau T1.then(T2) represents the Clifford operation with unitary C2*C1. Args: second: The result is equivalent to applying the second tableau after the receiving tableau. Examples: >>> import stim >>> t1 = stim.Tableau.random(4) >>> t2 = stim.Tableau.random(4) >>> t3 = t1.then(t2) >>> p = stim.PauliString.random(4) >>> t3(p) == t2(t1(p)) True """ def to_circuit( self, method: Literal["elimination", "graph_state"] = 'elimination', ) -> stim.Circuit: """Synthesizes a circuit that implements the tableau's Clifford operation. The circuits returned by this method are not guaranteed to be stable from version to version, and may be produced using randomization. Args: method: The method to use when synthesizing the circuit. Available values: "elimination": Cancels off-diagonal terms using Gaussian elimination. Gate set: H, S, CX Circuit qubit count: n Circuit operation count: O(n^2) Circuit depth: O(n^2) "graph_state": Prepares the tableau's state using a graph state circuit. Gate set: RX, CZ, H, S, X, Y, Z Circuit qubit count: n Circuit operation count: O(n^2) The circuit will be made up of three layers: 1. An RX layer initializing all qubits. 2. A CZ layer coupling the qubits. (Each CZ is an edge in the graph state.) 3. A single qubit rotation layer. Note: "graph_state" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. "mpp_state": Prepares the tableau's state using MPP and feedback. Gate set: MPP, CX rec, CY rec, CZ rec Circuit qubit count: n Circuit operation count: O(n^2) The circuit will be made up of two layers: 1. An MPP layer measuring each of the tableau's stabilizers. 2. A feedback layer using the measurement results to control whether or not to apply each of the tableau's destabilizers in order to get the correct sign for each stabilizer. Note: "mpp_state" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. "mpp_state_unsigned": Prepares the tableau's state up to sign using MPP. Gate set: MPP Circuit qubit count: n Circuit operation count: O(n^2) The circuit will contain a series of MPP measurements measuring each of the tableau's stabilizers. The stabilizers are measured in the order used by the tableau (i.e. tableau.z_output(k) is the k'th stabilizer measured). Note: "mpp_state_unsigned" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. Returns: The synthesized circuit. Example: >>> import stim >>> tableau = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("+YZ__"), ... stim.PauliString("-Y_XY"), ... stim.PauliString("+___Y"), ... stim.PauliString("+YZX_"), ... ], ... zs=[ ... stim.PauliString("+XZYY"), ... stim.PauliString("-XYX_"), ... stim.PauliString("-ZXXZ"), ... stim.PauliString("+XXZ_"), ... ], ... ) >>> tableau.to_circuit() stim.Circuit(''' S 0 H 0 1 3 CX 0 1 0 2 0 3 S 1 3 H 1 3 CX 1 0 3 0 3 1 1 3 3 1 H 1 S 1 CX 1 3 H 2 3 CX 2 1 3 1 3 2 2 3 3 2 H 3 CX 2 3 S 3 H 3 0 1 2 S 0 0 1 1 2 2 H 0 1 2 S 3 3 ''') >>> tableau.to_circuit("graph_state") stim.Circuit(''' RX 0 1 2 3 TICK CZ 0 3 1 2 1 3 TICK X 0 1 Z 2 S 2 3 H 3 S 3 ''') >>> tableau.to_circuit("mpp_state_unsigned") stim.Circuit(''' MPP X0*Z1*Y2*Y3 !X0*Y1*X2 !Z0*X1*X2*Z3 X0*X1*Z2 ''') >>> tableau.to_circuit("mpp_state") stim.Circuit(''' MPP X0*Z1*Y2*Y3 !X0*Y1*X2 !Z0*X1*X2*Z3 X0*X1*Z2 CX rec[-3] 2 rec[-1] 2 CY rec[-4] 0 rec[-3] 0 rec[-3] 3 rec[-2] 3 rec[-1] 0 CZ rec[-4] 1 rec[-1] 1 ''') """ def to_numpy( self, *, bit_packed: bool = False, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Decomposes the contents of the tableau into six numpy arrays. The first four numpy arrays correspond to the four quadrants of the table defined in Aaronson and Gottesman's "Improved Simulation of Stabilizer Circuits" ( https://arxiv.org/abs/quant-ph/0406196 ). The last two numpy arrays are the X and Z sign bit vectors of the tableau. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: An (x2x, x2z, z2x, z2z, x_signs, z_signs) tuple encoding the tableau. x2x: A 2d table of whether tableau(X_i)_j is X or Y (instead of I or Z). x2z: A 2d table of whether tableau(X_i)_j is Z or Y (instead of I or X). z2x: A 2d table of whether tableau(Z_i)_j is X or Y (instead of I or Z). z2z: A 2d table of whether tableau(Z_i)_j is Z or Y (instead of I or X). x_signs: A vector of whether tableau(X_i) is negative. z_signs: A vector of whether tableau(Z_i) is negative. If bit_packed=False then: x2x.dtype = np.bool_ x2z.dtype = np.bool_ z2x.dtype = np.bool_ z2z.dtype = np.bool_ x_signs.dtype = np.bool_ z_signs.dtype = np.bool_ x2x.shape = (len(tableau), len(tableau)) x2z.shape = (len(tableau), len(tableau)) z2x.shape = (len(tableau), len(tableau)) z2z.shape = (len(tableau), len(tableau)) x_signs.shape = len(tableau) z_signs.shape = len(tableau) x2x[i, j] = tableau.x_output_pauli(i, j) in [1, 2] x2z[i, j] = tableau.x_output_pauli(i, j) in [2, 3] z2x[i, j] = tableau.z_output_pauli(i, j) in [1, 2] z2z[i, j] = tableau.z_output_pauli(i, j) in [2, 3] If bit_packed=True then: x2x.dtype = np.uint8 x2z.dtype = np.uint8 z2x.dtype = np.uint8 z2z.dtype = np.uint8 x_signs.dtype = np.uint8 z_signs.dtype = np.uint8 x2x.shape = (len(tableau), math.ceil(len(tableau) / 8)) x2z.shape = (len(tableau), math.ceil(len(tableau) / 8)) z2x.shape = (len(tableau), math.ceil(len(tableau) / 8)) z2z.shape = (len(tableau), math.ceil(len(tableau) / 8)) x_signs.shape = math.ceil(len(tableau) / 8) z_signs.shape = math.ceil(len(tableau) / 8) (x2x[i, j // 8] >> (j % 8)) & 1 = tableau.x_output_pauli(i, j) in [1, 2] (x2z[i, j // 8] >> (j % 8)) & 1 = tableau.x_output_pauli(i, j) in [2, 3] (z2x[i, j // 8] >> (j % 8)) & 1 = tableau.z_output_pauli(i, j) in [1, 2] (z2z[i, j // 8] >> (j % 8)) & 1 = tableau.z_output_pauli(i, j) in [2, 3] Examples: >>> import stim >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> print(repr(cnot)) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+ZZ"), ], ) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = cnot.to_numpy() >>> x2x array([[ True, True], [False, True]]) >>> x2z array([[False, False], [False, False]]) >>> z2x array([[False, False], [False, False]]) >>> z2z array([[ True, False], [ True, True]]) >>> x_signs array([False, False]) >>> z_signs array([False, False]) >>> t = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("-Y_ZY"), ... stim.PauliString("-Y_YZ"), ... stim.PauliString("-XXX_"), ... stim.PauliString("+ZYX_"), ... ], ... zs=[ ... stim.PauliString("-_ZZX"), ... stim.PauliString("+YZXZ"), ... stim.PauliString("+XZ_X"), ... stim.PauliString("-YYXX"), ... ], ... ) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy() >>> x2x array([[ True, False, False, True], [ True, False, True, False], [ True, True, True, False], [False, True, True, False]]) >>> x2z array([[ True, False, True, True], [ True, False, True, True], [False, False, False, False], [ True, True, False, False]]) >>> z2x array([[False, False, False, True], [ True, False, True, False], [ True, False, False, True], [ True, True, True, True]]) >>> z2z array([[False, True, True, False], [ True, True, False, True], [False, True, False, False], [ True, True, False, False]]) >>> x_signs array([ True, True, True, False]) >>> z_signs array([ True, False, False, True]) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy(bit_packed=True) >>> x2x array([[9], [5], [7], [6]], dtype=uint8) >>> x2z array([[13], [13], [ 0], [ 3]], dtype=uint8) >>> z2x array([[ 8], [ 5], [ 9], [15]], dtype=uint8) >>> z2z array([[ 6], [11], [ 2], [ 3]], dtype=uint8) >>> x_signs array([7], dtype=uint8) >>> z_signs array([9], dtype=uint8) """ def to_pauli_string( self, ) -> stim.PauliString: """Return a Pauli string equivalent to the tableau. If the tableau is equivalent to a pauli product, creates an equivalent pauli string. If not, then an error is raised. Returns: The created pauli string Raises: ValueError: The Tableau isn't equivalent to a Pauli product. Example: >>> import stim >>> t = (stim.Tableau.from_named_gate("Z") + ... stim.Tableau.from_named_gate("Y") + ... stim.Tableau.from_named_gate("I") + ... stim.Tableau.from_named_gate("X")) >>> print(t) +-xz-xz-xz-xz- | -+ -- ++ +- | XZ __ __ __ | __ XZ __ __ | __ __ XZ __ | __ __ __ XZ >>> print(t.to_pauli_string()) +ZY_X """ def to_stabilizers( self, *, canonicalize: bool = False, ) -> List[stim.PauliString]: """Returns the stabilizer generators of the tableau, optionally canonicalized. The stabilizer generators of the tableau are its Z outputs. Canonicalizing standardizes the generators, so that states that are equal will produce the same generators. For example, [ZI, IZ], [ZI, ZZ], amd [ZZ, ZI] describe equal states and all canonicalize to [ZI, IZ]. The canonical form is computed as follows: 1. Get a list of stabilizers using `tableau.z_output(k)` for each k. 2. Perform Gaussian elimination. pivoting on standard generators. 2a) Pivot on g=X0 first, then Z0, X1, Z1, X2, Z2, etc. 2b) Find a stabilizer that uses the generator g. If there are none, go to the next g. 2c) Multiply that stabilizer into all other stabilizers that use the generator g. 2d) Swap that stabilizer with the stabilizer at position `r` then increment `r`. `r` starts at 0. Args: canonicalize: Defaults to False. When False, the tableau's Z outputs are returned unchanged. When True, the Z outputs are rewritten into a standard form. Two stabilizer states have the same standard form if and only if they describe equivalent quantum states. Returns: A List[stim.PauliString] of the tableau's stabilizer generators. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> raw_stabilizers = t.to_stabilizers() >>> for e in raw_stabilizers: ... print(repr(e)) stim.PauliString("+Z_") stim.PauliString("+ZZ") >>> canonical_stabilizers = t.to_stabilizers(canonicalize=True) >>> for e in canonical_stabilizers: ... print(repr(e)) stim.PauliString("+Z_") stim.PauliString("+_Z") """ def to_state_vector( self, *, endian: Literal["little", "big"] = 'little', ) -> np.ndarray[np.complex64]: """Returns the state vector produced by applying the tableau to the |0..0> state. This function takes O(n * 2**n) time and O(2**n) space, where n is the number of qubits. The computation is done by initialization a random state vector and iteratively projecting it into the +1 eigenspace of each stabilizer of the state. The state is then canonicalized so that zero values are actually exactly 0, and so that the first non-zero entry is positive. Args: endian: "little" (default): state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A `numpy.ndarray[numpy.complex64]` of computational basis amplitudes. If the result is in little endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_0, the qubit with index 1 is storing the bit b_1, etc. If the result is in big endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_{n-1}, the qubit with index 1 is storing the bit b_{n-2}, etc. Examples: >>> import stim >>> import numpy as np >>> i2 = stim.Tableau.from_named_gate('I') >>> x = stim.Tableau.from_named_gate('X') >>> h = stim.Tableau.from_named_gate('H') >>> (x + i2).to_state_vector(endian='little') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> (i2 + x).to_state_vector(endian='little') array([0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], dtype=complex64) >>> (i2 + x).to_state_vector(endian='big') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> (h + h).to_state_vector(endian='little') array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], dtype=complex64) """ def to_unitary_matrix( self, *, endian: Literal["little", "big"], ) -> np.ndarray[np.complex64]: """Converts the tableau into a unitary matrix. For an n-qubit tableau, this method performs O(n 4^n) work. It uses the state channel duality to transform the tableau into a list of stabilizers, then generates a random state vector and projects it into the +1 eigenspace of each stabilizer. Note that tableaus don't have a defined global phase, so the result's global phase may be different from what you expect. For example, the square of SQRT_X's unitary might equal -X instead of +X. Args: endian: "little": The first qubit is the least significant (corresponds to an offset of 1 in the state vector). "big": The first qubit is the most significant (corresponds to an offset of 2**(n - 1) in the state vector). Returns: A numpy array with dtype=np.complex64 and shape=(1 << len(tableau), 1 << len(tableau)). Example: >>> import stim >>> cnot = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("XX"), ... stim.PauliString("_X"), ... ], ... zs=[ ... stim.PauliString("Z_"), ... stim.PauliString("ZZ"), ... ], ... ) >>> cnot.to_unitary_matrix(endian='big') array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64) """ def x_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli X by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli X operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.x_output(0) stim.PauliString("+Z") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.x_output(0) stim.PauliString("+XX") >>> cnot.x_output(1) stim.PauliString("+_X") """ def x_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.x_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input X generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.x_output_pauli(0, 0) 2 >>> t.x_output_pauli(0, 1) 0 >>> t.x_output_pauli(1, 0) 2 >>> t.x_output_pauli(1, 1) 3 """ def x_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating an X generator. This operation runs in constant time. Args: target: The qubit the X generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("S_DAG").x_sign(0) -1 >>> stim.Tableau.from_named_gate("S").x_sign(0) 1 """ def y_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli Y by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli Y operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.y_output(0) stim.PauliString("-Y") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.y_output(0) stim.PauliString("+YX") >>> cnot.y_output(1) stim.PauliString("+ZY") """ def y_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.y_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input Y generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.y_output_pauli(0, 0) 1 >>> t.y_output_pauli(0, 1) 2 >>> t.y_output_pauli(1, 0) 0 >>> t.y_output_pauli(1, 1) 2 """ def y_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating a Y generator. Unlike x_sign and z_sign, this operation runs in linear time. The Y generator has to be computed by multiplying the X and Z outputs and the sign depends on all terms. Args: target: The qubit the Y generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("S_DAG").y_sign(0) 1 >>> stim.Tableau.from_named_gate("S").y_sign(0) -1 """ def z_output( self, target: int, ) -> stim.PauliString: """Returns the result of conjugating a Pauli Z by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli Z operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.z_output(0) stim.PauliString("+X") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.z_output(0) stim.PauliString("+Z_") >>> cnot.z_output(1) stim.PauliString("+ZZ") """ def z_output_pauli( self, input_index: int, output_index: int, ) -> int: """Constant-time version of `tableau.z_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input Z generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.z_output_pauli(0, 0) 3 >>> t.z_output_pauli(0, 1) 2 >>> t.z_output_pauli(1, 0) 2 >>> t.z_output_pauli(1, 1) 1 """ def z_sign( self, target: int, ) -> int: """Returns just the sign of the result of conjugating a Z generator. This operation runs in constant time. Args: target: The qubit the Z generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("SQRT_X_DAG").z_sign(0) 1 >>> stim.Tableau.from_named_gate("SQRT_X").z_sign(0) -1 """ class TableauIterator: """Iterates over all stabilizer tableaus of a specified size. Examples: >>> import stim >>> tableau_iterator = stim.Tableau.iter_all(1) >>> n = 0 >>> for single_qubit_clifford in tableau_iterator: ... n += 1 >>> n 24 """ def __iter__( self, ) -> stim.TableauIterator: """Returns an independent copy of the tableau iterator. Since for-loops and loop-comprehensions call `iter` on things they iterate, this effectively allows the iterator to be iterated multiple times. """ def __next__( self, ) -> stim.Tableau: """Returns the next iterated tableau. """ class TableauSimulator: """A stabilizer circuit simulator that tracks an inverse stabilizer tableau. Supports interactive usage, where gates and measurements are applied on demand. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> if s.measure(0): ... s.h(1) ... s.cnot(1, 2) >>> s.measure(1) == s.measure(2) True >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+X_"), stim.PauliString("+XZ"), ], ) """ def __init__( self, *, seed: Optional[int] = None, ) -> None: """Initializes a stim.TableauSimulator. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: An initialized stim.TableauSimulator. Examples: >>> import stim >>> s = stim.TableauSimulator(seed=0) >>> s2 = stim.TableauSimulator(seed=0) >>> s.h(0) >>> s2.h(0) >>> s.measure(0) == s2.measure(0) True """ def c_xyz( self, *targets, ) -> None: """Applies a C_XYZ gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.c_xyz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y +Z +X """ def c_zyx( self, *targets, ) -> None: """Applies a C_ZYX gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.c_zyx(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z +X +Y """ def canonical_stabilizers( self, ) -> List[stim.PauliString]: """Returns a standardized list of the simulator's current stabilizer generators. Two simulators have the same canonical stabilizers if and only if their current quantum state is equal (and tracking the same number of qubits). The canonical form is computed as follows: 1. Get a list of stabilizers using the `z_output`s of `simulator.current_inverse_tableau()**-1`. 2. Perform Gaussian elimination on each generator g. 2a) The generators are considered in order X0, Z0, X1, Z1, X2, Z2, etc. 2b) Pick any stabilizer that uses the generator g. If there are none, go to the next g. 2c) Multiply that stabilizer into all other stabilizers that use the generator g. 2d) Swap that stabilizer with the stabilizer at position `next_output` then increment `next_output`. Returns: A List[stim.PauliString] of the simulator's state's stabilizers. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.x(2) >>> for e in s.canonical_stabilizers(): ... print(repr(e)) stim.PauliString("+XX_") stim.PauliString("+ZZ_") stim.PauliString("-__Z") >>> # Scramble the stabilizers then check the canonical form is unchanged. >>> s.set_inverse_tableau(s.current_inverse_tableau()**-1) >>> s.cnot(0, 1) >>> s.cz(0, 2) >>> s.s(0, 2) >>> s.cy(2, 1) >>> s.set_inverse_tableau(s.current_inverse_tableau()**-1) >>> for e in s.canonical_stabilizers(): ... print(repr(e)) stim.PauliString("+XX_") stim.PauliString("+ZZ_") stim.PauliString("-__Z") """ def cnot( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cnot(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def copy( self, *, copy_rng: bool = False, seed: Optional[int] = None, ) -> stim.TableauSimulator: """Returns a simulator with the same internal state, except perhaps its prng. Args: copy_rng: By default, new simulator's prng is reinitialized with a random seed. However, one can set this argument to True in order to have the prng state copied together with the rest of the original simulator's state. Consequently, in this case the two simulators will produce the same measurement outcomes for the same quantum circuits. If both seed and copy_rng are set, an exception is raised. Defaults to False. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng state is either copied from the original simulator or reseeded from system entropy, depending on the copy_rng argument. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Examples: >>> import stim >>> s1 = stim.TableauSimulator() >>> s1.set_inverse_tableau(stim.Tableau.random(1)) >>> s2 = s1.copy() >>> s2 is s1 False >>> s2.current_inverse_tableau() == s1.current_inverse_tableau() True >>> s1 = stim.TableauSimulator() >>> s2 = s1.copy(copy_rng=True) >>> s1.h(0) >>> s2.h(0) >>> assert s1.measure(0) == s2.measure(0) >>> s = stim.TableauSimulator() >>> def brute_force_post_select(qubit, desired_result): ... global s ... while True: ... s2 = s.copy() ... if s2.measure(qubit) == desired_result: ... s = s2 ... break >>> s.h(0) >>> brute_force_post_select(qubit=0, desired_result=True) >>> s.measure(0) True """ def current_inverse_tableau( self, ) -> stim.Tableau: """Returns a copy of the internal state of the simulator as a stim.Tableau. Returns: A stim.Tableau copy of the simulator's state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> s.cnot(0, 1) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+X_"), stim.PauliString("+XZ"), ], ) """ def current_measurement_record( self, ) -> List[bool]: """Returns a copy of the record of all measurements performed by the simulator. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.current_measurement_record() [] >>> s.measure(0) False >>> s.x(0) >>> s.measure(0) True >>> s.current_measurement_record() [False, True] >>> s.do(stim.Circuit("M 0")) >>> s.current_measurement_record() [False, True, True] Returns: A list of booleans containing the result of every measurement performed by the simulator so far. """ def cx( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def cy( self, *targets, ) -> None: """Applies a controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ def cz( self, *targets, ) -> None: """Applies a controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def depolarize1( self, *targets: int, p: float, ): """Probabilistically applies single-qubit depolarization to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.depolarize1(0, 1, 2, p=0.01) """ def depolarize2( self, *targets: int, p: float, ): """Probabilistically applies two-qubit depolarization to targets. Args: *targets: The indices of the qubits to target with the noise. The pairs of qubits are formed by zip(targets[::1], targets[1::2]). p: The chance of the error being applied, independently, to each qubit pair. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.depolarize1(0, 1, 4, 5, p=0.01) """ def do( self, circuit_or_pauli_string: Union[stim.Circuit, stim.PauliString, stim.CircuitInstruction, stim.CircuitRepeatBlock], ) -> None: """Applies a circuit or pauli string to the simulator's state. Args: circuit_or_pauli_string: A stim.Circuit, stim.PauliString, stim.CircuitInstruction, or stim.CircuitRepeatBlock with operations to apply to the simulator's state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do(stim.Circuit(''' ... X 0 ... M 0 ... ''')) >>> s.current_measurement_record() [True] >>> s = stim.TableauSimulator() >>> s.do(stim.PauliString("IXYZ")) >>> s.measure_many(0, 1, 2, 3) [False, True, True, False] """ def do_circuit( self, circuit: stim.Circuit, ) -> None: """Applies a circuit to the simulator's state. Args: circuit: A stim.Circuit containing operations to apply. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do_circuit(stim.Circuit(''' ... X 0 ... M 0 ... ''')) >>> s.current_measurement_record() [True] """ def do_pauli_string( self, pauli_string: stim.PauliString, ) -> None: """Applies the paulis from a pauli string to the simulator's state. Args: pauli_string: A stim.PauliString containing Paulis to apply. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do_pauli_string(stim.PauliString("IXYZ")) >>> s.measure_many(0, 1, 2, 3) [False, True, True, False] """ def do_tableau( self, tableau: stim.Tableau, targets: List[int], ) -> None: """Applies a custom tableau operation to qubits in the simulator. Note that this method has to compute the inverse of the tableau, because the simulator's internal state is an inverse tableau. Args: tableau: A stim.Tableau representing the Clifford operation to apply. targets: The indices of the qubits to operate on. Examples: >>> import stim >>> sim = stim.TableauSimulator() >>> sim.h(1) >>> sim.h_yz(2) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+X', '+Y', '+Z'] >>> rot3 = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("_X_"), ... stim.PauliString("__X"), ... stim.PauliString("X__"), ... ], ... zs=[ ... stim.PauliString("_Z_"), ... stim.PauliString("__Z"), ... stim.PauliString("Z__"), ... ], ... ) >>> sim.do_tableau(rot3, [1, 2, 3]) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+Z', '+X', '+Y'] >>> sim.do_tableau(rot3, [1, 2, 3]) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+Y', '+Z', '+X'] """ def h( self, *targets, ) -> None: """Applies a Hadamard gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z -Y +X """ def h_xy( self, *targets, ) -> None: """Applies an operation that swaps the X and Y axes to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_xy(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y +X -Z """ def h_xz( self, *targets, ) -> None: """Applies a Hadamard gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_xz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z -Y +X """ def h_yz( self, *targets, ) -> None: """Applies an operation that swaps the Y and Z axes to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_yz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X +Z +Y """ def iswap( self, *targets, ) -> None: """Applies an ISWAP gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.iswap(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Y +Z """ def iswap_dag( self, *targets, ) -> None: """Applies an ISWAP_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.iswap_dag(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ -Y +Z """ def measure( self, target: int, ) -> bool: """Measures a single qubit. Unlike the other methods on TableauSimulator, this one does not broadcast over multiple targets. This is to avoid returning a list, which would create a pitfall where typing `if sim.measure(qubit)` would be a bug. To measure multiple qubits, use `TableauSimulator.measure_many`. Args: target: The index of the qubit to measure. Returns: The measurement result as a bool. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(1) >>> s.measure(0) False >>> s.measure(1) True """ def measure_kickback( self, target: int, ) -> tuple: """Measures a qubit and returns the result as well as its Pauli kickback (if any). The "Pauli kickback" of a stabilizer circuit measurement is a set of Pauli operations that flip the post-measurement system state between the two possible post-measurement states. For example, consider measuring one of the qubits in the state |00>+|11> in the Z basis. If the measurement result is False, then the system projects into the state |00>. If the measurement result is True, then the system projects into the state |11>. Applying a Pauli X operation to both qubits flips between |00> and |11>. Therefore the Pauli kickback of the measurement is `stim.PauliString("XX")`. Note that there are often many possible equivalent Pauli kickbacks. For example, if in the previous example there was a third qubit in the |0> state, then both `stim.PauliString("XX_")` and `stim.PauliString("XXZ")` are valid kickbacks. Measurements with deterministic results don't have a Pauli kickback. Args: target: The index of the qubit to measure. Returns: A (result, kickback) tuple. The result is a bool containing the measurement's output. The kickback is either None (meaning the measurement was deterministic) or a stim.PauliString (meaning the measurement was random, and the operations in the Pauli string flip between the two possible post-measurement states). Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.measure_kickback(0) (False, None) >>> s.h(0) >>> s.measure_kickback(0)[1] stim.PauliString("+X") >>> def pseudo_post_select(qubit, desired_result): ... m, kick = s.measure_kickback(qubit) ... if m != desired_result: ... if kick is None: ... raise ValueError("Post-selected the impossible!") ... s.do(kick) >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.cnot(0, 2) >>> pseudo_post_select(qubit=2, desired_result=True) >>> s.measure_many(0, 1, 2) [True, True, True] """ def measure_many( self, *targets, ) -> List[bool]: """Measures multiple qubits. Args: *targets: The indices of the qubits to measure. Returns: The measurement results as a list of bools. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(1) >>> s.measure_many(0, 1) [False, True] """ def measure_observable( self, observable: stim.PauliString, *, flip_probability: float = 0.0, ) -> bool: """Measures an pauli string observable, as if by an MPP instruction. Args: observable: The observable to measure, specified as a stim.PauliString. flip_probability: Probability of the recorded measurement result being flipped. Returns: The result of the measurement. The result is also recorded into the measurement record. Raises: ValueError: The given pauli string isn't Hermitian, or the given probability isn't a valid probability. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.measure_observable(stim.PauliString("XX")) False >>> s.measure_observable(stim.PauliString("YY")) True >>> s.measure_observable(stim.PauliString("-ZZ")) True """ @property def num_qubits( self, ) -> int: """Returns the number of qubits currently being tracked by the simulator. Note that the number of qubits being tracked will implicitly increase if qubits beyond the current limit are touched. Untracked qubits are always assumed to be in the |0> state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.num_qubits 0 >>> s.h(2) >>> s.num_qubits 3 """ def peek_bloch( self, target: int, ) -> stim.PauliString: """Returns the state of the qubit as a single-qubit stim.PauliString stabilizer. This is a non-physical operation. It reports information about the qubit without disturbing it. Args: target: The qubit to peek at. Returns: stim.PauliString("I"): The qubit is entangled. Its bloch vector is x=y=z=0. stim.PauliString("+Z"): The qubit is in the |0> state. Its bloch vector is z=+1, x=y=0. stim.PauliString("-Z"): The qubit is in the |1> state. Its bloch vector is z=-1, x=y=0. stim.PauliString("+Y"): The qubit is in the |i> state. Its bloch vector is y=+1, x=z=0. stim.PauliString("-Y"): The qubit is in the |-i> state. Its bloch vector is y=-1, x=z=0. stim.PauliString("+X"): The qubit is in the |+> state. Its bloch vector is x=+1, y=z=0. stim.PauliString("-X"): The qubit is in the |-> state. Its bloch vector is x=-1, y=z=0. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_bloch(0) stim.PauliString("+Z") >>> s.x(0) >>> s.peek_bloch(0) stim.PauliString("-Z") >>> s.h(0) >>> s.peek_bloch(0) stim.PauliString("-X") >>> s.sqrt_x(1) >>> s.peek_bloch(1) stim.PauliString("-Y") >>> s.cz(0, 1) >>> s.peek_bloch(0) stim.PauliString("+_") """ def peek_observable_expectation( self, observable: stim.PauliString, ) -> int: """Determines the expected value of an observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: observable: The observable to determine the expected value of. This observable must have a real sign, not an imaginary sign. Returns: +1: Observable will be deterministically false when measured. -1: Observable will be deterministically true when measured. 0: Observable will be random when measured. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_observable_expectation(stim.PauliString("+Z")) 1 >>> s.peek_observable_expectation(stim.PauliString("+X")) 0 >>> s.peek_observable_expectation(stim.PauliString("-Z")) -1 >>> s.do(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''')) >>> queries = ['XX', 'YY', 'ZZ', '-ZZ', 'ZI', 'II', 'IIZ'] >>> for q in queries: ... print(q, s.peek_observable_expectation(stim.PauliString(q))) XX 1 YY -1 ZZ 1 -ZZ -1 ZI 0 II 1 IIZ 1 """ def peek_x( self, target: int, ) -> int: """Returns the expected value of a qubit's X observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |+> state. -1: Qubit is in the |-> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_z(0) >>> s.peek_x(0) 0 >>> s.reset_x(0) >>> s.peek_x(0) 1 >>> s.z(0) >>> s.peek_x(0) -1 """ def peek_y( self, target: int, ) -> int: """Returns the expected value of a qubit's Y observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |i> state. -1: Qubit is in the |-i> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_z(0) >>> s.peek_y(0) 0 >>> s.reset_y(0) >>> s.peek_y(0) 1 >>> s.z(0) >>> s.peek_y(0) -1 """ def peek_z( self, target: int, ) -> int: """Returns the expected value of a qubit's Z observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |0> state. -1: Qubit is in the |1> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.peek_z(0) 0 >>> s.reset_z(0) >>> s.peek_z(0) 1 >>> s.x(0) >>> s.peek_z(0) -1 """ def postselect_observable( self, observable: stim.PauliString, *, desired_value: bool = False, ) -> None: """Projects into a desired observable, or raises an exception if it was impossible. Postselecting an observable forces it to collapse to a specific eigenstate, as if it was measured and that state was the result of the measurement. Args: observable: The observable to postselect, specified as a pauli string. The pauli string's sign must be -1 or +1 (not -i or +i). desired_value: False (default): Postselect into the +1 eigenstate of the observable. True: Postselect into the -1 eigenstate of the observable. Raises: ValueError: The given observable had an imaginary sign. OR The postselection was impossible. The observable was in the opposite eigenstate, so measuring it would never ever return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.postselect_observable(stim.PauliString("+XX")) >>> s.postselect_observable(stim.PauliString("+ZZ")) >>> s.peek_observable_expectation(stim.PauliString("+YY")) -1 """ def postselect_x( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the X basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |+> state. True: postselect targets into the |-> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_x(0) 0 >>> s.postselect_x(0, desired_value=False) >>> s.peek_x(0) 1 >>> s.h(0) >>> s.peek_x(0) 0 >>> s.postselect_x(0, desired_value=True) >>> s.peek_x(0) -1 """ def postselect_y( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the Y basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |i> state. True: postselect targets into the |-i> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_y(0) 0 >>> s.postselect_y(0, desired_value=False) >>> s.peek_y(0) 1 >>> s.reset_x(0) >>> s.peek_y(0) 0 >>> s.postselect_y(0, desired_value=True) >>> s.peek_y(0) -1 """ def postselect_z( self, targets: Union[int, Iterable[int]], *, desired_value: bool, ) -> None: """Postselects qubits in the Z basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |0> state. True: postselect targets into the |1> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.peek_z(0) 0 >>> s.postselect_z(0, desired_value=False) >>> s.peek_z(0) 1 >>> s.h(0) >>> s.peek_z(0) 0 >>> s.postselect_z(0, desired_value=True) >>> s.peek_z(0) -1 """ def reset( self, *targets, ) -> None: """Resets qubits to the |0> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(0) >>> s.reset(0) >>> s.peek_bloch(0) stim.PauliString("+Z") """ def reset_x( self, *targets, ) -> None: """Resets qubits to the |+> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.peek_bloch(0) stim.PauliString("+X") """ def reset_y( self, *targets, ) -> None: """Resets qubits to the |i> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_y(0) >>> s.peek_bloch(0) stim.PauliString("+Y") """ def reset_z( self, *targets, ) -> None: """Resets qubits to the |0> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.reset_z(0) >>> s.peek_bloch(0) stim.PauliString("+Z") """ def s( self, *targets, ) -> None: """Applies a SQRT_Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.s(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y -X +Z """ def s_dag( self, *targets, ) -> None: """Applies a SQRT_Z_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.s_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -Y +X +Z """ def set_inverse_tableau( self, new_inverse_tableau: stim.Tableau, ) -> None: """Overwrites the simulator's internal state with the given inverse tableau. The inverse tableau specifies how Pauli product observables of qubits at the current time transform into equivalent Pauli product observables at the beginning of time, when all qubits were in the |0> state. For example, if the Z observable on qubit 5 maps to a product of Z observables at the start of time then a Z basis measurement on qubit 5 will be deterministic and equal to the sign of the product. Whereas if it mapped to a product of observables including an X or a Y then the Z basis measurement would be random. Any qubits not within the length of the tableau are implicitly in the |0> state. Args: new_inverse_tableau: The tableau to overwrite the internal state with. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> t = stim.Tableau.random(4) >>> s.set_inverse_tableau(t) >>> s.current_inverse_tableau() == t True """ def set_num_qubits( self, new_num_qubits: int, ) -> None: """Resizes the simulator's internal state. This forces the simulator's internal state to track exactly the qubits whose indices are in `range(new_num_qubits)`. Note that untracked qubits are always assumed to be in the |0> state. Therefore, calling this method will effectively force any qubit whose index is outside `range(new_num_qubits)` to be reset to |0>. Note that this method does not prevent future operations from implicitly expanding the size of the tracked state (e.g. setting the number of qubits to 5 will not prevent a Hadamard from then being applied to qubit 100, increasing the number of qubits back to 101). Args: new_num_qubits: The length of the range of qubits the internal simulator should be tracking. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> len(s.current_inverse_tableau()) 0 >>> s.set_num_qubits(5) >>> len(s.current_inverse_tableau()) 5 >>> s.x(0, 1, 2, 3) >>> s.set_num_qubits(2) >>> s.measure_many(0, 1, 2, 3) [True, True, False, False] """ def set_state_from_stabilizers( self, stabilizers: Iterable[stim.PauliString], *, allow_redundant: bool = False, allow_underconstrained: bool = False, ) -> None: """Sets the tableau simulator's state to a state satisfying the given stabilizers. The old quantum state is completely overwritten, even if the new state is underconstrained by the given stabilizers. The number of qubits is changed to exactly match the number of qubits in the longest given stabilizer. Args: stabilizers: A list of `stim.PauliString`s specifying the stabilizers that the new state must have. It is permitted for stabilizers to have different lengths. All stabilizers are padded up to the length of the longest stabilizer by appending identity terms. allow_redundant: Defaults to False. If set to False, then the given stabilizers must all be independent. If any one of them is a product of the others (including the empty product), an exception will be raised. If set to True, then redundant stabilizers are simply ignored. allow_underconstrained: Defaults to False. If set to False, then the given stabilizers must form a complete set of generators. They must exactly specify the desired stabilizer state, with no degrees of freedom left over. For an n-qubit state there must be n independent stabilizers. If set to True, then there can be leftover degrees of freedom which can be set arbitrarily. Returns: Nothing. Mutates the states of the simulator to match the desired stabilizers. Guarantees that self.current_inverse_tableau().inverse_z_output(k) will be equal to the k'th independent stabilizer from the `stabilizers` argument. Raises: ValueError: A stabilizer is redundant but allow_redundant=True wasn't set. OR The given stabilizers are contradictory (e.g. "+Z" and "-Z" both specified). OR The given stabilizers anticommute (e.g. "+Z" and "+X" both specified). OR The stabilizers left behind a degree of freedom but allow_underconstrained=True wasn't set. OR A stabilizer has an imaginary sign (i or -i). Examples: >>> import stim >>> tab_sim = stim.TableauSimulator() >>> tab_sim.set_state_from_stabilizers([ ... stim.PauliString("XX"), ... stim.PauliString("ZZ"), ... ]) >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) >>> tab_sim.set_state_from_stabilizers([ ... stim.PauliString("XX_"), ... stim.PauliString("ZZ_"), ... stim.PauliString("-YY_"), ... stim.PauliString(""), ... ], allow_underconstrained=True, allow_redundant=True) >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z__"), stim.PauliString("+_X_"), stim.PauliString("+__X"), ], zs=[ stim.PauliString("+XX_"), stim.PauliString("+ZZ_"), stim.PauliString("+__Z"), ], ) """ def set_state_from_state_vector( self, state_vector: Iterable[float], *, endian: Literal["little", "big"], ) -> None: """Sets the simulator's state to a superposition specified by an amplitude vector. Args: state_vector: A list of complex amplitudes specifying a superposition. The vector must correspond to a state that is reachable using Clifford operations, and must be normalized (i.e. it must be a unit vector). endian: "little": state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: Nothing. Mutates the states of the simulator to match the desired state. Raises: ValueError: The given state vector isn't a list of complex values specifying a stabilizer state. OR The given endian value isn't 'little' or 'big'. Examples: >>> import stim >>> tab_sim = stim.TableauSimulator() >>> tab_sim.set_state_from_state_vector([ ... 0.5**0.5, ... 0.5**0.5 * 1j, ... ], endian='little') >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+Y"), ], ) >>> tab_sim.set_state_from_state_vector([ ... 0.5**0.5, ... 0, ... 0, ... 0.5**0.5, ... ], endian='little') >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) """ def sqrt_x( self, *targets, ) -> None: """Applies a SQRT_X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_x(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Z -Y """ def sqrt_x_dag( self, *targets, ) -> None: """Applies a SQRT_X_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_x_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X -Z +Y """ def sqrt_y( self, *targets, ) -> None: """Applies a SQRT_Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_y(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -Z +Y +X """ def sqrt_y_dag( self, *targets, ) -> None: """Applies a SQRT_Y_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_y_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z +Y -X """ def state_vector( self, *, endian: Literal["little", "big"] = 'little', ) -> np.ndarray[np.complex64]: """Returns a wavefunction for the simulator's current state. This function takes O(n * 2**n) time and O(2**n) space, where n is the number of qubits. The computation is done by initialization a random state vector and iteratively projecting it into the +1 eigenspace of each stabilizer of the state. The state is then canonicalized so that zero values are actually exactly 0, and so that the first non-zero entry is positive. Args: endian: "little" (default): state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A `numpy.ndarray[numpy.complex64]` of computational basis amplitudes. If the result is in little endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_0, the qubit with index 1 is storing the bit b_1, etc. If the result is in big endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_{n-1}, the qubit with index 1 is storing the bit b_{n-2}, etc. Examples: >>> import stim >>> import numpy as np >>> s = stim.TableauSimulator() >>> s.x(2) >>> s.state_vector(endian='little') array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> s.state_vector(endian='big') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> s.sqrt_x(1, 2) >>> s.state_vector() array([0.5+0.j , 0. +0.j , 0. -0.5j, 0. +0.j , 0. +0.5j, 0. +0.j , 0.5+0.j , 0. +0.j ], dtype=complex64) """ def swap( self, *targets, ) -> None: """Applies a swap gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.swap(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +Y +X +X +Z """ def x( self, *targets, ) -> None: """Applies a Pauli X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.x(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X -Y -Z """ def x_error( self, *targets: int, p: float, ): """Probabilistically applies X errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the X error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x_error(0, 1, 2, p=0.01) """ def xcx( self, *targets, ) -> None: """Applies an X-controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ def xcy( self, *targets, ) -> None: """Applies an X-controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ def xcz( self, *targets, ) -> None: """Applies an X-controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ def y( self, *targets, ) -> None: """Applies a Pauli Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.y(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X +Y -Z """ def y_error( self, *targets: int, p: float, ): """Probabilistically applies Y errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the Y error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.y_error(0, 1, 2, p=0.01) """ def ycx( self, *targets, ) -> None: """Applies a Y-controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def ycy( self, *targets, ) -> None: """Applies a Y-controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ """ def ycz( self, *targets, ) -> None: """Applies a Y-controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +_ +_ """ def z( self, *targets, ) -> None: """Applies a Pauli Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.z(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X -Y +Z """ def z_error( self, *targets: int, p: float, ): """Probabilistically applies Z errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the Z error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.z_error(0, 1, 2, p=0.01) """ def zcx( self, *targets, ) -> None: """Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ def zcy( self, *targets, ) -> None: """Applies a controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X """ def zcz( self, *targets, ) -> None: """Applies a controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X """ @overload def gate_data( name: str, ) -> stim.GateData: pass @overload def gate_data( ) -> Dict[str, stim.GateData]: pass def gate_data( name: Optional[str] = None, ) -> Union[str, Dict[str, stim.GateData]]: """Returns gate data for the given named gate, or all gates. Examples: >>> import stim >>> stim.gate_data('cnot').aliases ['CNOT', 'CX', 'ZCX'] >>> stim.gate_data('cnot').is_two_qubit_gate True >>> gate_dict = stim.gate_data() >>> len(gate_dict) > 50 True >>> gate_dict['MX'].produces_measurements True """ def main( *, command_line_args: List[str], ) -> int: """Runs the command line tool version of stim on the given arguments. Note that by default any input will be read from stdin, any output will print to stdout (as opposed to being intercepted). For most commands, you can use arguments like `--out` to write to a file instead of stdout and `--in` to read from a file instead of stdin. Returns: An exit code (0 means success, not zero means failure). Raises: A large variety of errors, depending on what you are doing and how it failed! Beware that many errors are caught by the main method itself and printed to stderr, with the only indication that something went wrong being the return code. Example: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f'{d}/tmp.out' ... return_code = stim.main(command_line_args=[ ... "gen", ... "--code=repetition_code", ... "--task=memory", ... "--rounds=1000", ... "--distance=2", ... "--out", ... path, ... ]) ... assert return_code == 0 ... with open(path) as f: ... print(f.read(), end='') # Generated repetition_code circuit. # task: memory # rounds: 1000 # distance: 2 # before_round_data_depolarization: 0 # before_measure_flip_probability: 0 # after_reset_flip_probability: 0 # after_clifford_depolarization: 0 # layout: # L0 Z1 d2 # Legend: # d# = data qubit # L# = data qubit with logical observable crossing # Z# = measurement qubit R 0 1 2 TICK CX 0 1 TICK CX 2 1 TICK MR 1 DETECTOR(1, 0) rec[-1] REPEAT 999 { TICK CX 0 1 TICK CX 2 1 TICK MR 1 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-1] rec[-2] } M 0 2 DETECTOR(1, 1) rec[-1] rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] """ @overload def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, ) -> np.ndarray: pass @overload def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, separate_observables: Literal[True], ) -> Tuple[np.ndarray, np.ndarray]: pass def read_shot_data_file( *, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, separate_observables: bool = False, ) -> Union[Tuple[np.ndarray, np.ndarray], np.ndarray]: """Reads shot data, such as measurement samples, from a file. Args: path: The path to the file to read the data from. format: The format that the data is stored in, such as 'b8'. See https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md bit_packed: Defaults to false. Determines whether the result is a bool_ numpy array with one bit per byte, or a uint8 numpy array with 8 bits per byte. num_measurements: How many measurements there are per shot. num_detectors: How many detectors there are per shot. num_observables: How many observables there are per shot. Note that this only refers to observables *stored in the file*, not to observables from the original circuit that was sampled. separate_observables: When set to True, the result is a tuple of two arrays, one containing the detection event data and the other containing the observable data, instead of a single array. Returns: If separate_observables=True: A tuple (dets, obs) of numpy arrays containing the loaded data. If bit_packed=False: dets.dtype = np.bool_ dets.shape = (num_shots, num_measurements + num_detectors) det bit b from shot s is at dets[s, b] obs.dtype = np.bool_ obs.shape = (num_shots, num_observables) obs bit b from shot s is at dets[s, b] If bit_packed=True: dets.dtype = np.uint8 dets.shape = (num_shots, math.ceil( (num_measurements + num_detectors) / 8)) obs.dtype = np.uint8 obs.shape = (num_shots, math.ceil(num_observables / 8)) det bit b from shot s is at dets[s, b // 8] & (1 << (b % 8)) obs bit b from shot s is at obs[s, b // 8] & (1 << (b % 8)) If separate_observables=False: A numpy array containing the loaded data. If bit_packed=False: dtype = np.bool_ shape = (num_shots, num_measurements + num_detectors + num_observables) bit b from shot s is at result[s, b] If bit_packed=True: dtype = np.uint8 shape = (num_shots, math.ceil( (num_measurements + num_detectors + num_observables) / 8)) bit b from shot s is at result[s, b // 8] & (1 << (b % 8)) Examples: >>> import stim >>> import pathlib >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = pathlib.Path(d) / 'shots' ... with open(path, 'w') as f: ... print("0000", file=f) ... print("0101", file=f) ... ... read = stim.read_shot_data_file( ... path=str(path), ... format='01', ... num_measurements=4) >>> read array([[False, False, False, False], [False, True, False, True]]) """ def target_combined_paulis( paulis: Union[stim.PauliString, List[stim.GateTarget]], invert: bool = False, ) -> stim.GateTarget: """Returns a list of targets encoding a pauli product for instructions like MPP. Args: paulis: The paulis to encode into the targets. This can be a `stim.PauliString` or a list of pauli targets from `stim.target_x`, `stim.target_pauli`, etc. invert: Defaults to False. If True, the product is inverted (like "!X2*Y3"). Note that this is in addition to any inversions specified by the `paulis` argument. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... *stim.target_combined_paulis(stim.PauliString("-XYZ")), ... *stim.target_combined_paulis([stim.target_x(2), stim.target_y(5)]), ... *stim.target_combined_paulis([stim.target_z(9)], invert=True), ... ]) >>> circuit stim.Circuit(''' MPP !X0*Y1*Z2 X2*Y5 !Z9 ''') """ def target_combiner( ) -> stim.GateTarget: """Returns a target combiner that can be used to build Pauli products. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*Y3*Z5 ''') """ def target_inv( qubit_index: Union[int, stim.GateTarget], ) -> stim.GateTarget: """Returns a target flagged as inverted. Inverted targets are used to indicate measurement results should be flipped. Args: qubit_index: The underlying qubit index of the inverted target. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("M", [2, stim.target_inv(3)]) >>> circuit stim.Circuit(''' M 2 !3 ''') For example, the '!1' in 'M 0 !1 2' is qubit 1 flagged as inverted, meaning the measurement result from qubit 1 should be inverted when reported. """ def target_logical_observable_id( index: int, ) -> stim.DemTarget: """Returns a logical observable id identifying a frame change. Args: index: The index of the observable. Returns: The logical observable target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_logical_observable_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) L13 ''') """ def target_pauli( qubit_index: int, pauli: Union[str, int], invert: bool = False, ) -> stim.GateTarget: """Returns a pauli target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. pauli: The pauli gate to use. This can either be a string identifying the pauli by name ("x", "X", "y", "Y", "z", or "Z") or an integer following the convention (1=X, 2=Y, 3=Z). Setting this argument to "I" or to 0 will return a qubit target instead of a pauli target. invert: Defaults to False. If True, the target is inverted (like "!X10"), indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_pauli(2, "X"), ... stim.target_combiner(), ... stim.target_pauli(3, "y", invert=True), ... stim.target_pauli(5, 3), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3 Z5 ''') >>> circuit.append("M", [ ... stim.target_pauli(7, "I"), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3 Z5 M 7 ''') """ def target_rec( lookback_index: int, ) -> stim.GateTarget: """Returns a measurement record target with the given lookback. Measurement record targets are used to refer back to the measurement record; the list of measurements that have been performed so far. Measurement record targets always specify an index relative to the *end* of the measurement record. The latest measurement is `stim.target_rec(-1)`, the next most recent measurement is `stim.target_rec(-2)`, and so forth. Indexing is done this way in order to make it possible to write loops. Args: lookback_index: A negative integer indicating how far to look back, relative to the end of the measurement record. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("M", [5, 7, 11]) >>> circuit.append("CX", [stim.target_rec(-2), 3]) >>> circuit stim.Circuit(''' M 5 7 11 CX rec[-2] 3 ''') """ def target_relative_detector_id( index: int, ) -> stim.DemTarget: """Returns a relative detector id (e.g. "D5" in a .dem file). Args: index: The index of the detector, relative to the current detector offset. Returns: The relative detector target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_relative_detector_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D13 ''') """ def target_separator( ) -> stim.DemTarget: """Returns a target separator (e.g. "^" in a .dem file). Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_relative_detector_id(1), ... stim.target_separator(), ... stim.target_relative_detector_id(2), ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D1 ^ D2 ''') """ def target_sweep_bit( sweep_bit_index: int, ) -> stim.GateTarget: """Returns a sweep bit target that can be passed into `stim.Circuit.append`. Args: sweep_bit_index: The index of the sweep bit to target. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("CX", [stim.target_sweep_bit(2), 5]) >>> circuit stim.Circuit(''' CX sweep[2] 5 ''') """ def target_x( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli X target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ def target_y( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli Y target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ def target_z( qubit_index: Union[int, stim.GateTarget], invert: bool = False, ) -> stim.GateTarget: """Returns a Pauli Z target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') """ def write_shot_data_file( *, data: np.ndarray, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, ) -> None: """Writes shot data, such as measurement samples, to a file. Args: data: The data to write to the file. This must be a numpy array. The dtype of the array determines whether or not the data is bit packed, and the shape must match the bits per shot. dtype=np.bool_: Not bit packed. Shape must be (num_shots, num_measurements + num_detectors + num_observables). dtype=np.uint8: Yes bit packed. Shape must be (num_shots, math.ceil( (num_measurements + num_detectors + num_observables) / 8)). path: The path to the file to write the data to. format: The format that the data is stored in, such as 'b8'. See https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md num_measurements: How many measurements there are per shot. num_detectors: How many detectors there are per shot. num_observables: How many observables there are per shot. Note that this only refers to observables *in the given shot data*, not to observables from the original circuit that was sampled. Examples: >>> import stim >>> import pathlib >>> import tempfile >>> import numpy as np >>> with tempfile.TemporaryDirectory() as d: ... path = pathlib.Path(d) / 'shots' ... shot_data = np.array([ ... [0, 1, 0], ... [0, 1, 1], ... ], dtype=np.bool_) ... ... stim.write_shot_data_file( ... path=str(path), ... data=shot_data, ... format='01', ... num_measurements=3) ... ... with open(path) as f: ... written = f.read() >>> written '010\n011\n' """ ================================================ FILE: glue/python/src/stim/_main_argv.py ================================================ import sys import stim def main_argv(): stim.main(command_line_args=sys.argv[1:]) if __name__ == '__main__': main_argv() ================================================ FILE: glue/sample/MANIFEST.in ================================================ recursive-include src *.py ================================================ FILE: glue/sample/README.md ================================================ # sinter: fast QEC sampling Sinter is a software tool/library for doing fast monte carlo sampling of quantum error correction circuits. - [How it works](#how_it_works) - [How to install](#how_to_install) - [How to use: Python API](#how_to_use_python) - [Sinter Python API Reference](doc/sinter_api.md) - [How to use: Linux Command Line](#how_to_use_linux) - [Sinter Command Line Reference](doc/sinter_command_line.md) - [The csv format for sample statistics](#csv_format) # How it works Sinter takes Stim circuits annotated with noise, detectors, and logical observables. It uses stim to sample the circuits and a decoder such as pymatching to predict whether the logical observables were flipped or not, given the detector data. It records how often this succeeds, and how often it fails (the error rate). Sinter uses python multiprocessing to do parallel sampling across multiple CPU cores, dynamically decides which circuits need more samples based on parameters specified by the user (such as a target number of errors), saves the results to as simple CSV format, and has some basic plotting functionality for viewing the results. Sinter doesn't support cloud compute, but it does scale well on a single machine. I've tested it on 2 core machines, 4 core machines, and 96 core machines. Although there are potential pitfalls (e.g. setting batch sizes too large causes thrashing), sinter generally achieves good resource utilization of the processes you assign to it. # How to install Sinter is available as a pypi package. It can be installed using pip: ``` pip install sinter ``` When you are in a python virtual environment with sinter installed, you have access to a command line command `sinter` which can be used to perform tasks from the command line. You can also `import sinter` in a python program in order to use sinter's python API. # How to use: Python API This example assumes you are in a python environment with `sinter` and `pymatching` installed. ```python import stim import sinter import matplotlib.pyplot as plt # Generates surface code circuit tasks using Stim's circuit generation. def generate_example_tasks(): for p in [0.001, 0.005, 0.01]: for d in [3, 5]: yield sinter.Task( circuit=stim.Circuit.generated( rounds=d, distance=d, after_clifford_depolarization=p, code_task=f'surface_code:rotated_memory_x', ), json_metadata={ 'p': p, 'd': d, }, ) def main(): # Collect the samples (takes a few minutes). samples = sinter.collect( num_workers=4, max_shots=1_000_000, max_errors=1000, tasks=generate_example_tasks(), decoders=['pymatching'], ) # Print samples as CSV data. print(sinter.CSV_HEADER) for sample in samples: print(sample.to_csv_line()) # Render a matplotlib plot of the data. fig, ax = plt.subplots(1, 1) sinter.plot_error_rate( ax=ax, stats=samples, group_func=lambda stat: f"Rotated Surface Code d={stat.json_metadata['d']}", x_func=lambda stat: stat.json_metadata['p'], ) ax.loglog() ax.set_ylim(1e-5, 1) ax.grid() ax.set_title('Logical Error Rate vs Physical Error Rate') ax.set_ylabel('Logical Error Probability (per shot)') ax.set_xlabel('Physical Error Rate') ax.legend() # Save to file and also open in a window. fig.savefig('plot.png') plt.show() # NOTE: This is actually necessary! If the code inside 'main()' was at the # module level, the multiprocessing children spawned by sinter.collect would # also attempt to run that code. if __name__ == '__main__': main() ``` Example output to stdout: ``` shots, errors, discards, seconds,decoder,strong_id,json_metadata 1000000, 837, 0, 36.6,pymatching,9f7e20c54fec45b6aef7491b774dd5c0a3b9a005aa82faf5b9c051d6e40d60a9,"{""d"":3,""p"":0.001}" 53498, 1099, 0, 6.52,pymatching,3f40432443a99b933fb548b831fb54e7e245d9d73a35c03ea5a2fb2ce270f8c8,"{""d"":3,""p"":0.005}" 16269, 1023, 0, 3.23,pymatching,17b2e0c99560d20307204494ac50e31b33e50721b4ebae99d9e3577ae7248874,"{""d"":3,""p"":0.01}" 1000000, 151, 0, 77.3,pymatching,e179a18739201250371ffaae0197d8fa19d26b58dfc2942f9f1c85568645387a,"{""d"":5,""p"":0.001}" 11363, 1068, 0, 12.5,pymatching,a4dec28934a033215ff1389651a26114ecc22016a6e122008830cf7dd04ba5ad,"{""d"":5,""p"":0.01}" 61569, 1001, 0, 24.5,pymatching,2fefcc356752482fb4c6d912c228f6d18762f5752796c668b6abeb7775f5de92,"{""d"":5,""p"":0.005}" ``` and the corresponding image saved to `plot.png`: ![Example plot](readme_example_plot.png) ## python API utility methods Sinter's python module exposes a variety of methods that are handy for plotting or analyzing QEC data. See the [sinter API reference](https://github.com/quantumlib/Stim/blob/main/doc/sinter_api.md). # How to use: Linux Command Line This example assumes you are using a linux command line in a python virtualenv with `sinter` installed. ## pick circuits For this example, we will use Stim's circuit generation functionality to produce circuits to benchmark. We will make rotated surface code circuits with various physical error rates, with filenames like `rotated_d5_p0.001_surface_code.stim`. ```bash mkdir -p circuits python -c " import stim for p in [0.001, 0.005, 0.01]: for d in [3, 5]: with open(f'circuits/d={d},p={p},b=X,type=rotated_surface_memory.stim', 'w') as f: c = stim.Circuit.generated( rounds=d, distance=d, after_clifford_depolarization=p, after_reset_flip_probability=p, before_measure_flip_probability=p, before_round_data_depolarization=p, code_task=f'surface_code:rotated_memory_x') print(c, file=f) " ``` Normally, making the circuit files is the hardest step, because they are what specifies the problem you are sampling from. Almost all of the work you do will generally involve creating the exact perfect circuit file for your needs. But this is just an example, so we'll use normal surface code circuits. # collect You can use sinter to collect statistics on each circuit by using the `sinter collect` command. This command takes options specifying how much data to collect, how to do decoding, etc. The `processes` argument decides how many workers to use. Set it to `auto` to set it to the number of CPUs on your machine. The `metadata_func` argument can be used to specify custom python expression that turns the `path` into a dictionary or other JSON object associated with the circuit. If you set `metadata_func` to `auto` then will use the method `sinter.comma_separated_key_values(path)` which parses stim circuit paths like `folder/a=2,b=test.stim` into a dictionary like `{'a': 2, 'b': 'test'}`. By default, sinter writes the collected statistics to stdout as CSV data. One particularly important option that changes this behavior is `--save_resume_filepath`, which allows the command to be interrupted and restarted without losing data. Any data already at the file specified by `--save_resume_filepath` will count towards the amount of statistics asked to be collected, and sinter will append new statistics to this file instead of overwriting it. ```bash sinter collect \ --processes auto \ --circuits circuits/*.stim \ --metadata_func auto \ --decoders pymatching \ --max_shots 1_000_000 \ --max_errors 1000 \ --save_resume_filepath stats.csv ``` Beware that if you SIGKILL or SIGTEM sinter, instead of just using SIGINT, it's possible (though unlikely) that you are killing it just as it writes a row of CSV data. This truncates the data, which requires manual intervention on your part to fix (e.g. by deleting the partial row using a text editor). # combine Note that the CSV data written by sinter will contain multiple rows for each case, because sinter starts by running small batches to see roughly what the error rate is before moving to larger batch sizes. You can get a single-row-per-case CSV file by using `sinter combine`: ```bash sinter combine stats.csv ``` ``` shots, errors, discards, seconds,decoder,strong_id,json_metadata 58591, 1067, 0, 5.50,pymatching,bb46c8fca4d9fd9d4d27a5039686332ac5e24011a7f2aea5a65f6040445567c0,"{""b"":""X"",""d"":3,""p"":0.005,""type"":""rotated_surface_memory""}" 1000000, 901, 0, 73.4,pymatching,4c0780830fe1747ab22767b69d1178f803943c83dd4afa6d241acf02e6dfa71f,"{""b"":""X"",""d"":3,""p"":0.001,""type"":""rotated_surface_memory""}" 16315, 1026, 0, 2.39,pymatching,64d81b177ef1a455644ac3e03f374394cd8ad385ba2ee0ac147b2405107564fc,"{""b"":""X"",""d"":3,""p"":0.01,""type"":""rotated_surface_memory""}" 1000000, 157, 0, 116.5,pymatching,100855c078af0936d098cecbd8bfb7591c0951ae69527c002c9c5f4c79bde129,"{""b"":""X"",""d"":5,""p"":0.001,""type"":""rotated_surface_memory""}" 61677, 1005, 0, 21.2,pymatching,6d7b8b312a5460c7fe08119d3c7a040daa25bd34d524611160e4aac6196293fe,"{""b"":""X"",""d"":5,""p"":0.005,""type"":""rotated_surface_memory""}" 10891, 1021, 0, 7.43,pymatching,477252e968f0f22f64ccb058c0e1e9c77b765f60f74df8b6707de7ec65ed13b7,"{""b"":""X"",""d"":5,""p"":0.01,""type"":""rotated_surface_memory""}" ``` # plot You can use `sinter plot` to view the results you've collected. This command takes a CSV file, an argument `--group_func` indicating how to group the statistics into curves, an argument `--x_func` indicating how to pick the X coordinate of each point, and various other arguments. Each `*_func` argument takes a string that will be evaluated as a python expression, with various useful values in scope such as a `metadata` value containing the json metadata for the various points being evaluated. There is also a special `m` value where `m.key` is shorthand for `metadata.get('key', None)`. Here is an example of a `sinter plot` command: ```bash sinter plot \ --in stats.csv \ --group_func "f'''Rotated Surface Code d={m.d}'''" \ --x_func m.p \ --xaxis "[log]Physical Error Rate" \ --fig_size 1024 1024 \ --out surface_code_figure.png \ --show ``` Which will save a png image of, and also open a window showing, a plot like this one: ![Example plot](readme_example_plot.png) # The csv format for sample statistics Sinter saves samples as a table using a Comma Separated Value format. For example: ``` shots,errors,discards,seconds,decoder,strong_id,json_metadata 1000000, 837, 0, 36.6,pymatching,9f7e20c54fec45b6aef7491b774dd5c0a3b9a005aa82faf5b9c051d6e40d60a9,"{""d"":3,""p"":0.001}" 53498, 1099, 0, 6.52,pymatching,3f40432443a99b933fb548b831fb54e7e245d9d73a35c03ea5a2fb2ce270f8c8,"{""d"":3,""p"":0.005}" 16269, 1023, 0, 3.23,pymatching,17b2e0c99560d20307204494ac50e31b33e50721b4ebae99d9e3577ae7248874,"{""d"":3,""p"":0.01}" 1000000, 151, 0, 77.3,pymatching,e179a18739201250371ffaae0197d8fa19d26b58dfc2942f9f1c85568645387a,"{""d"":5,""p"":0.001}" 11363, 1068, 0, 12.5,pymatching,a4dec28934a033215ff1389651a26114ecc22016a6e122008830cf7dd04ba5ad,"{""d"":5,""p"":0.01}" 61569, 1001, 0, 24.5,pymatching,2fefcc356752482fb4c6d912c228f6d18762f5752796c668b6abeb7775f5de92,"{""d"":5,""p"":0.005}" ``` The columns are: - `shots` (unsigned int): How many times the circuit was sampled. - `errors` (unsigned int): How many times the decoder failed to predict any logical observable. - `discards` (unsigned int): How many times a shot was discarded because a postselected detector fired or because the decoder incorrectly predicted the value of a postselected observable. Discarded shots never count as errors. - `seconds` (non-negative float): How many CPU core seconds it took to simulate and decode these shots. - `decoder` (str): Which decoder was used. - `strong_id` (str): Hex representation of a cryptographic hash of the problem being sampled from. The hashed data includes the exact circuit that was simulated, the decoder that was used, the exact detector error model that was given to the decoder, the postselection rules that were applied, and the metadata associated with the circuit. The purpose of the strong id is to make it impossible to accidentally combine shots that were from separate circuits or separate versions of a circuit. - `json_metadata` (json): A free form field that can store any value representable in [Java Script Object Notation](https://json.org). For example, this could be a dictionary with helpful keys like "noise_level" or "circuit_name". The json value is serialized into JSON and then escaped so that it can be put into the CSV data (e.g. quotes get doubled up). - `custom_counts` (json[Dict[str, int]]): An optional field that can store a dictionary from string keys to integer counts represented in [Java Script Object Notation](https://json.org). The counts can be a huge variety of things, ranging from per-observable error counts to detection event counts. In general, any value that should be added when merging rows could be in these counters. Note shots may be spread across multiple rows. For example, this data: ``` shots,errors,discards,seconds,decoder,strong_id,json_metadata 500000, 437, 0, 20.5,pymatching,9f7e20c54fec45b6aef7491b774dd5c0a3b9a005aa82faf5b9c051d6e40d60a9,"{""d"":3,""p"":0.001}" 500000, 400, 0, 16.1,pymatching,9f7e20c54fec45b6aef7491b774dd5c0a3b9a005aa82faf5b9c051d6e40d60a9,"{""d"":3,""p"":0.001}" ``` has the same total statistics as this data: ``` shots,errors,discards,seconds,decoder,strong_id,json_metadata 1000000, 837, 0, 36.6,pymatching,9f7e20c54fec45b6aef7491b774dd5c0a3b9a005aa82faf5b9c051d6e40d60a9,"{""d"":3,""p"":0.001}" ``` just split over two rows instead of combined into one. ================================================ FILE: glue/sample/requirements.txt ================================================ matplotlib numpy stim scipy ================================================ FILE: glue/sample/setup.py ================================================ # 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='sinter', version=__version__, author='Craig Gidney', author_email='craig.gidney@gmail.com', license='Apache 2', packages=['sinter', 'sinter._data', 'sinter._collection', 'sinter._command', 'sinter._decoding'], package_dir={'': 'src'}, description='Samples stim circuits and decodes them using pymatching.', long_description=long_description, long_description_content_type='text/markdown', python_requires='>=3.7.0', data_files=['README.md', 'requirements.txt', 'readme_example_plot.png'], install_requires=requirements, tests_require=['pytest', 'pymatching'], entry_points={ 'console_scripts': ['sinter=sinter._command._main:main'], }, ) ================================================ FILE: glue/sample/src/sinter/__init__.py ================================================ __version__ = '1.16.dev0' from sinter._collection import ( collect, iter_collect, post_selection_mask_from_4th_coord, Progress, ) from sinter._data import ( AnonTaskStats, CollectionOptions, CSV_HEADER, read_stats_from_csv_files, stats_from_csv_files, Task, TaskStats, ) from sinter._decoding import ( CompiledDecoder, Decoder, BUILT_IN_DECODERS, BUILT_IN_SAMPLERS, Sampler, CompiledSampler, ) from sinter._probability_util import ( comma_separated_key_values, Fit, fit_binomial, fit_line_slope, fit_line_y_at_x, log_binomial, log_factorial, shot_error_rate_to_piece_error_rate, ) from sinter._plotting import ( better_sorted_str_terms, plot_discard_rate, plot_error_rate, plot_custom, group_by, ) from sinter._predict import ( predict_discards_bit_packed, predict_observables_bit_packed, predict_on_disk, predict_observables, ) ================================================ FILE: glue/sample/src/sinter/_collection/__init__.py ================================================ from sinter._collection._collection import ( collect, iter_collect, post_selection_mask_from_4th_coord, post_selection_mask_from_predicate, Progress, ) from sinter._collection._printer import ( ThrottledProgressPrinter, ) ================================================ FILE: glue/sample/src/sinter/_collection/_collection.py ================================================ import contextlib import dataclasses import pathlib from typing import Any, Callable, Iterator, Optional, Union, Iterable, List, TYPE_CHECKING, Tuple, Dict import math import numpy as np import stim from sinter._data import CSV_HEADER, ExistingData, TaskStats, CollectionOptions, Task from sinter._collection._collection_manager import CollectionManager from sinter._collection._printer import ThrottledProgressPrinter if TYPE_CHECKING: import sinter @dataclasses.dataclass(frozen=True) class Progress: """Describes statistics and status messages from ongoing sampling. This is the type yielded by `sinter.iter_collect`, and given to the `progress_callback` argument of `sinter.collect`. Attributes: new_stats: New sampled statistics collected since the last progress update. status_message: A free form human readable string describing the current collection status, such as the number of tasks left and the estimated time to completion for each task. """ new_stats: Tuple[TaskStats, ...] status_message: str def iter_collect(*, num_workers: int, tasks: Union[Iterator['sinter.Task'], Iterable['sinter.Task']], hint_num_tasks: Optional[int] = None, additional_existing_data: Union[None, dict[str, 'TaskStats'], Iterable['TaskStats']] = None, max_shots: Optional[int] = None, max_errors: Optional[int] = None, decoders: Optional[Iterable[str]] = None, max_batch_seconds: Optional[int] = None, max_batch_size: Optional[int] = None, start_batch_size: Optional[int] = None, count_observable_error_combos: bool = False, count_detection_events: bool = False, custom_decoders: Optional[Dict[str, Union['sinter.Decoder', 'sinter.Sampler']]] = None, custom_error_count_key: Optional[str] = None, allowed_cpu_affinity_ids: Optional[Iterable[int]] = None, ) -> Iterator['sinter.Progress']: """Iterates error correction statistics collected from worker processes. It is important to iterate until the sequence ends, or worker processes will be left alive. The values yielded during iteration are progress updates from the workers. Note: if max_batch_size and max_batch_seconds are both not used (or explicitly set to None), a default batch-size-limiting mechanism will be chosen. Args: num_workers: The number of worker processes to use. tasks: Decoding problems to sample. hint_num_tasks: If `tasks` is an iterator or a generator, its length can be given here so that progress printouts can say how many cases are left. additional_existing_data: Defaults to None (no additional data). Statistical data that has already been collected, in addition to anything included in each task's `previous_stats` field. decoders: Defaults to None (specified by each Task). The names of the decoders to use on each Task. It must either be the case that each Task specifies a decoder and this is set to None, or this is an iterable and each Task has its decoder set to None. max_shots: Defaults to None (unused). Stops the sampling process after this many samples have been taken from the circuit. max_errors: Defaults to None (unused). Stops the sampling process after this many errors have been seen in samples taken from the circuit. The actual number sampled errors may be larger due to batching. count_observable_error_combos: Defaults to False. When set to to True, the returned stats will have a custom counts field with keys like `obs_mistake_mask=E_E__` counting how many times specific combinations of observables were mispredicted by the decoder. count_detection_events: Defaults to False. When set to True, the returned stats will have a custom counts field withs the key `detection_events` counting the number of times a detector fired and also `detectors_checked` counting the number of detectors that were executed. The detection fraction is the ratio of these two numbers. start_batch_size: Defaults to None (collector's choice). The very first shots taken from the circuit will use a batch of this size, and no other batches will be taken in parallel. Once this initial fact finding batch is done, batches can be taken in parallel and the normal batch size limiting processes take over. max_batch_size: Defaults to None (unused). Limits batches from taking more than this many shots at once. For example, this can be used to ensure memory usage stays below some limit. max_batch_seconds: Defaults to None (unused). When set, the recorded data from previous shots is used to estimate how much time is taken per shot. This information is then used to predict the biggest batch size that can finish in under the given number of seconds. Limits each batch to be no larger than that. custom_decoders: Custom decoders that can be used if requested by name. If not specified, only decoders built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. custom_error_count_key: Makes `max_errors` apply to `stat.custom_counts[key]` instead of `stat.errors`. allowed_cpu_affinity_ids: Controls which CPUs the workers can be pinned to. The set of allowed IDs should be at least as large as the number of workers, though this is not strictly required. If not set, defaults to all CPUs being allowed. Yields: sinter.Progress instances recording incremental statistical data as it is collected by workers. Examples: >>> import sinter >>> import stim >>> tasks = [ ... sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... distance=5, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ), ... json_metadata={'d': 5}, ... ), ... sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... distance=7, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ), ... json_metadata={'d': 7}, ... ), ... ] >>> iterator = sinter.iter_collect( ... tasks=tasks, ... decoders=['vacuous'], ... num_workers=2, ... max_shots=100, ... ) >>> total_shots = 0 >>> for progress in iterator: ... for stat in progress.new_stats: ... total_shots += stat.shots >>> print(total_shots) 200 """ existing_data: dict[str, TaskStats] if isinstance(additional_existing_data, ExistingData): existing_data = additional_existing_data.data elif isinstance(additional_existing_data, dict): existing_data = additional_existing_data elif additional_existing_data is None: existing_data = {} else: acc = ExistingData() for stat in additional_existing_data: acc.add_sample(stat) existing_data = acc.data if isinstance(decoders, str): decoders = [decoders] if hint_num_tasks is None: try: # noinspection PyTypeChecker hint_num_tasks = len(tasks) except TypeError: pass if decoders is not None: old_tasks = tasks tasks = ( Task( circuit=task.circuit, decoder=decoder, detector_error_model=task.detector_error_model, postselection_mask=task.postselection_mask, postselected_observables_mask=task.postselected_observables_mask, json_metadata=task.json_metadata, collection_options=task.collection_options, circuit_path=task.circuit_path, ) for task in old_tasks for decoder in (decoders if task.decoder is None else [task.decoder]) ) progress_log: list[Optional[TaskStats]] = [] def log_progress(e: Optional[TaskStats]): progress_log.append(e) with CollectionManager( num_workers=num_workers, tasks=tasks, collection_options=CollectionOptions( max_shots=max_shots, max_errors=max_errors, max_batch_seconds=max_batch_seconds, start_batch_size=start_batch_size, max_batch_size=max_batch_size, ), existing_data=existing_data, count_observable_error_combos=count_observable_error_combos, count_detection_events=count_detection_events, custom_error_count_key=custom_error_count_key, custom_decoders=custom_decoders or {}, allowed_cpu_affinity_ids=allowed_cpu_affinity_ids, worker_flush_period=max_batch_seconds or 120, progress_callback=log_progress, ) as manager: try: yield Progress( new_stats=(), status_message=f"Starting {num_workers} workers..." ) manager.start_workers() manager.start_distributing_work() while manager.task_states: manager.process_message() if progress_log: vals = list(progress_log) progress_log.clear() for e in vals: if e is not None: yield Progress( new_stats=(e,), status_message=manager.status_message(), ) except KeyboardInterrupt: yield Progress( new_stats=(), status_message='KeyboardInterrupt', ) raise def collect(*, num_workers: int, tasks: Union[Iterator['sinter.Task'], Iterable['sinter.Task']], existing_data_filepaths: Iterable[Union[str, pathlib.Path]] = (), save_resume_filepath: Union[None, str, pathlib.Path] = None, progress_callback: Optional[Callable[['sinter.Progress'], None]] = None, max_shots: Optional[int] = None, max_errors: Optional[int] = None, count_observable_error_combos: bool = False, count_detection_events: bool = False, decoders: Optional[Iterable[str]] = None, max_batch_seconds: Optional[int] = None, max_batch_size: Optional[int] = None, start_batch_size: Optional[int] = None, print_progress: bool = False, hint_num_tasks: Optional[int] = None, custom_decoders: Optional[Dict[str, Union['sinter.Decoder', 'sinter.Sampler']]] = None, custom_error_count_key: Optional[str] = None, allowed_cpu_affinity_ids: Optional[Iterable[int]] = None, ) -> List['sinter.TaskStats']: """Collects statistics from the given tasks, using multiprocessing. Args: num_workers: The number of worker processes to use. tasks: Decoding problems to sample. save_resume_filepath: Defaults to None (unused). If set to a filepath, results will be saved to that file while they are collected. If the python interpreter is stopped or killed, calling this method again with the same save_resume_filepath will load the previous results from the file so it can resume where it left off. The stats in this file will be counted in addition to each task's previous_stats field (as opposed to overriding the field). existing_data_filepaths: CSV data saved to these files will be loaded, included in the returned results, and count towards things like max_shots and max_errors. progress_callback: Defaults to None (unused). If specified, then each time new sample statistics are acquired from a worker this method will be invoked with the new `sinter.TaskStats`. hint_num_tasks: If `tasks` is an iterator or a generator, its length can be given here so that progress printouts can say how many cases are left. decoders: Defaults to None (specified by each Task). The names of the decoders to use on each Task. It must either be the case that each Task specifies a decoder and this is set to None, or this is an iterable and each Task has its decoder set to None. count_observable_error_combos: Defaults to False. When set to to True, the returned stats will have a custom counts field with keys like `obs_mistake_mask=E_E__` counting how many times specific combinations of observables were mispredicted by the decoder. count_detection_events: Defaults to False. When set to True, the returned stats will have a custom counts field withs the key `detection_events` counting the number of times a detector fired and also `detectors_checked` counting the number of detectors that were executed. The detection fraction is the ratio of these two numbers. max_shots: Defaults to None (unused). Stops the sampling process after this many samples have been taken from the circuit. max_errors: Defaults to None (unused). Stops the sampling process after this many errors have been seen in samples taken from the circuit. The actual number sampled errors may be larger due to batching. start_batch_size: Defaults to None (collector's choice). The very first shots taken from the circuit will use a batch of this size, and no other batches will be taken in parallel. Once this initial fact finding batch is done, batches can be taken in parallel and the normal batch size limiting processes take over. max_batch_size: Defaults to None (unused). Limits batches from taking more than this many shots at once. For example, this can be used to ensure memory usage stays below some limit. print_progress: When True, progress is printed to stderr while collection runs. max_batch_seconds: Defaults to None (unused). When set, the recorded data from previous shots is used to estimate how much time is taken per shot. This information is then used to predict the biggest batch size that can finish in under the given number of seconds. Limits each batch to be no larger than that. custom_decoders: Named child classes of `sinter.decoder`, that can be used if requested by name by a task or by the decoders list. If not specified, only decoders with support built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. custom_error_count_key: Makes `max_errors` apply to `stat.custom_counts[key]` instead of `stat.errors`. allowed_cpu_affinity_ids: Controls which CPUs the workers can be pinned to. The set of allowed IDs should be at least as large as the number of workers, though this is not strictly required. If not set, defaults to all CPUs being allowed. Returns: A list of sample statistics, one from each problem. The list is not in any specific order. This is the same data that would have been written to a CSV file, but aggregated so that each problem has exactly one sample statistic instead of potentially multiple. Examples: >>> import sinter >>> import stim >>> tasks = [ ... sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... distance=5, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ), ... json_metadata={'d': 5}, ... ), ... sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... distance=7, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ), ... json_metadata={'d': 7}, ... ), ... ] >>> stats = sinter.collect( ... tasks=tasks, ... decoders=['vacuous'], ... num_workers=2, ... max_shots=100, ... ) >>> for stat in sorted(stats, key=lambda e: e.json_metadata['d']): ... print(stat.json_metadata, stat.shots) {'d': 5} 100 {'d': 7} 100 """ # Load existing data. additional_existing_data = ExistingData() for existing in existing_data_filepaths: additional_existing_data += ExistingData.from_file(existing) if save_resume_filepath in existing_data_filepaths: raise ValueError("save_resume_filepath in existing_data_filepaths") progress_printer = ThrottledProgressPrinter( outs=[], print_progress=print_progress, min_progress_delay=0.1, ) with contextlib.ExitStack() as exit_stack: # Open save/resume file. if save_resume_filepath is not None: save_resume_filepath = pathlib.Path(save_resume_filepath) if save_resume_filepath.exists(): additional_existing_data += ExistingData.from_file(save_resume_filepath) save_resume_file = exit_stack.enter_context( open(save_resume_filepath, 'a')) # type: ignore else: save_resume_filepath.parent.mkdir(exist_ok=True) save_resume_file = exit_stack.enter_context( open(save_resume_filepath, 'w')) # type: ignore print(CSV_HEADER, file=save_resume_file, flush=True) else: save_resume_file = None # Collect data. result = ExistingData() result.data = dict(additional_existing_data.data) for progress in iter_collect( num_workers=num_workers, max_shots=max_shots, max_errors=max_errors, max_batch_seconds=max_batch_seconds, start_batch_size=start_batch_size, max_batch_size=max_batch_size, count_observable_error_combos=count_observable_error_combos, count_detection_events=count_detection_events, decoders=decoders, tasks=tasks, hint_num_tasks=hint_num_tasks, additional_existing_data=additional_existing_data, custom_decoders=custom_decoders, custom_error_count_key=custom_error_count_key, allowed_cpu_affinity_ids=allowed_cpu_affinity_ids, ): for stats in progress.new_stats: result.add_sample(stats) if save_resume_file is not None: print(stats.to_csv_line(), file=save_resume_file, flush=True) if print_progress: progress_printer.show_latest_progress(progress.status_message) if progress_callback is not None: progress_callback(progress) if print_progress: progress_printer.flush() return list(result.data.values()) def post_selection_mask_from_predicate( circuit_or_dem: Union[stim.Circuit, stim.DetectorErrorModel], *, postselected_detectors_predicate: Callable[[int, Any, Tuple[float, ...]], bool], metadata: Any, ) -> np.ndarray: num_dets = circuit_or_dem.num_detectors post_selection_mask = np.zeros(dtype=np.uint8, shape=math.ceil(num_dets / 8)) for k, coord in circuit_or_dem.get_detector_coordinates().items(): if postselected_detectors_predicate(k, metadata, coord): post_selection_mask[k // 8] |= 1 << (k % 8) return post_selection_mask def post_selection_mask_from_4th_coord(dem: Union[stim.Circuit, stim.DetectorErrorModel]) -> np.ndarray: """Returns a mask that postselects detector's with non-zero 4th coordinate. This method is a leftover from before the existence of the command line argument `--postselected_detectors_predicate`, when `--postselect_detectors_with_non_zero_4th_coord` was the only way to do post selection of detectors. Args: dem: The detector error model to pull coordinate data from. Returns: A bit packed numpy array where detectors with non-zero 4th coordinate data have a True bit at their corresponding index. Examples: >>> import sinter >>> import stim >>> dem = stim.DetectorErrorModel(''' ... detector(1, 2, 3) D0 ... detector(1, 1, 1, 1) D1 ... detector(1, 1, 1, 0) D2 ... detector(1, 1, 1, 999) D80 ... ''') >>> sinter.post_selection_mask_from_4th_coord(dem) array([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], dtype=uint8) """ num_dets = dem.num_detectors post_selection_mask = np.zeros(dtype=np.uint8, shape=math.ceil(num_dets / 8)) for k, coord in dem.get_detector_coordinates().items(): if len(coord) >= 4 and coord[3]: post_selection_mask[k // 8] |= 1 << (k % 8) return post_selection_mask ================================================ FILE: glue/sample/src/sinter/_collection/_collection_manager.py ================================================ import collections import contextlib import math import multiprocessing import os import pathlib import queue import tempfile import threading from typing import Any, Optional, List, Dict, Iterable, Callable, Tuple from typing import Union from typing import cast from sinter._collection._collection_worker_loop import collection_worker_loop from sinter._collection._mux_sampler import MuxSampler from sinter._collection._sampler_ramp_throttled import RampThrottledSampler from sinter._data import CollectionOptions, Task, AnonTaskStats, TaskStats from sinter._decoding import Sampler, Decoder class _ManagedWorkerState: def __init__(self, worker_id: int, *, cpu_pin: Optional[int] = None): self.worker_id: int = worker_id self.process: Union[multiprocessing.Process, threading.Thread, None] = None self.input_queue: Optional[multiprocessing.Queue[Tuple[str, Any]]] = None self.assigned_work_key: Any = None self.asked_to_drop_shots: int = 0 self.cpu_pin = cpu_pin # Shots transfer into this field when manager sends shot requests to workers. # Shots transfer out of this field when clients flush results or respond to work return requests. self.assigned_shots: int = 0 def send_message(self, message: Any): self.input_queue.put(message) def ask_to_return_all_shots(self): if self.asked_to_drop_shots == 0 and self.assigned_shots > 0: self.send_message(( 'return_shots', ( self.assigned_work_key, self.assigned_shots, ), )) self.asked_to_drop_shots = self.assigned_shots def has_returned_all_shots(self) -> bool: return self.assigned_shots == 0 and self.asked_to_drop_shots == 0 def is_available_to_reassign(self) -> bool: return self.assigned_work_key is None class _ManagedTaskState: def __init__(self, *, partial_task: Task, strong_id: str, shots_left: int, errors_left: int): self.partial_task = partial_task self.strong_id = strong_id self.shots_left = shots_left self.errors_left = errors_left self.shots_unassigned = shots_left self.shot_return_requests = 0 self.assigned_soft_error_flush_threshold: int = errors_left self.workers_assigned: list[int] = [] def is_completed(self) -> bool: return self.shots_left <= 0 or self.errors_left <= 0 class CollectionManager: def __init__( self, *, existing_data: Dict[Any, TaskStats], collection_options: CollectionOptions, custom_decoders: dict[str, Union[Decoder, Sampler]], num_workers: int, worker_flush_period: float, tasks: Iterable[Task], progress_callback: Callable[[Optional[TaskStats]], None], allowed_cpu_affinity_ids: Optional[Iterable[int]], count_observable_error_combos: bool = False, count_detection_events: bool = False, custom_error_count_key: Optional[str] = None, use_threads_for_debugging: bool = False, ): assert isinstance(custom_decoders, dict) self.existing_data = existing_data self.num_workers: int = num_workers self.custom_decoders = custom_decoders self.worker_flush_period: float = worker_flush_period self.progress_callback = progress_callback self.collection_options = collection_options self.partial_tasks: list[Task] = list(tasks) self.task_strong_ids: List[Optional[str]] = [None] * len(self.partial_tasks) self.allowed_cpu_affinity_ids = None if allowed_cpu_affinity_ids is None else sorted(set(allowed_cpu_affinity_ids)) self.count_observable_error_combos = count_observable_error_combos self.count_detection_events = count_detection_events self.custom_error_count_key = custom_error_count_key self.use_threads_for_debugging = use_threads_for_debugging self.shared_worker_output_queue: Optional[multiprocessing.SimpleQueue[Tuple[str, int, Any]]] = None self.task_states: Dict[Any, _ManagedTaskState] = {} self.started: bool = False self.total_collected = {k: v.to_anon_stats() for k, v in existing_data.items()} if self.allowed_cpu_affinity_ids is None: cpus = range(os.cpu_count()) else: num_cpus = os.cpu_count() cpus = [e for e in self.allowed_cpu_affinity_ids if e < num_cpus] self.worker_states: List[_ManagedWorkerState] = [] for index in range(num_workers): cpu_pin = None if len(cpus) == 0 else cpus[index % len(cpus)] self.worker_states.append(_ManagedWorkerState(index, cpu_pin=cpu_pin)) self.tmp_dir: Optional[pathlib.Path] = None def __enter__(self): self.exit_stack = contextlib.ExitStack().__enter__() self.tmp_dir = pathlib.Path(self.exit_stack.enter_context(tempfile.TemporaryDirectory())) return self def __exit__(self, exc_type, exc_val, exc_tb): self.hard_stop() self.exit_stack.__exit__(exc_type, exc_val, exc_tb) self.exit_stack = None self.tmp_dir = None def start_workers(self, *, actually_start_worker_processes: bool = True): assert not self.started # Use max_batch_size from collection_options if provided, otherwise default to 1024 as large # batch sizes can lead to thrashing max_batch_shots = self.collection_options.max_batch_size or 1024 sampler = RampThrottledSampler( sub_sampler=MuxSampler( custom_decoders=self.custom_decoders, count_observable_error_combos=self.count_observable_error_combos, count_detection_events=self.count_detection_events, tmp_dir=self.tmp_dir, ), target_batch_seconds=1, max_batch_shots=max_batch_shots, ) self.started = True current_method = multiprocessing.get_start_method() try: # To ensure the child processes do not accidentally share ANY state # related to random number generation, we use 'spawn' instead of 'fork'. multiprocessing.set_start_method('spawn', force=True) # Create queues after setting start method to work around a deadlock # bug that occurs otherwise. self.shared_worker_output_queue = multiprocessing.SimpleQueue() for worker_id in range(self.num_workers): worker_state = self.worker_states[worker_id] worker_state.input_queue = multiprocessing.Queue() worker_state.input_queue.cancel_join_thread() worker_state.assigned_work_key = None args = ( self.worker_flush_period, worker_id, sampler, worker_state.input_queue, self.shared_worker_output_queue, worker_state.cpu_pin, self.custom_error_count_key, ) if self.use_threads_for_debugging: worker_state.process = threading.Thread( target=collection_worker_loop, args=args, ) else: worker_state.process = multiprocessing.Process( target=collection_worker_loop, args=args, ) if actually_start_worker_processes: worker_state.process.start() finally: multiprocessing.set_start_method(current_method, force=True) def start_distributing_work(self): self._compute_task_ids() self._distribute_work() def _compute_task_ids(self): idle_worker_ids = list(range(self.num_workers)) unknown_task_ids = list(range(len(self.partial_tasks))) worker_to_task_map = {} while worker_to_task_map or unknown_task_ids: while idle_worker_ids and unknown_task_ids: worker_id = idle_worker_ids.pop() unknown_task_id = unknown_task_ids.pop() worker_to_task_map[worker_id] = unknown_task_id self.worker_states[worker_id].send_message(('compute_strong_id', self.partial_tasks[unknown_task_id])) try: message = self.shared_worker_output_queue.get() message_type, worker_id, message_body = message if message_type == 'computed_strong_id': assert worker_id in worker_to_task_map assert isinstance(message_body, str) self.task_strong_ids[worker_to_task_map.pop(worker_id)] = message_body idle_worker_ids.append(worker_id) elif message_type == 'stopped_due_to_exception': cur_task, cur_shots_left, unflushed_work_done, traceback, ex = message_body raise ValueError(f'Worker failed: traceback={traceback}') from ex else: raise NotImplementedError(f'{message_type=}') self.progress_callback(None) except queue.Empty: pass assert len(idle_worker_ids) == self.num_workers seen = set() for k in range(len(self.partial_tasks)): options = self.partial_tasks[k].collection_options.combine(self.collection_options) key: str = self.task_strong_ids[k] if key in seen: raise ValueError(f'Same task given twice: {self.partial_tasks[k]!r}') seen.add(key) shots_left = options.max_shots errors_left = options.max_errors if errors_left is None: errors_left = shots_left errors_left = min(errors_left, shots_left) if key in self.existing_data: val = self.existing_data[key] shots_left -= val.shots if self.custom_error_count_key is None: errors_left -= val.errors else: errors_left -= val.custom_counts[self.custom_error_count_key] if shots_left <= 0: continue self.task_states[key] = _ManagedTaskState( partial_task=self.partial_tasks[k], strong_id=key, shots_left=shots_left, errors_left=errors_left, ) if self.task_states[key].is_completed(): del self.task_states[key] def hard_stop(self): if not self.started: return removed_workers = [state.process for state in self.worker_states] for state in self.worker_states: if isinstance(state.process, threading.Thread): state.send_message('stop') state.process = None state.assigned_work_key = None state.input_queue = None self.shared_worker_output_queue = None self.started = False self.task_states.clear() # SIGKILL everything. for w in removed_workers: if isinstance(w, multiprocessing.Process): w.kill() # Wait for them to be done. for w in removed_workers: w.join() def _handle_task_progress(self, task_id: Any): task_state = self.task_states[task_id] if task_state.is_completed(): workers_ready = all(self.worker_states[worker_id].has_returned_all_shots() for worker_id in task_state.workers_assigned) if workers_ready: # Task is fully completed and can be forgotten entirely. Re-assign the workers. del self.task_states[task_id] for worker_id in task_state.workers_assigned: w = self.worker_states[worker_id] assert w.assigned_shots <= 0 assert w.asked_to_drop_shots == 0 w.assigned_work_key = None self._distribute_work() else: # Task is sufficiently sampled, but some workers are still running. for worker_id in task_state.workers_assigned: self.worker_states[worker_id].ask_to_return_all_shots() self.progress_callback(None) else: self._distribute_unassigned_workers_to_jobs() self._distribute_work_within_a_job(task_state) def state_summary(self) -> str: lines = [] for worker_id, worker in enumerate(self.worker_states): lines.append(f'worker {worker_id}:' f' asked_to_drop_shots={worker.asked_to_drop_shots}' f' assigned_shots={worker.assigned_shots}' f' assigned_work_key={worker.assigned_work_key}') for task in self.task_states.values(): lines.append(f'task {task.strong_id=}:\n' f' workers_assigned={task.workers_assigned}\n' f' shot_return_requests={task.shot_return_requests}\n' f' shots_left={task.shots_left}\n' f' errors_left={task.errors_left}\n' f' shots_unassigned={task.shots_unassigned}') return '\n' + '\n'.join(lines) + '\n' def process_message(self) -> bool: try: message = self.shared_worker_output_queue.get() except queue.Empty: return False message_type, worker_id, message_body = message worker_state = self.worker_states[worker_id] if message_type == 'flushed_results': task_strong_id, anon_stat = message_body assert isinstance(anon_stat, AnonTaskStats) assert worker_state.assigned_work_key == task_strong_id task_state = self.task_states[task_strong_id] worker_state.assigned_shots -= anon_stat.shots task_state.shots_left -= anon_stat.shots if worker_state.assigned_shots < 0: # Worker over-achieved. Correct the imbalance by giving them the shots. extra_shots = abs(worker_state.assigned_shots) worker_state.assigned_shots += extra_shots task_state.shots_unassigned -= extra_shots worker_state.send_message(( 'accept_shots', (task_state.strong_id, extra_shots), )) if self.custom_error_count_key is None: task_state.errors_left -= anon_stat.errors else: task_state.errors_left -= anon_stat.custom_counts[self.custom_error_count_key] stat = TaskStats( strong_id=task_state.strong_id, decoder=task_state.partial_task.decoder, json_metadata=task_state.partial_task.json_metadata, shots=anon_stat.shots, discards=anon_stat.discards, seconds=anon_stat.seconds, errors=anon_stat.errors, custom_counts=anon_stat.custom_counts, ) self._handle_task_progress(task_strong_id) if stat.strong_id not in self.total_collected: self.total_collected[stat.strong_id] = AnonTaskStats() self.total_collected[stat.strong_id] += stat.to_anon_stats() self.progress_callback(stat) elif message_type == 'changed_job': pass elif message_type == 'accepted_shots': pass elif message_type == 'returned_shots': task_key, shots_returned = message_body assert isinstance(shots_returned, int) assert shots_returned >= 0 assert worker_state.assigned_work_key == task_key assert worker_state.asked_to_drop_shots or worker_state.asked_to_drop_errors task_state = self.task_states[task_key] task_state.shot_return_requests -= 1 worker_state.asked_to_drop_shots = 0 worker_state.asked_to_drop_errors = 0 task_state.shots_unassigned += shots_returned worker_state.assigned_shots -= shots_returned assert worker_state.assigned_shots >= 0 self._handle_task_progress(task_key) elif message_type == 'stopped_due_to_exception': cur_task, cur_shots_left, unflushed_work_done, traceback, ex = message_body raise RuntimeError(f'Worker failed: traceback={traceback}') from ex else: raise NotImplementedError(f'{message_type=}') return True def run_until_done(self) -> bool: try: while self.task_states: self.process_message() return True except KeyboardInterrupt: return False finally: self.hard_stop() def _distribute_unassigned_workers_to_jobs(self): idle_workers = [ w for w in range(self.num_workers)[::-1] if self.worker_states[w].is_available_to_reassign() ] if not idle_workers or not self.started: return groups = collections.defaultdict(list) for work_state in self.task_states.values(): if not work_state.is_completed(): groups[len(work_state.workers_assigned)].append(work_state) for k in groups.keys(): groups[k] = groups[k][::-1] if not groups: return min_assigned = min(groups.keys(), default=0) # Distribute workers to unfinished jobs with the fewest workers. while idle_workers: task_state: _ManagedTaskState = groups[min_assigned].pop() groups[min_assigned + 1].append(task_state) if not groups[min_assigned]: min_assigned += 1 worker_id = idle_workers.pop() task_state.workers_assigned.append(worker_id) worker_state = self.worker_states[worker_id] worker_state.assigned_work_key = task_state.strong_id worker_state.send_message(( 'change_job', (task_state.partial_task, CollectionOptions(max_errors=task_state.errors_left), task_state.assigned_soft_error_flush_threshold), )) def _distribute_unassigned_work_to_workers_within_a_job(self, task_state: _ManagedTaskState): if not self.started or not task_state.workers_assigned or task_state.shots_left <= 0: return num_task_workers = len(task_state.workers_assigned) expected_shots_per_worker = (task_state.shots_left + num_task_workers - 1) // num_task_workers # Give unassigned shots to idle workers. for worker_id in sorted(task_state.workers_assigned, key=lambda wid: self.worker_states[wid].assigned_shots): worker_state = self.worker_states[worker_id] if worker_state.assigned_shots < expected_shots_per_worker: shots_to_assign = min(expected_shots_per_worker - worker_state.assigned_shots, task_state.shots_unassigned) if shots_to_assign > 0: task_state.shots_unassigned -= shots_to_assign worker_state.assigned_shots += shots_to_assign worker_state.send_message(( 'accept_shots', (task_state.strong_id, shots_to_assign), )) def status_message(self) -> str: num_known_tasks_ids = sum(e is not None for e in self.task_strong_ids) if num_known_tasks_ids < len(self.task_strong_ids): return f"Analyzed {num_known_tasks_ids}/{len(self.task_strong_ids)} tasks..." max_errors = self.collection_options.max_errors max_shots = self.collection_options.max_shots tasks_left = 0 lines = [] skipped_lines = [] for k, strong_id in enumerate(self.task_strong_ids): if strong_id not in self.task_states: continue c = self.total_collected.get(strong_id, AnonTaskStats()) tasks_left += 1 w = len(self.task_states[strong_id].workers_assigned) dt = None if max_shots is not None and c.shots: dt = (max_shots - c.shots) * c.seconds / c.shots c_errors = c.custom_counts[self.custom_error_count_key] if self.custom_error_count_key is not None else c.errors if max_errors is not None and c_errors and c.seconds: dt2 = (max_errors - c_errors) * c.seconds / c_errors if dt is None: dt = dt2 else: dt = min(dt, dt2) if dt is not None: dt /= 60 if dt is not None and w > 0: dt /= w line = [ f'{w}', self.partial_tasks[k].decoder, ("?" if dt is None or dt == 0 else "[draining]" if dt <= 0 else "<1m" if dt < 1 else str(round(dt)) + 'm') + ('·∞' if w == 0 else ''), f'{max_shots - c.shots}' if max_shots is not None else f'{c.shots}', f'{max_errors - c_errors}' if max_errors is not None else f'{c_errors}', ",".join( [f"{k}={v}" for k, v in self.partial_tasks[k].json_metadata.items()] if isinstance(self.partial_tasks[k].json_metadata, dict) else str(self.partial_tasks[k].json_metadata) ) ] if w == 0: skipped_lines.append(line) else: lines.append(line) if len(lines) < 50 and skipped_lines: missing_lines = 50 - len(lines) lines.extend(skipped_lines[:missing_lines]) skipped_lines = skipped_lines[missing_lines:] if lines: lines.insert(0, [ 'workers', 'decoder', 'eta', 'shots_left' if max_shots is not None else 'shots_taken', 'errors_left' if max_errors is not None else 'errors_seen', 'json_metadata']) justs = cast(list[Callable[[str, int], str]], [str.rjust, str.rjust, str.rjust, str.rjust, str.rjust, str.ljust]) cols = len(lines[0]) lengths = [ max(len(lines[row][col]) for row in range(len(lines))) for col in range(cols) ] lines = [ " " + " ".join(justs[col](row[col], lengths[col]) for col in range(cols)) for row in lines ] if skipped_lines: lines.append(' ... (' + str(len(skipped_lines)) + ' more tasks) ...') return f'{tasks_left} tasks left:\n' + '\n'.join(lines) def _update_soft_error_threshold_for_a_job(self, task_state: _ManagedTaskState): if task_state.errors_left <= len(task_state.workers_assigned): desired_threshold = 1 elif task_state.errors_left <= task_state.assigned_soft_error_flush_threshold * self.num_workers: desired_threshold = max(1, math.ceil(task_state.errors_left * 0.5 / self.num_workers)) else: return if task_state.assigned_soft_error_flush_threshold != desired_threshold: task_state.assigned_soft_error_flush_threshold = desired_threshold for wid in task_state.workers_assigned: self.worker_states[wid].send_message(('set_soft_error_flush_threshold', desired_threshold)) def _take_work_if_unsatisfied_workers_within_a_job(self, task_state: _ManagedTaskState): if not self.started or not task_state.workers_assigned or task_state.shots_left <= 0: return if all(self.worker_states[w].assigned_shots > 0 for w in task_state.workers_assigned): return w = len(task_state.workers_assigned) expected_shots_per_worker = (task_state.shots_left + w - 1) // w # There are idle workers that couldn't be given any shots. Take shots from other workers. for worker_id in sorted(task_state.workers_assigned, key=lambda w: self.worker_states[w].assigned_shots, reverse=True): worker_state = self.worker_states[worker_id] if worker_state.asked_to_drop_shots or worker_state.assigned_shots <= expected_shots_per_worker: continue shots_to_take = worker_state.assigned_shots - expected_shots_per_worker assert shots_to_take > 0 worker_state.asked_to_drop_shots = shots_to_take task_state.shot_return_requests += 1 worker_state.send_message(( 'return_shots', ( task_state.strong_id, shots_to_take, ), )) def _distribute_work_within_a_job(self, t: _ManagedTaskState): self._distribute_unassigned_work_to_workers_within_a_job(t) self._take_work_if_unsatisfied_workers_within_a_job(t) def _distribute_work(self): self._distribute_unassigned_workers_to_jobs() for w in self.task_states.values(): if not w.is_completed(): self._distribute_work_within_a_job(w) ================================================ FILE: glue/sample/src/sinter/_collection/_collection_manager_test.py ================================================ import multiprocessing import time from typing import Any, List, Union import sinter import stim from sinter._collection._collection_manager import CollectionManager def _assert_drain_queue(q: multiprocessing.Queue, expected_contents: List[Any]): for v in expected_contents: assert q.get(timeout=0.1) == v if not q.empty(): assert False, f'queue had another item: {q.get()=}' def _put_wait_not_empty(q: Union[multiprocessing.Queue, multiprocessing.SimpleQueue], item: Any): q.put(item) while q.empty(): time.sleep(0.0001) def test_manager(): log = [] t0 = sinter.Task( circuit=stim.Circuit('H 0'), detector_error_model=stim.DetectorErrorModel(), decoder='fusion_blossom', collection_options=sinter.CollectionOptions(max_shots=100_000_000, max_errors=100), json_metadata={'a': 3}, ) t1 = sinter.Task( circuit=stim.Circuit('M 0'), detector_error_model=stim.DetectorErrorModel(), decoder='pymatching', collection_options=sinter.CollectionOptions(max_shots=10_000_000), json_metadata=None, ) manager = CollectionManager( num_workers=3, worker_flush_period=30, tasks=[t0, t1], progress_callback=log.append, existing_data={}, collection_options=sinter.CollectionOptions(), custom_decoders={}, allowed_cpu_affinity_ids=None, ) assert manager.state_summary() == """ worker 0: asked_to_drop_shots=0 assigned_shots=0 assigned_work_key=None worker 1: asked_to_drop_shots=0 assigned_shots=0 assigned_work_key=None worker 2: asked_to_drop_shots=0 assigned_shots=0 assigned_work_key=None """ manager.start_workers(actually_start_worker_processes=False) manager.shared_worker_output_queue.put(('computed_strong_id', 2, 'c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa')) manager.shared_worker_output_queue.put(('computed_strong_id', 1, 'a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604')) manager.start_distributing_work() assert manager.state_summary() == """ worker 0: asked_to_drop_shots=0 assigned_shots=100000000 assigned_work_key=a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604 worker 1: asked_to_drop_shots=0 assigned_shots=5000000 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa worker 2: asked_to_drop_shots=0 assigned_shots=5000000 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa task task.strong_id='a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604': workers_assigned=[0] shot_return_requests=0 shots_left=100000000 errors_left=100 shots_unassigned=0 task task.strong_id='c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa': workers_assigned=[1, 2] shot_return_requests=0 shots_left=10000000 errors_left=10000000 shots_unassigned=0 """ _assert_drain_queue(manager.worker_states[0].input_queue, [ ( 'change_job', (t0, sinter.CollectionOptions(max_errors=100), 100), ), ( 'accept_shots', (t0.strong_id(), 100_000_000), ), ]) _assert_drain_queue(manager.worker_states[1].input_queue, [ ('compute_strong_id', t0), ( 'change_job', (t1, sinter.CollectionOptions(max_errors=10000000), 10000000), ), ( 'accept_shots', (t1.strong_id(), 5_000_000), ), ]) _assert_drain_queue(manager.worker_states[2].input_queue, [ ('compute_strong_id', t1), ( 'change_job', (t1, sinter.CollectionOptions(max_errors=10000000), 10000000), ), ( 'accept_shots', (t1.strong_id(), 5_000_000), ), ]) assert manager.shared_worker_output_queue.empty() assert log.pop() is None assert log.pop() is None assert not log _put_wait_not_empty(manager.shared_worker_output_queue, ( 'flushed_results', 2, (t1.strong_id(), sinter.AnonTaskStats( shots=5_000_000, errors=123, discards=0, seconds=1, )), )) assert manager.process_message() assert manager.state_summary() == """ worker 0: asked_to_drop_shots=0 assigned_shots=100000000 assigned_work_key=a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604 worker 1: asked_to_drop_shots=2500000 assigned_shots=5000000 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa worker 2: asked_to_drop_shots=0 assigned_shots=0 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa task task.strong_id='a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604': workers_assigned=[0] shot_return_requests=0 shots_left=100000000 errors_left=100 shots_unassigned=0 task task.strong_id='c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa': workers_assigned=[1, 2] shot_return_requests=1 shots_left=5000000 errors_left=9999877 shots_unassigned=0 """ assert log.pop() == sinter.TaskStats( strong_id=t1.strong_id(), decoder=t1.decoder, json_metadata=t1.json_metadata, shots=5_000_000, errors=123, discards=0, seconds=1, ) assert not log _assert_drain_queue(manager.worker_states[0].input_queue, []) _assert_drain_queue(manager.worker_states[1].input_queue, [ ( 'return_shots', (t1.strong_id(), 2_500_000), ), ]) _assert_drain_queue(manager.worker_states[2].input_queue, []) _put_wait_not_empty(manager.shared_worker_output_queue, ( 'returned_shots', 1, (t1.strong_id(), 2_000_000), )) assert manager.process_message() assert manager.state_summary() == """ worker 0: asked_to_drop_shots=0 assigned_shots=100000000 assigned_work_key=a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604 worker 1: asked_to_drop_shots=0 assigned_shots=3000000 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa worker 2: asked_to_drop_shots=0 assigned_shots=2000000 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa task task.strong_id='a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604': workers_assigned=[0] shot_return_requests=0 shots_left=100000000 errors_left=100 shots_unassigned=0 task task.strong_id='c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa': workers_assigned=[1, 2] shot_return_requests=0 shots_left=5000000 errors_left=9999877 shots_unassigned=0 """ _assert_drain_queue(manager.worker_states[0].input_queue, []) _assert_drain_queue(manager.worker_states[1].input_queue, []) _assert_drain_queue(manager.worker_states[2].input_queue, [ ( 'accept_shots', (t1.strong_id(), 2_000_000), ), ]) _put_wait_not_empty(manager.shared_worker_output_queue, ( 'flushed_results', 1, (t1.strong_id(), sinter.AnonTaskStats( shots=3_000_000, errors=444, discards=1, seconds=2, )) )) assert manager.process_message() assert manager.state_summary() == """ worker 0: asked_to_drop_shots=0 assigned_shots=100000000 assigned_work_key=a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604 worker 1: asked_to_drop_shots=0 assigned_shots=0 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa worker 2: asked_to_drop_shots=1000000 assigned_shots=2000000 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa task task.strong_id='a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604': workers_assigned=[0] shot_return_requests=0 shots_left=100000000 errors_left=100 shots_unassigned=0 task task.strong_id='c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa': workers_assigned=[1, 2] shot_return_requests=1 shots_left=2000000 errors_left=9999433 shots_unassigned=0 """ _put_wait_not_empty(manager.shared_worker_output_queue, ( 'flushed_results', 2, (t1.strong_id(), sinter.AnonTaskStats( shots=2_000_000, errors=555, discards=2, seconds=2.5, )) )) assert manager.process_message() assert manager.state_summary() == """ worker 0: asked_to_drop_shots=0 assigned_shots=100000000 assigned_work_key=a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604 worker 1: asked_to_drop_shots=0 assigned_shots=0 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa worker 2: asked_to_drop_shots=1000000 assigned_shots=0 assigned_work_key=c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa task task.strong_id='a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604': workers_assigned=[0] shot_return_requests=0 shots_left=100000000 errors_left=100 shots_unassigned=0 task task.strong_id='c03f7852e4579e2a99cefac80eeb6b09556907540ab3d7787a3d07309c3333aa': workers_assigned=[1, 2] shot_return_requests=1 shots_left=0 errors_left=9998878 shots_unassigned=0 """ assert manager.shared_worker_output_queue.empty() _put_wait_not_empty(manager.shared_worker_output_queue, ( 'returned_shots', 2, (t1.strong_id(), 0) )) assert manager.process_message() assert manager.shared_worker_output_queue.empty() assert manager.state_summary() == """ worker 0: asked_to_drop_shots=66666666 assigned_shots=100000000 assigned_work_key=a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604 worker 1: asked_to_drop_shots=0 assigned_shots=0 assigned_work_key=a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604 worker 2: asked_to_drop_shots=0 assigned_shots=0 assigned_work_key=a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604 task task.strong_id='a9165b6e4ab1053c04c017d0739a7bfff0910d62091fc9ee81716833eda7f604': workers_assigned=[0, 1, 2] shot_return_requests=1 shots_left=100000000 errors_left=100 shots_unassigned=0 """ _assert_drain_queue(manager.worker_states[0].input_queue, [ ('return_shots', (t0.strong_id(), 66666666)), ]) _assert_drain_queue(manager.worker_states[1].input_queue, [ ('change_job', (t0, sinter.CollectionOptions(max_errors=100), 100)), ]) _assert_drain_queue(manager.worker_states[2].input_queue, [ ('return_shots', (t1.strong_id(), 1000000)), ('change_job', (t0, sinter.CollectionOptions(max_errors=100), 100)), ]) ================================================ FILE: glue/sample/src/sinter/_collection/_collection_test.py ================================================ import collections import multiprocessing import pathlib import tempfile import time import pytest import sinter import stim def test_iter_collect(): result = collections.defaultdict(sinter.AnonTaskStats) for sample in sinter.iter_collect( num_workers=2, tasks=[ sinter.Task( circuit=stim.Circuit.generated( 'repetition_code:memory', rounds=3, distance=3, after_clifford_depolarization=p), decoder='pymatching', json_metadata={'p': p}, collection_options=sinter.CollectionOptions( max_shots=1000, max_errors=100, start_batch_size=100, max_batch_size=1000, ), ) for p in [0.01, 0.02, 0.03, 0.04] ], ): for stats in sample.new_stats: result[stats.json_metadata['p']] += stats.to_anon_stats() assert len(result) == 4 for k, v in result.items(): assert v.shots >= 1000 or v.errors >= 100 assert v.discards == 0 assert result[0.01].errors <= 10 assert result[0.02].errors <= 30 assert result[0.03].errors <= 70 assert 1 <= result[0.04].errors <= 100 def test_collect(): results = sinter.collect( num_workers=2, tasks=[ sinter.Task( circuit=stim.Circuit.generated( 'repetition_code:memory', rounds=3, distance=3, after_clifford_depolarization=p), decoder='pymatching', json_metadata={'p': p}, collection_options=sinter.CollectionOptions( max_shots=1000, max_errors=100, start_batch_size=100, max_batch_size=1000, ), ) for p in [0.01, 0.02, 0.03, 0.04] ] ) probabilities = [e.json_metadata['p'] for e in results] assert len(probabilities) == len(set(probabilities)) d = {e.json_metadata['p']: e for e in results} assert len(d) == 4 for k, v in d.items(): assert v.shots >= 1000 or v.errors >= 100 assert v.discards == 0 assert d[0.01].errors <= 10 assert d[0.02].errors <= 30 assert d[0.03].errors <= 70 assert 1 <= d[0.04].errors <= 100 def test_collect_from_paths(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) tasks = [] for p in [0.01, 0.02, 0.03, 0.04]: path = d / f'tmp{p}.stim' stim.Circuit.generated( 'repetition_code:memory', rounds=3, distance=3, after_clifford_depolarization=p, ).to_file(path) tasks.append(sinter.Task( circuit_path=path, decoder='pymatching', json_metadata={'p': p}, collection_options=sinter.CollectionOptions( max_shots=1000, max_errors=100, start_batch_size=100, max_batch_size=1000, ), )) results = sinter.collect( num_workers=2, tasks=tasks ) probabilities = [e.json_metadata['p'] for e in results] assert len(probabilities) == len(set(probabilities)) d = {e.json_metadata['p']: e for e in results} assert len(d) == 4 for k, v in d.items(): assert v.shots >= 1000 or v.errors >= 100 assert v.discards == 0 assert d[0.01].errors <= 10 assert d[0.02].errors <= 30 assert d[0.03].errors <= 70 assert 1 <= d[0.04].errors <= 100 class AlternatingPredictionsDecoder(sinter.Decoder): def decode_via_files(self, *, num_shots: int, num_dets: int, num_obs: int, dem_path: pathlib.Path, dets_b8_in_path: pathlib.Path, obs_predictions_b8_out_path: pathlib.Path, tmp_dir: pathlib.Path, ) -> None: bytes_per_shot = (num_obs + 7) // 8 with open(obs_predictions_b8_out_path, 'wb') as f: for k in range(num_shots): f.write((k % 3 == 0).to_bytes(length=bytes_per_shot, byteorder='little')) def test_collect_custom_decoder(): results = sinter.collect( num_workers=2, tasks=[ sinter.Task( circuit=stim.Circuit(""" M(0.1) 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """), json_metadata=None, ) ], max_shots=10000, decoders=['alternate'], custom_decoders={'alternate': AlternatingPredictionsDecoder()}, ) assert len(results) == 1 assert results[0].shots == 10000 assert 2500 < results[0].errors < 4000 def test_iter_collect_list(): result = collections.defaultdict(sinter.AnonTaskStats) for sample in sinter.iter_collect( num_workers=2, tasks=[ sinter.Task( circuit=stim.Circuit.generated( 'repetition_code:memory', rounds=3, distance=3, after_clifford_depolarization=p), decoder='pymatching', json_metadata={'p': p}, collection_options=sinter.CollectionOptions( max_errors=100, max_shots=1000, start_batch_size=100, max_batch_size=1000, ), ) for p in [0.01, 0.02, 0.03, 0.04] ], ): for stats in sample.new_stats: result[stats.json_metadata['p']] += stats.to_anon_stats() assert len(result) == 4 for k, v in result.items(): assert v.shots >= 1000 or v.errors >= 100 assert v.discards == 0 assert result[0.01].errors <= 10 assert result[0.02].errors <= 30 assert result[0.03].errors <= 70 assert 1 <= result[0.04].errors <= 100 def test_iter_collect_worker_fails(): with pytest.raises(RuntimeError, match="Worker failed"): _ = list(sinter.iter_collect( decoders=['NOT A VALID DECODER'], num_workers=1, tasks=iter([ sinter.Task( circuit=stim.Circuit.generated('repetition_code:memory', rounds=3, distance=3), collection_options=sinter.CollectionOptions( max_errors=1, max_shots=1, ), ), ]), )) class FixedSizeSampler(sinter.Sampler, sinter.CompiledSampler): def compiled_sampler_for_task(self, task: sinter.Task) -> sinter.CompiledSampler: return self def sample(self, suggested_shots: int) -> 'sinter.AnonTaskStats': return sinter.AnonTaskStats( shots=1024, errors=5, ) def test_fixed_size_sampler(): results = sinter.collect( num_workers=2, tasks=[ sinter.Task( circuit=stim.Circuit(), decoder='fixed_size_sampler', json_metadata={}, collection_options=sinter.CollectionOptions( max_shots=100_000, max_errors=1_000, ), ) ], custom_decoders={'fixed_size_sampler': FixedSizeSampler()} ) assert 100_000 <= results[0].shots <= 100_000 + 3000 class MockTimingSampler(sinter.Sampler, sinter.CompiledSampler): def compiled_sampler_for_task(self, task: sinter.Task) -> sinter.CompiledSampler: return self def sample(self, suggested_shots: int) -> 'sinter.AnonTaskStats': actual_shots = -(-suggested_shots // 1024) * 1024 time.sleep(actual_shots * 0.00001) return sinter.AnonTaskStats( shots=actual_shots, errors=5, seconds=actual_shots * 0.00001, ) def test_mock_timing_sampler(): results = sinter.collect( num_workers=12, tasks=[ sinter.Task( circuit=stim.Circuit(), decoder='MockTimingSampler', json_metadata={}, ) ], max_shots=1_000_000, max_errors=10_000, custom_decoders={'MockTimingSampler': MockTimingSampler()}, ) assert 1_000_000 <= results[0].shots <= 1_000_000 + 12000 class BatchSizeTrackingSampler(sinter.Sampler, sinter.CompiledSampler): """A sampler that tracks the suggested batch size requests it receives.""" def __init__(self, batch_sizes: list[int]): self.batch_sizes = batch_sizes def compiled_sampler_for_task(self, task: sinter.Task) -> sinter.CompiledSampler: return self def sample(self, suggested_shots: int) -> sinter.AnonTaskStats: self.batch_sizes.append(suggested_shots) return sinter.AnonTaskStats( shots=suggested_shots, errors=1, seconds=0.001, ) def test_ramp_throttled_sampler_respects_max_batch_size(): """Test that the CollectionManager instantiated RampThrottledSampler respects the `max_batch_size` parameter.""" # since the RampThrottledSampler and batch sizing happens in the worker process, we need a # shared list to track what goes on with the sampler with multiprocessing.Manager() as manager: tracking_sampler = BatchSizeTrackingSampler(manager.list()) sinter.collect( num_workers=1, tasks=[ sinter.Task( circuit=stim.Circuit(), decoder='tracking_sampler', json_metadata={'test': 'small_batch'}, ) ], max_shots=10_000, max_batch_size=128, # Set a small max batch size custom_decoders={'tracking_sampler': tracking_sampler}, ) # batch size should start at one and then maximum seen should be at most 128 assert tracking_sampler.batch_sizes[0] == 1 assert 1 < max(tracking_sampler.batch_sizes) <= 128 ================================================ FILE: glue/sample/src/sinter/_collection/_collection_worker_loop.py ================================================ import os from typing import Optional, TYPE_CHECKING from sinter._decoding import Sampler from sinter._collection._collection_worker_state import CollectionWorkerState if TYPE_CHECKING: import multiprocessing def collection_worker_loop( flush_period: float, worker_id: int, sampler: Sampler, inp: 'multiprocessing.Queue', out: 'multiprocessing.Queue', core_affinity: Optional[int], custom_error_count_key: Optional[str], ) -> None: try: if core_affinity is not None and hasattr(os, 'sched_setaffinity'): os.sched_setaffinity(0, {core_affinity}) except: # If setting the core affinity fails, we keep going regardless. pass worker = CollectionWorkerState( flush_period=flush_period, worker_id=worker_id, sampler=sampler, inp=inp, out=out, custom_error_count_key=custom_error_count_key, ) worker.run_message_loop() ================================================ FILE: glue/sample/src/sinter/_collection/_collection_worker_state.py ================================================ import queue import time from typing import Any from typing import Optional from typing import TYPE_CHECKING import stim from sinter._data import AnonTaskStats from sinter._data import CollectionOptions from sinter._data import Task from sinter._decoding import CompiledSampler from sinter._decoding import Sampler if TYPE_CHECKING: import multiprocessing def _fill_in_task(task: Task) -> Task: changed = False circuit = task.circuit if circuit is None: circuit = stim.Circuit.from_file(task.circuit_path) changed = True dem = task.detector_error_model if dem is None: try: dem = circuit.detector_error_model(decompose_errors=True, approximate_disjoint_errors=True) except ValueError: try: dem = circuit.detector_error_model(approximate_disjoint_errors=True) except ValueError: dem = circuit.detector_error_model(approximate_disjoint_errors=True, flatten_loops=True) changed = True if not changed: return task return Task( circuit=circuit, decoder=task.decoder, detector_error_model=dem, postselection_mask=task.postselection_mask, postselected_observables_mask=task.postselected_observables_mask, json_metadata=task.json_metadata, collection_options=task.collection_options, ) class CollectionWorkerState: def __init__( self, *, flush_period: float, worker_id: int, inp: 'multiprocessing.Queue', out: 'multiprocessing.Queue', sampler: Sampler, custom_error_count_key: Optional[str], ): assert isinstance(flush_period, (int, float)) assert isinstance(sampler, Sampler) self.max_flush_period = flush_period self.cur_flush_period = 0.01 self.inp = inp self.out = out self.sampler = sampler self.compiled_sampler: CompiledSampler | None = None self.worker_id = worker_id self.current_task: Task | None = None self.current_error_cutoff: int | None = None self.custom_error_count_key = custom_error_count_key self.current_task_shots_left: int = 0 self.unflushed_results: AnonTaskStats = AnonTaskStats() self.last_flush_message_time = time.monotonic() self.soft_error_flush_threshold: int = 1 def _send_message_to_manager(self, message: Any): self.out.put(message) def state_summary(self) -> str: lines = [ f'Worker(id={self.worker_id}) [', f' max_flush_period={self.max_flush_period}', f' cur_flush_period={self.cur_flush_period}', f' sampler={self.sampler}', f' compiled_sampler={self.compiled_sampler}', f' current_task={self.current_task}', f' current_error_cutoff={self.current_error_cutoff}', f' custom_error_count_key={self.custom_error_count_key}', f' current_task_shots_left={self.current_task_shots_left}', f' unflushed_results={self.unflushed_results}', f' last_flush_message_time={self.last_flush_message_time}', f' soft_error_flush_threshold={self.soft_error_flush_threshold}', f']', ] return '\n' + '\n'.join(lines) + '\n' def flush_results(self): if self.unflushed_results.shots > 0: self.last_flush_message_time = time.monotonic() self.cur_flush_period = min(self.cur_flush_period * 1.4, self.max_flush_period) self._send_message_to_manager(( 'flushed_results', self.worker_id, (self.current_task.strong_id(), self.unflushed_results), )) self.unflushed_results = AnonTaskStats() return True return False def accept_shots(self, *, shots_delta: int): assert shots_delta >= 0 self.current_task_shots_left += shots_delta self._send_message_to_manager(( 'accepted_shots', self.worker_id, (self.current_task.strong_id(), shots_delta), )) def return_shots(self, *, requested_shots: int): assert requested_shots >= 0 returned_shots = max(0, min(requested_shots, self.current_task_shots_left)) self.current_task_shots_left -= returned_shots if self.current_task_shots_left <= 0: self.flush_results() self._send_message_to_manager(( 'returned_shots', self.worker_id, (self.current_task.strong_id(), returned_shots), )) def compute_strong_id(self, *, new_task: Task): strong_id = _fill_in_task(new_task).strong_id() self._send_message_to_manager(( 'computed_strong_id', self.worker_id, strong_id, )) def change_job(self, *, new_task: Task, new_collection_options: CollectionOptions): self.flush_results() self.current_task = _fill_in_task(new_task) self.current_error_cutoff = new_collection_options.max_errors self.compiled_sampler = self.sampler.compiled_sampler_for_task(self.current_task) assert self.current_task.strong_id() is not None self.current_task_shots_left = 0 self.last_flush_message_time = time.monotonic() self._send_message_to_manager(( 'changed_job', self.worker_id, (self.current_task.strong_id(),), )) def process_messages(self) -> int: num_processed = 0 while True: try: message = self.inp.get_nowait() except queue.Empty: return num_processed num_processed += 1 message_type, message_body = message if message_type == 'stop': return -1 elif message_type == 'flush_results': self.flush_results() elif message_type == 'compute_strong_id': assert isinstance(message_body, Task) self.compute_strong_id(new_task=message_body) elif message_type == 'change_job': new_task, new_collection_options, soft_error_flush_threshold = message_body self.cur_flush_period = 0.01 self.soft_error_flush_threshold = soft_error_flush_threshold assert isinstance(new_task, Task) self.change_job(new_task=new_task, new_collection_options=new_collection_options) elif message_type == 'set_soft_error_flush_threshold': soft_error_flush_threshold = message_body self.soft_error_flush_threshold = soft_error_flush_threshold elif message_type == 'accept_shots': job_key, shots_delta = message_body assert isinstance(shots_delta, int) assert job_key == self.current_task.strong_id() self.accept_shots(shots_delta=shots_delta) elif message_type == 'return_shots': job_key, requested_shots = message_body assert isinstance(requested_shots, int) assert job_key == self.current_task.strong_id() self.return_shots(requested_shots=requested_shots) else: raise NotImplementedError(f'{message_type=}') def num_unflushed_errors(self) -> int: if self.custom_error_count_key is not None: return self.unflushed_results.custom_counts[self.custom_error_count_key] return self.unflushed_results.errors def do_some_work(self) -> bool: did_some_work = False # Sample some stats. if self.current_task_shots_left > 0: # Don't keep sampling if we've exceeded the number of errors needed. if self.current_error_cutoff is not None and self.current_error_cutoff <= 0: return self.flush_results() some_work_done = self.compiled_sampler.sample(self.current_task_shots_left) if some_work_done.shots < 1: raise ValueError(f"Sampler didn't do any work. It returned statistics with shots == 0: {some_work_done}.") assert isinstance(some_work_done, AnonTaskStats) self.current_task_shots_left -= some_work_done.shots if self.current_error_cutoff is not None: errors_done = some_work_done.custom_counts[self.custom_error_count_key] if self.custom_error_count_key is not None else some_work_done.errors self.current_error_cutoff -= errors_done self.unflushed_results += some_work_done did_some_work = True # Report them periodically. should_flush = False if self.num_unflushed_errors() >= self.soft_error_flush_threshold: should_flush = True if self.unflushed_results.shots > 0: if self.current_task_shots_left <= 0 or self.last_flush_message_time + self.cur_flush_period < time.monotonic(): should_flush = True if should_flush: did_some_work |= self.flush_results() return did_some_work def run_message_loop(self): try: while True: num_messages_processed = self.process_messages() if num_messages_processed == -1: break did_some_work = self.do_some_work() if not did_some_work and num_messages_processed == 0: time.sleep(0.01) except KeyboardInterrupt: pass except BaseException as ex: import traceback self._send_message_to_manager(( 'stopped_due_to_exception', self.worker_id, (None if self.current_task is None else self.current_task.strong_id(), self.current_task_shots_left, self.unflushed_results, traceback.format_exc(), ex), )) ================================================ FILE: glue/sample/src/sinter/_collection/_collection_worker_test.py ================================================ import collections import multiprocessing import time from typing import Any, List import sinter import stim from sinter._collection._collection_worker_state import CollectionWorkerState class MockWorkHandler(sinter.Sampler, sinter.CompiledSampler): def __init__(self): self.expected_task = None self.expected = collections.deque() def compiled_sampler_for_task(self, task: sinter.Task) -> sinter.CompiledSampler: assert task == self.expected_task return self def handles_throttling(self) -> bool: return True def sample(self, shots: int) -> sinter.AnonTaskStats: assert self.expected expected_shots, response = self.expected.popleft() assert shots == expected_shots return response def _assert_drain_queue(q: multiprocessing.Queue, expected_contents: List[Any]): for v in expected_contents: assert q.get(timeout=0.1) == v assert q.empty() def _put_wait_not_empty(q: multiprocessing.Queue, item: Any): q.put(item) while q.empty(): time.sleep(0.0001) def test_worker_stop(): handler = MockWorkHandler() inp = multiprocessing.Queue() out = multiprocessing.Queue() inp.cancel_join_thread() out.cancel_join_thread() worker = CollectionWorkerState( flush_period=-1, worker_id=5, sampler=handler, inp=inp, out=out, custom_error_count_key=None, ) assert worker.process_messages() == 0 _assert_drain_queue(out, []) t0 = sinter.Task( circuit=stim.Circuit('H 0'), detector_error_model=stim.DetectorErrorModel(), decoder='mock', collection_options=sinter.CollectionOptions(max_shots=100_000_000), json_metadata={'a': 3}, ) handler.expected_task = t0 _put_wait_not_empty(inp, ('change_job', (t0, sinter.CollectionOptions(max_errors=100_000_000), 100_000_000))) assert worker.process_messages() == 1 _assert_drain_queue(out, [('changed_job', 5, (t0.strong_id(),))]) _put_wait_not_empty(inp, ('stop', None)) assert worker.process_messages() == -1 def test_worker_skip_work(): handler = MockWorkHandler() inp = multiprocessing.Queue() out = multiprocessing.Queue() inp.cancel_join_thread() out.cancel_join_thread() worker = CollectionWorkerState( flush_period=-1, worker_id=5, sampler=handler, inp=inp, out=out, custom_error_count_key=None, ) assert worker.process_messages() == 0 _assert_drain_queue(out, []) t0 = sinter.Task( circuit=stim.Circuit('H 0'), detector_error_model=stim.DetectorErrorModel(), decoder='mock', collection_options=sinter.CollectionOptions(max_shots=100_000_000), json_metadata={'a': 3}, ) handler.expected_task = t0 _put_wait_not_empty(inp, ('change_job', (t0, sinter.CollectionOptions(max_errors=100_000_000), 100_000_000))) assert worker.process_messages() == 1 _assert_drain_queue(out, [('changed_job', 5, (t0.strong_id(),))]) _put_wait_not_empty(inp, ('accept_shots', (t0.strong_id(), 10000))) assert worker.process_messages() == 1 _assert_drain_queue(out, [('accepted_shots', 5, (t0.strong_id(), 10000))]) assert worker.current_task == t0 assert worker.current_task_shots_left == 10000 assert worker.process_messages() == 0 _assert_drain_queue(out, []) _put_wait_not_empty(inp, ('return_shots', (t0.strong_id(), 2000))) assert worker.process_messages() == 1 _assert_drain_queue(out, [ ('returned_shots', 5, (t0.strong_id(), 2000)), ]) _put_wait_not_empty(inp, ('return_shots', (t0.strong_id(), 20000000))) assert worker.process_messages() == 1 _assert_drain_queue(out, [ ('returned_shots', 5, (t0.strong_id(), 8000)), ]) assert not worker.do_some_work() def test_worker_finish_work(): handler = MockWorkHandler() inp = multiprocessing.Queue() out = multiprocessing.Queue() inp.cancel_join_thread() out.cancel_join_thread() worker = CollectionWorkerState( flush_period=-1, worker_id=5, sampler=handler, inp=inp, out=out, custom_error_count_key=None, ) assert worker.process_messages() == 0 _assert_drain_queue(out, []) ta = sinter.Task( circuit=stim.Circuit('H 0'), detector_error_model=stim.DetectorErrorModel(), decoder='mock', collection_options=sinter.CollectionOptions(max_shots=100_000_000), json_metadata={'a': 3}, ) handler.expected_task = ta _put_wait_not_empty(inp, ('change_job', (ta, sinter.CollectionOptions(max_errors=100_000_000), 100_000_000))) _put_wait_not_empty(inp, ('accept_shots', (ta.strong_id(), 10000))) t0 = time.monotonic() num_processed = 0 while True: num_processed += worker.process_messages() if num_processed >= 2: break if time.monotonic() - t0 > 1: raise ValueError("Messages not processed") assert num_processed == 2 _assert_drain_queue(out, [ ('changed_job', 5, (ta.strong_id(),)), ('accepted_shots', 5, (ta.strong_id(), 10000)), ]) assert worker.current_task == ta assert worker.current_task_shots_left == 10000 assert worker.process_messages() == 0 _assert_drain_queue(out, []) handler.expected.append(( 10000, sinter.AnonTaskStats( shots=1000, errors=23, discards=0, seconds=1, ), )) assert worker.do_some_work() worker.flush_results() _assert_drain_queue(out, [ ('flushed_results', 5, (ta.strong_id(), sinter.AnonTaskStats(shots=1000, errors=23, discards=0, seconds=1)))]) handler.expected.append(( 9000, sinter.AnonTaskStats( shots=9000, errors=13, discards=0, seconds=1, ), )) assert worker.do_some_work() worker.flush_results() _assert_drain_queue(out, [ ('flushed_results', 5, (ta.strong_id(), sinter.AnonTaskStats( shots=9000, errors=13, discards=0, seconds=1, ))), ]) assert not worker.do_some_work() worker.flush_results() _assert_drain_queue(out, []) ================================================ FILE: glue/sample/src/sinter/_collection/_mux_sampler.py ================================================ import pathlib from typing import Optional from typing import Union from sinter._data import Task from sinter._decoding._decoding_all_built_in_decoders import BUILT_IN_SAMPLERS from sinter._decoding._decoding_decoder_class import Decoder from sinter._decoding._sampler import CompiledSampler from sinter._decoding._sampler import Sampler from sinter._decoding._stim_then_decode_sampler import StimThenDecodeSampler class MuxSampler(Sampler): """Looks up the sampler to use for a task, by the task's decoder name.""" def __init__( self, *, custom_decoders: Union[dict[str, Union[Decoder, Sampler]], None], count_observable_error_combos: bool, count_detection_events: bool, tmp_dir: Optional[pathlib.Path], ): self.custom_decoders = custom_decoders self.count_observable_error_combos = count_observable_error_combos self.count_detection_events = count_detection_events self.tmp_dir = tmp_dir def compiled_sampler_for_task(self, task: Task) -> CompiledSampler: return self._resolve_sampler(task.decoder).compiled_sampler_for_task(task) def _resolve_sampler(self, name: str) -> Sampler: sub_sampler: Union[Decoder, Sampler] if name in self.custom_decoders: sub_sampler = self.custom_decoders[name] elif name in BUILT_IN_SAMPLERS: sub_sampler = BUILT_IN_SAMPLERS[name] else: raise NotImplementedError(f'Not a recognized decoder or sampler: {name=}. Did you forget to specify custom_decoders?') if isinstance(sub_sampler, Sampler): if self.count_detection_events: raise NotImplementedError("'count_detection_events' not supported when using a custom Sampler (instead of a custom Decoder).") if self.count_observable_error_combos: raise NotImplementedError("'count_observable_error_combos' not supported when using a custom Sampler (instead of a custom Decoder).") return sub_sampler elif isinstance(sub_sampler, Decoder) or hasattr(sub_sampler, 'compile_decoder_for_dem'): return StimThenDecodeSampler( decoder=sub_sampler, count_detection_events=self.count_detection_events, count_observable_error_combos=self.count_observable_error_combos, tmp_dir=self.tmp_dir, ) else: raise NotImplementedError(f"Don't know how to turn this into a Sampler: {sub_sampler!r}") ================================================ FILE: glue/sample/src/sinter/_collection/_printer.py ================================================ import threading from typing import List, Any import sys import time class ThrottledProgressPrinter: """Handles printing progress updates interspersed amongst output. Throttles the progress updates to not flood the screen when 100 show up at the same time, and instead only show the latest one. """ def __init__(self, *, outs: List[Any], print_progress: bool, min_progress_delay: float): self.outs = outs self.print_progress = print_progress self.next_can_print_time = time.monotonic() self.latest_msg = '' self.latest_printed_msg = '' self.min_progress_delay = min_progress_delay self.is_worker_running = False self.lock = threading.Lock() def print_out(self, msg: str) -> None: with self.lock: for out in self.outs: print(msg, file=out, flush=True) def show_latest_progress(self, msg: str) -> None: if not self.print_progress: return with self.lock: if msg == self.latest_msg: return self.latest_msg = msg if not self.is_worker_running: dt = self._try_print_else_delay() if dt > 0: self.is_worker_running = True threading.Thread(target=self._print_worker).start() def flush(self): with self.lock: if self.latest_msg != "" and self.latest_printed_msg != self.latest_msg: print('\033[31m' + self.latest_msg + '\033[0m', file=sys.stderr, flush=True) self.latest_printed_msg = self.latest_msg def _try_print_else_delay(self) -> float: t = time.monotonic() dt = self.next_can_print_time - t if dt <= 0: self.next_can_print_time = t + self.min_progress_delay self.is_worker_running = False if self.latest_msg != "" and self.latest_msg != self.latest_printed_msg: print('\033[31m' + self.latest_msg + '\033[0m', file=sys.stderr, flush=True) self.latest_printed_msg = self.latest_msg return max(dt, 0) def _print_worker(self): while True: with self.lock: dt = self._try_print_else_delay() if dt == 0: break time.sleep(dt) ================================================ FILE: glue/sample/src/sinter/_collection/_sampler_ramp_throttled.py ================================================ import time from sinter._decoding import Sampler, CompiledSampler from sinter._data import Task, AnonTaskStats class RampThrottledSampler(Sampler): """Wraps a sampler to adjust requested shots to hit a target time. This sampler will initially only take 1 shot per call. If the time taken significantly undershoots the target time, the maximum number of shots per call is increased by a constant factor. If it exceeds the target time, the maximum is reduced by a constant factor. The result is that the sampler "ramps up" how many shots it does per call until it takes roughly the target time, and then dynamically adapts to stay near it. """ def __init__(self, sub_sampler: Sampler, target_batch_seconds: float, max_batch_shots: int): self.sub_sampler = sub_sampler self.target_batch_seconds = target_batch_seconds self.max_batch_shots = max_batch_shots def __str__(self) -> str: return f'CompiledRampThrottledSampler({self.sub_sampler})' def compiled_sampler_for_task(self, task: Task) -> CompiledSampler: compiled_sub_sampler = self.sub_sampler.compiled_sampler_for_task(task) if compiled_sub_sampler.handles_throttling(): return compiled_sub_sampler return CompiledRampThrottledSampler( sub_sampler=compiled_sub_sampler, target_batch_seconds=self.target_batch_seconds, max_batch_shots=self.max_batch_shots, ) class CompiledRampThrottledSampler(CompiledSampler): def __init__(self, sub_sampler: CompiledSampler, target_batch_seconds: float, max_batch_shots: int): self.sub_sampler = sub_sampler self.target_batch_seconds = target_batch_seconds self.batch_shots = 1 self.max_batch_shots = max_batch_shots def __str__(self) -> str: return f'CompiledRampThrottledSampler({self.sub_sampler})' def sample(self, max_shots: int) -> AnonTaskStats: t0 = time.monotonic() actual_shots = min(max_shots, self.batch_shots) result = self.sub_sampler.sample(actual_shots) dt = time.monotonic() - t0 # Rebalance number of shots. if self.batch_shots > 1 and dt > self.target_batch_seconds * 1.3: self.batch_shots //= 2 if result.shots * 2 >= actual_shots: for _ in range(4): if self.batch_shots * 2 > self.max_batch_shots: break if dt > self.target_batch_seconds * 0.3: break self.batch_shots *= 2 dt *= 2 return result ================================================ FILE: glue/sample/src/sinter/_collection/_sampler_ramp_throttled_test.py ================================================ from unittest import mock import pytest import sinter import stim from sinter._collection._sampler_ramp_throttled import ( CompiledRampThrottledSampler, RampThrottledSampler, ) from sinter._data import AnonTaskStats, Task class MockSampler(sinter.Sampler, sinter.CompiledSampler): """Mock sampler that tracks `suggested_shots` parameter in `sample` calls.""" def __init__(self): self.calls = [] def compiled_sampler_for_task(self, task: Task) -> sinter.CompiledSampler: return self def sample(self, suggested_shots: int) -> AnonTaskStats: self.calls.append(suggested_shots) return AnonTaskStats( shots=suggested_shots, errors=1, seconds=0.001 * suggested_shots, # Simulate time proportional to shots ) @pytest.fixture def mock_sampler(): return MockSampler() def test_initial_batch_size(mock_sampler): """Test that the sampler starts with a batch size of 1.""" sampler = CompiledRampThrottledSampler( sub_sampler=mock_sampler, target_batch_seconds=1.0, max_batch_shots=1024, ) # First call should use batch_size=1 sampler.sample(100) assert mock_sampler.calls[0] == 1 def test_batch_size_ramps_up(mock_sampler): """Test that the batch size increases when execution is fast.""" sampler = CompiledRampThrottledSampler( sub_sampler=mock_sampler, target_batch_seconds=1.0, max_batch_shots=1024, ) # Mock time.monotonic to simulate fast execution # two calls per sample for tic/toc with mock.patch( "time.monotonic", side_effect=[0.0, 0.001, 0.02, 0.021, 0.03, 0.031] ): sampler.sample(100) # First call, batch_size=1 sampler.sample(100) # Should double 4 times to 16 sampler.sample(100) # Should double 4 times again but hit limit of 100 assert mock_sampler.calls == [1, 16, 100] def test_batch_size_decreases(mock_sampler): """Test that the batch size decreases when execution is slow.""" sampler = CompiledRampThrottledSampler( sub_sampler=mock_sampler, target_batch_seconds=0.1, max_batch_shots=1024, ) # Set initial batch size higher for this test sampler.batch_shots = 64 # Mock time.monotonic to simulate slow execution (>1.3x target) with mock.patch("time.monotonic", side_effect=[0.0, 0.15, 0.5, 0.65]): sampler.sample(100) # First call, batch_size=64 sampler.sample(100) # Should halve to 32 assert mock_sampler.calls == [64, 32] def test_respects_max_batch_shots(mock_sampler): """Test that the batch size never exceeds max_batch_shots.""" sampler = CompiledRampThrottledSampler( sub_sampler=mock_sampler, target_batch_seconds=1.0, max_batch_shots=16, # Small max for testing ) # Set initial batch size close to max sampler.batch_shots = 8 # Mock time.monotonic to simulate very fast execution # two calls per sample for tic/toc with mock.patch( "time.monotonic", side_effect=[0.0, 0.001, 0.02, 0.021, 0.03, 0.031] ): sampler.sample(100) # First call, batch_size=8 sampler.sample(100) # Should double to 16 sampler.sample(100) # Should stay at 16 (max) assert mock_sampler.calls == [8, 16, 16] def test_respects_max_shots_parameter(mock_sampler): """Test that the sampler respects the max_shots parameter.""" sampler = CompiledRampThrottledSampler( sub_sampler=mock_sampler, target_batch_seconds=1.0, max_batch_shots=1024, ) # Set batch size higher than max_shots sampler.batch_shots = 100 # Call with max_shots=10 sampler.sample(10) # Should only request 10 shots, not 100 assert mock_sampler.calls[0] == 10 def test_sub_sampler_parameter_pass_through(mock_sampler): """Test that parameters are passed through to compiled sub sampler.""" factory = RampThrottledSampler( sub_sampler=mock_sampler, target_batch_seconds=0.5, max_batch_shots=512, ) task = Task(circuit=stim.Circuit(), decoder="test") compiled = factory.compiled_sampler_for_task(task) assert isinstance(compiled, CompiledRampThrottledSampler) assert compiled.target_batch_seconds == 0.5 assert compiled.max_batch_shots == 512 assert compiled.batch_shots == 1 # Initial batch size ================================================ FILE: glue/sample/src/sinter/_command/__init__.py ================================================ ================================================ FILE: glue/sample/src/sinter/_command/_main.py ================================================ import sys from typing import Optional, List def main(*, command_line_args: Optional[List[str]] = None): if command_line_args is None: command_line_args = sys.argv[1:] mode = command_line_args[0] if command_line_args else None if mode == 'combine': from sinter._command._main_combine import main_combine return main_combine(command_line_args=command_line_args[1:]) if mode == 'collect': from sinter._command._main_collect import main_collect return main_collect(command_line_args=command_line_args[1:]) if mode == 'plot': from sinter._command._main_plot import main_plot return main_plot(command_line_args=command_line_args[1:]) if mode == 'predict': from sinter._command._main_predict import main_predict return main_predict(command_line_args=command_line_args[1:]) want_help = mode in ['help', 'h', '--help', '-help', '-h', '--h'] if not want_help: if command_line_args and not command_line_args[0].startswith('-'): print(f"\033[31mUnrecognized command: sinter {command_line_args[0]}\033[0m\n", file=sys.stderr) else: print(f"\033[31mDidn't specify a command.\033[0m\n", file=sys.stderr) print(f"Available commands are:\n" f" sinter collect\n" f" sinter combine\n" f" sinter plot" f"", file=sys.stderr) if not want_help: sys.exit(1) if __name__ == '__main__': main() ================================================ FILE: glue/sample/src/sinter/_command/_main_collect.py ================================================ import argparse import math import os import sys from typing import Iterator, Any, Tuple, List, Callable, Optional from typing import cast import numpy as np import stim from sinter._collection import ThrottledProgressPrinter from sinter._data import Task from sinter._collection import collect, Progress, post_selection_mask_from_predicate from sinter._command._main_combine import ExistingData, CSV_HEADER from sinter._decoding._decoding_all_built_in_decoders import BUILT_IN_SAMPLERS def iter_file_paths_into_goals(circuit_paths: Iterator[str], metadata_func: Callable, postselected_detectors_predicate: Optional[Callable[[int, Any, Tuple[float, ...]], bool]], postselected_observables_predicate: Callable[[int, Any], bool], ) -> Iterator[Task]: for path in circuit_paths: with open(path) as f: circuit_text = f.read() circuit = stim.Circuit(circuit_text) metadata = metadata_func(path=path, circuit=circuit) if postselected_detectors_predicate is not None: post_mask = post_selection_mask_from_predicate(circuit, metadata=metadata, postselected_detectors_predicate=postselected_detectors_predicate) if not np.any(post_mask): post_mask = None else: post_mask = None postselected_observables = [ k for k in range(circuit.num_observables) if postselected_observables_predicate(k, metadata) ] if any(postselected_observables): postselected_observables_mask = np.zeros(shape=math.ceil(circuit.num_observables / 8), dtype=np.uint8) for k in postselected_observables: postselected_observables_mask[k // 8] |= 1 << (k % 8) else: postselected_observables_mask = None yield Task( circuit=circuit, postselection_mask=post_mask, postselected_observables_mask=postselected_observables_mask, json_metadata=metadata, ) def parse_args(args: List[str]) -> Any: parser = argparse.ArgumentParser(description='Collect Monte Carlo samples.', prog='sinter collect') parser.add_argument('--circuits', nargs='+', required=True, help='Circuit files to sample from and decode.\n' 'This parameter can be given multiple arguments.') parser.add_argument('--decoders', type=str, nargs='+', required=True, help='The decoder to use to predict observables from detection events.') parser.add_argument('--custom_decoders_module_function', default=None, nargs='+', help='Use the syntax "module:function" to "import function from module" ' 'and use the result of "function()" as the custom_decoders ' 'dictionary. The dictionary must map strings to stim.Decoder ' 'instances.') parser.add_argument('--max_shots', type=int, default=None, help='Sampling of a circuit will stop if this many shots have been taken.') parser.add_argument('--max_errors', type=int, default=None, help='Sampling of a circuit will stop if this many errors have been seen.') parser.add_argument('--processes', default='auto', type=str, help='Number of processes to use for simultaneous sampling and decoding. ' 'Must be either a number or "auto" which sets it to the number of ' 'CPUs on the machine.') parser.add_argument('--save_resume_filepath', type=str, default=None, help='Activates MERGE mode.\n' "If save_resume_filepath doesn't exist, initializes it with a CSV header.\n" 'CSV data already at save_resume_filepath counts towards max_shots and max_errors.\n' 'Collected data is appended to save_resume_filepath.\n' 'Note that MERGE mode is tolerant to failures: if the process is killed, it can simply be restarted and it will pick up where it left off.\n' 'Note that MERGE mode is idempotent: if sufficient data has been collected, no additional work is done when run again.') parser.add_argument('--start_batch_size', type=int, default=100, help='Initial number of samples to batch together into one job.\n' 'Starting small prevents over-sampling of circuits above threshold.\n' 'The allowed batch size increases exponentially from this starting point.') parser.add_argument('--max_batch_size', type=int, default=None, help='Maximum number of samples to batch together into one job.\n' 'Bigger values increase the delay between jobs finishing.\n' 'Smaller values decrease the amount of aggregation of results, increasing the amount of output information.') parser.add_argument('--max_batch_seconds', type=int, default=None, help='Limits number of shots in a batch so that the estimated runtime of the batch is below this amount.') parser.add_argument('--postselect_detectors_with_non_zero_4th_coord', help='Turns on detector postselection. ' 'If any detector with a non-zero 4th coordinate fires, the shot is discarded.', action='store_true') parser.add_argument('--postselected_detectors_predicate', type=str, default='''False''', help='Specifies a predicate used to decide which detectors to postselect. ' 'When a postselected detector produces a detection event, the shot is discarded instead of being given to the decoder.' 'The number of discarded shots is tracked as a statistic.' 'Available values:\n' ' index: The unique number identifying the detector, determined by the order of detectors in the circuit file.\n' ' coords: The coordinate data associated with the detector. An empty tuple, if the circuit file did not specify detector coordinates.\n' ' metadata: The metadata associated with the task being sampled.\n' 'Expected expression type:\n' ' Something that can be given to `bool` to get False (do not postselect) or True (yes postselect).\n' 'Examples:\n' ''' --postselected_detectors_predicate "coords[2] == 0"\n''' ''' --postselected_detectors_predicate "coords[3] < metadata['postselection_level']"\n''') parser.add_argument('--postselected_observables_predicate', type=str, default='''False''', help='Specifies a predicate used to decide which observables to postselect. ' 'When a decoder mispredicts a postselected observable, the shot is discarded instead of counting as an error.' 'Available values:\n' ' index: The index of the observable to postselect or not.\n' ' metadata: The metadata associated with the task.\n' 'Expected expression type:\n' ' Something that can be given to `bool` to get False (do not postselect) or True (yes postselect).\n' 'Examples:\n' ''' --postselected_observables_predicate "False"\n''' ''' --postselected_observables_predicate "metadata['d'] == 5 and index >= 2"\n''') parser.add_argument('--count_observable_error_combos', help='When set, the returned stats will include custom ' 'counts like `obs_mistake_mask=E_E__` counting ' 'how many times the decoder made each pattern of ' 'observable mistakes.', action='store_true') parser.add_argument('--count_detection_events', help='When set, the returned stats will include custom ' 'counts `detectors_checked` and ' '`detection_events`. The detection fraction is ' 'the ratio of these two numbers.', action='store_true') parser.add_argument('--quiet', help='Disables writing progress to stderr.', action='store_true') parser.add_argument('--custom_error_count_key', type=str, help='Makes --max_errors apply to `stat.custom_counts[key]` ' 'instead of to `stat.errors`.', default=None) parser.add_argument('--allowed_cpu_affinity_ids', type=str, nargs='+', help='Controls which CPUs workers can be pinned to. By default, all' ' CPUs are used. Specifying this argument makes it so that ' 'only the given CPU ids can be pinned. The given arguments ' ' will be evaluated as python expressions. The expressions ' 'should be integers or iterables of integers. So values like' ' "1" and "[1, 2, 4]" and "range(5, 30)" all work.', default=None) parser.add_argument('--also_print_results_to_stdout', help='Even if writing to a file, also write results to stdout.', action='store_true') parser.add_argument('--existing_data_filepaths', nargs='*', type=str, default=(), help='CSV data from these files counts towards max_shots and max_errors.\n' 'This parameter can be given multiple arguments.') parser.add_argument('--metadata_func', type=str, default="{'path': path}", help='A python expression that associates json metadata with a circuit\'s results.\n' 'Set to "auto" to use "sinter.comma_separated_key_values(path)"\n' 'Values available to the expression:\n' ' path: Relative path to the circuit file, from the command line arguments.\n' ' circuit: The circuit itself, parsed from the file, as a stim.Circuit.\n' 'Expected type:\n' ' A value that can be serialized into JSON, like a Dict[str, int].\n' '\n' ' Note that the decoder field is already recorded separately, so storing\n' ' it in the metadata as well would be redundant. But something like\n' ' decoder version could be usefully added.\n' 'Examples:\n' ''' --metadata_func "{'path': path}"\n''' ''' --metadata_func "auto"\n''' ''' --metadata_func "{'n': circuit.num_qubits, 'p': float(path.split('/')[-1].split('.')[0])}"\n''' ) import sinter a = parser.parse_args(args=args) if a.metadata_func == 'auto': a.metadata_func = "sinter.comma_separated_key_values(path)" a.metadata_func = eval(compile( 'lambda *, path, circuit: ' + a.metadata_func, filename='metadata_func:command_line_arg', mode='eval'), {'sinter': sinter}) a.postselected_observables_predicate = eval(compile( 'lambda index, metadata: ' + a.postselected_observables_predicate, filename='postselected_observables_predicate:command_line_arg', mode='eval')) if a.postselected_detectors_predicate == 'False': if a.postselect_detectors_with_non_zero_4th_coord: a.postselected_detectors_predicate = lambda index, metadata, coords: coords[3] else: a.postselected_detectors_predicate = None else: if a.postselect_detectors_with_non_zero_4th_coord: raise ValueError("Can't specify both --postselect_detectors_with_non_zero_4th_coord and --postselected_detectors_predicate") a.postselected_detectors_predicate = eval(compile( 'lambda index, metadata, coords: ' + cast(str, a.postselected_detectors_predicate), filename='postselected_detectors_predicate:command_line_arg', mode='eval')) if a.custom_decoders_module_function is not None: all_custom_decoders = {} for entry in a.custom_decoders_module_function: terms = entry.split(':') if len(terms) != 2: raise ValueError("--custom_decoders_module_function didn't have exactly one colon " "separating a module name from a function name. Expected an argument " "of the form --custom_decoders_module_function 'module:function'") module, function = terms vals = {'__name__': '[]'} exec(f"from {module} import {function} as _custom_decoders", vals) custom_decoders = vals['_custom_decoders']() all_custom_decoders = {**all_custom_decoders, **custom_decoders} a.custom_decoders = all_custom_decoders else: a.custom_decoders = None for decoder in a.decoders: if decoder not in BUILT_IN_SAMPLERS and (a.custom_decoders is None or decoder not in a.custom_decoders): message = f"Not a recognized decoder or sampler: {decoder=}.\n" message += f"Available built-in decoders and samplers: {sorted(e for e in BUILT_IN_SAMPLERS.keys() if 'internal' not in e)}.\n" if a.custom_decoders is None: message += f"No custom decoders are available. --custom_decoders_module_function wasn't specified." else: message += f"Available custom decoders: {sorted(a.custom_decoders.keys())}." raise ValueError(message) if a.allowed_cpu_affinity_ids is not None: vals: List[int] = [] e: str for e in a.allowed_cpu_affinity_ids: try: v = eval(e, {}, {}) if isinstance(v, int): vals.append(v) elif all(isinstance(e, int) for e in v): vals.extend(v) else: raise ValueError("Not an integer or iterable of integers.") except Exception as ex: raise ValueError("Failed to eval {e!r} for --allowed_cpu_affinity_ids") from ex a.allowed_cpu_affinity_ids = vals return a def open_merge_file(path: str) -> Tuple[Any, ExistingData]: try: existing = ExistingData.from_file(path) return open(path, 'a'), existing except FileNotFoundError: f = open(path, 'w') print(CSV_HEADER, file=f) return f, ExistingData() def main_collect(*, command_line_args: List[str]): args = parse_args(args=command_line_args) iter_tasks = iter_file_paths_into_goals( circuit_paths=args.circuits, metadata_func=args.metadata_func, postselected_detectors_predicate=args.postselected_detectors_predicate, postselected_observables_predicate=args.postselected_observables_predicate, ) num_tasks = len(args.circuits) * len(args.decoders) print_to_stdout = args.also_print_results_to_stdout or args.save_resume_filepath is None did_work = False printer = ThrottledProgressPrinter( outs=[], print_progress=not args.quiet, min_progress_delay=0.03 if args.also_print_results_to_stdout else 0.1, ) if print_to_stdout: printer.outs.append(sys.stdout) def on_progress(sample: Progress) -> None: nonlocal did_work for stats in sample.new_stats: if not did_work: printer.print_out(CSV_HEADER) did_work = True printer.print_out(stats.to_csv_line()) msg = sample.status_message if msg == 'KeyboardInterrupt': printer.show_latest_progress('\nInterrupted. Output is flushed. Cleaning up workers...') printer.flush() else: printer.show_latest_progress(msg) if args.processes == 'auto': num_workers = os.cpu_count() else: try: num_workers = int(args.processes) except ValueError: num_workers = 0 if num_workers < 1: raise ValueError(f'--processes must be a non-negative integer, or "auto", but was: {args.processes}') try: collect( num_workers=num_workers, hint_num_tasks=num_tasks, tasks=iter_tasks, print_progress=False, save_resume_filepath=args.save_resume_filepath, existing_data_filepaths=args.existing_data_filepaths, progress_callback=on_progress, max_errors=args.max_errors, max_shots=args.max_shots, count_detection_events=args.count_detection_events, count_observable_error_combos=args.count_observable_error_combos, decoders=args.decoders, max_batch_seconds=args.max_batch_seconds, max_batch_size=args.max_batch_size, start_batch_size=args.start_batch_size, custom_decoders=args.custom_decoders, custom_error_count_key=args.custom_error_count_key, allowed_cpu_affinity_ids=args.allowed_cpu_affinity_ids, ) except KeyboardInterrupt: pass ================================================ FILE: glue/sample/src/sinter/_command/_main_collect_test.py ================================================ import collections import pathlib import tempfile import stim import pytest import sinter from sinter._command._main import main from sinter._command._main_combine import ExistingData from sinter._plotting import split_by def test_split_by(): assert split_by('abcdefcccghi', lambda e: e == 'c') == [list('ab'), list('c'), list('def'), list('ccc'), list('ghi')] def test_main_collect(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) for distance in [3, 5, 7]: c = stim.Circuit.generated( 'repetition_code:memory', rounds=3, distance=distance, after_clifford_depolarization=0.02) with open(d / f'{distance}.stim', 'w') as f: print(c, file=f) # Collects requested stats. main(command_line_args=[ "collect", "--circuits", str(d / "3.stim"), str(d / "5.stim"), str(d / "7.stim"), "--max_shots", "1000", "--max_errors", "10", "--decoders", "pymatching", "--processes", "4", "--quiet", "--save_resume_filepath", str(d / "out.csv"), ]) data = ExistingData.from_file(d / "out.csv").data assert len(data) == 3 for k, v in data.items(): assert v.discards == 0 assert v.errors <= 50 assert v.shots >= 1000 # No more work when existing stats at merge location are sufficient. with open(d / "out.csv") as f: contents1 = f.read() main(command_line_args=[ "collect", "--circuits", str(d / "3.stim"), str(d / "5.stim"), str(d / "7.stim"), "--max_shots", "1000", "--max_errors", "10", "--decoders", "pymatching", "--processes", "4", "--quiet", "--save_resume_filepath", str(d / "out.csv"), ]) with open(d / "out.csv") as f: contents2 = f.read() assert contents1 == contents2 # No more work when existing work is sufficient. main(command_line_args=[ "collect", "--circuits", str(d / "3.stim"), str(d / "5.stim"), str(d / "7.stim"), "--max_shots", "1000", "--max_errors", "10", "--decoders", "pymatching", "--processes", "4", "--quiet", "--existing_data_filepaths", str(d / "out.csv"), "--save_resume_filepath", str(d / "out2.csv"), ]) data2 = ExistingData.from_file(d / "out2.csv").data assert len(data2) == 0 class AlternatingPredictionsDecoder(sinter.Decoder): def decode_via_files(self, *, num_shots: int, num_dets: int, num_obs: int, dem_path: pathlib.Path, dets_b8_in_path: pathlib.Path, obs_predictions_b8_out_path: pathlib.Path, tmp_dir: pathlib.Path, ) -> None: bytes_per_shot = (num_obs + 7) // 8 with open(obs_predictions_b8_out_path, 'wb') as f: for k in range(num_shots): f.write((k % 3 == 0).to_bytes(length=bytes_per_shot, byteorder='little')) def _make_custom_decoders(): return {'alternate': AlternatingPredictionsDecoder()} def test_main_collect_with_custom_decoder(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'tmp.stim', 'w') as f: print(""" M(0.1) 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """, file=f) with pytest.raises(ValueError, match="Not a recognized decoder"): main(command_line_args=[ "collect", "--circuits", str(d / "tmp.stim"), "--max_shots", "1000", "--decoders", "NOTEXIST", "--custom_decoders_module_function", "sinter._command._main_collect_test:_make_custom_decoders", "--processes", "2", "--quiet", "--save_resume_filepath", str(d / "out.csv"), ]) # Collects requested stats. main(command_line_args=[ "collect", "--circuits", str(d / "tmp.stim"), "--max_shots", "1000", "--decoders", "alternate", "--custom_decoders_module_function", "sinter._command._main_collect_test:_make_custom_decoders", "--processes", "2", "--quiet", "--save_resume_filepath", str(d / "out.csv"), ]) data = ExistingData.from_file(d / "out.csv").data assert len(data) == 1 v, = data.values() assert v.shots == 1000 assert 50 < v.errors < 500 assert v.discards == 0 def test_main_collect_post_select_observables(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'circuit.stim', 'w') as f: print(""" M(0.125) 0 1 OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(11) rec[-1] rec[-2] """, file=f) # Collects requested stats. main(command_line_args=[ "collect", "--postselected_observables_predicate", "index == 11", "--circuits", str(d / "circuit.stim"), "--max_shots", "10000", "--max_errors", "10000", "--decoders", "pymatching", "--processes", "4", "--quiet", "--save_resume_filepath", str(d / "out.csv"), ]) data = sinter.stats_from_csv_files(d / "out.csv") assert len(data) == 1 stats, = data assert stats.shots == 10000 assert 0.21875 - 0.1 < stats.discards / stats.shots < 0.21875 + 0.1 assert 0.015625 - 0.01 <= stats.errors / (stats.shots - stats.discards) <= 0.015625 + 0.02 def test_main_collect_comma_separated_key_values(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) paths = [] for distance in [3, 5, 7]: c = stim.Circuit.generated( 'repetition_code:memory', rounds=3, distance=distance, after_clifford_depolarization=0.02) path = d / f'd={distance},p=0.02,r=3.0,c=rep_code.stim' paths.append(str(path)) with open(path, 'w') as f: print(c, file=f) # Collects requested stats. main(command_line_args=[ "collect", "--circuits", *paths, "--max_shots", "1000", "--metadata_func", "sinter.comma_separated_key_values(path)", "--max_errors", "10", "--decoders", "pymatching", "--processes", "4", "--quiet", "--save_resume_filepath", str(d / "out.csv"), ]) data = sinter.stats_from_csv_files(d / "out.csv") seen_metadata = frozenset(repr(e.json_metadata) for e in data) assert seen_metadata == frozenset([ "{'c': 'rep_code', 'd': 3, 'p': 0.02, 'r': 3.0}", "{'c': 'rep_code', 'd': 5, 'p': 0.02, 'r': 3.0}", "{'c': 'rep_code', 'd': 7, 'p': 0.02, 'r': 3.0}", ]) def test_main_collect_count_observable_error_combos(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / 'a=3.stim', 'w') as f: print(""" X_ERROR(0.1) 0 X_ERROR(0.2) 1 M 0 1 OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(1) rec[-2] """, file=f) # Collects requested stats. main(command_line_args=[ "collect", "--circuits", str(d / 'a=3.stim'), "--max_shots", "100000", "--metadata_func", "sinter.comma_separated_key_values(path)", "--max_errors", "10000", "--decoders", "pymatching", "--count_observable_error_combos", "--processes", "4", "--quiet", "--save_resume_filepath", str(d / "out.csv"), ]) data = sinter.stats_from_csv_files(d / "out.csv") assert len(data) == 1 item, = data assert set(item.custom_counts.keys()) == {"obs_mistake_mask=E_", "obs_mistake_mask=_E", "obs_mistake_mask=EE"} assert 0.1*0.8 - 0.01 < item.custom_counts['obs_mistake_mask=_E'] / item.shots < 0.1*0.8 + 0.01 assert 0.9*0.2 - 0.01 < item.custom_counts['obs_mistake_mask=E_'] / item.shots < 0.9*0.2 + 0.01 assert 0.1*0.2 - 0.01 < item.custom_counts['obs_mistake_mask=EE'] / item.shots < 0.1*0.2 + 0.01 def test_main_collect_count_detection_events(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / 'a=3.stim', 'w') as f: print(""" X_ERROR(0.1) 0 X_ERROR(0.2) 1 M 0 1 OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(1) rec[-2] DETECTOR rec[-2] """, file=f) # Collects requested stats. main(command_line_args=[ "collect", "--circuits", str(d / 'a=3.stim'), "--max_shots", "100000", "--metadata_func", "sinter.comma_separated_key_values(path)", "--decoders", "pymatching", "--count_detection_events", "--processes", "4", "--quiet", "--save_resume_filepath", str(d / "out.csv"), ]) data = sinter.stats_from_csv_files(d / "out.csv") assert len(data) == 1 item, = data assert set(item.custom_counts.keys()) == {"detection_events", "detectors_checked"} assert item.custom_counts['detectors_checked'] == 100000 assert 100000 * 0.1 * 0.5 < item.custom_counts['detection_events'] < 100000 * 0.1 * 1.5 def test_cpu_pin(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / 'a=3.stim', 'w') as f: print(""" X_ERROR(0.1) 0 X_ERROR(0.2) 1 M 0 1 OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(1) rec[-2] DETECTOR rec[-2] """, file=f) # Collects requested stats. main(command_line_args=[ "collect", "--circuits", str(d / 'a=3.stim'), "--max_shots", "100000", "--metadata_func", "auto", "--decoders", "pymatching", "--count_detection_events", "--processes", "4", "--quiet", "--save_resume_filepath", str(d / "out.csv"), "--allowed_cpu_affinity_ids", "0", "range(1, 9, 2)", "[4, 20]" ]) data = sinter.stats_from_csv_files(d / "out.csv") assert len(data) == 1 item, = data assert set(item.custom_counts.keys()) == {"detection_events", "detectors_checked"} assert item.custom_counts['detectors_checked'] == 100000 assert 100000 * 0.1 * 0.5 < item.custom_counts['detection_events'] < 100000 * 0.1 * 1.5 def test_custom_error_stopping_count(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) stim.Circuit.generated( 'repetition_code:memory', rounds=25, distance=5, after_clifford_depolarization=0.1, ).to_file(d / 'a=3.stim') # Collects requested stats. main(command_line_args=[ "collect", "--circuits", str(d / 'a=3.stim'), "--max_shots", "100_000_000_000_000", "--quiet", "--max_errors", "1_000_000", "--metadata_func", "auto", "--decoders", "vacuous", "--count_detection_events", "--processes", "4", "--save_resume_filepath", str(d / "out.csv"), "--custom_error_count_key", "detection_events", ]) data = sinter.stats_from_csv_files(d / "out.csv") assert len(data) == 1 item, = data # Would normally need >1_000_000 shots to see 1_000_000 errors. assert item.shots < 100_000 assert item.errors < 90_000 assert item.custom_counts['detection_events'] > 1_000_000 def test_auto_processes(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) stim.Circuit.generated( 'repetition_code:memory', rounds=5, distance=3, after_clifford_depolarization=0.1, ).to_file(d / 'a=3.stim') # Collects requested stats. main(command_line_args=[ "collect", "--circuits", str(d / 'a=3.stim'), "--max_shots", "200", "--quiet", "--metadata_func", "auto", "--decoders", "vacuous", "--processes", "auto", "--save_resume_filepath", str(d / "out.csv"), ]) data = sinter.stats_from_csv_files(d / "out.csv") assert len(data) == 1 def test_implicit_auto_processes(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) stim.Circuit.generated( 'repetition_code:memory', rounds=5, distance=3, after_clifford_depolarization=0.1, ).to_file(d / 'a=3.stim') # Collects requested stats. main(command_line_args=[ "collect", "--circuits", str(d / 'a=3.stim'), "--max_shots", "200", "--quiet", "--metadata_func", "auto", "--decoders", "perfectionist", "--save_resume_filepath", str(d / "out.csv"), ]) data = sinter.stats_from_csv_files(d / "out.csv") assert len(data) == 1 assert data[0].discards > 0 ================================================ FILE: glue/sample/src/sinter/_command/_main_combine.py ================================================ import argparse import collections import json import sys from typing import List, Any import sinter from sinter._data import CSV_HEADER, ExistingData from sinter._plotting import better_sorted_str_terms def main_combine(*, command_line_args: List[str]): parser = argparse.ArgumentParser() parser.add_argument('--order', choices=('preserve', 'metadata', 'error'), default='metadata', help='Determines the order of output rows.\n' ' metadata (default): sort ascending by metadata.' ' preserve: match order of input rows.\n' ' error: sort ascending by error rate') parser.add_argument('--strip_custom_counts', help='Removes custom counts from the output.', action='store_true') parser.add_argument('rest', nargs=argparse.REMAINDER, type=str, help='Paths to CSV files containing sample statistics.') args = parser.parse_args(command_line_args) if args.rest: total = ExistingData() for path in args.rest: total += ExistingData.from_file(path) else: total = ExistingData.from_file(sys.stdin) total = list(total.data.values()) if args.strip_custom_counts: total = [ sinter.TaskStats( strong_id=task.strong_id, decoder=task.decoder, json_metadata=task.json_metadata, shots=task.shots, errors=task.errors, discards=task.discards, seconds=task.seconds, ) for task in total ] else: total = [ sinter.TaskStats( strong_id=task.strong_id, decoder=task.decoder, json_metadata=task.json_metadata, shots=task.shots, errors=task.errors, discards=task.discards, seconds=task.seconds, custom_counts=collections.Counter(dict(sorted(task.custom_counts.items(), key=better_sorted_str_terms))), ) for task in total ] if args.order == 'metadata': output = sorted(total, key=lambda e: better_sorted_str_terms(json.dumps(e.json_metadata, separators=(',', ':'), sort_keys=True))) elif args.order == 'preserve': output = list(total) elif args.order == 'error': def err_rate_key(stats: sinter.TaskStats) -> Any: num_kept = stats.shots - stats.discards err_rate = 0 if num_kept == 0 else stats.errors / num_kept discard_rate = 0 if stats.shots == 0 else stats.discards / stats.shots return err_rate, discard_rate output = sorted(total, key=err_rate_key) else: raise NotImplementedError(f'order={args.order}') print(CSV_HEADER) for value in output: print(value.to_csv_line()) ================================================ FILE: glue/sample/src/sinter/_command/_main_combine_test.py ================================================ import contextlib import io import pathlib import tempfile from sinter._command._main import main def test_main_combine(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 300,1,20,1.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""path"":""a.stim""}" 300,100,200,2.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""path"":""a.stim""}" 9,5,4,6.0,pymatching,5fe5a6cd4226b1a910d57e5479d1ba6572e0b3115983c9516360916d1670000f,"{""path"":""b.stim""}" """.strip(), file=f) out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "combine", str(d / "input.csv"), ]) assert out.getvalue() == """ shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts 600, 101, 220, 3.00,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""path"":""a.stim""}", 9, 5, 4, 6.00,pymatching,5fe5a6cd4226b1a910d57e5479d1ba6572e0b3115983c9516360916d1670000f,"{""path"":""b.stim""}", """ out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "combine", str(d / "input.csv"), str(d / "input.csv"), ]) assert out.getvalue() == """ shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts 1200, 202, 440, 6.00,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""path"":""a.stim""}", 18, 10, 8, 12.0,pymatching,5fe5a6cd4226b1a910d57e5479d1ba6572e0b3115983c9516360916d1670000f,"{""path"":""b.stim""}", """ def test_main_combine_legacy_custom_counts(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'old.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 100,1,20,1.0,pymatching,abc123,"{""path"":""a.stim""}" """.strip(), file=f) with open(d / f'new.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata,custom_counts 300,1,20,1.0,pymatching,abc123,"{""path"":""a.stim""}","{""x"":2}" 300,1,20,1.0,pymatching,abc123,"{""path"":""a.stim""}","{""y"":3}" """.strip(), file=f) out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "combine", str(d / "old.csv"), str(d / "new.csv"), ]) assert out.getvalue() == """ shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts 700, 3, 60, 3.00,pymatching,abc123,"{""path"":""a.stim""}","{""x"":2,""y"":3}" """ def test_order_flag(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder, strong_id,json_metadata 1000, 100, 4, 2.0, pymatching,deadbeef0,"{""d"":19}" 2000, 300, 3, 3.0, pymatching,deadbeef1,"{""d"":9}" 3000, 200, 2000, 5.0, pymatching,deadbeef2,"{""d"":200}" 4000, 100, 1, 7.0, pymatching,deadbeef3,"{""d"":3}" 5000, 100, 0, 11, pymatching,deadbeef4,"{""d"":5}" """.strip(), file=f) out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "combine", "--order", "preserve", str(d / "input.csv"), str(d / "input.csv"), ]) assert out.getvalue() == """ shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts 2000, 200, 8, 4.00,pymatching,deadbeef0,"{""d"":19}", 4000, 600, 6, 6.00,pymatching,deadbeef1,"{""d"":9}", 6000, 400, 4000, 10.0,pymatching,deadbeef2,"{""d"":200}", 8000, 200, 2, 14.0,pymatching,deadbeef3,"{""d"":3}", 10000, 200, 0, 22.0,pymatching,deadbeef4,"{""d"":5}", """ out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "combine", "--order", "metadata", str(d / "input.csv"), str(d / "input.csv"), ]) assert out.getvalue() == """ shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts 8000, 200, 2, 14.0,pymatching,deadbeef3,"{""d"":3}", 10000, 200, 0, 22.0,pymatching,deadbeef4,"{""d"":5}", 4000, 600, 6, 6.00,pymatching,deadbeef1,"{""d"":9}", 2000, 200, 8, 4.00,pymatching,deadbeef0,"{""d"":19}", 6000, 400, 4000, 10.0,pymatching,deadbeef2,"{""d"":200}", """ out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "combine", "--order", "error", str(d / "input.csv"), str(d / "input.csv"), ]) assert out.getvalue() == """ shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts 10000, 200, 0, 22.0,pymatching,deadbeef4,"{""d"":5}", 8000, 200, 2, 14.0,pymatching,deadbeef3,"{""d"":3}", 2000, 200, 8, 4.00,pymatching,deadbeef0,"{""d"":19}", 4000, 600, 6, 6.00,pymatching,deadbeef1,"{""d"":9}", 6000, 400, 4000, 10.0,pymatching,deadbeef2,"{""d"":200}", """ def test_order_custom_counts(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder, strong_id,json_metadata,custom_counts 1000, 100, 4, 2.0, pymatching,deadbeef0,[],"{""d4"":3,""d2"":30}" """.strip(), file=f) out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "combine", str(d / "input.csv"), ]) assert out.getvalue() == """ shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts 1000, 100, 4, 2.00,pymatching,deadbeef0,[],"{""d2"":30,""d4"":3}" """ ================================================ FILE: glue/sample/src/sinter/_command/_main_plot.py ================================================ import math import sys from typing import Any, Callable, Iterable, List, Optional, TYPE_CHECKING, Tuple, Union, Dict, Sequence, cast import argparse import matplotlib.pyplot as plt import numpy as np from sinter._command._main_combine import ExistingData from sinter._plotting import plot_discard_rate, plot_custom from sinter._plotting import plot_error_rate from sinter._probability_util import shot_error_rate_to_piece_error_rate, Fit if TYPE_CHECKING: import sinter def parse_args(args: List[str]) -> Any: parser = argparse.ArgumentParser(description='Plot collected CSV data.', prog='sinter plot') parser.add_argument('--filter_func', type=str, default="True", help='A python expression that determines whether a case is kept or not.\n' 'Values available to the python expression:\n' ' metadata: The parsed value from the json_metadata for the data point.\n' ' m: `m.key` is a shorthand for `metadata.get("key", None)`.\n' ' decoder: The decoder that decoded the data for the data point.\n' ' strong_id: The cryptographic hash of the case that was sampled for the data point.\n' ' stat: The sinter.TaskStats object for the data point.\n' 'Expected expression type:\n' ' Something that can be given to `bool` to get True or False.\n' 'Examples:\n' ''' --filter_func "decoder=='pymatching'"\n''' ''' --filter_func "0.001 < metadata['p'] < 0.005"\n''') parser.add_argument('--preprocess_stats_func', type=str, default=None, help='An expression that operates on a `stats` value, returning a new list of stats to plot.\n' 'For example, this could double add a field to json_metadata or merge stats together.\n' 'Examples:\n' ''' --preprocess_stats_func "[stat for stat in stats if stat.errors > 0]\n''' ''' --preprocess_stats_func "[stat.with_edits(errors=stat.custom_counts['severe_errors']) for stat in stats]\n''' ''' --preprocess_stats_func "__import__('your_custom_module').your_custom_function(stats)"\n''' ) parser.add_argument('--x_func', type=str, default="1", help='A python expression that determines where points go on the x axis.\n' 'Values available to the python expression:\n' ' metadata: The parsed value from the json_metadata for the data point.\n' ' m: `m.key` is a shorthand for `metadata.get("key", None)`.\n' ' decoder: The decoder that decoded the data for the data point.\n' ' strong_id: The cryptographic hash of the case that was sampled for the data point.\n' ' stat: The sinter.TaskStats object for the data point.\n' 'Expected expression type:\n' ' Something that can be given to `float` to get a float.\n' 'Examples:\n' ''' --x_func "metadata['p']"\n''' ''' --x_func m.p\n''' ''' --x_func "metadata['path'].split('/')[-1].split('.')[0]"\n''' ) parser.add_argument('--point_label_func', type=str, default="None", help='A python expression that determines text to put next to data points.\n' 'Values available to the python expression:\n' ' metadata: The parsed value from the json_metadata for the data point.\n' ' m: `m.key` is a shorthand for `metadata.get("key", None)`.\n' ' decoder: The decoder that decoded the data for the data point.\n' ' strong_id: The cryptographic hash of the case that was sampled for the data point.\n' ' stat: The sinter.TaskStats object for the data point.\n' 'Expected expression type:\n' ' Something Falsy (no label), or something that can be given to `str` to get a string.\n' 'Examples:\n' ''' --point_label_func "f'p={m.p}'"\n''' ) parser.add_argument('--y_func', type=str, default=None, help='A python expression that determines where points go on the y axis.\n' 'This argument is not used by error rate or discard rate plots; only\n' 'by the "custom_y" type plot.' 'Values available to the python expression:\n' ' metadata: The parsed value from the json_metadata for the data point.\n' ' m: `m.key` is a shorthand for `metadata.get("key", None)`.\n' ' decoder: The decoder that decoded the data for the data point.\n' ' strong_id: The cryptographic hash of the case that was sampled for the data point.\n' ' stat: The sinter.TaskStats object for the data point.\n' 'Expected expression type:\n' ' A `sinter.Fit` specifying an uncertainty region,.\n' ' or else something that can be given to `float` to get a float.\n' 'Examples:\n' ''' --x_func "metadata['p']"\n''' ''' --x_func "metadata['path'].split('/')[-1].split('.')[0]"\n''' ) parser.add_argument('--fig_size', type=int, nargs=2, default=None, help='Desired figure width and height in pixels.') parser.add_argument('--dpi', type=float, default=100, help='Dots per inch. Determines resolution of the figure.') parser.add_argument('--group_func', type=str, default="'all data (use -group_func and -x_func to group into curves)'", help='A python expression that determines how points are grouped into curves.\n' 'If this evaluates to a dict, different keys control different groupings (e.g. "color" and "marker")\n' 'Values available to the python expression:\n' ' metadata: The parsed value from the json_metadata for the data point.\n' ' m: `m.key` is a shorthand for `metadata.get("key", None)`.\n' ' decoder: The decoder that decoded the data for the data point.\n' ' strong_id: The cryptographic hash of the case that was sampled for the data point.\n' ' stat: The sinter.TaskStats object for the data point.\n' 'Expected expression type:\n' ' A dict, or something that can be given to `str` to get a useful string.\n' 'Recognized dict keys:\n' ' "color": controls color grouping\n' ' "marker": controls marker grouping\n' ' "linestyle": controls linestyle grouping\n' ' "order": controls ordering in the legend\n' ' "label": the text shown in the legend\n' 'Examples:\n' ''' --group_func "(decoder, m.d)"\n''' ''' --group_func "{'color': decoder, 'marker': m.d, 'label': (decoder, m.d)}"\n''' ''' --group_func "metadata['path'].split('/')[-2]"\n''' ) parser.add_argument('--failure_unit_name', type=str, default=None, help='The unit of failure, typically either "shot" (the default) or "round".\n' 'If this argument is specified, --failure_units_per_shot_func must also be specified.\n''' ) parser.add_argument('--failure_units_per_shot_func', type=str, default=None, help='A python expression that evaluates to the number of failure units there are per shot.\n' 'For example, if the failure unit is rounds, this should be an expression that returns\n' 'the number of rounds in a shot. Sinter has no way of knowing what you consider a round\n' 'to be, otherwise.\n' '\n' 'This value is used to rescale the logical error rate plots. For example, if there are 4\n' 'failure units per shot then a shot error rate of 10%% corresponds to a unit failure rate\n' 'of 2.7129%%. The conversion formula (assuming less than 50%% error rates) is:\n' '\n' ' P_unit = 0.5 - 0.5 * (1 - 2 * P_shot)**(1/units_per_shot)\n' '\n' 'Values available to the python expression:\n' ' metadata: The parsed value from the json_metadata for the data point.\n' ' m: `m.key` is a shorthand for `metadata.get("key", None)`.\n' ' decoder: The decoder that decoded the data for the data point.\n' ' strong_id: The cryptographic hash of the case that was sampled for the data point.\n' ' stat: The sinter.TaskStats object for the data point.\n' '\n' 'Expected expression type:\n' ' float.\n' '\n' 'Examples:\n' ''' --failure_units_per_shot_func "metadata['rounds']"\n''' ''' --failure_units_per_shot_func m.r\n''' ''' --failure_units_per_shot_func "m.distance * 3"\n''' ''' --failure_units_per_shot_func "10"\n''' ) parser.add_argument('--failure_values_func', type=str, default=None, help='A python expression that evaluates to the number of independent ways a shot can fail.\n' 'For example, if a shot corresponds to a memory experiment preserving two observables,\n' 'then the failure unions is 2.\n' '\n' 'This value is necessary to correctly rescale the logical error rate plots when using\n' '--failure_values_func. By default it is assumed to be 1.\n' '\n' 'Values available to the python expression:\n' ' metadata: The parsed value from the json_metadata for the data point.\n' ' m: `m.key` is a shorthand for `metadata.get("key", None)`.\n' ' decoder: The decoder that decoded the data for the data point.\n' ' strong_id: The cryptographic hash of the case that was sampled for the data point.\n' ' stat: The sinter.TaskStats object for the data point.\n' '\n' 'Expected expression type:\n' ' float.\n' '\n' 'Examples:\n' ''' --failure_values_func "metadata['num_obs']"\n''' ''' --failure_values_func "2"\n''' ) parser.add_argument('--plot_args_func', type=str, default='''{}''', help='A python expression used to customize the look of curves.\n' 'Values available to the python expression:\n' ' index: A unique integer identifying the curve.\n' ' key: The group key (returned from --group_func) identifying the curve.\n' ' stats: The list of sinter.TaskStats object in the group.\n' ' metadata: (From one arbitrary data point in the group.) The parsed value from the json_metadata for the data point.\n' ' m: `m.key` is a shorthand for `metadata.get("key", None)`.\n' ' decoder: (From one arbitrary data point in the group.) The decoder that decoded the data for the data point.\n' ' strong_id: (From one arbitrary data point in the group.) The cryptographic hash of the case that was sampled for the data point.\n' ' stat: (From one arbitrary data point in the group.) The sinter.TaskStats object for the data point.\n' 'Expected expression type:\n' ' A dictionary to give to matplotlib plotting functions as a **kwargs argument.\n' 'Examples:\n' """ --plot_args_func "'''{'label': 'curve #' + str(index), 'linewidth': 5}'''"\n""" """ --plot_args_func "'''{'marker': 'ov*sp^<>8PhH+xXDd|'[index %% 18]}'''"\n""" ) parser.add_argument('--in', type=str, nargs='+', required=True, help='Input files to get data from.') parser.add_argument('--type', choices=['error_rate', 'discard_rate', 'custom_y'], nargs='+', default=(), help='Picks the figures to include.') parser.add_argument('--out', type=str, default=None, help='Write the plot to a file instead of showing it.\n' '(Use --show to still show the plot.)') parser.add_argument('--xaxis', type=str, default='[log]', help='Customize the X axis label. ' 'Prefix [log] for logarithmic scale. ' 'Prefix [sqrt] for square root scale.') parser.add_argument('--yaxis', type=str, default=None, help='Customize the Y axis label. ' 'Prefix [log] for logarithmic scale. ' 'Prefix [sqrt] for square root scale.') parser.add_argument('--custom_error_count_keys', type=str, nargs='+', default=None, help="Replaces the stat's error count with one of its custom counts. Stats " "without this count end up with an error count of 0. Adds the json " "metadata field 'custom_error_count_key' to identify the custom count " "used. Specifying multiple values turns each stat into multiple " "stats.") parser.add_argument('--show', action='store_true', help='Displays the plot in a window even when --out is specified.') parser.add_argument('--xmin', default=None, type=float, help='Forces the minimum value of the x axis.') parser.add_argument('--xmax', default=None, type=float, help='Forces the maximum value of the x axis.') parser.add_argument('--ymin', default=None, type=float, help='Forces the minimum value of the y axis.') parser.add_argument('--ymax', default=None, type=float, help='Forces the maximum value of the y axis.') parser.add_argument('--title', default=None, type=str, help='Sets the title of the plot.') parser.add_argument('--subtitle', default=None, type=str, help='Sets the subtitle of the plot.\n' '\n' 'Note: The pattern "{common}" will expand to text including\n' 'all json metadata values that are the same across all stats.') parser.add_argument('--highlight_max_likelihood_factor', type=float, default=1000, help='The relative likelihood ratio that determines the color highlights around curves.\n' 'Set this to 1 or larger. Set to 1 to disable highlighting.') parser.add_argument('--line_fits', action='store_true', help='Adds dashed line fits to every curve.') a = parser.parse_args(args=args) if 'custom_y' in a.type and a.y_func is None: raise ValueError("--type custom_y requires --y_func.") if a.y_func is not None and a.type and 'custom_y' not in a.type: raise ValueError("--y_func only works with --type custom_y.") if (len(a.type) == 0 and a.y_func is not None) or list(a.type) == 'custom_y': if a.failure_units_per_shot_func is not None: raise ValueError("--failure_units_per_shot_func doesn't affect --type custom_y") if (a.failure_units_per_shot_func is not None) != (a.failure_unit_name is not None): raise ValueError("--failure_units_per_shot_func and --failure_unit_name can only be specified together.") if a.failure_values_func is not None and a.failure_units_per_shot_func is None: raise ValueError('Specified --failure_values_func without --failure_units_per_shot_func') if a.failure_units_per_shot_func is None: a.failure_units_per_shot_func = "1" if a.failure_values_func is None: a.failure_values_func = "1" if a.failure_unit_name is None: a.failure_unit_name = 'shot' def _compile_argument_into_func(arg_name: str, arg_val: Any = ()): if arg_val == (): arg_val = getattr(a, arg_name) raw_func = eval(compile( f'lambda *, stat, decoder, metadata, m, strong_id, sinter, math, np: {arg_val}', filename=f'{arg_name}:command_line_arg', mode='eval', )) import sinter return lambda stat: raw_func( sinter=sinter, math=math, np=np, stat=stat, decoder=stat.decoder, metadata=stat.json_metadata, m=_FieldToMetadataWrapper(stat.json_metadata), strong_id=stat.strong_id) a.preprocess_stats_func = None if a.preprocess_stats_func is None else eval(compile( f'lambda *, stats: {a.preprocess_stats_func}', filename='preprocess_stats_func:command_line_arg', mode='eval')) a.x_func = _compile_argument_into_func('x_func', a.x_func) if a.y_func is not None: a.y_func = _compile_argument_into_func('y_func') a.point_label_func = _compile_argument_into_func('point_label_func') a.group_func = _compile_argument_into_func('group_func') a.filter_func = _compile_argument_into_func('filter_func') a.failure_units_per_shot_func = _compile_argument_into_func('failure_units_per_shot_func') a.failure_values_func = _compile_argument_into_func('failure_values_func') raw_plot_args_func = eval(compile( f'lambda *, index, key, stats, stat, decoder, metadata, m, strong_id: {a.plot_args_func}', filename='plot_args_func:command_line_arg', mode='eval')) a.plot_args_func = lambda index, group_key, stats: raw_plot_args_func( index=index, key=group_key, stats=stats, stat=stats[0], decoder=stats[0].decoder, metadata=stats[0].json_metadata, m=_FieldToMetadataWrapper(stats[0].json_metadata), strong_id=stats[0].strong_id) return a def _log_ticks( min_v: float, max_v: float, ) -> Tuple[float, float, List[float], List[float]]: d0 = math.floor(math.log10(min_v) + 0.0001) d1 = math.ceil(math.log10(max_v) - 0.0001) if d1 == d0: d1 += 1 d0 -= 1 return ( 10**d0, 10**d1, [10**k for k in range(d0, d1 + 1)], [d*10**k for k in range(d0, d1) for d in range(2, 10)], ) def _sqrt_ticks( min_v: float, max_v: float, ) -> Tuple[float, float, List[float], List[float]]: if max_v == min_v: max_v *= 2 min_v /= 2 if max_v == min_v: max_v = 1 min_v = 0 d = max_v - min_v step = 10**math.floor(math.log10(d)) small_step = step / 10 start_k = math.floor(min_v / step) end_k = math.ceil(max_v / step) + 1 major_ticks = [step * k for k in range(start_k, end_k)] if len(major_ticks) < 5: step /= 2 start_k = math.floor(min_v / step) end_k = math.ceil(max_v / step) + 1 major_ticks = [step * k for k in range(start_k, end_k)] small_start_k = math.floor(major_ticks[0] / small_step) small_end_k = math.ceil(major_ticks[-1] / small_step) + 1 minor_ticks = [small_step * k for k in range(small_start_k, small_end_k)] return ( major_ticks[0], major_ticks[-1], major_ticks, minor_ticks, ) def _pick_min_max( *, plotted_stats: Sequence['sinter.TaskStats'], v_func: Callable[['sinter.TaskStats'], Optional[float]], default_min: float, default_max: float, forced_min: Optional[float], forced_max: Optional[float], want_positive: bool, want_strictly_positive: bool, ) -> Tuple[float, float]: assert default_max >= default_min vs = [] for stat in plotted_stats: v = v_func(stat) if isinstance(v, (int, float)): vs.append(v) elif isinstance(v, Fit): for e in [v.low, v.best, v.high]: if e is not None: vs.append(e) elif v is None: pass else: raise NotImplementedError(f'{v=}') if want_positive: vs = [v for v in vs if v > 0] min_v = min(vs, default=default_min) max_v = max(vs, default=default_max) if forced_min is not None: min_v = forced_min max_v = max(min_v, max_v) if forced_max is not None: max_v = forced_max min_v = min(min_v, max_v) if want_positive: assert min_v >= 0 if want_strictly_positive: assert min_v > 0 assert max_v >= min_v return min_v, max_v def _set_axis_scale_label_ticks( *, ax: Optional[plt.Axes], y_not_x: bool, axis_label: str, default_scale: str = 'linear', default_min_v: float = 0, default_max_v: float = 0, v_func: Callable[['sinter.TaskStats'], Optional[float]], forced_min_v: Optional[float] = None, forced_max_v: Optional[float] = None, plotted_stats: Sequence['sinter.TaskStats'], ) -> Optional[str]: if ax is None: return None set_scale = ax.set_yscale if y_not_x else ax.set_xscale set_label = ax.set_ylabel if y_not_x else ax.set_xlabel set_lim = cast(Callable[[Optional[float], Optional[float]], None], ax.set_ylim if y_not_x else ax.set_xlim) set_ticks = ax.set_yticks if y_not_x else ax.set_xticks if axis_label.startswith('[') and ']' in axis_label: axis_split = axis_label.index(']') scale_name = axis_label[1:axis_split] axis_label = axis_label[axis_split + 1:] else: scale_name = default_scale set_label(axis_label) min_v, max_v = _pick_min_max( plotted_stats=plotted_stats, v_func=v_func, default_min=default_min_v, default_max=default_max_v, forced_min=forced_min_v, forced_max=forced_max_v, want_positive=scale_name != 'linear', want_strictly_positive=scale_name == 'log', ) if scale_name == 'linear': set_lim(min_v, max_v) elif scale_name == 'log': set_scale('log') min_v, max_v, major_ticks, minor_ticks = _log_ticks(min_v, max_v) if forced_min_v is not None: min_v = forced_min_v if forced_max_v is not None: max_v = forced_max_v set_ticks(major_ticks) set_ticks(minor_ticks, minor=True) set_lim(min_v, max_v) elif scale_name == 'sqrt': from matplotlib.scale import FuncScale min_v, max_v, major_ticks, minor_ticks = _sqrt_ticks(min_v, max_v) if forced_min_v is not None: min_v = forced_min_v if forced_max_v is not None: max_v = forced_max_v set_scale(FuncScale(ax, (lambda e: e**0.5, lambda e: e**2))) set_ticks(major_ticks) set_ticks(minor_ticks, minor=True) set_lim(min_v, max_v) else: raise NotImplemented(f'{scale_name=}') return scale_name def _common_json_properties(stats: List['sinter.TaskStats']) -> Dict[str, Any]: vals = {} for stat in stats: if isinstance(stat.json_metadata, dict): for k in stat.json_metadata: vals[k] = set() for stat in stats: if isinstance(stat.json_metadata, dict): for k in vals: v = stat.json_metadata.get(k) if v is None or isinstance(v, (float, str, int)): vals[k].add(v) if 'decoder' not in vals: vals['decoder'] = set() for stat in stats: vals['decoder'].add(stat.decoder) return {k: next(iter(v)) for k, v in vals.items() if len(v) == 1} def _plot_helper( *, samples: Union[Iterable['sinter.TaskStats'], ExistingData], group_func: Callable[['sinter.TaskStats'], Any], filter_func: Callable[['sinter.TaskStats'], Any], preprocess_stats_func: Optional[Callable], failure_units_per_shot_func: Callable[['sinter.TaskStats'], Any], failure_values_func: Callable[['sinter.TaskStats'], Any], x_func: Callable[['sinter.TaskStats'], Any], y_func: Optional[Callable[['sinter.TaskStats'], Any]], failure_unit: str, plot_types: Sequence[str], highlight_max_likelihood_factor: Optional[float], xaxis: str, yaxis: Optional[str], min_y: Optional[float], max_y: Optional[float], max_x: Optional[float], min_x: Optional[float], title: Optional[str], subtitle: Optional[str], fig_size: Optional[Tuple[int, int]], plot_args_func: Callable[[int, Any, List['sinter.TaskStats']], Dict[str, Any]], line_fits: bool, point_label_func: Callable[['sinter.TaskStats'], Any] = lambda _: None, dpi: float, ) -> Tuple[plt.Figure, List[plt.Axes]]: if isinstance(samples, ExistingData): total = samples else: total = ExistingData() for sample in samples: total.add_sample(sample) total.data = {k: v for k, v in total.data.items() if bool(filter_func(v))} if preprocess_stats_func is not None: processed_stats = preprocess_stats_func(stats=list(total.data.values())) total.data = {} for stat in processed_stats: total.add_sample(stat) if not plot_types: if y_func is not None: plot_types = ['custom_y'] else: plot_types = ['error_rate'] if any(s.discards for s in total.data.values()): plot_types.append('discard_rate') include_error_rate_plot = 'error_rate' in plot_types include_discard_rate_plot = 'discard_rate' in plot_types include_custom_plot = 'custom_y' in plot_types num_plots = include_error_rate_plot + include_discard_rate_plot + include_custom_plot fig: plt.Figure ax_err: Optional[plt.Axes] = None ax_dis: Optional[plt.Axes] = None ax_cus: Optional[plt.Axes] = None fig, axes = plt.subplots(1, num_plots) if num_plots == 1: axes = [axes] axes = list(axes) pop_axes = list(axes) if include_custom_plot: ax_cus = pop_axes.pop() if include_discard_rate_plot: ax_dis = pop_axes.pop() if include_error_rate_plot: ax_err = pop_axes.pop() assert not pop_axes plotted_stats: List['sinter.TaskStats'] = [ stat for stat in total.data.values() ] def stat_to_err_rate(stat: 'sinter.TaskStats') -> Optional[float]: if stat.shots <= stat.discards: return None err_rate = stat.errors / (stat.shots - stat.discards) pieces = failure_units_per_shot_func(stat) return shot_error_rate_to_piece_error_rate(err_rate, pieces=pieces) x_scale_name: Optional[str] = None for ax in [ax_err, ax_dis, ax_cus]: v = _set_axis_scale_label_ticks( ax=ax, y_not_x=False, axis_label=xaxis, default_scale='linear', default_min_v=1, default_max_v=10, forced_max_v=max_x, forced_min_v=min_x, plotted_stats=plotted_stats, v_func=x_func, ) x_scale_name = x_scale_name or v y_scale_name: Optional[str] = None if ax_err is not None: y_scale_name = y_scale_name or _set_axis_scale_label_ticks( ax=ax_err, y_not_x=True, axis_label=f"Logical Error Rate (per {failure_unit})" if yaxis is None else yaxis, default_scale='log', forced_max_v=max_y if max_y is not None else 1 if min_y is None or 1 > min_y else None, default_min_v=1e-4, default_max_v=1, forced_min_v=min_y, plotted_stats=plotted_stats, v_func=stat_to_err_rate, ) assert x_scale_name is not None assert y_scale_name is not None plot_error_rate( ax=ax_err, stats=plotted_stats, group_func=group_func, x_func=x_func, failure_units_per_shot_func=failure_units_per_shot_func, failure_values_func=failure_values_func, highlight_max_likelihood_factor=highlight_max_likelihood_factor, plot_args_func=plot_args_func, line_fits=None if not line_fits else (x_scale_name, y_scale_name), point_label_func=point_label_func, ) ax_err.grid(which='major', color='#000000') ax_err.grid(which='minor', color='#DDDDDD') ax_err.legend() if ax_dis is not None: plot_discard_rate( ax=ax_dis, stats=plotted_stats, group_func=group_func, failure_units_per_shot_func=failure_units_per_shot_func, x_func=x_func, highlight_max_likelihood_factor=highlight_max_likelihood_factor, plot_args_func=plot_args_func, point_label_func=point_label_func, ) ax_dis.set_yticks([p / 10 for p in range(11)], labels=[f'{10*p}%' for p in range(11)]) ax_dis.set_ylim(0, 1) ax_dis.grid(which='major', color='#000000') ax_dis.grid(which='minor', color='#DDDDDD') if yaxis is not None and not include_custom_plot and ax_err is None: ax_dis.set_ylabel(yaxis) else: ax_dis.set_ylabel(f"Discard Rate (per {failure_unit})") ax_dis.legend() if ax_cus is not None: assert y_func is not None y_scale_name = y_scale_name or _set_axis_scale_label_ticks( ax=ax_cus, y_not_x=True, axis_label='custom' if yaxis is None else yaxis, default_scale='linear', default_min_v=1e-4, default_max_v=1, plotted_stats=plotted_stats, v_func=y_func, forced_min_v=min_y, forced_max_v=max_y, ) plot_custom( ax=ax_cus, stats=plotted_stats, x_func=x_func, y_func=y_func, group_func=group_func, plot_args_func=plot_args_func, line_fits=None if not line_fits else (x_scale_name, y_scale_name), point_label_func=point_label_func, ) ax_cus.grid(which='major', color='#000000') ax_cus.grid(which='minor', color='#DDDDDD') ax_cus.legend() stripped_xaxis = xaxis if stripped_xaxis is not None: if stripped_xaxis.startswith('[') and ']' in stripped_xaxis: stripped_xaxis = stripped_xaxis[stripped_xaxis.index(']') + 1:] vs_suffix = '' if stripped_xaxis is not None: vs_suffix = f' vs {stripped_xaxis}' if ax_err is not None: ax_err.set_title(f'Logical Error Rate per {failure_unit}{vs_suffix}') if title is not None: ax_err.set_title(title) if ax_dis is not None: ax_dis.set_title(f'Discard Rate per {failure_unit}{vs_suffix}') if ax_cus is not None: if title is not None: ax_cus.set_title(title) else: ax_cus.set_title(f'Custom Plot') if subtitle is not None: if '{common}' in subtitle: auto_subtitle = ', '.join(f'{k}={v}' for k, v in sorted(_common_json_properties(plotted_stats).items())) subtitle = subtitle.replace('{common}', auto_subtitle) for ax in axes: ax.set_title(ax.title.get_text() + '\n' + subtitle) if fig_size is None: fig.set_dpi(dpi) fig.set_size_inches(1000 * num_plots / dpi, 1000 / dpi) else: w, h = fig_size fig.set_dpi(dpi) fig.set_size_inches(w / dpi, h / dpi) fig.tight_layout() axs = [e for e in [ax_err, ax_dis] if e is not None] return fig, axs class _FieldToMetadataWrapper: def __init__(self, d: Dict): self.__private_d = d def __getattr__(self, item): if isinstance(self.__private_d, dict): return self.__private_d.get(item, None) return None def main_plot(*, command_line_args: List[str]): args = parse_args(command_line_args) total = ExistingData() for file in getattr(args, 'in'): total += ExistingData.from_file(file) if args.custom_error_count_keys: seen_keys = {k for stat in total.data.values() for k in stat.custom_counts} missing = [] for k in args.custom_error_count_keys: if k not in seen_keys: missing.append(k) if missing: print("Warning: the following custom error count keys didn't appear in any statistic:", file=sys.stderr) for k in sorted(missing): print(f' {k!r}', file=sys.stderr) print("Here are the keys that do appear:", file=sys.stderr) for k in sorted(seen_keys): print(f' {k!r}', file=sys.stderr) total.data = { s.strong_id: s for v in total.data.values() for s in v._split_custom_counts(args.custom_error_count_keys) } fig, _ = _plot_helper( samples=total, group_func=args.group_func, x_func=args.x_func, point_label_func=args.point_label_func, y_func=args.y_func, filter_func=args.filter_func, failure_units_per_shot_func=args.failure_units_per_shot_func, failure_values_func=args.failure_values_func, plot_args_func=args.plot_args_func, failure_unit=args.failure_unit_name, plot_types=args.type, xaxis=args.xaxis, yaxis=args.yaxis, fig_size=args.fig_size, min_y=args.ymin, max_y=args.ymax, max_x=args.xmax, min_x=args.xmin, highlight_max_likelihood_factor=args.highlight_max_likelihood_factor, title=args.title, subtitle=args.subtitle, line_fits=args.line_fits, preprocess_stats_func=args.preprocess_stats_func, dpi=args.dpi, ) if args.out is not None: fig.savefig(args.out, dpi=args.dpi) if args.show or args.out is None: plt.show() ================================================ FILE: glue/sample/src/sinter/_command/_main_plot_test.py ================================================ import contextlib import io import pathlib import tempfile import pytest from sinter._command._main import main from sinter._command._main_plot import _log_ticks, _sqrt_ticks def test_main_plot(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 300,1,20,1.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""path"":""a.stim""}" 300,100,200,2.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""path"":""a.stim""}" 9,5,4,6.0,pymatching,5fe5a6cd4226b1a910d57e5479d1ba6572e0b3115983c9516360916d1670000f,"{""path"":""b.stim""}" """.strip(), file=f) out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--x_func", "int('a' in metadata['path'])", "--group_func", "decoder", "--ymin", "1e-3", "--title", "test_plot", ]) assert (d / "output.png").exists() def test_main_plot_2(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 300,1,20,1.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""path"":""a.stim""}" 300,100,200,2.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""path"":""a.stim""}" 9,5,4,6.0,pymatching,5fe5a6cd4226b1a910d57e5479d1ba6572e0b3115983c9516360916d1670000f,"{""path"":""b.stim""}" """.strip(), file=f) out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--x_func", "int('a' in metadata['path'])", "--group_func", "decoder", "--plot_args_func", "{'lw': 2, 'color': 'r' if decoder == 'never' else 'b'}", ]) assert (d / "output.png").exists() def test_main_plot_failure_units(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 300,1,20,1.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""r"":15,""d"":5}" 300,100,200,2.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf7,"{""r"":9,""d"":3}" 9,5,4,6.0,pymatching,5fe5a6cd4226b1a910d57e5479d1ba6572e0b3115983c9516360916d1670000f,"{""r"":6,""d"":2}" """.strip(), file=f) out = io.StringIO() with pytest.raises(ValueError, match="specified together"): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--x_func", "metadata['d']", "--group_func", "decoder", "--failure_units_per_shot_func", "metadata['r']", ]) with pytest.raises(ValueError, match="specified together"): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--x_func", "metadata['d']", "--group_func", "decoder", "--failure_unit_name", "Rounds", ]) assert not (d / "output.png").exists() with contextlib.redirect_stdout(out): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--x_func", "metadata['d']", "--group_func", "decoder", "--failure_units_per_shot_func", "metadata['r']", "--failure_unit_name", "Rounds", ]) assert (d / "output.png").exists() def test_main_plot_xaxis(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 300,1,20,1.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""r"":15,""d"":5}" 300,100,200,2.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf7,"{""r"":9,""d"":3}" 9,5,4,6.0,pymatching,5fe5a6cd4226b1a910d57e5479d1ba6572e0b3115983c9516360916d1670000f,"{""r"":6,""d"":2}" """.strip(), file=f) out = io.StringIO() with contextlib.redirect_stdout(out): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--x_func", "metadata['d']", "--xaxis", "[sqrt]distance root", ]) assert (d / "output.png").exists() with contextlib.redirect_stdout(out): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output2.png"), "--x_func", "metadata['d']", "--xaxis", "[log]distance log", ]) assert (d / "output2.png").exists() with contextlib.redirect_stdout(out): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output3.png"), "--x_func", "metadata['d']", "--xaxis", "distance raw", ]) assert (d / "output3.png").exists() def test_main_plot_custom_y_func(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 300,1,20,1.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf6,"{""r"":15,""d"":5}" 300,100,200,2.0,pymatching,f256bab362f516ebe4d59a08ae67330ff7771ff738757cd738f4b30605ddccf7,"{""r"":9,""d"":3}" 9,5,4,6.0,pymatching,5fe5a6cd4226b1a910d57e5479d1ba6572e0b3115983c9516360916d1670000f,"{""r"":6,""d"":2}" """.strip(), file=f) out = io.StringIO() with pytest.raises(AttributeError, match="secondsX"): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--x_func", "metadata['d']", "--group_func", "decoder", "--y_func", "stat.secondsX", "--yaxis", "test axis" ]) assert not (d / "output.png").exists() with contextlib.redirect_stdout(out): main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--x_func", "metadata['d']", "--group_func", "decoder", "--y_func", "stat.seconds", "--yaxis", "test axis" ]) assert (d / "output.png").exists() def test_log_ticks(): assert _log_ticks(12, 499) == ( 10, 1000, [10, 100, 1000], [20, 30, 40, 50, 60, 70, 80, 90, 200, 300, 400, 500, 600, 700, 800, 900], ) assert _log_ticks(1.2, 4.9) == ( 1, 10, [1, 10], [2, 3, 4, 5, 6, 7, 8, 9], ) def test_sqrt_ticks(): assert _sqrt_ticks(12, 499) == ( 0, 500, [0, 100, 200, 300, 400, 500], [10*k for k in range(51)], ) assert _sqrt_ticks(105, 499) == ( 100, 500, [100, 200, 300, 400, 500], [10*k for k in range(10, 51)], ) assert _sqrt_ticks(305, 590) == ( 300, 600, [300, 350, 400, 450, 500, 550, 600], [10*k for k in range(30, 61)], ) assert _sqrt_ticks(305000, 590000) == ( 300000, 600000, [300000, 350000, 400000, 450000, 500000, 550000, 600000], [10000*k for k in range(30, 61)], ) def test_main_plot_degenerate_data(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 100, 0, 0, 1.00,magical,000000000,"{}" """.strip(), file=f) main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), ]) assert (d / "output.png").exists() def test_main_plot_degenerate_data_sqrt_axis(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 100, 0, 0, 1.00,magical,000000000,"{}" """.strip(), file=f) main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--xaxis", "[sqrt]x", ]) assert (d / "output.png").exists() def test_failure_values_func(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 1000, 400, 0, 1.00,magical,000000001,"{""f"":1}" 1000, 400, 0, 1.00,magical,000000002,"{""f"":2}" 1000, 400, 0, 1.00,magical,000000003,"{""f"":3}" 1000, 400, 0, 1.00,magical,000000005,"{""f"":5}" """.strip(), file=f) main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--xaxis", "values", "--x_func", "metadata['f']", "--subtitle", "test", "--failure_values_func", "metadata['f']", "--failure_units_per_shot_func", "100", "--failure_unit_name", "rounds", ]) assert (d / "output.png").exists() def test_m_fields(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 1000, 400, 0, 1.00,magical,000000001,"{""f"":1}" 1000, 400, 0, 1.00,magical,000000002,"{""f"":2}" 1000, 400, 0, 1.00,magical,000000003,"{""f"":3}" 1000, 400, 0, 1.00,magical,000000005,"{""f"":5}" """.strip(), file=f) main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--xaxis", "values", "--x_func", "m.f", "--group_func", "m.g", "--subtitle", "test", ]) assert (d / "output.png").exists() def test_split_custom_counts(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata,custom_counts 1000, 400, 0, 1.00,magical,000000001,"{""f"":1}", 1000, 400, 0, 1.00,magical,000000002,"{""f"":2}","{""a"":3}" 1000, 400, 0, 1.00,magical,000000003,"{""f"":3}","{""b"":3,""c"":4}" 1000, 400, 0, 1.00,magical,000000005,"{""f"":5}", """.strip(), file=f) main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--xaxis", "values", "--x_func", "m.f", "--group_func", "m.g", "--subtitle", "test", "--custom_error_count_keys", "a", "b", ]) assert (d / "output.png").exists() def test_line_fits(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata,custom_counts 1000, 400, 0, 1.00,magical,000000001,"{""a"":1,""b"":1}", 1000, 400, 0, 1.00,magical,000000002,"{""a"":2,""b"":1}" """.strip(), file=f) main(command_line_args=[ "plot", "--in", str(d / "input.csv"), "--out", str(d / "output.png"), "--xaxis", "values", "--x_func", "m.a", "--group_func", "m.b", "--xmin", "0", "--xmax", "10", "--line_fits" ]) assert (d / "output.png").exists() ================================================ FILE: glue/sample/src/sinter/_command/_main_predict.py ================================================ import argparse from typing import Any from typing import List from sinter._predict import predict_on_disk def parse_args(args: List[str]) -> Any: parser = argparse.ArgumentParser( description='Predict observable flips from detector data.', prog='sinter predict', ) parser.add_argument('--dets', type=str, required=True, help='File to read detection event data from.') parser.add_argument('--dets_format', type=str, required=True, help='Format detection event data is stored in.\n' 'For example: b8 or 01') parser.add_argument('--dem', type=str, required=True, help='File to read detector error model from.') parser.add_argument('--decoder', type=str, required=True, help='Decoder to use.') parser.add_argument('--obs_out', type=str, required=True, help='Location to write predictions from decoder.') parser.add_argument('--obs_out_format', type=str, required=True, help='Format to write predictions in.') parser.add_argument('--postselect_detectors_with_non_zero_4th_coord', help='Turns on postselection. ' 'If any detector with a non-zero 4th coordinate fires, the shot is discarded.', action='store_true') parser.add_argument('--discards_out', type=str, default=None, help='Location to write whether each shot should be discarded.' 'Specified if and only if --postselect_detectors_with_non_zero_4th_coord.') parser.add_argument('--discards_out_format', type=str, default=None, help='Format to write discard data in.' 'Specified if and only if --postselect_detectors_with_non_zero_4th_coord.') result = parser.parse_args(args) if result.postselect_detectors_with_non_zero_4th_coord and result.discards_out is None: raise ValueError("Must specify --discards_out to record results of --postselect_detectors_with_non_zero_4th_coord.") if result.discards_out is not None and result.discards_out_format is None: raise ValueError("Must specify --discards_out_format to specify how to record results of --discards_out.") return result def main_predict(*, command_line_args: List[str]): parsed = parse_args(command_line_args) predict_on_disk( decoder=parsed.decoder, dem_path=parsed.dem, dets_path=parsed.dets, dets_format=parsed.dets_format, obs_out_path=parsed.obs_out, obs_out_format=parsed.obs_out_format, postselect_detectors_with_non_zero_4th_coord=parsed.postselect_detectors_with_non_zero_4th_coord, discards_out_path=parsed.discards_out, discards_out_format=parsed.discards_out_format, ) ================================================ FILE: glue/sample/src/sinter/_command/_main_predict_test.py ================================================ import pathlib import tempfile from sinter._command._main import main def test_main_predict(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / f'input.dets', 'w') as f: print(""" shot D0 shot """, file=f) with open(d / f'input.dem', 'w') as f: print(""" error(0.1) D0 L0 """, file=f) main(command_line_args=[ "predict", "--dets", str(d / "input.dets"), "--dem", str(d / "input.dem"), "--decoder", "pymatching", "--dets_format", "dets", "--obs_out", str(d / "output.01"), "--obs_out_format", "01", ]) with open(d / 'output.01') as f: assert f.read() == '1\n0\n' ================================================ FILE: glue/sample/src/sinter/_data/__init__.py ================================================ from sinter._data._anon_task_stats import ( AnonTaskStats, ) from sinter._data._collection_options import ( CollectionOptions, ) from sinter._data._csv_out import ( CSV_HEADER, ) from sinter._data._existing_data import ( read_stats_from_csv_files, stats_from_csv_files, ExistingData, ) from sinter._data._task import ( Task, ) from sinter._data._task_stats import ( TaskStats, ) ================================================ FILE: glue/sample/src/sinter/_data/_anon_task_stats.py ================================================ import collections import dataclasses from typing import Counter, Union, TYPE_CHECKING import numpy as np if TYPE_CHECKING: from sinter._data._task_stats import TaskStats @dataclasses.dataclass(frozen=True) class AnonTaskStats: """Statistics sampled from an unspecified task. Attributes: shots: Number of times the task was sampled. errors: Number of times a sample resulted in an error. discards: Number of times a sample resulted in a discard. Note that discarded a task is not an error. seconds: The amount of CPU core time spent sampling the tasks, in seconds. custom_counts: A counter mapping string keys to integer values. Used for tracking arbitrary values, such as per-observable error counts or the number of times detectors fired. The meaning of the information in the counts is not specified; the only requirement is that it should be correct to add each key's counts when merging statistics. Although this field is an editable object, it's invalid to edit the counter after the stats object is initialized. """ shots: int = 0 errors: int = 0 discards: int = 0 seconds: float = 0 custom_counts: Counter[str] = dataclasses.field(default_factory=collections.Counter) def __post_init__(self): assert isinstance(self.errors, (int, np.integer)) assert isinstance(self.shots, (int, np.integer)) assert isinstance(self.discards, (int, np.integer)) assert isinstance(self.seconds, (int, float, np.integer, np.floating)) assert isinstance(self.custom_counts, collections.Counter) assert self.errors >= 0 assert self.discards >= 0 assert self.seconds >= 0 assert self.shots >= self.errors + self.discards assert all(isinstance(k, str) and isinstance(v, (int, np.integer)) for k, v in self.custom_counts.items()) def __repr__(self) -> str: terms = [] if self.shots != 0: terms.append(f'shots={self.shots!r}') if self.errors != 0: terms.append(f'errors={self.errors!r}') if self.discards != 0: terms.append(f'discards={self.discards!r}') if self.seconds != 0: terms.append(f'seconds={self.seconds!r}') if self.custom_counts: terms.append(f'custom_counts={self.custom_counts!r}') return f'sinter.AnonTaskStats({", ".join(terms)})' def __add__(self, other: 'AnonTaskStats') -> 'AnonTaskStats': """Returns the sum of the statistics from both anonymous stats. Adds the shots, the errors, the discards, and the seconds. Examples: >>> import sinter >>> a = sinter.AnonTaskStats( ... shots=100, ... errors=20, ... ) >>> b = sinter.AnonTaskStats( ... shots=1000, ... errors=200, ... ) >>> a + b sinter.AnonTaskStats(shots=1100, errors=220) """ if isinstance(other, AnonTaskStats): return AnonTaskStats( shots=self.shots + other.shots, errors=self.errors + other.errors, discards=self.discards + other.discards, seconds=self.seconds + other.seconds, custom_counts=self.custom_counts + other.custom_counts, ) return NotImplemented ================================================ FILE: glue/sample/src/sinter/_data/_anon_task_stats_test.py ================================================ import collections import sinter def test_repr(): v = sinter.AnonTaskStats(shots=22, errors=3, discards=4, seconds=5) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.AnonTaskStats() assert eval(repr(v), {"sinter": sinter}) == v v = sinter.AnonTaskStats(shots=22) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.AnonTaskStats(shots=21, errors=4) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.AnonTaskStats(shots=21, discards=4) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.AnonTaskStats(seconds=4) assert eval(repr(v), {"sinter": sinter}) == v def test_add(): a0 = sinter.AnonTaskStats(shots=220, errors=30, discards=40, seconds=50) b0 = sinter.AnonTaskStats(shots=50, errors=4, discards=3, seconds=2) assert a0 + b0 == sinter.AnonTaskStats(shots=270, errors=34, discards=43, seconds=52) assert a0 + sinter.AnonTaskStats() == a0 a = sinter.AnonTaskStats(shots=220, errors=30, discards=40, seconds=50, custom_counts=collections.Counter({'a': 10, 'b': 20})) b = sinter.AnonTaskStats(shots=50, errors=4, discards=3, seconds=2, custom_counts=collections.Counter({'a': 1, 'c': 3})) assert a + b == sinter.AnonTaskStats(shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 11, 'b': 20, 'c': 3})) assert a + sinter.AnonTaskStats() == a assert sinter.AnonTaskStats() + b == b assert a + b0 == sinter.AnonTaskStats(shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 10, 'b': 20})) assert a0 + b == sinter.AnonTaskStats(shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 1, 'c': 3})) ================================================ FILE: glue/sample/src/sinter/_data/_collection_options.py ================================================ import dataclasses from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: import sinter @dataclasses.dataclass(frozen=True) class CollectionOptions: """Describes options for how data is collected for a decoding problem. Attributes: max_shots: Defaults to None (unused). Stops the sampling process after this many samples have been taken from the circuit. max_errors: Defaults to None (unused). Stops the sampling process after this many errors have been seen in samples taken from the circuit. The actual number sampled errors may be larger due to batching. start_batch_size: Defaults to None (collector's choice). The very first shots taken from the circuit will use a batch of this size, and no other batches will be taken in parallel. Once this initial fact finding batch is done, batches can be taken in parallel and the normal batch size limiting processes take over. max_batch_size: Defaults to None (unused). Limits batches from taking more than this many shots at once. For example, this can be used to ensure memory usage stays below some limit. max_batch_seconds: Defaults to None (unused). When set, the recorded data from previous shots is used to estimate how much time is taken per shot. This information is then used to predict the biggest batch size that can finish in under the given number of seconds. Limits each batch to be no larger than that. """ max_shots: Optional[int] = None max_errors: Optional[int] = None start_batch_size: Optional[int] = None max_batch_size: Optional[int] = None max_batch_seconds: Optional[float] = None def __post_init__(self): if self.max_shots is not None and self.max_shots < 0: raise ValueError(f'max_shots is not None and max_shots={self.max_shots} < 0') if self.max_errors is not None and self.max_errors < 0: raise ValueError(f'max_errors is not None and max_errors={self.max_errors} < 0') if self.start_batch_size is not None and self.start_batch_size <= 0: raise ValueError(f'start_batch_size is not None and start_batch_size={self.start_batch_size} <= 0') if self.max_batch_size is not None and self.max_batch_size <= 0: raise ValueError( f'max_batch_size={self.max_batch_size} is not None and max_batch_size <= 0') if self.max_batch_seconds is not None and self.max_batch_seconds <= 0: raise ValueError( f'max_batch_seconds={self.max_batch_seconds} is not None and max_batch_seconds <= 0') def __repr__(self) -> str: terms = [] if self.max_shots is not None: terms.append(f'max_shots={self.max_shots!r}') if self.max_errors is not None: terms.append(f'max_errors={self.max_errors!r}') if self.start_batch_size is not None: terms.append(f'start_batch_size={self.start_batch_size!r}') if self.max_batch_size is not None: terms.append(f'max_batch_size={self.max_batch_size!r}') if self.max_batch_seconds is not None: terms.append(f'max_batch_seconds={self.max_batch_seconds!r}') return f'sinter.CollectionOptions({", ".join(terms)})' def combine(self, other: 'sinter.CollectionOptions') -> 'sinter.CollectionOptions': """Returns a combination of multiple collection options. All fields are combined by taking the minimum from both collection options objects, with None treated as being infinitely large. Args: other: The collections options to combine with. Returns: The combined collection options. Examples: >>> import sinter >>> a = sinter.CollectionOptions( ... max_shots=1_000_000, ... start_batch_size=100, ... ) >>> b = sinter.CollectionOptions( ... max_shots=100_000, ... max_errors=100, ... ) >>> a.combine(b) sinter.CollectionOptions(max_shots=100000, max_errors=100, start_batch_size=100) """ return CollectionOptions( max_shots=nullable_min(self.max_shots, other.max_shots), max_errors=nullable_min(self.max_errors, other.max_errors), start_batch_size=nullable_min(self.start_batch_size, other.start_batch_size), max_batch_size=nullable_min(self.max_batch_size, other.max_batch_size), max_batch_seconds=nullable_min(self.max_batch_seconds, other.max_batch_seconds)) def nullable_min(a: Optional[int], b: Optional[int]) -> Optional[int]: if a is None: return b if b is None: return a return min(a, b) ================================================ FILE: glue/sample/src/sinter/_data/_collection_options_test.py ================================================ import sinter def test_repr(): v = sinter.CollectionOptions() assert eval(repr(v), {"sinter": sinter}) == v v = sinter.CollectionOptions(max_shots=100) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.CollectionOptions(max_errors=100) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.CollectionOptions(start_batch_size=10) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.CollectionOptions(max_batch_size=100) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.CollectionOptions(max_batch_seconds=30) assert eval(repr(v), {"sinter": sinter}) == v v = sinter.CollectionOptions(max_shots=100, max_errors=90, start_batch_size=80, max_batch_size=200, max_batch_seconds=30) assert eval(repr(v), {"sinter": sinter}) == v def test_combine(): a = sinter.CollectionOptions(max_shots=200, max_batch_seconds=300) b = sinter.CollectionOptions(max_errors=100, max_batch_seconds=400) assert a.combine(b) == sinter.CollectionOptions(max_errors=100, max_shots=200, max_batch_seconds=300) ================================================ FILE: glue/sample/src/sinter/_data/_csv_out.py ================================================ import collections import csv import io import json from typing import Any, Optional def escape_csv(text: Any, width: Optional[int]) -> str: output = io.StringIO() csv.writer(output).writerow([text]) text = output.getvalue().strip() if width is not None: text = text.rjust(width) return text def csv_line(*, shots: Any, errors: Any, discards: Any, seconds: Any, decoder: Any, strong_id: Any, json_metadata: Any, custom_counts: Any, is_header: bool = False) -> str: if isinstance(seconds, float): if seconds < 1: seconds = f'{seconds:0.3f}' elif seconds < 10: seconds = f'{seconds:0.2f}' else: seconds = f'{seconds:0.1f}' if not is_header: json_metadata = json.dumps(json_metadata, separators=(',', ':'), sort_keys=True) if custom_counts: # Ensure all custom_counts values are integers before dumping to JSON for k in custom_counts: custom_counts[k] = int(custom_counts[k]) custom_counts = escape_csv( json.dumps(custom_counts, separators=(',', ':'), sort_keys=True), None) else: custom_counts = '' shots = escape_csv(shots, 10) if isinstance(errors, (dict, collections.Counter)): errors = json.dumps(errors, separators=(',', ':'), sort_keys=True) errors = escape_csv(errors, 10) discards = escape_csv(discards, 10) seconds = escape_csv(seconds, 8) decoder = escape_csv(decoder, None) strong_id = escape_csv(strong_id, None) json_metadata = escape_csv(json_metadata, None) return (f'{shots},' f'{errors},' f'{discards},' f'{seconds},' f'{decoder},' f'{strong_id},' f'{json_metadata},' f'{custom_counts}') CSV_HEADER = csv_line(shots='shots', errors='errors', discards='discards', seconds='seconds', strong_id='strong_id', decoder='decoder', json_metadata='json_metadata', custom_counts='custom_counts', is_header=True) ================================================ FILE: glue/sample/src/sinter/_data/_existing_data.py ================================================ import collections import json import pathlib from typing import Any, Dict, List, TYPE_CHECKING from sinter._data._task_stats import TaskStats from sinter._data._task import Task from sinter._data._anon_task_stats import AnonTaskStats if TYPE_CHECKING: import sinter class ExistingData: def __init__(self): self.data: Dict[str, TaskStats] = {} def stats_for(self, case: Task) -> AnonTaskStats: if isinstance(case, Task): key = case.strong_id() else: raise NotImplementedError(f'{type(case)}') if key not in self.data: return AnonTaskStats() return self.data[key].to_anon_stats() def add_sample(self, sample: TaskStats) -> None: k = sample.strong_id current = self.data.get(k) if current is not None: self.data[k] = current + sample else: self.data[k] = sample def __iadd__(self, other: 'ExistingData') -> 'ExistingData': for sample in other.data.values(): self.add_sample(sample) return self @staticmethod def from_file(path_or_file: Any) -> 'ExistingData': expected_fields = { "shots", "discards", "errors", "seconds", "strong_id", "decoder", "json_metadata", } # Import is done locally to reduce cost of importing sinter. import csv if isinstance(path_or_file, (str, pathlib.Path)): with open(path_or_file) as csvfile: return ExistingData.from_file(csvfile) reader = csv.DictReader(path_or_file) reader.fieldnames = [e.strip() for e in reader.fieldnames] actual_fields = set(reader.fieldnames) if not (expected_fields <= actual_fields): raise ValueError( f"Bad CSV data. " f"Got columns {sorted(actual_fields)!r} " f"but expected columns {sorted(expected_fields)!r}") has_custom_counts = 'custom_counts' in actual_fields result = ExistingData() for row in reader: if has_custom_counts: custom_counts = row['custom_counts'] if custom_counts is None or custom_counts == '': custom_counts = collections.Counter() else: custom_counts = json.loads(custom_counts) if not isinstance(custom_counts, dict) or not all(isinstance(k, str) or not isinstance(v, int) for k, v in custom_counts.items()): raise ValueError(f"{row['custom_counts']=} isn't empty or a dictionary from string keys to integer values.") custom_counts = collections.Counter(custom_counts) else: custom_counts = collections.Counter() result.add_sample(TaskStats( shots=int(row['shots']), discards=int(row['discards']), errors=int(row['errors']), custom_counts=custom_counts, seconds=float(row['seconds']), strong_id=row['strong_id'], decoder=row['decoder'], json_metadata=json.loads(row['json_metadata']), )) return result def stats_from_csv_files(*paths_or_files: Any) -> List['sinter.TaskStats']: """Reads and aggregates shot statistics from CSV files. (An old alias of `read_stats_from_csv_files`, kept around for backwards compatibility.) Assumes the CSV file was written by printing `sinter.CSV_HEADER` and then a list of `sinter.TaskStats`. When statistics from the same task appear in multiple files (identified by the strong id being the same), the statistics for that task are folded together (so only the total shots, total errors, etc for each task are included in the results). Args: *paths_or_files: Each argument should be either a path (in the form of a string or a pathlib.Path) or a TextIO object (e.g. as returned by `open`). File data is read from each argument. Returns: A list of task stats, where each task appears only once in the list and the stats associated with it are the totals aggregated from all files. Examples: >>> import sinter >>> import io >>> in_memory_file = io.StringIO() >>> _ = in_memory_file.write(''' ... shots,errors,discards,seconds,decoder,strong_id,json_metadata ... 1000,42,0,0.125,pymatching,9c31908e2b,"{""d"":9}" ... 3000,24,0,0.125,pymatching,9c31908e2b,"{""d"":9}" ... 1000,250,0,0.125,pymatching,deadbeef08,"{""d"":7}" ... '''.strip()) >>> _ = in_memory_file.seek(0) >>> stats = sinter.stats_from_csv_files(in_memory_file) >>> for stat in stats: ... print(repr(stat)) sinter.TaskStats(strong_id='9c31908e2b', decoder='pymatching', json_metadata={'d': 9}, shots=4000, errors=66, seconds=0.25) sinter.TaskStats(strong_id='deadbeef08', decoder='pymatching', json_metadata={'d': 7}, shots=1000, errors=250, seconds=0.125) """ result = ExistingData() for p in paths_or_files: result += ExistingData.from_file(p) return list(result.data.values()) def read_stats_from_csv_files(*paths_or_files: Any) -> List['sinter.TaskStats']: """Reads and aggregates shot statistics from CSV files. Assumes the CSV file was written by printing `sinter.CSV_HEADER` and then a list of `sinter.TaskStats`. When statistics from the same task appear in multiple files (identified by the strong id being the same), the statistics for that task are folded together (so only the total shots, total errors, etc for each task are included in the results). Args: *paths_or_files: Each argument should be either a path (in the form of a string or a pathlib.Path) or a TextIO object (e.g. as returned by `open`). File data is read from each argument. Returns: A list of task stats, where each task appears only once in the list and the stats associated with it are the totals aggregated from all files. Examples: >>> import sinter >>> import io >>> in_memory_file = io.StringIO() >>> _ = in_memory_file.write(''' ... shots,errors,discards,seconds,decoder,strong_id,json_metadata ... 1000,42,0,0.125,pymatching,9c31908e2b,"{""d"":9}" ... 3000,24,0,0.125,pymatching,9c31908e2b,"{""d"":9}" ... 1000,250,0,0.125,pymatching,deadbeef08,"{""d"":7}" ... '''.strip()) >>> _ = in_memory_file.seek(0) >>> stats = sinter.read_stats_from_csv_files(in_memory_file) >>> for stat in stats: ... print(repr(stat)) sinter.TaskStats(strong_id='9c31908e2b', decoder='pymatching', json_metadata={'d': 9}, shots=4000, errors=66, seconds=0.25) sinter.TaskStats(strong_id='deadbeef08', decoder='pymatching', json_metadata={'d': 7}, shots=1000, errors=250, seconds=0.125) """ result = ExistingData() for p in paths_or_files: result += ExistingData.from_file(p) return list(result.data.values()) ================================================ FILE: glue/sample/src/sinter/_data/_existing_data_test.py ================================================ import collections import pathlib import tempfile import sinter def test_read_stats_from_csv_files(): with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) with open(d / 'tmp.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 300, 1, 20, 1.0,pymatching,abc123,"{""d"":3}" 1000, 3, 40, 3.0,pymatching,abc123,"{""d"":3}" 2000, 0, 10, 2.0,pymatching,def456,"{""d"":5}" """.strip(), file=f) assert sinter.read_stats_from_csv_files(d / 'tmp.csv') == [ sinter.TaskStats(strong_id='abc123', decoder='pymatching', json_metadata={'d': 3}, shots=1300, errors=4, discards=60, seconds=4.0), sinter.TaskStats(strong_id='def456', decoder='pymatching', json_metadata={'d': 5}, shots=2000, errors=0, discards=10, seconds=2.0), ] with open(d / 'tmp2.csv', 'w') as f: print(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata,custom_counts 300, 1, 20, 1.0,pymatching,abc123,"{""d"":3}","{""dets"":1234}" 1000, 3, 40, 3.0,pymatching,abc123,"{""d"":3}", 2000, 0, 10, 2.0,pymatching,def456,"{""d"":5}" """.strip(), file=f) assert sinter.read_stats_from_csv_files(d / 'tmp2.csv') == [ sinter.TaskStats(strong_id='abc123', decoder='pymatching', json_metadata={'d': 3}, shots=1300, errors=4, discards=60, seconds=4.0, custom_counts=collections.Counter({'dets': 1234})), sinter.TaskStats(strong_id='def456', decoder='pymatching', json_metadata={'d': 5}, shots=2000, errors=0, discards=10, seconds=2.0), ] assert sinter.read_stats_from_csv_files(d / 'tmp.csv', d / 'tmp2.csv') == [ sinter.TaskStats(strong_id='abc123', decoder='pymatching', json_metadata={'d': 3}, shots=2600, errors=8, discards=120, seconds=8.0, custom_counts=collections.Counter({'dets': 1234})), sinter.TaskStats(strong_id='def456', decoder='pymatching', json_metadata={'d': 5}, shots=4000, errors=0, discards=20, seconds=4.0), ] ================================================ FILE: glue/sample/src/sinter/_data/_task.py ================================================ import pathlib from typing import Any, Dict, Optional, TYPE_CHECKING import hashlib import json import math from typing import Union import numpy as np from sinter._data._collection_options import CollectionOptions if TYPE_CHECKING: import sinter import stim class Task: """A decoding problem that sinter can sample from. Attributes: circuit: The annotated noisy circuit to sample detection event data and logical observable data form. decoder: The decoder to use to predict the logical observable data from the detection event data. This can be set to None if it will be specified later (e.g. by the call to `collect`). detector_error_model: Specifies the error model to give to the decoder. Defaults to None, indicating that it should be automatically derived using `stim.Circuit.detector_error_model`. postselection_mask: Defaults to None (unused). A bit packed bitmask identifying detectors that must not fire. Shots where the indicated detectors fire are discarded. postselected_observables_mask: Defaults to None (unused). A bit packed bitmask identifying observable indices to postselect on. Anytime the decoder's predicted flip for one of these observables doesn't agree with the actual measured flip value of the observable, the shot is discarded instead of counting as an error. json_metadata: Defaults to None. Custom additional data describing the problem. Must be JSON serializable. For example, this could be a dictionary with "physical_error_rate" and "code_distance" keys. collection_options: Specifies custom options for collecting this single task. These options are merged with the global options to determine what happens. For example, if a task has `collection_options` set to `sinter.CollectionOptions(max_shots=1000, max_errors=100)` and `sinter.collect` was called with `max_shots=500` and `max_errors=200`, then either 500 shots or 100 errors will be collected for the task (whichever comes first). Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit.generated( ... 'repetition_code:memory', ... rounds=10, ... distance=10, ... before_round_data_depolarization=1e-3, ... ), ... ) """ def __init__( self, *, circuit: Optional['stim.Circuit'] = None, decoder: Optional[str] = None, detector_error_model: Optional['stim.DetectorErrorModel'] = None, postselection_mask: Optional[np.ndarray] = None, postselected_observables_mask: Optional[np.ndarray] = None, json_metadata: Any = None, collection_options: 'sinter.CollectionOptions' = CollectionOptions(), skip_validation: bool = False, circuit_path: Optional[Union[str, pathlib.Path]] = None, _unvalidated_strong_id: Optional[str] = None, ) -> None: """ Args: circuit: The annotated noisy circuit to sample detection event data and logical observable data form. decoder: The decoder to use to predict the logical observable data from the detection event data. This can be set to None if it will be specified later (e.g. by the call to `collect`). detector_error_model: Specifies the error model to give to the decoder. Defaults to None, indicating that it should be automatically derived using `stim.Circuit.detector_error_model`. postselection_mask: Defaults to None (unused). A bit packed bitmask identifying detectors that must not fire. Shots where the indicated detectors fire are discarded. postselected_observables_mask: Defaults to None (unused). A bit packed bitmask identifying observable indices to postselect on. Anytime the decoder's predicted flip for one of these observables doesn't agree with the actual measured flip value of the observable, the shot is discarded instead of counting as an error. json_metadata: Defaults to None. Custom additional data describing the problem. Must be JSON serializable. For example, this could be a dictionary with "physical_error_rate" and "code_distance" keys. collection_options: Specifies custom options for collecting this single task. These options are merged with the global options to determine what happens. For example, if a task has `collection_options` set to `sinter.CollectionOptions(max_shots=1000, max_errors=100)` and `sinter.collect` was called with `max_shots=500` and `max_errors=200`, then either 500 shots or 100 errors will be collected for the task (whichever comes first). skip_validation: Defaults to False. Normally the arguments given to this method are checked for consistency (e.g. the detector error model should have the same number of detectors as the circuit). Setting this argument to True will skip doing the consistency checks. Note that this can result in confusing errors later, if the arguments are not actually consistent. circuit_path: Typically set to None. If the circuit isn't specified, this is the filepath to read it from. Not included in the strong id. _unvalidated_strong_id: Must be set to None unless `skip_validation` is set to True. Otherwise, if this is specified then it should be equal to the value returned by self.strong_id(). """ if not skip_validation: if circuit_path is None and circuit is None: raise ValueError('circuit_path is None and circuit is None') if _unvalidated_strong_id is not None: raise ValueError("_unvalidated_strong_id is not None and not skip_validation") dem = detector_error_model if circuit is not None: num_dets = circuit.num_detectors num_obs = circuit.num_observables if dem is not None: if circuit.num_detectors != dem.num_detectors: raise ValueError(f"circuit.num_detectors={num_dets!r} != detector_error_model.num_detectors={dem.num_detectors!r}") if num_obs != dem.num_observables: raise ValueError(f"circuit.num_observables={num_obs!r} != detector_error_model.num_observables={dem.num_observables!r}") if postselection_mask is not None: shape = (math.ceil(num_dets / 8),) if postselection_mask.shape != shape: raise ValueError(f"postselection_mask.shape={postselection_mask.shape!r} != (math.ceil(circuit.num_detectors / 8),)={shape!r}") if postselected_observables_mask is not None: shape = (math.ceil(num_obs / 8),) if postselected_observables_mask.shape != shape: raise ValueError(f"postselected_observables_mask.shape={postselected_observables_mask.shape!r} != (math.ceil(circuit.num_observables / 8),)={shape!r}") if postselection_mask is not None: if not isinstance(postselection_mask, np.ndarray): raise ValueError(f"not isinstance(postselection_mask={postselection_mask!r}, np.ndarray)") if postselection_mask.dtype != np.uint8: raise ValueError(f"postselection_mask.dtype={postselection_mask.dtype!r} != np.uint8") if postselected_observables_mask is not None: if not isinstance(postselected_observables_mask, np.ndarray): raise ValueError(f"not isinstance(postselected_observables_mask={postselected_observables_mask!r}, np.ndarray)") if postselected_observables_mask.dtype != np.uint8: raise ValueError(f"postselected_observables_mask.dtype={postselected_observables_mask.dtype!r} != np.uint8") self.circuit_path = None if circuit_path is None else pathlib.Path(circuit_path) self.circuit = circuit self.decoder = decoder self.detector_error_model = detector_error_model self.postselection_mask = postselection_mask self.postselected_observables_mask = postselected_observables_mask self.json_metadata = json_metadata self.collection_options = collection_options self._unvalidated_strong_id = _unvalidated_strong_id def strong_id_value(self) -> Dict[str, Any]: """Contains all raw values that affect the strong id. This value is converted into the actual strong id by: - Serializing it into text using JSON. - Serializing the JSON text into bytes using UTF8. - Hashing the UTF8 bytes using SHA256. Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit('H 0'), ... detector_error_model=stim.DetectorErrorModel(), ... decoder='pymatching', ... ) >>> task.strong_id_value() {'circuit': 'H 0', 'decoder': 'pymatching', 'decoder_error_model': '', 'postselection_mask': None, 'json_metadata': None} """ if self.circuit is None: raise ValueError("Can't compute strong_id until `circuit` is set.") if self.decoder is None: raise ValueError("Can't compute strong_id until `decoder` is set.") if self.detector_error_model is None: raise ValueError("Can't compute strong_id until `detector_error_model` is set.") result = { 'circuit': str(self.circuit), 'decoder': self.decoder, 'decoder_error_model': str(self.detector_error_model), 'postselection_mask': None if self.postselection_mask is None else [int(e) for e in self.postselection_mask], 'json_metadata': self.json_metadata, } if self.postselected_observables_mask is not None: result['postselected_observables_mask'] = [int(e) for e in self.postselected_observables_mask] return result def strong_id_text(self) -> str: """The text that is serialized and hashed to get the strong id. This value is converted into the actual strong id by: - Serializing into bytes using UTF8. - Hashing the UTF8 bytes using SHA256. Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit('H 0'), ... detector_error_model=stim.DetectorErrorModel(), ... decoder='pymatching', ... ) >>> task.strong_id_text() '{"circuit": "H 0", "decoder": "pymatching", "decoder_error_model": "", "postselection_mask": null, "json_metadata": null}' """ return json.dumps(self.strong_id_value()) def strong_id_bytes(self) -> bytes: """The bytes that are hashed to get the strong id. This value is converted into the actual strong id by: - Hashing these bytes using SHA256. Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit('H 0'), ... detector_error_model=stim.DetectorErrorModel(), ... decoder='pymatching', ... ) >>> task.strong_id_bytes() b'{"circuit": "H 0", "decoder": "pymatching", "decoder_error_model": "", "postselection_mask": null, "json_metadata": null}' """ return self.strong_id_text().encode('utf8') def _recomputed_strong_id(self) -> str: return hashlib.sha256(self.strong_id_bytes()).hexdigest() def strong_id(self) -> str: """Computes a cryptographically unique identifier for this task. This value is affected by: - The exact circuit. - The exact detector error model. - The decoder. - The json metadata. - The postselection mask. Examples: >>> import sinter >>> import stim >>> task = sinter.Task( ... circuit=stim.Circuit(), ... detector_error_model=stim.DetectorErrorModel(), ... decoder='pymatching', ... ) >>> task.strong_id() '7424ea021693d4abc1c31c12e655a48779f61a7c2969e457ae4fe400c852bee5' """ if self._unvalidated_strong_id is None: self._unvalidated_strong_id = self._recomputed_strong_id() return self._unvalidated_strong_id def __repr__(self) -> str: terms = [] if self.circuit is not None: terms.append(f'circuit={self.circuit!r}') if self.decoder is not None: terms.append(f'decoder={self.decoder!r}') if self.detector_error_model is not None: terms.append(f'detector_error_model={self.detector_error_model!r}') if self.postselection_mask is not None: nd = self.circuit.num_detectors bits = list(np.unpackbits(self.postselection_mask, count=nd, bitorder='little')) terms.append(f'''postselection_mask=np.packbits({bits!r}, bitorder='little')''') if self.postselected_observables_mask is not None: no = self.circuit.num_observables bits = list(np.unpackbits(self.postselected_observables_mask, count=no, bitorder='little')) terms.append(f'''postselected_observables_mask=np.packbits({bits!r}, bitorder='little')''') if self.json_metadata is not None: terms.append(f'json_metadata={self.json_metadata!r}') if self.collection_options != CollectionOptions(): terms.append(f'collection_options={self.collection_options!r}') if self.circuit_path is not None: terms.append(f'circuit_path={self.circuit_path!r}') return f'sinter.Task({", ".join(terms)})' def __eq__(self, other: Any) -> bool: if not isinstance(other, Task): return NotImplemented if self._unvalidated_strong_id is not None and other._unvalidated_strong_id is not None: return self._unvalidated_strong_id == other._unvalidated_strong_id return ( self.circuit_path == other.circuit_path and self.circuit == other.circuit and self.decoder == other.decoder and self.detector_error_model == other.detector_error_model and np.array_equal(self.postselection_mask, other.postselection_mask) and np.array_equal(self.postselected_observables_mask, other.postselected_observables_mask) and self.json_metadata == other.json_metadata and self.collection_options == other.collection_options ) ================================================ FILE: glue/sample/src/sinter/_data/_task_stats.py ================================================ import collections import dataclasses from typing import Counter, List, Any from typing import Optional from typing import Union from typing import overload import numpy as np from sinter._data._anon_task_stats import AnonTaskStats from sinter._data._csv_out import csv_line def _is_equal_json_values(json1: Any, json2: Any): if json1 == json2: return True if type(json1) == type(json2): if isinstance(json1, dict): return json1.keys() == json2.keys() and all(_is_equal_json_values(json1[k], json2[k]) for k in json1.keys()) elif isinstance(json1, (list, tuple)): return len(json1) == len(json2) and all(_is_equal_json_values(a, b) for a, b in zip(json1, json2)) elif isinstance(json1, (list, tuple)) and isinstance(json2, (list, tuple)): return _is_equal_json_values(tuple(json1), tuple(json2)) return False @dataclasses.dataclass(frozen=True) class TaskStats: """Statistics sampled from a task. The rows in the CSV files produced by sinter correspond to instances of `sinter.TaskStats`. For example, a row can be produced by printing a `sinter.TaskStats`. Attributes: strong_id: The cryptographically unique identifier of the task, from `sinter.Task.strong_id()`. decoder: The name of the decoder that was used to decode the task. Errors are counted when this decoder made a wrong prediction. json_metadata: A JSON-encodable value (such as a dictionary from strings to integers) that were included with the task in order to describe what the task was. This value can be a huge variety of things, but typically it will be a dictionary with fields such as 'd' for the code distance. shots: Number of times the task was sampled. errors: Number of times a sample resulted in an error. discards: Number of times a sample resulted in a discard. Note that discarded a task is not an error. seconds: The amount of CPU core time spent sampling the tasks, in seconds. custom_counts: A counter mapping string keys to integer values. Used for tracking arbitrary values, such as per-observable error counts or the number of times detectors fired. The meaning of the information in the counts is not specified; the only requirement is that it should be correct to add each key's counts when merging statistics. Although this field is an editable object, it's invalid to edit the counter after the stats object is initialized. """ # Information describing the problem that was sampled. strong_id: str decoder: str json_metadata: Any # Information describing the results of sampling. shots: int = 0 errors: int = 0 discards: int = 0 seconds: float = 0 custom_counts: Counter[str] = dataclasses.field(default_factory=collections.Counter) def __post_init__(self): assert isinstance(self.errors, (int, np.integer)) assert isinstance(self.shots, (int, np.integer)) assert isinstance(self.discards, (int, np.integer)) assert isinstance(self.seconds, (int, float, np.integer, np.floating)) assert isinstance(self.custom_counts, collections.Counter) assert isinstance(self.decoder, str) assert isinstance(self.strong_id, str) assert self.json_metadata is None or isinstance(self.json_metadata, (int, float, str, dict, list, tuple)) assert self.errors >= 0 assert self.discards >= 0 assert self.seconds >= 0 assert self.shots >= self.errors + self.discards assert all(isinstance(k, str) and isinstance(v, (int, np.integer)) for k, v in self.custom_counts.items()) def with_edits( self, *, strong_id: Optional[str] = None, decoder: Optional[str] = None, json_metadata: Optional[Any] = None, shots: Optional[int] = None, errors: Optional[int] = None, discards: Optional[int] = None, seconds: Optional[float] = None, custom_counts: Optional[Counter[str]] = None, ) -> 'TaskStats': return TaskStats( strong_id=self.strong_id if strong_id is None else strong_id, decoder=self.decoder if decoder is None else decoder, json_metadata=self.json_metadata if json_metadata is None else json_metadata, shots=self.shots if shots is None else shots, errors=self.errors if errors is None else errors, discards=self.discards if discards is None else discards, seconds=self.seconds if seconds is None else seconds, custom_counts=self.custom_counts if custom_counts is None else custom_counts, ) @overload def __add__(self, other: AnonTaskStats) -> AnonTaskStats: pass @overload def __add__(self, other: 'TaskStats') -> 'TaskStats': pass def __add__(self, other: Union[AnonTaskStats, 'TaskStats']) -> Union[AnonTaskStats, 'TaskStats']: if isinstance(other, AnonTaskStats): return self.to_anon_stats() + other if isinstance(other, TaskStats): if self.strong_id != other.strong_id: raise ValueError(f'{self.strong_id=} != {other.strong_id=}') if not _is_equal_json_values(self.json_metadata, other.json_metadata) or self.decoder != other.decoder: raise ValueError( "A stat had the same strong id as another, but their other identifying information (json_metadata, decoder) differed.\n" "The strong id is supposed to be a cryptographic hash that uniquely identifies what was sampled, so this is an error.\n" "\n" "This failure can occur when post-processing data (e.g. combining X basis stats and Z basis stats into synthetic both-basis stats).\n" "To fix it, ensure any post-processing sets the strong id of the synthetic data in some cryptographically secure way.\n" "\n" "In some cases this can be caused by attempting to add a value that has gone through JSON serialization+parsing to one\n" "that hasn't, which causes things like tuples transforming into lists.\n" "\n" f"The two stats:\n1. {self!r}\n2. {other!r}") total = self.to_anon_stats() + other.to_anon_stats() return TaskStats( decoder=self.decoder, strong_id=self.strong_id, json_metadata=self.json_metadata, shots=total.shots, errors=total.errors, discards=total.discards, seconds=total.seconds, custom_counts=total.custom_counts, ) return NotImplemented __radd__ = __add__ def to_anon_stats(self) -> AnonTaskStats: """Returns a `sinter.AnonTaskStats` with the same statistics. Examples: >>> import sinter >>> stat = sinter.TaskStats( ... strong_id='test', ... json_metadata={'a': [1, 2, 3]}, ... decoder='pymatching', ... shots=22, ... errors=3, ... discards=4, ... seconds=5, ... ) >>> stat.to_anon_stats() sinter.AnonTaskStats(shots=22, errors=3, discards=4, seconds=5) """ return AnonTaskStats( shots=self.shots, errors=self.errors, discards=self.discards, seconds=self.seconds, custom_counts=self.custom_counts.copy(), ) def to_csv_line(self) -> str: """Converts into a line that can be printed into a CSV file. Examples: >>> import sinter >>> stat = sinter.TaskStats( ... strong_id='test', ... json_metadata={'a': [1, 2, 3]}, ... decoder='pymatching', ... shots=22, ... errors=3, ... seconds=5, ... ) >>> print(sinter.CSV_HEADER) shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts >>> print(stat.to_csv_line()) 22, 3, 0, 5.00,pymatching,test,"{""a"":[1,2,3]}", """ return csv_line( shots=int(self.shots), errors=int(self.errors), seconds=float(self.seconds), discards=int(self.discards), strong_id=self.strong_id, decoder=self.decoder, json_metadata=self.json_metadata, custom_counts=self.custom_counts, ) def _split_custom_counts(self, custom_keys: List[str]) -> List['TaskStats']: result = [] for k in custom_keys: m = self.json_metadata if isinstance(m, dict): m = dict(m) m.setdefault('custom_error_count_key', k) m.setdefault('original_error_count', self.errors) result.append(TaskStats( strong_id=f'{self.strong_id}:{k}', decoder=self.decoder, json_metadata=m, shots=self.shots, errors=self.custom_counts[k], discards=self.discards, seconds=self.seconds, custom_counts=self.custom_counts, )) return result def __str__(self) -> str: return self.to_csv_line() def __repr__(self) -> str: terms = [] terms.append(f'strong_id={self.strong_id!r}') terms.append(f'decoder={self.decoder!r}') terms.append(f'json_metadata={self.json_metadata!r}') if self.shots: terms.append(f'shots={self.shots!r}') if self.errors: terms.append(f'errors={self.errors!r}') if self.discards: terms.append(f'discards={self.discards!r}') if self.seconds: terms.append(f'seconds={self.seconds!r}') if self.custom_counts: terms.append(f'custom_counts={self.custom_counts!r}') return f'sinter.TaskStats({", ".join(terms)})' ================================================ FILE: glue/sample/src/sinter/_data/_task_stats_test.py ================================================ import collections import pytest import sinter from sinter._data._task_stats import _is_equal_json_values def test_repr(): v = sinter.TaskStats( strong_id='test', json_metadata={'a': [1, 2, 3]}, decoder='pymatching', shots=22, errors=3, discards=4, seconds=5, ) assert eval(repr(v), {"sinter": sinter}) == v def test_to_csv_line(): v = sinter.TaskStats( strong_id='test', json_metadata={'a': [1, 2, 3]}, decoder='pymatching', shots=22, errors=3, discards=4, seconds=5, ) assert v.to_csv_line() == str(v) == ' 22, 3, 4, 5.00,pymatching,test,"{""a"":[1,2,3]}",' def test_to_anon_stats(): v = sinter.TaskStats( strong_id='test', json_metadata={'a': [1, 2, 3]}, decoder='pymatching', shots=22, errors=3, discards=4, seconds=5, ) assert v.to_anon_stats() == sinter.AnonTaskStats(shots=22, errors=3, discards=4, seconds=5) def test_add(): a = sinter.TaskStats( decoder='pymatching', json_metadata={'a': 2}, strong_id='abcdef', shots=220, errors=30, discards=40, seconds=50, custom_counts=collections.Counter({'a': 10, 'b': 20}), ) b = sinter.TaskStats( decoder='pymatching', json_metadata={'a': 2}, strong_id='abcdef', shots=50, errors=4, discards=3, seconds=2, custom_counts=collections.Counter({'a': 1, 'c': 3}), ) c = sinter.TaskStats( decoder='pymatching', json_metadata={'a': 2}, strong_id='abcdef', shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 11, 'b': 20, 'c': 3}), ) assert a + b == c with pytest.raises(ValueError): a + sinter.TaskStats( decoder='pymatching', json_metadata={'a': 2}, strong_id='abcdefDIFFERENT', shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 11, 'b': 20, 'c': 3}), ) def test_with_edits(): v = sinter.TaskStats( decoder='pymatching', json_metadata={'a': 2}, strong_id='abcdefDIFFERENT', shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 11, 'b': 20, 'c': 3}), ) assert v.with_edits(json_metadata={'b': 3}) == sinter.TaskStats( decoder='pymatching', json_metadata={'b': 3}, strong_id='abcdefDIFFERENT', shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 11, 'b': 20, 'c': 3}), ) assert v == sinter.TaskStats(strong_id='', json_metadata={}, decoder='').with_edits( decoder='pymatching', json_metadata={'a': 2}, strong_id='abcdefDIFFERENT', shots=270, errors=34, discards=43, seconds=52, custom_counts=collections.Counter({'a': 11, 'b': 20, 'c': 3}), ) def test_is_equal_json_values(): assert _is_equal_json_values([1, 2], (1, 2)) assert _is_equal_json_values([1, [3, (5, 6)]], (1, (3, [5, 6]))) assert not _is_equal_json_values([1, [3, (5, 6)]], (1, (3, [5, 7]))) assert not _is_equal_json_values([1, [3, (5, 6)]], (1, (3, [5]))) assert not _is_equal_json_values([1, 2], (1, 3)) assert not _is_equal_json_values([1, 2], {1, 2}) assert _is_equal_json_values({'x': [1, 2]}, {'x': (1, 2)}) assert _is_equal_json_values({'x': (1, 2)}, {'x': (1, 2)}) assert not _is_equal_json_values({'x': (1, 2)}, {'y': (1, 2)}) assert not _is_equal_json_values({'x': (1, 2)}, {'x': (1, 2), 'y': []}) assert not _is_equal_json_values({'x': (1, 2), 'y': []}, {'x': (1, 2)}) assert not _is_equal_json_values({'x': (1, 2)}, {'x': (1, 3)}) assert not _is_equal_json_values(1, 2) assert _is_equal_json_values(1, 1) ================================================ FILE: glue/sample/src/sinter/_data/_task_test.py ================================================ import numpy as np import stim import sinter def test_repr(): circuit = stim.Circuit(""" X_ERROR(0.1) 0 1 2 M 0 1 2 DETECTOR rec[-1] rec[-2] DETECTOR rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] """) v = sinter.Task(circuit=circuit) assert eval(repr(v), {"stim": stim, "sinter": sinter, "np": np}) == v v = sinter.Task(circuit=circuit, detector_error_model=circuit.detector_error_model()) assert eval(repr(v), {"stim": stim, "sinter": sinter, "np": np}) == v v = sinter.Task(circuit=circuit, postselection_mask=np.array([1], dtype=np.uint8)) assert eval(repr(v), {"stim": stim, "sinter": sinter, "np": np}) == v v = sinter.Task(circuit=circuit, postselection_mask=np.array([2], dtype=np.uint8)) assert eval(repr(v), {"stim": stim, "sinter": sinter, "np": np}) == v v = sinter.Task(circuit=circuit, postselected_observables_mask=np.array([1], dtype=np.uint8)) assert eval(repr(v), {"stim": stim, "sinter": sinter, "np": np}) == v v = sinter.Task(circuit=circuit, collection_options=sinter.CollectionOptions(max_shots=10)) assert eval(repr(v), {"stim": stim, "sinter": sinter, "np": np}) == v v = sinter.Task(circuit=circuit, json_metadata={'a': 5}) assert eval(repr(v), {"stim": stim, "sinter": sinter, "np": np}) == v v = sinter.Task(circuit=circuit, decoder='pymatching') assert eval(repr(v), {"stim": stim, "sinter": sinter, "np": np}) == v ================================================ FILE: glue/sample/src/sinter/_decoding/__init__.py ================================================ from sinter._decoding._decoding import ( streaming_post_select, sample_decode, ) from sinter._decoding._decoding_decoder_class import ( CompiledDecoder, Decoder, ) from sinter._decoding._decoding_all_built_in_decoders import ( BUILT_IN_DECODERS, BUILT_IN_SAMPLERS, ) from sinter._decoding._sampler import ( Sampler, CompiledSampler, ) ================================================ FILE: glue/sample/src/sinter/_decoding/_decoding.py ================================================ import collections from typing import Iterable from typing import Optional, Dict, Tuple, TYPE_CHECKING, Union import contextlib import pathlib import tempfile import math import time import numpy as np import stim from sinter._data import AnonTaskStats from sinter._decoding._decoding_all_built_in_decoders import BUILT_IN_DECODERS from sinter._decoding._decoding_decoder_class import CompiledDecoder, Decoder if TYPE_CHECKING: import sinter def streaming_post_select(*, num_dets: int, num_obs: int, dets_in_b8: pathlib.Path, obs_in_b8: Optional[pathlib.Path], dets_out_b8: pathlib.Path, obs_out_b8: Optional[pathlib.Path], discards_out_b8: Optional[pathlib.Path], num_shots: int, post_mask: np.ndarray) -> int: if post_mask.shape != ((num_dets + 7) // 8,): raise ValueError(f"post_mask.shape={post_mask.shape} != (math.ceil(num_detectors / 8),)") if post_mask.dtype != np.uint8: raise ValueError(f"post_mask.dtype={post_mask.dtype} != np.uint8") assert (obs_in_b8 is None) == (obs_out_b8 is None) num_det_bytes = math.ceil(num_dets / 8) num_obs_bytes = math.ceil(num_obs / 8) num_shots_left = num_shots num_discards = 0 with contextlib.ExitStack() as ctx: dets_in_f = ctx.enter_context(open(dets_in_b8, 'rb')) dets_out_f = ctx.enter_context(open(dets_out_b8, 'wb')) if obs_in_b8 is not None and obs_out_b8 is not None: obs_in_f = ctx.enter_context(open(obs_in_b8, 'rb')) obs_out_f = ctx.enter_context(open(obs_out_b8, 'wb')) else: obs_in_f = None obs_out_f = None if discards_out_b8 is not None: discards_out_f = ctx.enter_context(open(discards_out_b8, 'wb')) else: discards_out_f = None while num_shots_left: batch_size = min(num_shots_left, math.ceil(10 ** 6 / max(1, num_dets))) det_batch = np.fromfile(dets_in_f, dtype=np.uint8, count=num_det_bytes * batch_size) det_batch.shape = (batch_size, num_det_bytes) discarded = np.any(det_batch & post_mask, axis=1) det_left = det_batch[~discarded, :] det_left.tofile(dets_out_f) if obs_in_f is not None and obs_out_f is not None: obs_batch = np.fromfile(obs_in_f, dtype=np.uint8, count=num_obs_bytes * batch_size) obs_batch.shape = (batch_size, num_obs_bytes) obs_left = obs_batch[~discarded, :] obs_left.tofile(obs_out_f) if discards_out_f is not None: discarded.tofile(discards_out_f) num_discards += np.count_nonzero(discarded) num_shots_left -= batch_size return num_discards def _streaming_count_mistakes( *, num_shots: int, num_obs: int, num_det: int, postselected_observable_mask: Optional[np.ndarray] = None, dets_in: pathlib.Path, obs_in: pathlib.Path, predictions_in: pathlib.Path, count_detection_events: bool, count_observable_error_combos: bool, ) -> Tuple[int, int, collections.Counter]: num_det_bytes = math.ceil(num_det / 8) num_obs_bytes = math.ceil(num_obs / 8) num_errors = 0 num_discards = 0 custom_counts = collections.Counter() if count_detection_events: with open(dets_in, 'rb') as dets_in_f: num_shots_left = num_shots while num_shots_left: batch_size = min(num_shots_left, math.ceil(10**6 / max(num_obs, 1))) det_data = np.fromfile(dets_in_f, dtype=np.uint8, count=num_det_bytes * batch_size) for b in range(8): custom_counts['detection_events'] += np.count_nonzero(det_data & (1 << b)) num_shots_left -= batch_size custom_counts['detectors_checked'] += num_shots * num_det with open(obs_in, 'rb') as obs_in_f: with open(predictions_in, 'rb') as predictions_in_f: num_shots_left = num_shots while num_shots_left: batch_size = min(num_shots_left, math.ceil(10**6 / max(num_obs, 1))) obs_batch = np.fromfile(obs_in_f, dtype=np.uint8, count=num_obs_bytes * batch_size) pred_batch = np.fromfile(predictions_in_f, dtype=np.uint8, count=num_obs_bytes * batch_size) obs_batch.shape = (batch_size, num_obs_bytes) pred_batch.shape = (batch_size, num_obs_bytes) cmp_table = pred_batch ^ obs_batch err_mask = np.any(cmp_table, axis=1) if postselected_observable_mask is not None: discard_mask = np.any(cmp_table & postselected_observable_mask, axis=1) err_mask &= ~discard_mask num_discards += np.count_nonzero(discard_mask) if count_observable_error_combos: for misprediction_arr in cmp_table[err_mask]: err_key = "obs_mistake_mask=" + ''.join('_E'[b] for b in np.unpackbits(misprediction_arr, count=num_obs, bitorder='little')) custom_counts[err_key] += 1 num_errors += np.count_nonzero(err_mask) num_shots_left -= batch_size return num_discards, num_errors, custom_counts def sample_decode(*, circuit_obj: Optional[stim.Circuit], circuit_path: Union[None, str, pathlib.Path], dem_obj: Optional[stim.DetectorErrorModel], dem_path: Union[None, str, pathlib.Path], post_mask: Optional[np.ndarray] = None, postselected_observable_mask: Optional[np.ndarray] = None, count_observable_error_combos: bool = False, count_detection_events: bool = False, num_shots: int, decoder: str, tmp_dir: Union[str, pathlib.Path, None] = None, custom_decoders: Optional[Dict[str, 'sinter.Decoder']] = None, __private__unstable__force_decode_on_disk: Optional[bool] = None, ) -> AnonTaskStats: """Samples how many times a decoder correctly predicts the logical frame. Args: circuit_obj: The noisy circuit to sample from and decode results for. Must specify circuit_obj XOR circuit_path. circuit_path: The file storing the circuit to sample from. Must specify circuit_obj XOR circuit_path. dem_obj: The error model to give to the decoder. Must specify dem_obj XOR dem_path. dem_path: The file storing the error model to give to the decoder. Must specify dem_obj XOR dem_path. post_mask: Postselection mask. Any samples that have a non-zero result at a location where the mask has a 1 bit are discarded. If set to None, no postselection is performed. postselected_observable_mask: Bit packed mask indicating which observables to postselect on. If the decoder incorrectly predicts any of these observables, the shot is discarded instead of counted as an error. count_observable_error_combos: Defaults to False. When set to to True, the returned AnonTaskStats will have a custom counts field with keys like `obs_mistake_mask=E_E__` counting how many times specific combinations of observables were mispredicted by the decoder. count_detection_events: Defaults to False. When set to True, the returned AnonTaskStats will have a custom counts field withs the key `detection_events` counting the number of times a detector fired and also `detectors_checked` counting the number of detectors that were executed. The detection fraction is the ratio of these two numbers. num_shots: The number of sample shots to take from the circuit. decoder: The name of the decoder to use. Allowed values are: "pymatching": Use pymatching min-weight-perfect-match decoder. "internal": Use internal decoder with uncorrelated decoding. "internal_correlated": Use internal decoder with correlated decoding. tmp_dir: An existing directory that is currently empty where temporary files can be written as part of performing decoding. If set to None, one is created using the tempfile package. custom_decoders: Custom decoders that can be used if requested by name. If not specified, only decoders built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. """ if (circuit_obj is None) == (circuit_path is None): raise ValueError('(circuit_obj is None) == (circuit_path is None)') if (dem_obj is None) == (dem_path is None): raise ValueError('(dem_obj is None) == (dem_path is None)') if num_shots == 0: return AnonTaskStats() decoder_obj: Optional[Decoder] = None if custom_decoders is not None: decoder_obj = custom_decoders.get(decoder) if decoder_obj is None: decoder_obj = BUILT_IN_DECODERS.get(decoder) if decoder_obj is None: raise NotImplementedError(f"Unrecognized decoder: {decoder!r}") dem: stim.DetectorErrorModel if dem_obj is None: dem = stim.DetectorErrorModel.from_file(dem_path) else: dem = dem_obj circuit: stim.Circuit if circuit_path is not None: circuit = stim.Circuit.from_file(circuit_path) else: circuit = circuit_obj start_time = time.monotonic() try: if __private__unstable__force_decode_on_disk: raise NotImplementedError() compiled_decoder = decoder_obj.compile_decoder_for_dem(dem=dem) return _sample_decode_helper_using_memory( circuit=circuit, post_mask=post_mask, postselected_observable_mask=postselected_observable_mask, compiled_decoder=compiled_decoder, total_num_shots=num_shots, num_det=circuit.num_detectors, mini_batch_size=1024, start_time_monotonic=start_time, num_obs=circuit.num_observables, count_observable_error_combos=count_observable_error_combos, count_detection_events=count_detection_events, ) except NotImplementedError: assert __private__unstable__force_decode_on_disk or __private__unstable__force_decode_on_disk is None pass return _sample_decode_helper_using_disk( circuit=circuit, dem=dem, dem_path=dem_path, post_mask=post_mask, postselected_observable_mask=postselected_observable_mask, num_shots=num_shots, decoder_obj=decoder_obj, tmp_dir=tmp_dir, start_time_monotonic=start_time, count_observable_error_combos=count_observable_error_combos, count_detection_events=count_detection_events, ) def _sample_decode_helper_using_memory( *, circuit: stim.Circuit, post_mask: Optional[np.ndarray], postselected_observable_mask: Optional[np.ndarray], num_obs: int, num_det: int, total_num_shots: int, mini_batch_size: int, compiled_decoder: CompiledDecoder, start_time_monotonic: float, count_observable_error_combos: bool, count_detection_events: bool, ) -> AnonTaskStats: sampler: stim.CompiledDetectorSampler = circuit.compile_detector_sampler() out_num_discards = 0 out_num_errors = 0 shots_left = total_num_shots custom_counts = collections.Counter() while shots_left > 0: cur_num_shots = min(shots_left, mini_batch_size) dets_data, obs_data = sampler.sample(shots=cur_num_shots, separate_observables=True, bit_packed=True) # Discard any shots that contain a postselected detection events. if post_mask is not None: discarded_flags = np.any(dets_data & post_mask, axis=1) cur_num_discarded_shots = np.count_nonzero(discarded_flags) if cur_num_discarded_shots: out_num_discards += cur_num_discarded_shots dets_data = dets_data[~discarded_flags, :] obs_data = obs_data[~discarded_flags, :] # Have the decoder predict which observables are flipped. predict_data = compiled_decoder.decode_shots_bit_packed(bit_packed_detection_event_data=dets_data) # Discard any shots where the decoder predicts a flipped postselected observable. if postselected_observable_mask is not None: discarded_flags = np.any(postselected_observable_mask & (predict_data ^ obs_data), axis=1) cur_num_discarded_shots = np.count_nonzero(discarded_flags) if cur_num_discarded_shots: out_num_discards += cur_num_discarded_shots obs_data = obs_data[~discarded_flags, :] predict_data = predict_data[~discarded_flags, :] # Count how many mistakes the decoder made on non-discarded shots. mispredictions = obs_data ^ predict_data err_mask = np.any(mispredictions, axis=1) if count_detection_events: for b in range(8): custom_counts['detection_events'] += np.count_nonzero(dets_data & (1 << b)) if count_observable_error_combos: for misprediction_arr in mispredictions[err_mask]: err_key = "obs_mistake_mask=" + ''.join('_E'[b] for b in np.unpackbits(misprediction_arr, count=num_obs, bitorder='little')) custom_counts[err_key] += 1 out_num_errors += np.count_nonzero(err_mask) shots_left -= cur_num_shots if count_detection_events: custom_counts['detectors_checked'] += num_det * total_num_shots return AnonTaskStats( shots=total_num_shots, errors=out_num_errors, discards=out_num_discards, seconds=time.monotonic() - start_time_monotonic, custom_counts=custom_counts, ) def _sample_decode_helper_using_disk( *, circuit: stim.Circuit, dem: stim.DetectorErrorModel, dem_path: Union[str, pathlib.Path], post_mask: Optional[np.ndarray], postselected_observable_mask: Optional[np.ndarray], num_shots: int, decoder_obj: Decoder, tmp_dir: Union[str, pathlib.Path, None], start_time_monotonic: float, count_observable_error_combos: bool, count_detection_events: bool, ) -> AnonTaskStats: with contextlib.ExitStack() as exit_stack: if tmp_dir is None: tmp_dir = exit_stack.enter_context(tempfile.TemporaryDirectory()) tmp_dir = pathlib.Path(tmp_dir) if dem_path is None: dem_path = tmp_dir / 'tmp.dem' dem.to_file(dem_path) dem_path = pathlib.Path(dem_path) dets_all_path = tmp_dir / 'sinter_dets.all.b8' obs_all_path = tmp_dir / 'sinter_obs.all.b8' dets_kept_path = tmp_dir / 'sinter_dets.kept.b8' obs_kept_path = tmp_dir / 'sinter_obs.kept.b8' predictions_path = tmp_dir / 'sinter_predictions.b8' num_dets = circuit.num_detectors num_obs = circuit.num_observables # Sample data using Stim. sampler: stim.CompiledDetectorSampler = circuit.compile_detector_sampler() sampler.sample_write( num_shots, filepath=str(dets_all_path), obs_out_filepath=str(obs_all_path), format='b8', obs_out_format='b8', ) # Postselect, then split into detection event data and observable data. if post_mask is None: num_det_discards = 0 dets_used_path = dets_all_path obs_used_path = obs_all_path else: num_det_discards = streaming_post_select( num_shots=num_shots, num_dets=num_dets, num_obs=num_obs, dets_in_b8=dets_all_path, dets_out_b8=dets_kept_path, obs_in_b8=obs_all_path, obs_out_b8=obs_kept_path, post_mask=post_mask, discards_out_b8=None, ) dets_used_path = dets_kept_path obs_used_path = obs_kept_path num_kept_shots = num_shots - num_det_discards # Perform syndrome decoding to predict observables from detection events. decoder_obj.decode_via_files( num_shots=num_kept_shots, num_dets=num_dets, num_obs=num_obs, dem_path=dem_path, dets_b8_in_path=dets_used_path, obs_predictions_b8_out_path=predictions_path, tmp_dir=tmp_dir, ) # Count how many predictions matched the actual observable data. num_obs_discards, num_errors, custom_counts = _streaming_count_mistakes( num_shots=num_kept_shots, num_obs=num_obs, num_det=num_dets, dets_in=dets_all_path, obs_in=obs_used_path, predictions_in=predictions_path, postselected_observable_mask=postselected_observable_mask, count_detection_events=count_detection_events, count_observable_error_combos=count_observable_error_combos, ) return AnonTaskStats( shots=num_shots, errors=num_errors, discards=num_obs_discards + num_det_discards, seconds=time.monotonic() - start_time_monotonic, custom_counts=custom_counts, ) ================================================ FILE: glue/sample/src/sinter/_decoding/_decoding_all_built_in_decoders.py ================================================ from typing import Dict from typing import Union from sinter._decoding._decoding_decoder_class import Decoder from sinter._decoding._decoding_fusion_blossom import FusionBlossomDecoder from sinter._decoding._decoding_pymatching import PyMatchingDecoder from sinter._decoding._decoding_vacuous import VacuousDecoder from sinter._decoding._perfectionist_sampler import PerfectionistSampler from sinter._decoding._sampler import Sampler from sinter._decoding._decoding_mwpf import HyperUFDecoder, MwpfDecoder BUILT_IN_DECODERS: Dict[str, Decoder] = { 'vacuous': VacuousDecoder(), 'pymatching': PyMatchingDecoder(), 'fusion_blossom': FusionBlossomDecoder(), # an implementation of (weighted) hypergraph UF decoder (https://arxiv.org/abs/2103.08049) 'hypergraph_union_find': HyperUFDecoder(), # Minimum-Weight Parity Factor using similar primal-dual method the blossom algorithm (https://pypi.org/project/mwpf/) 'mw_parity_factor': MwpfDecoder(), } BUILT_IN_SAMPLERS: Dict[str, Union[Decoder, Sampler]] = { **BUILT_IN_DECODERS, 'perfectionist': PerfectionistSampler(), } ================================================ FILE: glue/sample/src/sinter/_decoding/_decoding_decoder_class.py ================================================ import abc import pathlib import numpy as np import stim class CompiledDecoder(metaclass=abc.ABCMeta): """Abstract class for decoders preconfigured to a specific decoding task. This is the type returned by `sinter.Decoder.compile_decoder_for_dem`. The idea is that, when many shots of the same decoding task are going to be performed, it is valuable to pay the cost of configuring the decoder only once instead of once per batch of shots. Custom decoders can optionally implement that method, and return this type, to increase sampling efficiency. """ @abc.abstractmethod def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: np.ndarray, ) -> np.ndarray: """Predicts observable flips from the given detection events. All data taken and returned must be bit packed with bitorder='little'. Args: bit_packed_detection_event_data: Detection event data stored as a bit packed numpy array. The numpy array will have the following dtype/shape: dtype: uint8 shape: (num_shots, ceil(dem.num_detectors / 8)) where `num_shots` is the number of shots to decoder and `dem` is the detector error model this instance was compiled to decode. It's guaranteed that the data will be laid out in memory so that detection events within a shot are contiguous in memory (i.e. that bit_packed_detection_event_data.strides[1] == 1). Returns: Bit packed observable flip data stored as a bit packed numpy array. The numpy array must have the following dtype/shape: dtype: uint8 shape: (num_shots, ceil(dem.num_observables / 8)) where `num_shots` is bit_packed_detection_event_data.shape[0] and `dem` is the detector error model this instance was compiled to decode. """ pass class Decoder: """Abstract base class for custom decoders. Custom decoders can be explained to sinter by inheriting from this class and implementing its methods. Decoder classes MUST be serializable (e.g. via pickling), so that they can be given to worker processes when using python multiprocessing. Child classes should implement `compile_decoder_for_dem`, but (for legacy reasons) can alternatively implement `decode_via_files`. At least one of the two methods must be implemented. """ def compile_decoder_for_dem( self, *, dem: stim.DetectorErrorModel, ) -> CompiledDecoder: """Creates a decoder preconfigured for the given detector error model. This method is optional to implement. By default, it will raise a NotImplementedError. When sampling, sinter will attempt to use this method first and otherwise fallback to using `decode_via_files`. The idea is that the preconfigured decoder amortizes the cost of configuration over more calls. This makes smaller batch sizes efficient, reducing the amount of memory used for storing each batch, improving overall efficiency. Args: dem: A detector error model for the samples that will need to be decoded. What to configure the decoder to decode. Returns: An instance of `sinter.CompiledDecoder` that can be used to invoke the preconfigured decoder. Raises: NotImplementedError: This `sinter.Decoder` doesn't support compiling for a dem. """ raise NotImplementedError('compile_decoder_for_dem') def decode_via_files(self, *, num_shots: int, num_dets: int, num_obs: int, dem_path: pathlib.Path, dets_b8_in_path: pathlib.Path, obs_predictions_b8_out_path: pathlib.Path, tmp_dir: pathlib.Path, ) -> None: """Performs decoding by reading/writing problems and answers from disk. Args: num_shots: The number of times the circuit was sampled. The number of problems to be solved. num_dets: The number of detectors in the circuit. The number of detection event bits in each shot. num_obs: The number of observables in the circuit. The number of predicted bits in each shot. dem_path: The file path where the detector error model should be read from, e.g. using `stim.DetectorErrorModel.from_file`. The error mechanisms specified by the detector error model should be used to configure the decoder. dets_b8_in_path: The file path that detection event data should be read from. Note that the file may be a named pipe instead of a fixed size object. The detection events will be in b8 format (see https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md ). The number of detection events per shot is available via the `num_dets` argument or via the detector error model at `dem_path`. obs_predictions_b8_out_path: The file path that decoder predictions must be written to. The predictions must be written in b8 format (see https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md ). The number of observables per shot is available via the `num_obs` argument or via the detector error model at `dem_path`. tmp_dir: Any temporary files generated by the decoder during its operation MUST be put into this directory. The reason for this requirement is because sinter is allowed to kill the decoding process without warning, without giving it time to clean up any temporary objects. All cleanup should be done via sinter deleting this directory after killing the decoder. """ dem = stim.DetectorErrorModel.from_file(dem_path) try: compiled = self.compile_decoder_for_dem(dem=dem) except NotImplementedError as ex: raise NotImplementedError(f"{type(self).__qualname__} didn't implement `compile_decoder_for_dem` or `decode_via_files`.") from ex num_det_bytes = -(-num_dets // 8) num_obs_bytes = -(-num_obs // 8) dets = np.fromfile(dets_b8_in_path, dtype=np.uint8, count=num_shots * num_det_bytes) dets = dets.reshape(num_shots, num_det_bytes) obs = compiled.decode_shots_bit_packed(bit_packed_detection_event_data=dets) if obs.dtype != np.uint8 or obs.shape != (num_shots, num_obs_bytes): raise ValueError(f"Got a numpy array with dtype={obs.dtype},shape={obs.shape} instead of dtype={np.uint8},shape={(num_shots, num_obs_bytes)} from {type(self).__qualname__}(...).compile_decoder_for_dem(...).decode_shots_bit_packed(...).") obs.tofile(obs_predictions_b8_out_path) ================================================ FILE: glue/sample/src/sinter/_decoding/_decoding_fusion_blossom.py ================================================ import math import pathlib from typing import Callable, List, TYPE_CHECKING, Tuple import numpy as np import stim from sinter._decoding._decoding_decoder_class import Decoder, CompiledDecoder if TYPE_CHECKING: import fusion_blossom class FusionBlossomCompiledDecoder(CompiledDecoder): def __init__(self, solver: 'fusion_blossom.SolverSerial', fault_masks: 'np.ndarray', num_dets: int, num_obs: int): self.solver = solver self.fault_masks = fault_masks self.num_dets = num_dets self.num_obs = num_obs def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: 'np.ndarray', ) -> 'np.ndarray': num_shots = bit_packed_detection_event_data.shape[0] predictions = np.zeros(shape=(num_shots, (self.num_obs + 7) // 8), dtype=np.uint8) import fusion_blossom for shot in range(num_shots): dets_sparse = np.flatnonzero(np.unpackbits(bit_packed_detection_event_data[shot], count=self.num_dets, bitorder='little')) syndrome = fusion_blossom.SyndromePattern(syndrome_vertices=dets_sparse) self.solver.solve(syndrome) prediction = int(np.bitwise_xor.reduce(self.fault_masks[self.solver.subgraph()])) predictions[shot] = np.packbits( np.array(list(np.binary_repr(prediction, width=self.num_obs))[::-1],dtype=np.uint8), bitorder="little", ) self.solver.clear() return predictions class FusionBlossomDecoder(Decoder): """Use fusion blossom to predict observables from detection events.""" def compile_decoder_for_dem(self, *, dem: 'stim.DetectorErrorModel') -> CompiledDecoder: try: import fusion_blossom except ImportError as ex: raise ImportError( "The decoder 'fusion_blossom' isn't installed\n" "To fix this, install the python package 'fusion_blossom' into your environment.\n" "For example, if you are using pip, run `pip install fusion_blossom`.\n" ) from ex solver, fault_masks = detector_error_model_to_fusion_blossom_solver_and_fault_masks(dem) return FusionBlossomCompiledDecoder(solver, fault_masks, dem.num_detectors, dem.num_observables) def decode_via_files(self, *, num_shots: int, num_dets: int, num_obs: int, dem_path: pathlib.Path, dets_b8_in_path: pathlib.Path, obs_predictions_b8_out_path: pathlib.Path, tmp_dir: pathlib.Path, ) -> None: try: import fusion_blossom except ImportError as ex: raise ImportError( "The decoder 'fusion_blossom' isn't installed\n" "To fix this, install the python package 'fusion-blossom' into your environment.\n" "For example, if you are using pip, run `pip install fusion-blossom~=0.1.4`.\n" ) from ex error_model = stim.DetectorErrorModel.from_file(dem_path) solver, fault_masks = detector_error_model_to_fusion_blossom_solver_and_fault_masks(error_model) num_det_bytes = math.ceil(num_dets / 8) with open(dets_b8_in_path, 'rb') as dets_in_f: with open(obs_predictions_b8_out_path, 'wb') as obs_out_f: for _ in range(num_shots): dets_bit_packed = np.fromfile(dets_in_f, dtype=np.uint8, count=num_det_bytes) if dets_bit_packed.shape != (num_det_bytes,): raise IOError('Missing dets data.') dets_sparse = np.flatnonzero(np.unpackbits(dets_bit_packed, count=num_dets, bitorder='little')) syndrome = fusion_blossom.SyndromePattern(syndrome_vertices=dets_sparse) solver.solve(syndrome) prediction = int(np.bitwise_xor.reduce(fault_masks[solver.subgraph()])) obs_out_f.write(prediction.to_bytes((num_obs + 7) // 8, byteorder='little')) solver.clear() def iter_flatten_model(model: stim.DetectorErrorModel, handle_error: Callable[[float, List[int], List[int]], None], handle_detector_coords: Callable[[int, np.ndarray], None]): det_offset = 0 coords_offset = np.zeros(100, dtype=np.float64) def _helper(m: stim.DetectorErrorModel, reps: int): nonlocal det_offset nonlocal coords_offset for _ in range(reps): for instruction in m: if isinstance(instruction, stim.DemRepeatBlock): _helper(instruction.body_copy(), instruction.repeat_count) elif isinstance(instruction, stim.DemInstruction): if instruction.type == "error": dets: List[int] = [] frames: List[int] = [] t: stim.DemTarget p = instruction.args_copy()[0] for t in instruction.targets_copy(): if t.is_relative_detector_id(): dets.append(t.val + det_offset) elif t.is_logical_observable_id(): frames.append(t.val) elif t.is_separator(): # Treat each component of a decomposed error as an independent error. # (Ideally we could configure some sort of correlated analysis; oh well.) handle_error(p, dets, frames) frames = [] dets = [] # Handle last component. handle_error(p, dets, frames) elif instruction.type == "shift_detectors": det_offset += instruction.targets_copy()[0] a = np.array(instruction.args_copy()) coords_offset[:len(a)] += a elif instruction.type == "detector": a = np.array(instruction.args_copy()) for t in instruction.targets_copy(): handle_detector_coords(t.val + det_offset, a + coords_offset[:len(a)]) elif instruction.type == "logical_observable": pass else: raise NotImplementedError() else: raise NotImplementedError() _helper(model, 1) def detector_error_model_to_fusion_blossom_solver_and_fault_masks(model: stim.DetectorErrorModel) -> Tuple['fusion_blossom.SolverSerial', np.ndarray]: """Convert a stim error model into a NetworkX graph.""" import fusion_blossom def handle_error(p: float, dets: List[int], frame_changes: List[int]): if p == 0: return if len(dets) == 0: # No symptoms for this error. # Code probably has distance 1. # Accept it and keep going, though of course decoding will probably perform terribly. return if len(dets) == 1: dets = [dets[0], num_detectors] if len(dets) > 2: raise NotImplementedError( f"Error with more than 2 symptoms can't become an edge or boundary edge: {dets!r}.") if p > 0.5: # fusion_blossom doesn't support negative edge weights. # approximate them as weight 0. p = 0.5 weight = math.log((1 - p) / p) mask = sum(1 << k for k in frame_changes) edges.append((dets[0], dets[1], weight, mask)) def handle_detector_coords(detector: int, coords: np.ndarray): pass num_detectors = model.num_detectors edges: List[Tuple[int, int, float, int]] = [] iter_flatten_model( model, handle_error=handle_error, handle_detector_coords=handle_detector_coords, ) max_weight = max(1e-4, max((w for _, _, w, _ in edges), default=1)) rescaled_edges = [ (a, b, round(w * 2**10 / max_weight) * 2) for a, b, w, _ in edges ] fault_masks = np.array([e[3] for e in edges], dtype=np.uint64) initializer = fusion_blossom.SolverInitializer( num_detectors + 1, # Total number of nodes. rescaled_edges, # Weighted edges. [num_detectors], # Boundary node. ) return fusion_blossom.SolverSerial(initializer), fault_masks ================================================ FILE: glue/sample/src/sinter/_decoding/_decoding_mwpf.py ================================================ import math import pathlib from typing import Callable, List, TYPE_CHECKING, Tuple, Any, Optional import numpy as np import stim from sinter._decoding._decoding_decoder_class import Decoder, CompiledDecoder if TYPE_CHECKING: import mwpf def mwpf_import_error() -> ImportError: return ImportError( "The decoder 'MWPF' isn't installed\n" "To fix this, install the python package 'MWPF' into your environment.\n" "For example, if you are using pip, run `pip install MWPF~=0.1.5`.\n" ) class MwpfCompiledDecoder(CompiledDecoder): def __init__( self, solver: "mwpf.SolverSerialJointSingleHair", fault_masks: "np.ndarray", num_dets: int, num_obs: int, ): self.solver = solver self.fault_masks = fault_masks self.num_dets = num_dets self.num_obs = num_obs def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: "np.ndarray", ) -> "np.ndarray": num_shots = bit_packed_detection_event_data.shape[0] predictions = np.zeros( shape=(num_shots, (self.num_obs + 7) // 8), dtype=np.uint8 ) import mwpf for shot in range(num_shots): dets_sparse = np.flatnonzero( np.unpackbits( bit_packed_detection_event_data[shot], count=self.num_dets, bitorder="little", ) ) syndrome = mwpf.SyndromePattern(defect_vertices=dets_sparse) if self.solver is None: prediction = 0 else: self.solver.solve(syndrome) prediction = int( np.bitwise_xor.reduce(self.fault_masks[self.solver.subgraph()]) ) self.solver.clear() predictions[shot] = np.packbits( np.array( list(np.binary_repr(prediction, width=self.num_obs))[::-1], dtype=np.uint8, ), bitorder="little", ) return predictions class MwpfDecoder(Decoder): """Use MWPF to predict observables from detection events.""" def __init__( self, decoder_cls: Any = None, # decoder class used to construct the MWPF decoder. # in the Rust implementation, all of them inherits from the class of `SolverSerialPlugins` # but just provide different plugins for optimizing the primal and/or dual solutions. # For example, `SolverSerialUnionFind` is the most basic solver without any plugin: it only # grows the clusters until the first valid solution appears; some more optimized solvers uses # one or more plugins to further optimize the solution, which requires longer decoding time. cluster_node_limit: int = 50, # The maximum number of nodes in a cluster, ): self.decoder_cls = decoder_cls self.cluster_node_limit = cluster_node_limit super().__init__() def compile_decoder_for_dem( self, *, dem: "stim.DetectorErrorModel", ) -> CompiledDecoder: solver, fault_masks = detector_error_model_to_mwpf_solver_and_fault_masks( dem, decoder_cls=self.decoder_cls, cluster_node_limit=self.cluster_node_limit, ) return MwpfCompiledDecoder( solver, fault_masks, dem.num_detectors, dem.num_observables, ) def decode_via_files( self, *, num_shots: int, num_dets: int, num_obs: int, dem_path: pathlib.Path, dets_b8_in_path: pathlib.Path, obs_predictions_b8_out_path: pathlib.Path, tmp_dir: pathlib.Path, ) -> None: import mwpf error_model = stim.DetectorErrorModel.from_file(dem_path) solver, fault_masks = detector_error_model_to_mwpf_solver_and_fault_masks( error_model, decoder_cls=self.decoder_cls, cluster_node_limit=self.cluster_node_limit, ) num_det_bytes = math.ceil(num_dets / 8) with open(dets_b8_in_path, "rb") as dets_in_f: with open(obs_predictions_b8_out_path, "wb") as obs_out_f: for _ in range(num_shots): dets_bit_packed = np.fromfile( dets_in_f, dtype=np.uint8, count=num_det_bytes ) if dets_bit_packed.shape != (num_det_bytes,): raise IOError("Missing dets data.") dets_sparse = np.flatnonzero( np.unpackbits( dets_bit_packed, count=num_dets, bitorder="little" ) ) syndrome = mwpf.SyndromePattern(defect_vertices=dets_sparse) if solver is None: prediction = 0 else: solver.solve(syndrome) prediction = int( np.bitwise_xor.reduce(fault_masks[solver.subgraph()]) ) solver.clear() obs_out_f.write( prediction.to_bytes((num_obs + 7) // 8, byteorder="little") ) class HyperUFDecoder(MwpfDecoder): def __init__(self): super().__init__(decoder_cls="SolverSerialUnionFind", cluster_node_limit=0) def iter_flatten_model( model: stim.DetectorErrorModel, handle_error: Callable[[float, List[int], List[int]], None], handle_detector_coords: Callable[[int, np.ndarray], None], ): det_offset = 0 coords_offset = np.zeros(100, dtype=np.float64) def _helper(m: stim.DetectorErrorModel, reps: int): nonlocal det_offset nonlocal coords_offset for _ in range(reps): for instruction in m: if isinstance(instruction, stim.DemRepeatBlock): _helper(instruction.body_copy(), instruction.repeat_count) elif isinstance(instruction, stim.DemInstruction): if instruction.type == "error": dets: set[int] = set() frames: set[int] = set() t: stim.DemTarget p = instruction.args_copy()[0] for t in instruction.targets_copy(): if t.is_relative_detector_id(): dets ^= {t.val + det_offset} elif t.is_logical_observable_id(): frames ^= {t.val} handle_error(p, list(dets), list(frames)) elif instruction.type == "shift_detectors": det_offset += instruction.targets_copy()[0] a = np.array(instruction.args_copy()) coords_offset[: len(a)] += a elif instruction.type == "detector": a = np.array(instruction.args_copy()) for t in instruction.targets_copy(): handle_detector_coords( t.val + det_offset, a + coords_offset[: len(a)] ) elif instruction.type == "logical_observable": pass else: raise NotImplementedError() else: raise NotImplementedError() _helper(model, 1) def deduplicate_hyperedges( hyperedges: List[Tuple[List[int], float, int]] ) -> List[Tuple[List[int], float, int]]: indices: dict[frozenset[int], Tuple[int, float]] = dict() result: List[Tuple[List[int], float, int]] = [] for dets, weight, mask in hyperedges: dets_set = frozenset(dets) if dets_set in indices: idx, min_weight = indices[dets_set] p1 = 1 / (1 + math.exp(weight)) p2 = 1 / (1 + math.exp(result[idx][1])) p = p1 * (1 - p2) + p2 * (1 - p1) # choosing the mask from the most likely error new_mask = result[idx][2] if weight < min_weight: indices[dets_set] = (idx, weight) new_mask = mask result[idx] = (dets, math.log((1 - p) / p), new_mask) else: indices[dets_set] = (len(result), weight) result.append((dets, weight, mask)) return result def detector_error_model_to_mwpf_solver_and_fault_masks( model: stim.DetectorErrorModel, decoder_cls: Any = None, cluster_node_limit: int = 50, ) -> Tuple[Optional["mwpf.SolverSerialJointSingleHair"], np.ndarray]: """Convert a stim error model into a NetworkX graph.""" try: import mwpf except ImportError as ex: raise mwpf_import_error() from ex num_detectors = model.num_detectors is_detector_connected = np.full(num_detectors, False, dtype=bool) hyperedges: List[Tuple[List[int], float, int]] = [] def handle_error(p: float, dets: List[int], frame_changes: List[int]): if p == 0: return if len(dets) == 0: # No symptoms for this error. # Code probably has distance 1. # Accept it and keep going, though of course decoding will probably perform terribly. return if p > 0.5: # mwpf doesn't support negative edge weights (yet, will be supported in the next version). # approximate them as weight 0. p = 0.5 weight = math.log((1 - p) / p) mask = sum(1 << k for k in frame_changes) is_detector_connected[dets] = True hyperedges.append((dets, weight, mask)) def handle_detector_coords(detector: int, coords: np.ndarray): pass iter_flatten_model( model, handle_error=handle_error, handle_detector_coords=handle_detector_coords, ) # mwpf package panic on duplicate edges, thus we need to handle them here hyperedges = deduplicate_hyperedges(hyperedges) # fix the input by connecting an edge to all isolated vertices; will be supported in the next version for idx in range(num_detectors): if not is_detector_connected[idx]: hyperedges.append(([idx], 0, 0)) max_weight = max(1e-4, max((w for _, w, _ in hyperedges), default=1)) rescaled_edges = [ mwpf.HyperEdge(v, round(w * 2**10 / max_weight) * 2) for v, w, _ in hyperedges ] fault_masks = np.array([e[2] for e in hyperedges], dtype=np.uint64) initializer = mwpf.SolverInitializer( num_detectors, # Total number of nodes. rescaled_edges, # Weighted edges. ) if decoder_cls is None: # default to the solver with highest accuracy decoder_cls = mwpf.SolverSerialJointSingleHair elif isinstance(decoder_cls, str): decoder_cls = getattr(mwpf, decoder_cls) return ( ( decoder_cls(initializer, config={"cluster_node_limit": cluster_node_limit}) if num_detectors > 0 and len(rescaled_edges) > 0 else None ), fault_masks, ) ================================================ FILE: glue/sample/src/sinter/_decoding/_decoding_pymatching.py ================================================ from sinter._decoding._decoding_decoder_class import Decoder, CompiledDecoder class PyMatchingCompiledDecoder(CompiledDecoder): def __init__(self, matcher: 'pymatching.Matching'): self.matcher = matcher def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: 'np.ndarray', ) -> 'np.ndarray': return self.matcher.decode_batch( shots=bit_packed_detection_event_data, bit_packed_shots=True, bit_packed_predictions=True, return_weights=False, ) class PyMatchingDecoder(Decoder): """Use pymatching to predict observables from detection events.""" def compile_decoder_for_dem(self, *, dem: 'stim.DetectorErrorModel') -> CompiledDecoder: try: import pymatching except ImportError as ex: raise ImportError( "The decoder 'pymatching' isn't installed\n" "To fix this, install the python package 'pymatching' into your environment.\n" "For example, if you are using pip, run `pip install pymatching`.\n" ) from ex return PyMatchingCompiledDecoder(pymatching.Matching.from_detector_error_model(dem)) def decode_via_files(self, *, num_shots: int, num_dets: int, num_obs: int, dem_path: 'pathlib.Path', dets_b8_in_path: 'pathlib.Path', obs_predictions_b8_out_path: 'pathlib.Path', tmp_dir: 'pathlib.Path', ) -> None: try: import pymatching except ImportError as ex: raise ImportError( "The decoder 'pymatching' isn't installed\n" "To fix this, install the python package 'pymatching' into your environment.\n" "For example, if you are using pip, run `pip install pymatching`.\n" ) from ex if num_dets == 0: with open(obs_predictions_b8_out_path, 'wb') as f: f.write(b'\0' * (num_obs * num_shots)) return if not hasattr(pymatching, 'cli'): raise ValueError(""" The installed version of pymatching has no `pymatching.cli` method. sinter requires pymatching 2.1.0 or later. If you're using pip to install packages, this can be fixed by running ``` pip install "pymatching~=2.1" --upgrade ``` """) result = pymatching.cli(command_line_args=[ "predict", "--dem", str(dem_path), "--in", str(dets_b8_in_path), "--in_format", "b8", "--out", str(obs_predictions_b8_out_path), "--out_format", "b8", ]) if result: raise ValueError("pymatching.cli returned a non-zero exit code") ================================================ FILE: glue/sample/src/sinter/_decoding/_decoding_test.py ================================================ import os import pathlib import tempfile from typing import Dict, List, Optional, Tuple import numpy as np import pytest import sinter import stim from sinter import CompiledDecoder from sinter._collection import post_selection_mask_from_4th_coord from sinter._decoding._decoding_all_built_in_decoders import BUILT_IN_DECODERS from sinter._decoding._decoding import sample_decode from sinter._decoding._decoding_vacuous import VacuousDecoder def get_test_decoders() -> Tuple[List[str], Dict[str, sinter.Decoder]]: available_decoders = list(BUILT_IN_DECODERS.keys()) custom_decoders = {} try: import pymatching except ImportError: available_decoders.remove('pymatching') try: import fusion_blossom except ImportError: available_decoders.remove('fusion_blossom') try: import mwpf except ImportError: available_decoders.remove('hypergraph_union_find') available_decoders.remove('mw_parity_factor') e = os.environ.get('SINTER_PYTEST_CUSTOM_DECODERS') if e is not None: for term in e.split(';'): module, method = term.split(':') for name, obj in getattr(__import__(module), method)().items(): custom_decoders[name] = obj available_decoders.append(name) available_decoders.append("also_vacuous") custom_decoders["also_vacuous"] = VacuousDecoder() return available_decoders, custom_decoders TEST_DECODER_NAMES, TEST_CUSTOM_DECODERS = get_test_decoders() DECODER_CASES = [ (decoder, force_streaming) for decoder in TEST_DECODER_NAMES for force_streaming in [None, True] ] @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_decode_repetition_code(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit.generated('repetition_code:memory', rounds=3, distance=3, after_clifford_depolarization=0.05) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, num_shots=1000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 if 'vacuous' not in decoder: assert 1 <= result.errors <= 100 assert result.shots == 1000 @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_decode_surface_code(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit.generated( "surface_code:rotated_memory_x", distance=3, rounds=15, after_clifford_depolarization=0.001, ) stats = sample_decode( num_shots=1000, circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) if 'vacuous' not in decoder: assert 0 <= stats.errors <= 50 @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_empty(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit() result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, num_shots=1000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert result.shots == 1000 assert result.errors == 0 @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_no_observables(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit(""" X_ERROR(0.1) 0 M 0 DETECTOR rec[-1] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, num_shots=1000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert result.shots == 1000 assert result.errors == 0 @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_invincible_observables(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit(""" X_ERROR(0.1) 0 M 0 1 DETECTOR rec[-2] OBSERVABLE_INCLUDE(1) rec[-1] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, num_shots=1000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert result.shots == 1000 assert result.errors == 0 @pytest.mark.parametrize('decoder,force_streaming,offset', [(a, b, c) for a, b in DECODER_CASES for c in range(8)]) def test_observable_offsets_mod8(decoder: str, force_streaming: bool, offset: int): circuit = stim.Circuit(""" X_ERROR(0.1) 0 MR 0 DETECTOR rec[-1] """) * (8 + offset) + stim.Circuit(""" X_ERROR(0.1) 0 MR 0 OBSERVABLE_INCLUDE(0) rec[-1] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, num_shots=1000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert result.shots == 1000 assert 50 <= result.errors <= 150 @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_no_detectors(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit(""" X_ERROR(0.1) 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, num_shots=1000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert 50 <= result.errors <= 150 @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_no_detectors_with_post_mask(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit(""" X_ERROR(0.1) 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, post_mask=np.array([], dtype=np.uint8), num_shots=1000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert 50 <= result.errors <= 150 @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_post_selection(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit(""" X_ERROR(0.6) 0 M 0 DETECTOR(2, 0, 0, 1) rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] X_ERROR(0.5) 1 M 1 DETECTOR(1, 0, 0) rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] X_ERROR(0.1) 2 M 2 OBSERVABLE_INCLUDE(0) rec[-1] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, post_mask=post_selection_mask_from_4th_coord(circuit), num_shots=2000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert 1050 <= result.discards <= 1350 if 'vacuous' not in decoder: assert 40 <= result.errors <= 160 @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_observable_post_selection(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit(""" X_ERROR(0.1) 0 X_ERROR(0.2) 1 M 0 1 OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(1) rec[-1] rec[-2] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, post_mask=None, postselected_observable_mask=np.array([1], dtype=np.uint8), num_shots=10000, decoder=decoder, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) np.testing.assert_allclose(result.discards / result.shots, 0.2, atol=0.1) if 'vacuous' not in decoder: np.testing.assert_allclose(result.errors / (result.shots - result.discards), 0.1, atol=0.05) @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_error_splitting(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit(""" X_ERROR(0.1) 0 X_ERROR(0.2) 1 M 0 1 OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(1) rec[-1] rec[-2] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, post_mask=None, num_shots=10000, decoder=decoder, count_observable_error_combos=True, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert set(result.custom_counts.keys()) == {'obs_mistake_mask=E_', 'obs_mistake_mask=_E', 'obs_mistake_mask=EE'} if 'vacuous' not in decoder: np.testing.assert_allclose(result.errors / result.shots, 1 - 0.8 * 0.9, atol=0.05) np.testing.assert_allclose(result.custom_counts['obs_mistake_mask=E_'] / result.shots, 0.1 * 0.2, atol=0.05) np.testing.assert_allclose(result.custom_counts['obs_mistake_mask=_E'] / result.shots, 0.1 * 0.8, atol=0.05) np.testing.assert_allclose(result.custom_counts['obs_mistake_mask=EE'] / result.shots, 0.9 * 0.2, atol=0.05) @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_detector_counting(decoder: str, force_streaming: Optional[bool]): circuit = stim.Circuit(""" X_ERROR(0.1) 0 X_ERROR(0.2) 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(1) rec[-1] rec[-2] """) result = sample_decode( circuit_obj=circuit, circuit_path=None, dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, post_mask=None, num_shots=10000, decoder=decoder, count_detection_events=True, __private__unstable__force_decode_on_disk=force_streaming, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert result.custom_counts['detectors_checked'] == 20000 assert 0.3 * 10000 * 0.5 <= result.custom_counts['detection_events'] <= 0.3 * 10000 * 2.0 assert set(result.custom_counts.keys()) == {'detectors_checked', 'detection_events'} @pytest.mark.parametrize('decoder,force_streaming', DECODER_CASES) def test_decode_fails_correctly(decoder: str, force_streaming: Optional[bool]): decoder_obj = BUILT_IN_DECODERS.get(decoder) with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) circuit = stim.Circuit(""" REPEAT 9 { MR(0.001) 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] } """) dem = circuit.detector_error_model() circuit.to_file(d / 'circuit.stim') dem.to_file(d / 'dem.dem') with open(d / 'bad_dets.b8', 'wb') as f: f.write(b'!') if 'vacuous' not in decoder: with pytest.raises(Exception): decoder_obj.decode_via_files( num_shots=1, num_dets=dem.num_detectors, num_obs=dem.num_observables, dem_path=d / 'dem.dem', dets_b8_in_path=d / 'bad_dets.b8', obs_predictions_b8_out_path=d / 'predict.b8', tmp_dir=d, ) @pytest.mark.parametrize('decoder', TEST_DECODER_NAMES) def test_full_scale(decoder: str): result, = sinter.collect( num_workers=2, tasks=[sinter.Task(circuit=stim.Circuit())], decoders=[decoder], max_shots=1000, custom_decoders=TEST_CUSTOM_DECODERS, ) assert result.discards == 0 assert result.shots == 1000 assert result.errors == 0 def test_infer_decode_via_files_from_decode_from_compile_decoder_for_dem(): class IncompleteDecoder(sinter.Decoder): pass class WrongDecoder(sinter.Decoder, sinter.CompiledDecoder): def compile_decoder_for_dem( self, *, dem: stim.DetectorErrorModel, ) -> CompiledDecoder: return self def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: np.ndarray, ) -> np.ndarray: return np.zeros(shape=5, dtype=np.bool_) class TrivialCompiledDecoder(sinter.CompiledDecoder): def __init__(self, num_obs: int): self.num_obs = -(-num_obs // 8) def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: np.ndarray, ) -> np.ndarray: return np.zeros(dtype=np.uint8, shape=(bit_packed_detection_event_data.shape[0], self.num_obs)) class TrivialDecoder(sinter.Decoder): def compile_decoder_for_dem( self, *, dem: stim.DetectorErrorModel, ) -> CompiledDecoder: return TrivialCompiledDecoder(num_obs=dem.num_observables) circuit = stim.Circuit.generated("repetition_code:memory", distance=3, rounds=3) dem = circuit.detector_error_model() with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) circuit.compile_detector_sampler().sample_write( shots=10, filepath=d / 'dets.b8', format='b8', ) dem.to_file(d / 'dem.dem') with pytest.raises(NotImplementedError, match='compile_decoder_for_dem'): IncompleteDecoder().decode_via_files( num_shots=10, num_dets=dem.num_detectors, num_obs=dem.num_observables, dem_path=d / 'dem.dem', dets_b8_in_path=d / 'dets.b8', obs_predictions_b8_out_path=d / 'obs.b8', tmp_dir=d, ) with pytest.raises(ValueError, match='shape='): WrongDecoder().decode_via_files( num_shots=10, num_dets=dem.num_detectors, num_obs=dem.num_observables, dem_path=d / 'dem.dem', dets_b8_in_path=d / 'dets.b8', obs_predictions_b8_out_path=d / 'obs.b8', tmp_dir=d, ) TrivialDecoder().decode_via_files( num_shots=10, num_dets=dem.num_detectors, num_obs=dem.num_observables, dem_path=d / 'dem.dem', dets_b8_in_path=d / 'dets.b8', obs_predictions_b8_out_path=d / 'obs.b8', tmp_dir=d, ) obs = np.fromfile(d / 'obs.b8', dtype=np.uint8, count=10) np.testing.assert_array_equal(obs, [0] * 10) ================================================ FILE: glue/sample/src/sinter/_decoding/_decoding_vacuous.py ================================================ import numpy as np from sinter._decoding._decoding_decoder_class import Decoder, CompiledDecoder class VacuousDecoder(Decoder): """An example decoder that always predicts the observables aren't flipped. """ def compile_decoder_for_dem(self, *, dem: 'stim.DetectorErrorModel') -> CompiledDecoder: return VacuousCompiledDecoder(shape=(dem.num_observables + 7) // 8) def decode_via_files(self, *, num_shots: int, num_dets: int, num_obs: int, dem_path: 'pathlib.Path', dets_b8_in_path: 'pathlib.Path', obs_predictions_b8_out_path: 'pathlib.Path', tmp_dir: 'pathlib.Path', ) -> None: with open(obs_predictions_b8_out_path, 'wb') as f: f.write(b'\0' * (num_obs * num_shots)) class VacuousCompiledDecoder(CompiledDecoder): """An example decoder that always predicts the observables aren't flipped. """ def __init__(self, shape: int): self.shape = shape def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: 'np.ndarray', ) -> 'np.ndarray': return np.zeros(shape=(bit_packed_detection_event_data.shape[0], self.shape), dtype=np.uint8) ================================================ FILE: glue/sample/src/sinter/_decoding/_perfectionist_sampler.py ================================================ import time import numpy as np from sinter._data import Task, AnonTaskStats from sinter._decoding._sampler import Sampler, CompiledSampler class PerfectionistSampler(Sampler): """Predicts obs aren't flipped. Discards shots with any detection events.""" def compiled_sampler_for_task(self, task: Task) -> CompiledSampler: return CompiledPerfectionistSampler(task) class CompiledPerfectionistSampler(CompiledSampler): def __init__(self, task: Task): self.stim_sampler = task.circuit.compile_detector_sampler() def sample(self, max_shots: int) -> AnonTaskStats: t0 = time.monotonic() dets, obs = self.stim_sampler.sample( shots=max_shots, bit_packed=True, separate_observables=True, ) num_shots = dets.shape[0] discards = np.any(dets, axis=1) errors = np.any(obs, axis=1) num_discards = np.count_nonzero(discards) num_errors = np.count_nonzero(errors & ~discards) t1 = time.monotonic() return AnonTaskStats( shots=num_shots, errors=num_errors, discards=num_discards, seconds=t1 - t0, ) ================================================ FILE: glue/sample/src/sinter/_decoding/_sampler.py ================================================ import abc from typing import TYPE_CHECKING if TYPE_CHECKING: import sinter class CompiledSampler(metaclass=abc.ABCMeta): """A sampler that has been configured for efficiently sampling some task.""" @abc.abstractmethod def sample(self, suggested_shots: int) -> 'sinter.AnonTaskStats': """Samples shots and returns statistics. Args: suggested_shots: The number of shots being requested. The sampler may perform more shots or fewer shots than this, so technically this argument can just be ignored. If a sampler is optimized for a specific batch size, it can simply return one batch per call regardless of this parameter. However, this parameter is a useful hint about the amount of work being done. The sampler can use this to optimize its behavior. For example, it could adjust its batch size downward if the suggested shots is very small. Whereas if the suggested shots is very high, the sampler should focus entirely on achieving the best possible throughput. Note that, in typical workloads, the sampler will be called repeatedly with the same value of suggested_shots. Therefore it is reasonable to allocate buffers sized to accomodate the current suggested_shots, expecting them to be useful again for the next shot. Returns: A sinter.AnonTaskStats saying how many shots were actually taken, how many errors were seen, etc. The returned stats must have at least one shot. """ pass def handles_throttling(self) -> bool: """Return True to disable sinter wrapping samplers with throttling. By default, sinter will wrap samplers so that they initially only do a small number of shots then slowly ramp up. Sometimes this behavior is not desired (e.g. in unit tests). Override this method to return True to disable it. """ return False class Sampler(metaclass=abc.ABCMeta): """A strategy for producing stats from tasks. Call `sampler.compiled_sampler_for_task(task)` to get a compiled sampler for a task, then call `compiled_sampler.sample(shots)` to collect statistics. A sampler differs from a `sinter.Decoder` because the sampler is responsible for the full sampling process (e.g. simulating the circuit), whereas a decoder can do nothing except predict observable flips from detection event data. This prevents the decoders from cheating, but makes them less flexible overall. A sampler can do things like use simulators other than stim, or really anything at all as long as it ends with returning statistics about shot counts, error counts, and etc. """ @abc.abstractmethod def compiled_sampler_for_task(self, task: 'sinter.Task') -> 'sinter.CompiledSampler': """Creates, configures, and returns an object for sampling the task.""" pass ================================================ FILE: glue/sample/src/sinter/_decoding/_stim_then_decode_sampler.py ================================================ import collections import pathlib import random import time from typing import Optional from typing import Union import numpy as np from sinter._data import Task, AnonTaskStats from sinter._decoding._sampler import Sampler, CompiledSampler from sinter._decoding._decoding_decoder_class import Decoder, CompiledDecoder class StimThenDecodeSampler(Sampler): """Samples shots using stim, then decodes using the given decoder. This is the default sampler; the one used to wrap decoders with no specified sampler. The decoder's goal is to predict the observable flips given the detection event data. Errors are when the prediction is wrong. Discards are when the decoder returns an extra byte of prediction data for each shot, and the extra byte is not zero. """ def __init__( self, *, decoder: Decoder, count_observable_error_combos: bool, count_detection_events: bool, tmp_dir: Optional[pathlib.Path], ): self.decoder = decoder self.count_observable_error_combos = count_observable_error_combos self.count_detection_events = count_detection_events self.tmp_dir = tmp_dir def compiled_sampler_for_task(self, task: Task) -> CompiledSampler: return _CompiledStimThenDecodeSampler( decoder=self.decoder, task=task, count_detection_events=self.count_detection_events, count_observable_error_combos=self.count_observable_error_combos, tmp_dir=self.tmp_dir, ) def classify_discards_and_errors( *, actual_obs: np.ndarray, predictions: np.ndarray, postselected_observables_mask: Union[np.ndarray, None], out_count_observable_error_combos: Union[None, collections.Counter[str]], num_obs: int, ) -> tuple[int, int]: num_discards = 0 # Added bytes are used for signalling discards. if predictions.shape[1] == actual_obs.shape[1] + 1: discard_mask = predictions[:, -1] != 0 predictions = predictions[:, :-1] num_discards += np.count_nonzero(discard_mask) discard_mask ^= True actual_obs = actual_obs[discard_mask] predictions = predictions[discard_mask] # Mispredicted observables can be used for signalling discards. if postselected_observables_mask is not None: discard_mask = np.any((actual_obs ^ predictions) & postselected_observables_mask, axis=1) num_discards += np.count_nonzero(discard_mask) discard_mask ^= True actual_obs = actual_obs[discard_mask] predictions = predictions[discard_mask] fail_mask = np.any(actual_obs != predictions, axis=1) if out_count_observable_error_combos is not None: for k in np.flatnonzero(fail_mask): mistakes = np.unpackbits(actual_obs[k] ^ predictions[k], count=num_obs, bitorder='little') err_key = "obs_mistake_mask=" + ''.join('_E'[b] for b in mistakes) out_count_observable_error_combos[err_key] += 1 num_errors = np.count_nonzero(fail_mask) return num_discards, num_errors class DiskDecoder(CompiledDecoder): def __init__(self, decoder: Decoder, task: Task, tmp_dir: pathlib.Path): self.decoder = decoder self.task = task self.top_tmp_dir: pathlib.Path = tmp_dir while True: k = random.randint(0, 2**64) self.top_tmp_dir = tmp_dir / f'disk_decoder_{k}' try: self.top_tmp_dir.mkdir() break except FileExistsError: pass self.decoder_tmp_dir: pathlib.Path = self.top_tmp_dir / 'dec' self.decoder_tmp_dir.mkdir() self.num_obs = task.detector_error_model.num_observables self.num_dets = task.detector_error_model.num_detectors self.dem_path = self.top_tmp_dir / 'dem.dem' self.dets_b8_in_path = self.top_tmp_dir / 'dets.b8' self.obs_predictions_b8_out_path = self.top_tmp_dir / 'obs.b8' self.task.detector_error_model.to_file(self.dem_path) def decode_shots_bit_packed( self, *, bit_packed_detection_event_data: np.ndarray, ) -> np.ndarray: num_shots = bit_packed_detection_event_data.shape[0] with open(self.dets_b8_in_path, 'wb') as f: bit_packed_detection_event_data.tofile(f) self.decoder.decode_via_files( num_shots=num_shots, num_obs=self.num_obs, num_dets=self.num_dets, dem_path=self.dem_path, dets_b8_in_path=self.dets_b8_in_path, obs_predictions_b8_out_path=self.obs_predictions_b8_out_path, tmp_dir=self.decoder_tmp_dir, ) num_obs_bytes = (self.num_obs + 7) // 8 with open(self.obs_predictions_b8_out_path, 'rb') as f: prediction = np.fromfile(f, dtype=np.uint8, count=num_obs_bytes * num_shots) assert prediction.shape == (num_obs_bytes * num_shots,) self.obs_predictions_b8_out_path.unlink() self.dets_b8_in_path.unlink() return prediction.reshape((num_shots, num_obs_bytes)) def _compile_decoder_with_disk_fallback( decoder: Decoder, task: Task, tmp_dir: Optional[pathlib.Path], ) -> CompiledDecoder: try: return decoder.compile_decoder_for_dem(dem=task.detector_error_model) except NotImplementedError: pass if tmp_dir is None: raise ValueError(f"Decoder {task.decoder=} didn't implement `compile_decoder_for_dem`, but no temporary directory was provided for falling back to `decode_via_files`.") return DiskDecoder(decoder, task, tmp_dir) class _CompiledStimThenDecodeSampler(CompiledSampler): def __init__( self, *, decoder: Decoder, task: Task, count_observable_error_combos: bool, count_detection_events: bool, tmp_dir: Optional[pathlib.Path], ): self.task = task self.compiled_decoder = _compile_decoder_with_disk_fallback(decoder, task, tmp_dir) self.stim_sampler = task.circuit.compile_detector_sampler() self.count_observable_error_combos = count_observable_error_combos self.count_detection_events = count_detection_events self.num_det = self.task.circuit.num_detectors self.num_obs = self.task.circuit.num_observables def sample(self, max_shots: int) -> AnonTaskStats: t0 = time.monotonic() dets, actual_obs = self.stim_sampler.sample( shots=max_shots, bit_packed=True, separate_observables=True, ) num_shots = dets.shape[0] custom_counts = collections.Counter() if self.count_detection_events: custom_counts['detectors_checked'] += self.num_det * num_shots for b in range(8): custom_counts['detection_events'] += np.count_nonzero(dets & (1 << b)) # Discard any shots that contain a postselected detection events. if self.task.postselection_mask is not None: discarded_flags = np.any(dets & self.task.postselection_mask, axis=1) num_discards_1 = np.count_nonzero(discarded_flags) if num_discards_1: dets = dets[~discarded_flags, :] actual_obs = actual_obs[~discarded_flags, :] else: num_discards_1 = 0 predictions = self.compiled_decoder.decode_shots_bit_packed(bit_packed_detection_event_data=dets) if not isinstance(predictions, np.ndarray): raise ValueError("not isinstance(predictions, np.ndarray)") if predictions.dtype != np.uint8: raise ValueError("predictions.dtype != np.uint8") if len(predictions.shape) != 2: raise ValueError("len(predictions.shape) != 2") if predictions.shape[0] != num_shots: raise ValueError("predictions.shape[0] != num_shots") if predictions.shape[1] < actual_obs.shape[1]: raise ValueError("predictions.shape[1] < actual_obs.shape[1]") if predictions.shape[1] > actual_obs.shape[1] + 1: raise ValueError("predictions.shape[1] > actual_obs.shape[1] + 1") num_discards_2, num_errors = classify_discards_and_errors( actual_obs=actual_obs, predictions=predictions, postselected_observables_mask=self.task.postselected_observables_mask, out_count_observable_error_combos=custom_counts if self.count_observable_error_combos else None, num_obs=self.num_obs, ) t1 = time.monotonic() return AnonTaskStats( shots=num_shots, errors=num_errors, discards=num_discards_1 + num_discards_2, seconds=t1 - t0, custom_counts=custom_counts, ) ================================================ FILE: glue/sample/src/sinter/_decoding/_stim_then_decode_sampler_test.py ================================================ import collections import numpy as np from sinter._decoding._stim_then_decode_sampler import \ classify_discards_and_errors def test_classify_discards_and_errors(): assert classify_discards_and_errors( actual_obs=np.array([ [1, 2], [2, 2], [3, 2], [4, 3], [1, 3], [0, 3], [0, 3], ], dtype=np.uint8), predictions=np.array([ [1, 2], [2, 2], [3, 2], [4, 3], [1, 3], [0, 3], [0, 3], ], dtype=np.uint8), postselected_observables_mask=None, out_count_observable_error_combos=None, num_obs=16, ) == (0, 0) assert classify_discards_and_errors( actual_obs=np.array([ [1, 2], [2, 2], [3, 2], [4, 3], [1, 3], [0, 3], [0, 3], ], dtype=np.uint8), predictions=np.array([ [0, 0], [2, 2], [3, 2], [4, 1], [1, 3], [0, 3], [0, 3], ], dtype=np.uint8), postselected_observables_mask=None, out_count_observable_error_combos=None, num_obs=16, ) == (0, 2) assert classify_discards_and_errors( actual_obs=np.array([ [1, 2], [2, 2], [3, 2], [4, 3], [1, 3], [0, 3], [0, 3], ], dtype=np.uint8), predictions=np.array([ [0, 0, 0], [2, 2, 0], [3, 2, 0], [4, 1, 0], [1, 3, 0], [0, 3, 0], [0, 3, 0], ], dtype=np.uint8), postselected_observables_mask=None, out_count_observable_error_combos=None, num_obs=16, ) == (0, 2) assert classify_discards_and_errors( actual_obs=np.array([ [1, 2], [2, 2], [3, 2], [4, 3], [1, 3], [0, 3], [0, 3], ], dtype=np.uint8), predictions=np.array([ [0, 0, 0], [2, 2, 1], [3, 2, 0], [4, 1, 0], [1, 3, 0], [0, 3, 0], [0, 3, 0], ], dtype=np.uint8), postselected_observables_mask=None, out_count_observable_error_combos=None, num_obs=16, ) == (1, 2) assert classify_discards_and_errors( actual_obs=np.array([ [1, 2], [2, 2], [3, 2], [4, 3], [1, 3], [0, 3], [0, 3], ], dtype=np.uint8), predictions=np.array([ [0, 0, 1], [2, 2, 0], [3, 2, 0], [4, 1, 0], [1, 3, 0], [0, 3, 0], [0, 3, 0], ], dtype=np.uint8), postselected_observables_mask=None, out_count_observable_error_combos=None, num_obs=16, ) == (1, 1) assert classify_discards_and_errors( actual_obs=np.array([ [1, 2], [2, 2], [3, 2], [4, 3], [1, 3], [0, 3], [0, 3], ], dtype=np.uint8), predictions=np.array([ [0, 0, 1], [2, 2, 1], [3, 2, 0], [4, 1, 0], [1, 3, 0], [0, 3, 0], [0, 3, 0], ], dtype=np.uint8), postselected_observables_mask=None, out_count_observable_error_combos=None, num_obs=16, ) == (2, 1) assert classify_discards_and_errors( actual_obs=np.array([ [1, 2], [2, 2], [3, 2], [4, 3], [1, 3], [2, 3], [1, 3], ], dtype=np.uint8), predictions=np.array([ [0, 0, 1], [2, 2, 1], [3, 2, 0], [4, 1, 0], [1, 3, 0], [0, 3, 0], [0, 3, 0], ], dtype=np.uint8), postselected_observables_mask=np.array([1, 0]), out_count_observable_error_combos=None, num_obs=16, ) == (3, 2) counter = collections.Counter() assert classify_discards_and_errors( actual_obs=np.array([ [1, 2], [1, 2], ], dtype=np.uint8), predictions=np.array([ [1, 0], [1, 2], ], dtype=np.uint8), postselected_observables_mask=np.array([1, 0]), out_count_observable_error_combos=counter, num_obs=13, ) == (0, 1) assert counter == collections.Counter(["obs_mistake_mask=_________E___"]) ================================================ FILE: glue/sample/src/sinter/_plotting.py ================================================ import math from typing import Callable, TypeVar, List, Any, Iterable, Optional, TYPE_CHECKING, Dict, Union, Literal, Tuple from typing import Sequence from typing import cast import numpy as np from sinter._probability_util import fit_binomial, shot_error_rate_to_piece_error_rate, Fit if TYPE_CHECKING: import sinter import matplotlib.pyplot as plt MARKERS: str = "ov*sp^<>8PhH+xXDd|" * 100 LINESTYLES: tuple[str, ...] = ( 'solid', 'dotted', 'dashed', 'dashdot', 'loosely dotted', 'dotted', 'densely dotted', 'long dash with offset', 'loosely dashed', 'dashed', 'densely dashed', 'loosely dashdotted', 'dashdotted', 'densely dashdotted', 'dashdotdotted', 'loosely dashdotdotted', 'densely dashdotdotted', ) T = TypeVar('T') TVal = TypeVar('TVal') TKey = TypeVar('TKey') def split_by(vs: Iterable[T], key_func: Callable[[T], Any]) -> List[List[T]]: cur_key: Any = None out: List[List[T]] = [] buf: List[T] = [] for item in vs: key = key_func(item) if key != cur_key: cur_key = key if buf: out.append(buf) buf = [] buf.append(item) if buf: out.append(buf) return out class LooseCompare: def __init__(self, val: Any): self.val: Any = None self.val = val.val if isinstance(val, LooseCompare) else val def __lt__(self, other: Any) -> bool: other_val = other.val if isinstance(other, LooseCompare) else other if isinstance(self.val, (int, float)) and isinstance(other_val, (int, float)): return self.val < other_val if isinstance(self.val, (tuple, list)) and isinstance(other_val, (tuple, list)): return tuple(LooseCompare(e) for e in self.val) < tuple(LooseCompare(e) for e in other_val) return str(self.val) < str(other_val) def __gt__(self, other: Any) -> bool: other_val = other.val if isinstance(other, LooseCompare) else other if isinstance(self.val, (int, float)) and isinstance(other_val, (int, float)): return self.val > other_val if isinstance(self.val, (tuple, list)) and isinstance(other_val, (tuple, list)): return tuple(LooseCompare(e) for e in self.val) > tuple(LooseCompare(e) for e in other_val) return str(self.val) > str(other_val) def __str__(self) -> str: return str(self.val) def __eq__(self, other: Any) -> bool: if isinstance(other, LooseCompare): other_val = other.val else: other_val = other if isinstance(self.val, (int, float)) and isinstance(other_val, (int, float)): return self.val == other_val return str(self.val) == str(other_val) def better_sorted_str_terms(val: Any) -> Any: """A function that orders "a10000" after "a9", instead of before. Normally, sorting strings sorts them lexicographically, treating numbers so that "1999999" ends up being less than "2". This method splits the string into a tuple of text pairs and parsed number parts, so that sorting by this key puts "2" before "1999999". Because this method is intended for use in plotting, where it's more important to see a bad result than to see nothing, it returns a type that tries to be comparable to everything. Args: val: The value to convert into a value with a better sorting order. Returns: A custom type of object with a better sorting order. Examples: >>> import sinter >>> items = [ ... "distance=199999, rounds=3", ... "distance=2, rounds=3", ... "distance=199999, rounds=199999", ... "distance=2, rounds=199999", ... ] >>> for e in sorted(items, key=sinter.better_sorted_str_terms): ... print(e) distance=2, rounds=3 distance=2, rounds=199999 distance=199999, rounds=3 distance=199999, rounds=199999 """ if val is None: return 'None' if isinstance(val, tuple): return tuple(better_sorted_str_terms(e) for e in val) if not isinstance(val, str): return LooseCompare(val) terms = split_by(val, lambda c: c in '.0123456789') result = [] for term in terms: term = ''.join(term) if '.' in term: try: term = float(term) except ValueError: try: term = tuple(int(e) for e in term.split('.')) except ValueError: pass else: try: term = int(term) except ValueError: pass result.append(term) if len(result) == 1 and isinstance(result[0], (int, float)): return LooseCompare(result[0]) return tuple(LooseCompare(e) for e in result) def group_by(items: Iterable[TVal], *, key: Callable[[TVal], TKey], ) -> Dict[TKey, List[TVal]]: """Groups items based on whether they produce the same key from a function. Args: items: The items to group. key: Items that produce the same value from this function get grouped together. Returns: A dictionary mapping outputs that were produced by the grouping function to the list of items that produced that output. Examples: >>> import sinter >>> sinter.group_by([1, 2, 3], key=lambda i: i == 2) {False: [1, 3], True: [2]} >>> sinter.group_by(range(10), key=lambda i: i % 3) {0: [0, 3, 6, 9], 1: [1, 4, 7], 2: [2, 5, 8]} """ result: Dict[TKey, List[TVal]] = {} for item in items: curve_id = key(item) result.setdefault(curve_id, []).append(item) return result TCurveId = TypeVar('TCurveId') class _FrozenDict: def __init__(self, v: dict): self._v = dict(v) self._eq = frozenset(v.items()) self._hash = hash(self._eq) terms = [] for k in sorted(self._v.keys(), key=lambda e: (e != 'sort', e)): terms.append(k) terms.append(better_sorted_str_terms(self._v[k]) ) self._order = tuple(terms) def __eq__(self, other): if isinstance(other, _FrozenDict): return self._eq == other._eq return NotImplemented def __lt__(self, other): if isinstance(other, _FrozenDict): return self._order < other._order return NotImplemented def __ne__(self, other): return not (self == other) def __hash__(self): return self._hash def __getitem__(self, item): return self._v[item] def get(self, item, alternate = None): return self._v.get(item, alternate) def __str__(self): return " ".join(str(v) for _, v in sorted(self._v.items())) def plot_discard_rate( *, ax: 'plt.Axes', stats: 'Iterable[sinter.TaskStats]', x_func: Callable[['sinter.TaskStats'], Any], failure_units_per_shot_func: Callable[['sinter.TaskStats'], Any] = lambda _: 1, group_func: Callable[['sinter.TaskStats'], TCurveId] = lambda _: None, filter_func: Callable[['sinter.TaskStats'], Any] = lambda _: True, plot_args_func: Callable[[int, TCurveId, List['sinter.TaskStats']], Dict[str, Any]] = lambda index, group_key, group_stats: dict(), highlight_max_likelihood_factor: Optional[float] = 1e3, point_label_func: Callable[['sinter.TaskStats'], Any] = lambda _: None, ) -> None: """Plots discard rates in curves with uncertainty highlights. Args: ax: The plt.Axes to plot onto. For example, the `ax` value from `fig, ax = plt.subplots(1, 1)`. stats: The collected statistics to plot. x_func: The X coordinate to use for each stat's data point. For example, this could be `x_func=lambda stat: stat.json_metadata['physical_error_rate']`. failure_units_per_shot_func: How many discard chances there are per shot. This rescales what the discard rate means. By default, it is the discard rate per shot, but this allows you to instead make it the discard rate per round. For example, if the metadata associated with a shot has a field 'r' which is the number of rounds, then this can be achieved with `failure_units_per_shot_func=lambda stats: stats.metadata['r']`. group_func: Optional. When specified, multiple curves will be plotted instead of one curve. The statistics are grouped into curves based on whether or not they get the same result out of this function. For example, this could be `group_func=lambda stat: stat.decoder`. If the result of the function is a dictionary, then optional keys in the dictionary will also control the plotting of each curve. Available keys are: 'label': the label added to the legend for the curve 'color': the color used for plotting the curve 'marker': the marker used for the curve 'linestyle': the linestyle used for the curve 'sort': the order in which the curves will be plotted and added to the legend e.g. if two curves (with different resulting dictionaries from group_func) share the same value for key 'marker', they will be plotted with the same marker. Colors, markers and linestyles are assigned in order, sorted by the values for those keys. filter_func: Optional. When specified, some curves will not be plotted. The statistics are filtered and only plotted if filter_func(stat) returns True. For example, `filter_func=lambda s: s.json_metadata['basis'] == 'x'` would plot only stats where the saved metadata indicates the basis was 'x'. plot_args_func: Optional. Specifies additional arguments to give the underlying calls to `plot` and `fill_between` used to do the actual plotting. For example, this can be used to specify markers and colors. Takes the index of the curve in sorted order and also a curve_id (these will be 0 and None respectively if group_func is not specified). For example, this could be: plot_args_func=lambda index, curve_id: {'color': 'red' if curve_id == 'pymatching' else 'blue'} highlight_max_likelihood_factor: Controls how wide the uncertainty highlight region around curves is. Must be 1 or larger. Hypothesis probabilities at most that many times as unlikely as the max likelihood hypothesis will be highlighted. point_label_func: Optional. Specifies text to draw next to data points. """ if highlight_max_likelihood_factor is None: highlight_max_likelihood_factor = 1 def y_func(stat: 'sinter.TaskStats') -> Union[float, 'sinter.Fit']: result = fit_binomial( num_shots=stat.shots, num_hits=stat.discards, max_likelihood_factor=highlight_max_likelihood_factor, ) pieces = failure_units_per_shot_func(stat) result = Fit( low=shot_error_rate_to_piece_error_rate(result.low, pieces=pieces), best=shot_error_rate_to_piece_error_rate(result.best, pieces=pieces), high=shot_error_rate_to_piece_error_rate(result.high, pieces=pieces), ) if highlight_max_likelihood_factor == 1: return result.best return result plot_custom( ax=ax, stats=stats, x_func=x_func, y_func=y_func, group_func=group_func, filter_func=filter_func, plot_args_func=plot_args_func, point_label_func=point_label_func, ) def plot_error_rate( *, ax: 'plt.Axes', stats: 'Iterable[sinter.TaskStats]', x_func: Callable[['sinter.TaskStats'], Any], failure_units_per_shot_func: Callable[['sinter.TaskStats'], Any] = lambda _: 1, failure_values_func: Callable[['sinter.TaskStats'], Any] = lambda _: 1, group_func: Callable[['sinter.TaskStats'], TCurveId] = lambda _: None, filter_func: Callable[['sinter.TaskStats'], Any] = lambda _: True, plot_args_func: Callable[[int, TCurveId, List['sinter.TaskStats']], Dict[str, Any]] = lambda index, group_key, group_stats: dict(), highlight_max_likelihood_factor: Optional[float] = 1e3, line_fits: Optional[Tuple[Literal['linear', 'log', 'sqrt'], Literal['linear', 'log', 'sqrt']]] = None, point_label_func: Callable[['sinter.TaskStats'], Any] = lambda _: None, ) -> None: """Plots error rates in curves with uncertainty highlights. Args: ax: The plt.Axes to plot onto. For example, the `ax` value from `fig, ax = plt.subplots(1, 1)`. stats: The collected statistics to plot. x_func: The X coordinate to use for each stat's data point. For example, this could be `x_func=lambda stat: stat.json_metadata['physical_error_rate']`. failure_units_per_shot_func: How many error chances there are per shot. This rescales what the logical error rate means. By default, it is the logical error rate per shot, but this allows you to instead make it the logical error rate per round. For example, if the metadata associated with a shot has a field 'r' which is the number of rounds, then this can be achieved with `failure_units_per_shot_func=lambda stats: stats.metadata['r']`. failure_values_func: How many independent ways there are for a shot to fail, such as the number of independent observables in a memory experiment. This affects how the failure units rescaling plays out (e.g. with 1 independent failure the "center" of the conversion is at 50% whereas for 2 independent failures the "center" is at 75%). group_func: Optional. When specified, multiple curves will be plotted instead of one curve. The statistics are grouped into curves based on whether or not they get the same result out of this function. For example, this could be `group_func=lambda stat: stat.decoder`. If the result of the function is a dictionary, then optional keys in the dictionary will also control the plotting of each curve. Available keys are: 'label': the label added to the legend for the curve 'color': the color used for plotting the curve 'marker': the marker used for the curve 'linestyle': the linestyle used for the curve 'sort': the order in which the curves will be plotted and added to the legend e.g. if two curves (with different resulting dictionaries from group_func) share the same value for key 'marker', they will be plotted with the same marker. Colors, markers and linestyles are assigned in order, sorted by the values for those keys. filter_func: Optional. When specified, some curves will not be plotted. The statistics are filtered and only plotted if filter_func(stat) returns True. For example, `filter_func=lambda s: s.json_metadata['basis'] == 'x'` would plot only stats where the saved metadata indicates the basis was 'x'. plot_args_func: Optional. Specifies additional arguments to give the underlying calls to `plot` and `fill_between` used to do the actual plotting. For example, this can be used to specify markers and colors. Takes the index of the curve in sorted order and also a curve_id (these will be 0 and None respectively if group_func is not specified). For example, this could be: plot_args_func=lambda index, curve_id: {'color': 'red' if curve_id == 'pymatching' else 'blue'} highlight_max_likelihood_factor: Controls how wide the uncertainty highlight region around curves is. Must be 1 or larger. Hypothesis probabilities at most that many times as unlikely as the max likelihood hypothesis will be highlighted. line_fits: Defaults to None. Set this to a tuple (x_scale, y_scale) to include a dashed line fit to every curve. The scales determine how to transform the coordinates before performing the fit, and can be set to 'linear', 'sqrt', or 'log'. point_label_func: Optional. Specifies text to draw next to data points. """ if highlight_max_likelihood_factor is None: highlight_max_likelihood_factor = 1 if not (highlight_max_likelihood_factor >= 1): raise ValueError(f"not (highlight_max_likelihood_factor={highlight_max_likelihood_factor} >= 1)") def y_func(stat: 'sinter.TaskStats') -> Union[float, 'sinter.Fit']: result = fit_binomial( num_shots=stat.shots - stat.discards, num_hits=stat.errors, max_likelihood_factor=highlight_max_likelihood_factor, ) pieces = failure_units_per_shot_func(stat) values = failure_values_func(stat) result = Fit( low=shot_error_rate_to_piece_error_rate(result.low, pieces=pieces, values=values), best=shot_error_rate_to_piece_error_rate(result.best, pieces=pieces, values=values), high=shot_error_rate_to_piece_error_rate(result.high, pieces=pieces, values=values), ) if stat.errors == 0: result = Fit(low=result.low, high=result.high, best=float('nan')) if highlight_max_likelihood_factor == 1: return result.best return result plot_custom( ax=ax, stats=stats, x_func=x_func, y_func=y_func, group_func=group_func, filter_func=filter_func, plot_args_func=plot_args_func, line_fits=line_fits, point_label_func=point_label_func, ) def _rescale(v: Sequence[float], scale: str, invert: bool) -> np.ndarray: if scale == 'linear': return np.array(v) elif scale == 'log': return np.exp(v) if invert else np.log(v) elif scale == 'sqrt': return np.array(v)**2 if invert else np.sqrt(v) else: raise NotImplementedError(f'{scale=}') def plot_custom( *, ax: 'plt.Axes', stats: 'Iterable[sinter.TaskStats]', x_func: Callable[['sinter.TaskStats'], Any], y_func: Callable[['sinter.TaskStats'], Union['sinter.Fit', float, int]], group_func: Callable[['sinter.TaskStats'], TCurveId] = lambda _: None, point_label_func: Callable[['sinter.TaskStats'], Any] = lambda _: None, filter_func: Callable[['sinter.TaskStats'], Any] = lambda _: True, plot_args_func: Callable[[int, TCurveId, List['sinter.TaskStats']], Dict[str, Any]] = lambda index, group_key, group_stats: dict(), line_fits: Optional[Tuple[Literal['linear', 'log', 'sqrt'], Literal['linear', 'log', 'sqrt']]] = None, ) -> None: """Plots error rates in curves with uncertainty highlights. Args: ax: The plt.Axes to plot onto. For example, the `ax` value from `fig, ax = plt.subplots(1, 1)`. stats: The collected statistics to plot. x_func: The X coordinate to use for each stat's data point. For example, this could be `x_func=lambda stat: stat.json_metadata['physical_error_rate']`. y_func: The Y value to use for each stat's data point. This can be a float or it can be a sinter.Fit value, in which case the curve will follow the fit.best value and a highlighted area will be shown from fit.low to fit.high. group_func: Optional. When specified, multiple curves will be plotted instead of one curve. The statistics are grouped into curves based on whether or not they get the same result out of this function. For example, this could be `group_func=lambda stat: stat.decoder`. If the result of the function is a dictionary, then optional keys in the dictionary will also control the plotting of each curve. Available keys are: 'label': the label added to the legend for the curve 'color': the color used for plotting the curve 'marker': the marker used for the curve 'linestyle': the linestyle used for the curve 'sort': the order in which the curves will be plotted and added to the legend e.g. if two curves (with different resulting dictionaries from group_func) share the same value for key 'marker', they will be plotted with the same marker. Colors, markers and linestyles are assigned in order, sorted by the values for those keys. point_label_func: Optional. Specifies text to draw next to data points. filter_func: Optional. When specified, some curves will not be plotted. The statistics are filtered and only plotted if filter_func(stat) returns True. For example, `filter_func=lambda s: s.json_metadata['basis'] == 'x'` would plot only stats where the saved metadata indicates the basis was 'x'. plot_args_func: Optional. Specifies additional arguments to give the underlying calls to `plot` and `fill_between` used to do the actual plotting. For example, this can be used to specify markers and colors. Takes the index of the curve in sorted order and also a curve_id (these will be 0 and None respectively if group_func is not specified). For example, this could be: plot_args_func=lambda index, group_key, group_stats: { 'color': ( 'red' if group_key == 'decoder=pymatching p=0.001' else 'blue' ), } line_fits: Defaults to None. Set this to a tuple (x_scale, y_scale) to include a dashed line fit to every curve. The scales determine how to transform the coordinates before performing the fit, and can be set to 'linear', 'sqrt', or 'log'. """ def group_dict_func(item: 'sinter.TaskStats') -> _FrozenDict: e = group_func(item) return _FrozenDict(e if isinstance(e, dict) else {'label': str(e)}) # Backwards compatibility to when the group stats argument wasn't present. import inspect if len(inspect.signature(plot_args_func).parameters) == 2: old_plot_args_func = cast(Callable[[int, TCurveId], Any], plot_args_func) plot_args_func = lambda a, b, _: old_plot_args_func(a, b) filtered_stats: List['sinter.TaskStats'] = [ stat for stat in stats if filter_func(stat) ] curve_groups = group_by(filtered_stats, key=group_dict_func) colors = { k: f'C{i}' for i, k in enumerate(sorted({g.get('color', g) for g in curve_groups.keys()}, key=better_sorted_str_terms)) } markers = { k: MARKERS[i % len(MARKERS)] for i, k in enumerate(sorted({g.get('marker', g) for g in curve_groups.keys()}, key=better_sorted_str_terms)) } linestyles = { k: LINESTYLES[i % len(LINESTYLES)] for i, k in enumerate(sorted({g.get('linestyle', None) for g in curve_groups.keys()}, key=better_sorted_str_terms)) } def sort_key(a: Any) -> Any: if isinstance(a, _FrozenDict): return a.get('sort', better_sorted_str_terms(a)) return better_sorted_str_terms(a) for k, group_key in enumerate(sorted(curve_groups.keys(), key=sort_key)): group = curve_groups[group_key] group = sorted(group, key=x_func) color = colors[group_key.get('color', group_key)] marker = markers[group_key.get('marker', group_key)] linestyle = linestyles[group_key.get('linestyle', None)] label = str(group_key.get('label', group_key)) xs_label: list[float] = [] ys_label: list[float] = [] vs_label: list[float] = [] xs_best: list[float] = [] ys_best: list[float] = [] xs_low_high: list[float] = [] ys_low: list[float] = [] ys_high: list[float] = [] for item in group: x = x_func(item) y = y_func(item) point_label = point_label_func(item) if isinstance(y, Fit): if y.low is not None and y.high is not None and not math.isnan(y.low) and not math.isnan(y.high): xs_low_high.append(x) ys_low.append(y.low) ys_high.append(y.high) if y.best is not None and not math.isnan(y.best): ys_best.append(y.best) xs_best.append(x) if point_label: cy = None for e in [y.best, y.high, y.low]: if e is not None and not math.isnan(e): cy = e break if cy is not None: xs_label.append(x) ys_label.append(cy) vs_label.append(point_label) elif not math.isnan(y): xs_best.append(x) ys_best.append(y) if point_label: xs_label.append(x) ys_label.append(y) vs_label.append(point_label) args = dict(plot_args_func(k, group_func(group[0]), group)) if 'linestyle' not in args: args['linestyle'] = linestyle if 'marker' not in args: args['marker'] = marker if 'color' not in args: args['color'] = color if 'label' not in args: args['label'] = label ax.plot(xs_best, ys_best, **args) for x, y, lbl in zip(xs_label, ys_label, vs_label): if lbl: ax.annotate(lbl, (x, y)) if len(xs_low_high) > 1: ax.fill_between(xs_low_high, ys_low, ys_high, color=args['color'], alpha=0.2, zorder=-100) elif len(xs_low_high) == 1: l, = ys_low h, = ys_high m = (l + h) / 2 ax.errorbar(xs_low_high, [m], yerr=([m - l], [h - m]), marker='', elinewidth=1, ecolor=color, capsize=5) if line_fits is not None and len(set(xs_best)) >= 2: x_scale, y_scale = line_fits fit_xs = _rescale(xs_best, x_scale, False) fit_ys = _rescale(ys_best, y_scale, False) from scipy.stats import linregress line_fit = linregress(fit_xs, fit_ys) x0 = fit_xs[0] x1 = fit_xs[-1] dx = x1 - x0 x0 -= dx*10 x1 += dx*10 if x0 < 0 <= fit_xs[0] > x0 and x_scale == 'sqrt': x0 = 0 out_xs = np.linspace(x0, x1, 1000) out_ys = out_xs * line_fit.slope + line_fit.intercept out_xs = _rescale(out_xs, x_scale, True) out_ys = _rescale(out_ys, y_scale, True) line_fit_kwargs = args.copy() line_fit_kwargs.pop('marker', None) line_fit_kwargs.pop('label', None) line_fit_kwargs['linestyle'] = '--' line_fit_kwargs.setdefault('linewidth', 1) line_fit_kwargs['linewidth'] /= 2 ax.plot(out_xs, out_ys, **line_fit_kwargs) ================================================ FILE: glue/sample/src/sinter/_plotting_test.py ================================================ import io from matplotlib import pyplot as plt import sinter def test_better_sorted_str_terms(): f = sinter.better_sorted_str_terms assert f('everyone et al. 2020') == ('everyone et al', '.', ' ', 2020) assert f('a') == ('a',) assert f('abc') == ('abc',) assert f('a1b2') == ('a', 1, 'b', 2) assert f('a1.5b2') == ('a', 1.5, 'b', 2) assert f('a1.5.3b2') == ('a', (1, 5, 3), 'b', 2) assert f(1) < f(None) assert f(1) < f('2') assert f('2') > f(1) assert sorted([ "planar d=10 r=30", "planar d=16 r=36", "planar d=4 r=12", "toric d=10 r=30", "toric d=18 r=54", ], key=f) == [ "planar d=4 r=12", "planar d=10 r=30", "planar d=16 r=36", "toric d=10 r=30", "toric d=18 r=54", ] assert sorted([ "a1", "1a", ], key=f) == [ "1a", "a1", ] def test_plotting_does_not_crash(): data = io.StringIO() data.write(""" shots,errors,discards,seconds,decoder,strong_id,json_metadata 1000000, 837,0,36.6,pymatching,9f7e20c54fec45b6aef7491b774dd5c0a3b9a005aa82faf5b9c051d6e40d60a9,"{""d"":3,""p"":0.001}" 53498,1099,0,6.52,pymatching,3f40432443a99b933fb548b831fb54e7e245d9d73a35c03ea5a2fb2ce270f8c8,"{""d"":3,""p"":0.005}" 16269,1023,0,3.23,pymatching,17b2e0c99560d20307204494ac50e31b33e50721b4ebae99d9e3577ae7248874,"{""d"":3,""p"":0.01}" 1000000, 151,0,77.3,pymatching,e179a18739201250371ffaae0197d8fa19d26b58dfc2942f9f1c85568645387a,"{""d"":5,""p"":0.001}" 11363,1068,0,12.5,pymatching,a4dec28934a033215ff1389651a26114ecc22016a6e122008830cf7dd04ba5ad,"{""d"":5,""p"":0.01}" 61569,1001,0,24.5,pymatching,2fefcc356752482fb4c6d912c228f6d18762f5752796c668b6abeb7775f5de92,"{""d"":5,""p"":0.005}" """) data.seek(0) stats = sinter.stats_from_csv_files(data) fig, ax = plt.subplots(1, 1) sinter.plot_error_rate( ax=ax, stats=stats, group_func=lambda e: f"Rotated Surface Code d={e.json_metadata['d']}", x_func=lambda e: e.json_metadata['p'], plot_args_func=lambda k, e: {'marker': "ov*sp^<>8PhH+xXDd|"[k]}, ) sinter.plot_error_rate( ax=ax, stats=stats, group_func=lambda e: f"Rotated Surface Code d={e.json_metadata['d']}", x_func=lambda e: e.json_metadata['p'], plot_args_func=lambda k, e: {'marker': "ov*sp^<>8PhH+xXDd|"[k]}, failure_units_per_shot_func=lambda stats: stats.json_metadata['d'] * 3, ) sinter.plot_error_rate( ax=ax, stats=stats, group_func=lambda e: f"Rotated Surface Code d={e.json_metadata['d']}", x_func=lambda e: e.json_metadata['p'], plot_args_func=lambda k, e: {'marker': "ov*sp^<>8PhH+xXDd|"[k]}, failure_units_per_shot_func=lambda stats: stats.json_metadata['d'] * 3, highlight_max_likelihood_factor=None, ) sinter.plot_discard_rate( ax=ax, stats=stats, group_func=lambda e: f"Rotated Surface Code d={e.json_metadata['d']}", x_func=lambda e: e.json_metadata['p'], plot_args_func=lambda k, e: {'marker': "ov*sp^<>8PhH+xXDd|"[k]}, ) sinter.plot_discard_rate( ax=ax, stats=stats, group_func=lambda e: f"Rotated Surface Code d={e.json_metadata['d']}", x_func=lambda e: e.json_metadata['p'], plot_args_func=lambda k, e: {'marker': "ov*sp^<>8PhH+xXDd|"[k]}, highlight_max_likelihood_factor=None, ) sinter.plot_discard_rate( ax=ax, stats=stats, group_func=lambda e: f"Rotated Surface Code d={e.json_metadata['d']}", x_func=lambda e: e.json_metadata['p'], plot_args_func=lambda k, e: {'marker': "ov*sp^<>8PhH+xXDd|"[k]}, failure_units_per_shot_func=lambda stats: stats.json_metadata['d'] * 3, ) def test_group_by(): assert sinter.group_by([1, 2, 3], key=lambda i: i == 2) == {False: [1, 3], True: [2]} assert sinter.group_by(range(10), key=lambda i: i % 3) == {0: [0, 3, 6, 9], 1: [1, 4, 7], 2: [2, 5, 8]} assert sinter.group_by([], key=lambda i: 0) == {} assert sinter.group_by([1, 2, 3, 1], key=lambda i: 0) == {0: [1, 2, 3, 1]} ================================================ FILE: glue/sample/src/sinter/_predict.py ================================================ import os.path import pathlib import math import numpy as np import stim import tempfile from typing import Optional, Union, Dict, TYPE_CHECKING from sinter._collection import post_selection_mask_from_4th_coord from sinter._decoding import Decoder, BUILT_IN_DECODERS, streaming_post_select if TYPE_CHECKING: import sinter def _converted_on_disk( in_path: pathlib.Path, out_path: pathlib.Path, num_dets: int, num_obs: int, in_format: str, out_format: str) -> pathlib.Path: if in_format == out_format: return in_path raw = stim.read_shot_data_file( path=str(in_path), format=in_format, bit_pack=True, num_detectors=num_dets, num_observables=num_obs, ) stim.write_shot_data_file( data=raw, path=str(out_path), format=out_format, num_detectors=num_dets, num_observables=num_obs, ) return out_path def predict_on_disk( *, decoder: str, dem_path: Union[str, pathlib.Path], dets_path: Union[str, pathlib.Path], dets_format: str, obs_out_path: Union[str, pathlib.Path], obs_out_format: str, postselect_detectors_with_non_zero_4th_coord: bool = False, discards_out_path: Optional[Union[str, pathlib.Path]] = None, discards_out_format: Optional[str] = None, custom_decoders: Dict[str, 'sinter.Decoder'] = None, ) -> None: """Performs decoding and postselection on disk. Args: decoder: The decoder to use for decoding. dem_path: The detector error model to use to configure the decoder. dets_path: Where the detection event data is stored on disk. dets_format: The format the detection event data is stored in (e.g. '01' or 'b8'). obs_out_path: Where to write predicted observable flip data on disk. Note that the predicted observable flip data will not included data from shots discarded by postselection. Use the data in discards_out_path to determine which shots were discarded. obs_out_format: The format to write the observable flip data in (e.g. '01' or 'b8'). postselect_detectors_with_non_zero_4th_coord: Activates postselection. Detectors that have a non-zero 4th coordinate will be postselected. Any shot where a postselected detector fires will be discarded. Requires specifying discards_out_path, for indicating which shots were discarded. discards_out_path: Only used if postselection is being used. Where to write discard data on disk. discards_out_format: The format to write discard data in (e.g. '01' or 'b8'). custom_decoders: Custom decoders that can be used if requested by name. """ if (discards_out_path is not None) != (discards_out_format is not None): raise ValueError('(discards_out_path is not None) != (discards_out_format is not None)') if (discards_out_path is not None) != postselect_detectors_with_non_zero_4th_coord: raise ValueError('(discards_out_path is not None) != postselect_detectors_with_non_zero_4th_coord') dem_path = pathlib.Path(dem_path) dets_path = pathlib.Path(dets_path) obs_out_path = pathlib.Path(obs_out_path) if discards_out_path is not None: discards_out_path = pathlib.Path(discards_out_path) with tempfile.TemporaryDirectory() as tmp_dir: tmp_dir = pathlib.Path(tmp_dir) decode_obj: Optional[Decoder] = None if custom_decoders is not None: decode_obj = custom_decoders.get(decoder) if decode_obj is None: decode_obj = BUILT_IN_DECODERS.get(decoder) if decode_obj is None: raise NotImplementedError(f"Unrecognized decoder: {decoder!r}") with open(dem_path) as f: dem = stim.DetectorErrorModel(f.read()) num_dets = dem.num_detectors num_det_bytes = math.ceil(num_dets / 8) num_obs = dem.num_observables dets_b8_path = _converted_on_disk( in_path=dets_path, out_path=tmp_dir / 'sinter_dets.b8', in_format=dets_format, out_format='b8', num_dets=num_dets, num_obs=0) if num_det_bytes == 0: raise NotImplementedError("Don't know how many shots there are, because num_det_bytes=0.") num_shots = os.path.getsize(dets_b8_path) // num_det_bytes if discards_out_path is not None: if discards_out_format == 'b8': discards_b8_path = discards_out_path else: discards_b8_path = tmp_dir / 'sinter_discards.b8' post_selection_mask = np.zeros(dtype=np.uint8, shape=math.ceil(num_dets / 8)) if postselect_detectors_with_non_zero_4th_coord: post_selection_mask = post_selection_mask_from_4th_coord(dem) kept_dets_b8_path = tmp_dir / 'sinter_dets.kept.b8' num_discards = streaming_post_select( num_shots=num_shots, num_dets=num_dets, num_obs=num_obs, dets_in_b8=dets_b8_path, obs_in_b8=None, obs_out_b8=None, discards_out_b8=discards_b8_path, dets_out_b8=kept_dets_b8_path, post_mask=post_selection_mask, ) assert discards_out_format is not None _converted_on_disk( in_path=discards_b8_path, out_path=discards_out_path, out_format=discards_out_format, in_format='b8', num_dets=1, num_obs=0, ) num_kept_shots = num_shots - num_discards else: kept_dets_b8_path = dets_b8_path num_kept_shots = num_shots if postselect_detectors_with_non_zero_4th_coord: raise ValueError('postselect_detectors_with_non_zero_4th_coord and discards_out_path is None') if obs_out_format != 'b8': obs_inter = tmp_dir / 'sinter_obs_inter.b8' else: obs_inter = obs_out_path decode_obj.decode_via_files( num_shots=num_kept_shots, num_dets=num_dets, num_obs=num_obs, dem_path=dem_path, dets_b8_in_path=kept_dets_b8_path, obs_predictions_b8_out_path=obs_inter, tmp_dir=tmp_dir, ) _converted_on_disk( in_path=obs_inter, out_path=obs_out_path, out_format=obs_out_format, in_format='b8', num_dets=0, num_obs=num_obs, ) def predict_discards_bit_packed( *, dem: stim.DetectorErrorModel, dets_bit_packed: np.ndarray, postselect_detectors_with_non_zero_4th_coord: bool, ) -> np.ndarray: """Determines which shots to discard due to postselected detectors firing. Args: dem: The detector error model the detector data applies to. This is also where coordinate data is read from, in order to determine which detectors to postselect as not having fired. dets_bit_packed: A uint8 numpy array with shape (num_shots, math.ceil(num_dets / 8)). Contains bit packed detection event data. postselect_detectors_with_non_zero_4th_coord: Determines how postselection is done. Currently, this is the only option so it has to be set to True. Any detector from the detector error model that specifies coordinate data with at least four coordinates where the fourth coordinate (coord index 3) is non-zero will be postselected. Returns: A numpy bool_ array with shape (num_shots,) where False means not discarded and True means yes discarded. """ if not postselect_detectors_with_non_zero_4th_coord: raise ValueError("not postselect_detectors_with_non_zero_4th_coord") num_dets = dem.num_detectors nb = math.ceil(num_dets / 8) if len(dets_bit_packed.shape) != 2: raise ValueError(f'len(dets_data_bit_packed.shape={dets_bit_packed.shape}) != 2') if dets_bit_packed.shape[1] != nb: raise ValueError(f'dets_data_bit_packed.shape[1]={dets_bit_packed.shape[1]} != math.ceil(dem.num_detectors={dem.num_detectors} / 8)') if dets_bit_packed.dtype != np.uint8: raise ValueError(f'dets_data_bit_packed.dtype={dets_bit_packed.dtype} != np.uint8') post_selection_mask = np.zeros(dtype=np.uint8, shape=nb) if postselect_detectors_with_non_zero_4th_coord: for k, coord in dem.get_detector_coordinates().items(): if len(coord) >= 4 and coord[3]: post_selection_mask[k // 8] |= 1 << (k % 8) return np.any(dets_bit_packed & post_selection_mask, axis=1) def predict_observables( *, dem: stim.DetectorErrorModel, dets: np.ndarray, decoder: str, bit_pack_result: bool = False, custom_decoders: Optional[Dict[str, 'sinter.Decoder']] = None, ) -> np.ndarray: """Predicts which observables were flipped based on detection event data. Args: dem: The detector error model the detector data applies to. This is also where coordinate data is read from, in order to determine which detectors to postselect as not having fired. dets: The detection event data. Can be bit packed or not bit packed. If dtype=np.bool_ then shape=(num_shots, num_detectors) If dtype=np.uint8 then shape=(num_shots, math.ceil(num_detectors/8)) decoder: The decoder to use for decoding, e.g. "pymatching". bit_pack_result: Defaults to False. Determines if the result is bit packed or not. custom_decoders: Custom decoders that can be used if requested by name. If not specified, only decoders built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. Returns: If bit_packed_result=False (default): dtype=np.bool_ shape=(num_shots, num_observables) If bit_packed_result=True: dtype=np.uint8 shape=(num_shots, math.ceil(num_observables / 8)) Examples: >>> import numpy as np >>> import sinter >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.1) D0 L0 ... error(0.1) D0 D1 ... error(0.1) D1 ... ''') >>> sinter.predict_observables( ... dem=dem, ... dets=np.array([ ... [False, False], ... [True, False], ... [False, True], ... [True, True], ... ], dtype=np.bool_), ... decoder='vacuous', # try replacing with 'pymatching' ... bit_pack_result=False, ... ) array([[False], [False], [False], [False]]) """ if dets.dtype == np.bool_: dets = np.packbits(dets, axis=1, bitorder='little') result = predict_observables_bit_packed( dem=dem, dets_bit_packed=dets, decoder=decoder, custom_decoders=custom_decoders, ) if not bit_pack_result: return np.unpackbits(result, axis=1, bitorder='little', count=dem.num_observables).astype(np.bool_) return result def predict_observables_bit_packed( *, dem: stim.DetectorErrorModel, dets_bit_packed: np.ndarray, decoder: str, custom_decoders: Optional[Dict[str, 'sinter.Decoder']] = None, ) -> np.ndarray: """Predicts which observables were flipped based on detection event data. This method predates `sinter.predict_observables` gaining optional bit packing arguments. Args: dem: The detector error model the detector data applies to. This is also where coordinate data is read from, in order to determine which detectors to postselect as not having fired. dets_bit_packed: A uint8 numpy array with shape (num_shots, math.ceil(num_dets / 8)). Contains bit packed detection event data. decoder: The decoder to use for decoding, e.g. "pymatching". custom_decoders: Custom decoders that can be used if requested by name. If not specified, only decoders built into sinter, such as 'pymatching' and 'fusion_blossom', can be used. Returns: A numpy uint8 array with shape (num_shots, math.ceil(num_obs / 8)). Contains bit packed observable prediction data. Examples: >>> import numpy as np >>> import sinter >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.1) D0 L0 ... error(0.1) D0 D1 ... error(0.1) D1 ... ''') >>> sinter.predict_observables_bit_packed( ... dem=dem, ... dets_bit_packed=np.array([ ... [0b00], ... [0b01], ... [0b10], ... [0b11], ... ], dtype=np.uint8), ... decoder='vacuous', # try replacing with 'pymatching' ... ) array([[0], [0], [0], [0]], dtype=uint8) """ decode_obj: Optional[Decoder] = None if custom_decoders is not None: decode_obj = custom_decoders.get(decoder) if decode_obj is None: decode_obj = BUILT_IN_DECODERS.get(decoder) if decode_obj is None: raise NotImplementedError(f"Unrecognized decoder: {decoder!r}") with tempfile.TemporaryDirectory() as tmp_dir: tmp_dir = pathlib.Path(tmp_dir) dets_b8_path = tmp_dir / 'sinter_dets.b8' pred_b8_path = tmp_dir / 'sinter_predictions.b8' dem_path = tmp_dir / 'dem.dem' dem.to_file(dem_path) num_dets = dem.num_detectors num_obs = dem.num_observables stim.write_shot_data_file( data=dets_bit_packed, path=str(dets_b8_path), format='b8', num_detectors=num_dets, num_observables=0, ) decode_obj.decode_via_files( num_shots=dets_bit_packed.shape[0], num_dets=num_dets, num_obs=num_obs, dem_path=dem_path, dets_b8_in_path=dets_b8_path, obs_predictions_b8_out_path=pred_b8_path, tmp_dir=tmp_dir, ) return stim.read_shot_data_file( path=str(pred_b8_path), format='b8', bit_pack=True, num_detectors=0, num_observables=num_obs, ) ================================================ FILE: glue/sample/src/sinter/_predict_test.py ================================================ import pathlib import numpy as np import stim import tempfile import sinter def test_predict_on_disk_no_postselect(): with tempfile.TemporaryDirectory() as tmp_dir: tmp_dir = pathlib.Path(tmp_dir) dem_path = tmp_dir / 'dem' dets_path = tmp_dir / 'dets' obs_out_path = tmp_dir / 'obs' with open(dem_path, 'w') as f: print(""" error(0.1) D0 L0 detector(0, 0, 0, 1) D0 """, file=f) with open(dets_path, 'w') as f: print("0", file=f) print("1", file=f) sinter.predict_on_disk( decoder='pymatching', dem_path=dem_path, dets_path=dets_path, dets_format='01', obs_out_path=obs_out_path, obs_out_format='01', postselect_detectors_with_non_zero_4th_coord=False, discards_out_path=None, discards_out_format=None, ) with open(obs_out_path) as f: assert f.read() == '0\n1\n' def test_predict_on_disk_yes_postselect(): with tempfile.TemporaryDirectory() as tmp_dir: tmp_dir = pathlib.Path(tmp_dir) dem_path = tmp_dir / 'dem' dets_path = tmp_dir / 'dets' obs_out_path = tmp_dir / 'obs' discards_out_path = tmp_dir / 'discards' with open(dem_path, 'w') as f: print(""" error(0.1) D0 L0 detector(0, 0, 0, 1) D0 """, file=f) with open(dets_path, 'w') as f: print("0", file=f) print("1", file=f) sinter.predict_on_disk( decoder='pymatching', dem_path=dem_path, dets_path=dets_path, dets_format='01', obs_out_path=obs_out_path, obs_out_format='01', postselect_detectors_with_non_zero_4th_coord=True, discards_out_path=discards_out_path, discards_out_format='01', ) with open(obs_out_path) as f: assert f.read() == '0\n' with open(discards_out_path) as f: assert f.read() == '0\n1\n' def test_predict_discards_bit_packed_none_postselected(): dem = stim.DetectorErrorModel(""" error(0.1) D0 L0 """) actual = sinter.predict_discards_bit_packed( dem=dem, dets_bit_packed=np.packbits(np.array([ [False], [True], ], dtype=np.bool_), bitorder='little', axis=1), postselect_detectors_with_non_zero_4th_coord=True, ) np.testing.assert_array_equal( actual, [False, False], ) def test_predict_discards_bit_packed_some_postselected(): dem = stim.DetectorErrorModel(""" error(0.1) D0 L0 detector(0, 0, 0, 1) D0 """) actual = sinter.predict_discards_bit_packed( dem=dem, dets_bit_packed=np.packbits(np.array([ [False], [True], ], dtype=np.bool_), bitorder='little', axis=1), postselect_detectors_with_non_zero_4th_coord=True, ) np.testing.assert_array_equal( actual, [False, True], ) def test_predict_observables_bit_packed(): dem = stim.DetectorErrorModel(""" error(0.1) D0 L0 """) actual = sinter.predict_observables_bit_packed( dem=dem, dets_bit_packed=np.packbits(np.array([ [False], [True], ], dtype=np.bool_), bitorder='little', axis=1), decoder='pymatching', ) np.testing.assert_array_equal( np.unpackbits(actual, bitorder='little', count=1, axis=1), [ [False], [True], ], ) def test_predict_observables(): dem = stim.DetectorErrorModel(""" error(0.1) D0 L0 """) actual = sinter.predict_observables( dem=dem, dets=np.packbits(np.array([ [False], [True], ], dtype=np.bool_), bitorder='little', axis=1), decoder='pymatching', bit_pack_result=True, ) np.testing.assert_array_equal( np.unpackbits(actual, bitorder='little', count=1, axis=1), [ [False], [True], ], ) actual = sinter.predict_observables( dem=dem, dets=np.array([ [False], [True], ], dtype=np.bool_), decoder='pymatching', bit_pack_result=True, ) np.testing.assert_array_equal( np.unpackbits(actual, bitorder='little', count=1, axis=1), [ [False], [True], ], ) actual = sinter.predict_observables( dem=dem, dets=np.packbits(np.array([ [False], [True], ], dtype=np.bool_), bitorder='little', axis=1), decoder='pymatching', bit_pack_result=False, ) np.testing.assert_array_equal( actual, [[False], [True]], ) actual = sinter.predict_observables( dem=dem, dets=np.array([ [False], [True], ], dtype=np.bool_), decoder='pymatching', bit_pack_result=False, ) np.testing.assert_array_equal( actual, [[False], [True]], ) actual = sinter.predict_observables( dem=dem, dets=np.packbits(np.array([ [False], [True], ], dtype=np.bool_), bitorder='little', axis=1), decoder='pymatching', ) np.testing.assert_array_equal( actual, [[False], [True]], ) actual = sinter.predict_observables( dem=dem, dets=np.array([ [False], [True], ], dtype=np.bool_), decoder='pymatching', ) np.testing.assert_array_equal( actual, [[False], [True]], ) ================================================ FILE: glue/sample/src/sinter/_probability_util.py ================================================ import dataclasses import math import pathlib from typing import Any, Dict, Union, Callable, Sequence, TYPE_CHECKING, overload from typing import Optional import numpy as np if TYPE_CHECKING: import sinter # Go on a magical journey looking for scipy's linear regression type. try: from scipy.stats._stats_py import LinregressResult except ImportError: try: from scipy.stats._stats_mstats_common import LinregressResult except ImportError: from scipy.stats import linregress LinregressResult = type(linregress([0, 1], [0, 1])) def log_binomial(*, p: Union[float, np.ndarray], n: int, hits: int) -> np.ndarray: r"""Approximates the natural log of a binomial distribution's probability. When working with large binomials, it's often necessary to work in log space to represent the result. For example, suppose that out of two million samples 200_000 are hits. The maximum likelihood estimate is p=0.2. Even if this is the true probability, the chance of seeing *exactly* 20% hits out of a million shots is roughly 10^-217322. Whereas the smallest representable double is roughly 10^-324. But ln(10^-217322) ~= -500402.4 is representable. This method evaluates $\ln(P(hits = B(n, p)))$, with all computations done in log space to ensure intermediate values can be represented as floating point numbers without underflowing to 0 or overflowing to infinity. This method can be broadcast over multiple hypothesis probabilities by giving a numpy array for `p` instead of a single float. Args: p: The hypotehsis probability. The independent probability of a hit occurring for each sample. This can also be an array of probabilities, in which case the function is broadcast over the array. n: The number of samples that were taken. hits: The number of hits that were observed amongst the samples that were taken. Returns: $\ln(P(hits = B(n, p)))$ Examples: >>> import sinter >>> sinter.log_binomial(p=0.5, n=100, hits=50) array(-2.5308762, dtype=float32) >>> sinter.log_binomial(p=0.2, n=1_000_000, hits=1_000) array(-216626.97, dtype=float32) >>> sinter.log_binomial(p=0.1, n=1_000_000, hits=1_000) array(-99654.86, dtype=float32) >>> sinter.log_binomial(p=0.01, n=1_000_000, hits=1_000) array(-6742.573, dtype=float32) >>> sinter.log_binomial(p=[0.01, 0.1, 0.2], n=1_000_000, hits=1_000) array([ -6742.573, -99654.86 , -216626.97 ], dtype=float32) """ # Clamp probabilities into the valid [0, 1] range (in case float error put them outside it). p_clipped = np.clip(p, 0, 1) result = np.zeros(shape=p_clipped.shape, dtype=np.float32) misses = n - hits # Handle p=0 and p=1 cases separately, to avoid arithmetic warnings. if hits: result[p_clipped == 0] = -np.inf if misses: result[p_clipped == 1] = -np.inf # Multiply p**hits and (1-p)**misses onto the total, in log space. result[p_clipped != 0] += np.log(p_clipped[p_clipped != 0]) * float(hits) result[p_clipped != 1] += np.log1p(-p_clipped[p_clipped != 1]) * float(misses) # Multiply (n choose hits) onto the total, in log space. log_n_choose_hits = log_factorial(n) - log_factorial(misses) - log_factorial(hits) result += log_n_choose_hits return result def log_factorial(n: int) -> float: r"""Approximates $\ln(n!)$; the natural logarithm of a factorial. Args: n: The input to the factorial. Returns: Evaluates $ln(n!)$ using `math.lgamma(n+1)`. Examples: >>> import sinter >>> sinter.log_factorial(0) 0.0 >>> sinter.log_factorial(1) 0.0 >>> sinter.log_factorial(2) 0.693147180559945 >>> sinter.log_factorial(100) 363.73937555556347 """ return math.lgamma(n + 1) def binary_search(*, func: Callable[[int], float], min_x: int, max_x: int, target: float) -> int: """Performs an approximate granular binary search over a monotonically ascending function.""" while max_x > min_x + 1: med_x = (min_x + max_x) // 2 out = func(med_x) if out < target: min_x = med_x elif out > target: max_x = med_x else: return med_x fmax = func(max_x) fmin = func(min_x) dmax = 0 if fmax == target else fmax - target dmin = 0 if fmin == target else fmin - target return max_x if abs(dmax) < abs(dmin) else min_x def binary_intercept(*, func: Callable[[float], float], start_x: float, step: float, target_y: float, atol: float) -> float: """Performs an approximate granular binary search over a monotonically ascending function.""" start_y = func(start_x) if abs(start_y - target_y) <= atol: return start_x while (func(start_x + step) >= target_y) == (start_y >= target_y): step *= 2 if np.isinf(step) or step == 0: raise ValueError("Failed.") xs = [start_x, start_x + step] min_x = min(xs) max_x = max(xs) increasing = func(min_x) < func(max_x) while True: med_x = (min_x + max_x) / 2 med_y = func(med_x) if abs(med_y - target_y) <= atol: return med_x assert med_x not in [min_x, max_x] if (med_y < target_y) == increasing: min_x = med_x else: max_x = med_x def least_squares_cost(*, xs: np.ndarray, ys: np.ndarray, intercept: float, slope: float) -> float: assert len(xs.shape) == 1 assert xs.shape == ys.shape return np.sum((intercept + slope*xs - ys)**2) def least_squares_through_point(*, xs: np.ndarray, ys: np.ndarray, required_x: float, required_y: float) -> 'LinregressResult': # Local import to reduce initial cost of importing sinter. from scipy.optimize import leastsq from scipy.stats import linregress # HACK: get scipy's linear regression result type LinregressResult = type(linregress([0, 1], [0, 1])) xs2 = xs - required_x ys2 = ys - required_y def err(slope: float) -> float: return least_squares_cost(xs=xs2, ys=ys2, intercept=0, slope=slope) (best_slope,), _ = leastsq(func=err, x0=0.0) intercept = required_y - required_x * best_slope return LinregressResult(best_slope, intercept, None, None, None, intercept_stderr=False) def least_squares_with_slope(*, xs: np.ndarray, ys: np.ndarray, required_slope: float) -> 'LinregressResult': def err(intercept: float) -> float: return least_squares_cost(xs=xs, ys=ys, intercept=intercept, slope=required_slope) # Local import to reduce initial cost of importing sinter. from scipy.optimize import leastsq # HACK: get scipy's linear regression result type from scipy.stats import linregress LinregressResult = type(linregress([0, 1], [0, 1])) (best_intercept,), _ = leastsq(func=err, x0=0.0) return LinregressResult(required_slope, best_intercept, None, None, None, intercept_stderr=False) @dataclasses.dataclass(frozen=True) class Fit: """The result of a fitting process. Attributes: low: The hypothesis with the smallest parameter whose cost or score was still "close to" the cost of the best hypothesis. For example, this could be a hypothesis whose squared error was within some tolerance of the best fit's square error, or whose likelihood was within some maximum Bayes factor of the max likelihood hypothesis. best: The max likelihood hypothesis. The hypothesis that had the lowest squared error, or the best fitting score. high: The hypothesis with the larger parameter whose cost or score was still "close to" the cost of the best hypothesis. For example, this could be a hypothesis whose squared error was within some tolerance of the best fit's square error, or whose likelihood was within some maximum Bayes factor of the max likelihood hypothesis. """ low: Optional[float] best: Optional[float] high: Optional[float] def __repr__(self) -> str: return f'sinter.Fit(low={self.low!r}, best={self.best!r}, high={self.high!r})' def fit_line_y_at_x(*, xs: Sequence[float], ys: Sequence[float], target_x: float, max_extra_squared_error: float) -> 'sinter.Fit': """Performs a line fit, focusing on the line's y coord at a given x coord. Finds the y value at the given x of the best fit, but also the minimum and maximum values for y at the given x amongst all possible line fits whose squared error cost is within the given `max_extra_squared_error` cost of the best fit. Args: xs: The x coordinates of points to fit. ys: The y coordinates of points to fit. target_x: The fit values are the value of y at this x coordinate. max_extra_squared_error: When computing the low and high fits, this is the maximum additional squared error that can be introduced by varying the slope away from the best fit. Returns: A sinter.Fit containing the best fit for y at the given x, as well as low and high fits that are as far as possible from the best fit while respecting the given max_extra_squared_error. Examples: >>> import sinter >>> sinter.fit_line_y_at_x( ... xs=[1, 2, 3], ... ys=[10, 12, 14], ... target_x=4, ... max_extra_squared_error=1, ... ) sinter.Fit(low=14.47247314453125, best=16.0, high=17.52752685546875) """ # Local import to reduce initial cost of importing sinter. from scipy.stats import linregress xs = np.array(xs, dtype=np.float64) ys = np.array(ys, dtype=np.float64) fit = linregress(xs, ys) base_cost = least_squares_cost(xs=xs, ys=ys, intercept=fit.intercept, slope=fit.slope) base_y = float(fit.intercept + target_x * fit.slope) def cost_for_y(y2: float) -> float: fit2 = least_squares_through_point(xs=xs, ys=ys, required_x=target_x, required_y=y2) return least_squares_cost(xs=xs, ys=ys, intercept=fit2.intercept, slope=fit2.slope) low_y = binary_intercept(start_x=base_y, step=-1, target_y=base_cost + max_extra_squared_error, func=cost_for_y, atol=1e-5) high_y = binary_intercept(start_x=base_y, step=1, target_y=base_cost + max_extra_squared_error, func=cost_for_y, atol=1e-5) return Fit(low=low_y, best=base_y, high=high_y) def fit_line_slope(*, xs: Sequence[float], ys: Sequence[float], max_extra_squared_error: float) -> 'sinter.Fit': """Performs a line fit of the given points, focusing on the line's slope. Finds the slope of the best fit, but also the minimum and maximum slopes for line fits whose squared error cost is within the given `max_extra_squared_error` cost of the best fit. Note that the extra squared error is computed while including a specific offset of some specific line. So the low/high estimates are for specific lines, not for the general class of lines with a given slope, adding together the contributions of all lines in that class. Args: xs: The x coordinates of points to fit. ys: The y coordinates of points to fit. max_extra_squared_error: When computing the low and high fits, this is the maximum additional squared error that can be introduced by varying the slope away from the best fit. Returns: A sinter.Fit containing the best fit, as well as low and high fits that are as far as possible from the best fit while respective the given max_extra_squared_error. Examples: >>> import sinter >>> sinter.fit_line_slope( ... xs=[1, 2, 3], ... ys=[10, 12, 14], ... max_extra_squared_error=1, ... ) sinter.Fit(low=1.2928924560546875, best=2.0, high=2.7071075439453125) """ # Local import to reduce initial cost of importing sinter. from scipy.stats import linregress xs = np.array(xs, dtype=np.float64) ys = np.array(ys, dtype=np.float64) fit = linregress(xs, ys) base_cost = least_squares_cost(xs=xs, ys=ys, intercept=fit.intercept, slope=fit.slope) def cost_for_slope(slope: float) -> float: fit2 = least_squares_with_slope(xs=xs, ys=ys, required_slope=slope) return least_squares_cost(xs=xs, ys=ys, intercept=fit2.intercept, slope=fit2.slope) low_slope = binary_intercept(start_x=fit.slope, step=-1, target_y=base_cost + max_extra_squared_error, func=cost_for_slope, atol=1e-5) high_slope = binary_intercept(start_x=fit.slope, step=1, target_y=base_cost + max_extra_squared_error, func=cost_for_slope, atol=1e-5) return Fit(low=float(low_slope), best=float(fit.slope), high=float(high_slope)) def fit_binomial( *, num_shots: int, num_hits: int, max_likelihood_factor: float) -> 'sinter.Fit': """Determine hypothesis probabilities compatible with the given hit ratio. The result includes the best fit (the max likelihood hypothis) as well as the smallest and largest probabilities whose likelihood is within the given factor of the maximum likelihood hypothesis. Args: num_shots: The number of samples that were taken. num_hits: The number of hits that were seen in the samples. max_likelihood_factor: The maximum Bayes factor between the low/high hypotheses and the best hypothesis (the max likelihood hypothesis). This value should be larger than 1 (as opposed to between 0 and 1). Returns: A `sinter.Fit` with the low, best, and high hypothesis probabilities. Examples: >>> import sinter >>> sinter.fit_binomial( ... num_shots=100_000_000, ... num_hits=2, ... max_likelihood_factor=1000, ... ) sinter.Fit(low=2e-10, best=2e-08, high=1.259e-07) >>> sinter.fit_binomial( ... num_shots=10, ... num_hits=5, ... max_likelihood_factor=9, ... ) sinter.Fit(low=0.202, best=0.5, high=0.798) """ if max_likelihood_factor < 1: raise ValueError(f'max_likelihood_factor={max_likelihood_factor} < 1') if num_shots == 0: return Fit(low=0, high=1, best=0.5) log_max_likelihood = log_binomial(p=num_hits / num_shots, n=num_shots, hits=num_hits) target_log_likelihood = log_max_likelihood - math.log(max_likelihood_factor) acc = 100 low = binary_search( func=lambda exp_err: log_binomial(p=exp_err / (acc * num_shots), n=num_shots, hits=num_hits), target=target_log_likelihood, min_x=0, max_x=num_hits * acc) / acc high = binary_search( func=lambda exp_err: -log_binomial(p=exp_err / (acc * num_shots), n=num_shots, hits=num_hits), target=-target_log_likelihood, min_x=num_hits * acc, max_x=num_shots * acc) / acc return Fit(best=num_hits / num_shots, low=low / num_shots, high=high / num_shots) @overload def shot_error_rate_to_piece_error_rate(shot_error_rate: float, *, pieces: float, values: float = 1) -> float: pass @overload def shot_error_rate_to_piece_error_rate(shot_error_rate: 'sinter.Fit', *, pieces: float, values: float = 1) -> 'sinter.Fit': pass def shot_error_rate_to_piece_error_rate(shot_error_rate: Union[float, 'sinter.Fit'], *, pieces: float, values: float = 1) -> Union[float, 'sinter.Fit']: """Convert from total error rate to per-piece error rate. Args: shot_error_rate: The rate at which shots fail. If this is set to a sinter.Fit, the conversion broadcasts over the low,best,high of the fit. pieces: The number of xor-pieces we want to subdivide each shot into, as if each piece was an independent chance for the shot to fail and the total chance of a shot failing was the xor of each piece failing. values: The number of or-pieces each shot's failure is being formed out of. Returns: Let N = `pieces` (number of rounds) Let V = `values` (number of observables) Let S = `shot_error_rate` Let R = the returned result R satisfies the following property. Let X be the probability of each observable flipping, each round. R will be the probability that any of the observables is flipped after 1 round, given this X. X is chosen to satisfy the following condition. If a Bernoulli distribution with probability X is sampled V*N times, and the results grouped into V groups of N, and each group is reduced to a single value using XOR, and then the reduced group values are reduced to a single final value using OR, then this final value will be True with probability S. Or, in other words, if a shot consists of N rounds which V independent observables must survive, then R is like the per-round failure for any of the observables. Examples: >>> import sinter >>> sinter.shot_error_rate_to_piece_error_rate( ... shot_error_rate=0.1, ... pieces=2, ... ) 0.05278640450004207 >>> sinter.shot_error_rate_to_piece_error_rate( ... shot_error_rate=0.05278640450004207, ... pieces=1 / 2, ... ) 0.10000000000000003 >>> sinter.shot_error_rate_to_piece_error_rate( ... shot_error_rate=1e-9, ... pieces=100, ... ) 1.000000082740371e-11 >>> sinter.shot_error_rate_to_piece_error_rate( ... shot_error_rate=0.6, ... pieces=10, ... values=2, ... ) 0.12052311142021144 """ if isinstance(shot_error_rate, Fit): return Fit( low=shot_error_rate_to_piece_error_rate(shot_error_rate=shot_error_rate.low, pieces=pieces, values=values), best=shot_error_rate_to_piece_error_rate(shot_error_rate=shot_error_rate.best, pieces=pieces, values=values), high=shot_error_rate_to_piece_error_rate(shot_error_rate=shot_error_rate.high, pieces=pieces, values=values), ) if not (0 <= shot_error_rate <= 1): raise ValueError(f'need (0 <= shot_error_rate={shot_error_rate} <= 1)') if pieces <= 0: raise ValueError('need pieces > 0') if not isinstance(pieces, (int, float)): raise ValueError('need isinstance(pieces, (int, float)') if not isinstance(values, (int, float)): raise ValueError('need isinstance(values, (int, float)') if pieces == 1: return shot_error_rate if values != 1: p = 1 - (1 - shot_error_rate)**(1 / values) p = shot_error_rate_to_piece_error_rate(p, pieces=pieces) return 1 - (1 - p)**values if shot_error_rate > 0.5: return 1 - shot_error_rate_to_piece_error_rate(1 - shot_error_rate, pieces=pieces) assert 0 <= shot_error_rate <= 0.5 randomize_rate = 2*shot_error_rate round_randomize_rate = 1 - (1 - randomize_rate)**(1 / pieces) round_error_rate = round_randomize_rate / 2 if round_error_rate == 0: # The intermediate numbers got too small. Fallback to division approximation. return shot_error_rate / pieces return round_error_rate def comma_separated_key_values(path: str) -> Dict[str, Any]: """Converts paths like 'folder/d=5,r=3.stim' into dicts like {'d':5,'r':3}. On the command line, specifying `--metadata_func auto` results in this method being used to extra metadata from the circuit file paths. Integers and floats will be parsed into their values, instead of being stored as strings. Args: path: A file path where the name of the file has a series of terms like 'a=b' separated by commas and ending in '.stim'. Returns: A dictionary from named keys to parsed values. Examples: >>> import sinter >>> sinter.comma_separated_key_values("folder/d=5,r=3.5,x=abc.stim") {'d': 5, 'r': 3.5, 'x': 'abc'} """ name = pathlib.Path(path).name if '.' in name: name = name[:name.rindex('.')] result = {} for term in name.split(','): parts = term.split('=') if len(parts) != 2: raise ValueError(f"Expected a path with a filename containing comma-separated key=value terms like 'a=2,b=3.stim', but got {path!r}.") k, v = parts try: v = int(v) except ValueError: try: v = float(v) except ValueError: pass result[k] = v return result ================================================ FILE: glue/sample/src/sinter/_probability_util_test.py ================================================ import math from typing import Union import numpy as np import pytest import sinter from sinter._probability_util import ( binary_search, log_binomial, log_factorial, fit_line_y_at_x, fit_line_slope, binary_intercept, least_squares_through_point, fit_binomial, shot_error_rate_to_piece_error_rate, ) from sinter._probability_util import comma_separated_key_values @pytest.mark.parametrize( "arg,result", { 0: 0, 1: 0, 2: math.log(2), 3: math.log(2) + math.log(3), # These values were taken from wolfram alpha: 10: 15.1044125730755152952257093292510, 100: 363.73937555556349014407999336965, 1000: 5912.128178488163348878130886725, 10000: 82108.9278368143534553850300635, 100000: 1051299.2218991218651292781082, }.items(), ) def test_log_factorial(arg, result): np.testing.assert_allclose(log_factorial(arg), result, rtol=1e-11) @pytest.mark.parametrize( "n,p,hits,result", [ (1, 0.5, 0, np.log(0.5)), (1, 0.5, 1, np.log(0.5)), (1, 0.1, 0, np.log(0.9)), (1, 0.1, 1, np.log(0.1)), (2, [0, 1, 0.1, 0.5], 0, [0, -np.inf, np.log(0.9 ** 2), np.log(0.25)]), (2, [0, 1, 0.1, 0.5], 1, [-np.inf, -np.inf, np.log(0.1 * 0.9 * 2), np.log(0.5)]), (2, [0, 1, 0.1, 0.5], 2, [-np.inf, 0, np.log(0.1 ** 2), np.log(0.25)]), # Magic number comes from PDF[BinomialDistribution[10^10, 10^-6], 10000] on wolfram alpha. (10 ** 10, 10 ** -6, 10 ** 4, np.log(0.0039893915536591)), # Corner cases. (1, 0.0, 0, 0), (1, 0.0, 1, -np.inf), (1, 1.0, 0, -np.inf), (1, 1.0, 1, 0), # Array broadcast. (2, np.array([0.0, 0.5, 1.0]), 0, np.array([0.0, np.log(0.25), -np.inf])), (2, np.array([0.0, 0.5, 1.0]), 1, np.array([-np.inf, np.log(0.5), -np.inf])), (2, np.array([0.0, 0.5, 1.0]), 2, np.array([-np.inf, np.log(0.25), 0.0])), ], ) def test_log_binomial( n: int, p: Union[float, np.ndarray], hits: int, result: Union[float, np.ndarray] ) -> None: np.testing.assert_allclose(log_binomial(n=n, p=p, hits=hits), result, rtol=1e-2) def test_binary_search(): assert binary_search(func=lambda x: x**2, min_x=0, max_x=10**100, target=100.1) == 10 assert binary_search(func=lambda x: x**2, min_x=0, max_x=10**100, target=100) == 10 assert binary_search(func=lambda x: x**2, min_x=0, max_x=10**100, target=99.9) == 10 assert binary_search(func=lambda x: x**2, min_x=0, max_x=10**100, target=90) == 9 assert binary_search(func=lambda x: x**2, min_x=0, max_x=10**100, target=92) == 10 assert binary_search(func=lambda x: x**2, min_x=0, max_x=10**100, target=-100) == 0 assert binary_search(func=lambda x: x**2, min_x=0, max_x=10**100, target=10**300) == 10**100 def test_least_squares_through_point(): fit = least_squares_through_point( xs=np.array([1, 2, 3]), ys=np.array([2, 3, 4]), required_x=1, required_y=2) np.testing.assert_allclose(fit.slope, 1) np.testing.assert_allclose(fit.intercept, 1) fit = least_squares_through_point( xs=np.array([1, 2, 3]), ys=np.array([2, 3, 4]), required_x=1, required_y=1) np.testing.assert_allclose(fit.slope, 1.6, rtol=1e-5) np.testing.assert_allclose(fit.intercept, -0.6, atol=1e-5) def test_binary_intercept(): t = binary_intercept(func=lambda x: x**2, start_x=5, step=1, target_y=82.3, atol=0.01) assert t > 0 and abs(t**2 - 82.3) <= 0.01 t = binary_intercept(func=lambda x: -x**2, start_x=5, step=1, target_y=-82.3, atol=0.01) assert t > 0 and abs(t**2 - 82.3) <= 0.01 t = binary_intercept(func=lambda x: x**2, start_x=0, step=-1, target_y=82.3, atol=0.01) assert t < 0 and abs(t**2 - 82.3) <= 0.01 t = binary_intercept(func=lambda x: -x**2, start_x=0, step=-1, target_y=-82.3, atol=0.2) assert t < 0 and abs(t**2 - 82.3) <= 0.2 def test_fit_y_at_x(): fit = fit_line_y_at_x( xs=[1, 2, 3], ys=[1, 5, 9], target_x=100, max_extra_squared_error=1, ) assert 300 < fit.low < 390 < fit.best < 410 < fit.high < 500 def test_fit_slope(): fit = fit_line_slope( xs=[1, 2, 3], ys=[1, 5, 9], max_extra_squared_error=1, ) np.testing.assert_allclose(fit.best, 4) assert 3 < fit.low < 3.5 < fit.best < 4.5 < fit.high < 5 def test_fit_binomial_shrink_towards_half(): with pytest.raises(ValueError, match='max_likelihood_factor'): fit_binomial(num_shots=10 ** 5, num_hits=10 ** 5 / 2, max_likelihood_factor=0.1) fit = fit_binomial(num_shots=10 ** 5, num_hits=10 ** 5 / 2, max_likelihood_factor=1e3) np.testing.assert_allclose( (fit.low, fit.best, fit.high), (0.494122, 0.5, 0.505878), rtol=1e-4, ) fit = fit_binomial(num_shots=10 ** 4, num_hits=10 ** 4 / 2, max_likelihood_factor=1e3) np.testing.assert_allclose( (fit.low, fit.best, fit.high), (0.481422, 0.5, 0.518578), rtol=1e-4, ) fit = fit_binomial(num_shots=10 ** 4, num_hits=10 ** 4 / 2, max_likelihood_factor=1e2) np.testing.assert_allclose( (fit.low, fit.best, fit.high), (0.48483, 0.5, 0.51517), rtol=1e-4, ) fit = fit_binomial(num_shots=1000, num_hits=500, max_likelihood_factor=1e3) np.testing.assert_allclose( (fit.low, fit.best, fit.high), (0.44143, 0.5, 0.55857), rtol=1e-4, ) fit = fit_binomial(num_shots=100, num_hits=50, max_likelihood_factor=1e3) np.testing.assert_allclose( (fit.low, fit.best, fit.high), (0.3204, 0.5, 0.6796), rtol=1e-4, ) @pytest.mark.parametrize("n,c,factor", [ (100, 50, 1e1), (100, 50, 1e2), (100, 50, 1e3), (1000, 500, 1e3), (10**6, 100, 1e3), (10**6, 100, 1e2), ]) def test_fit_binomial_vs_log_binomial(n: int, c: int, factor: float): fit = fit_binomial(num_shots=n, num_hits=n - c, max_likelihood_factor=factor) a = fit.low b = fit.high raw = log_binomial(p=(n - c) / n, n=n, hits=n - c) low = log_binomial(p=a, n=n, hits=n - c) high = log_binomial(p=b, n=n, hits=n - c) np.testing.assert_allclose( fit.best, (n - c) / n, rtol=1e-4, ) np.testing.assert_allclose( np.exp(raw - low), factor, rtol=1e-2, ) np.testing.assert_allclose( np.exp(raw - high), factor, rtol=1e-2, ) def test_comma_separated_key_values(): d = comma_separated_key_values("folder/a=2,b=3.0,c=test.stim") assert d == { 'a': 2, 'b': 3.0, 'c': 'test', } assert type(d['a']) == int assert type(d['b']) == float with pytest.raises(ValueError, match='separated'): comma_separated_key_values("folder/a,b=3.0,c=test.stim") def test_shot_error_rate_to_piece_error_rate(): np.testing.assert_allclose( shot_error_rate_to_piece_error_rate( shot_error_rate=0.2 * (1 - 0.2) * 2, pieces=2, ), 0.2, rtol=1e-5) np.testing.assert_allclose( shot_error_rate_to_piece_error_rate( shot_error_rate=0.001 * (1 - 0.001) * 2, pieces=2, ), 0.001, rtol=1e-5) np.testing.assert_allclose( shot_error_rate_to_piece_error_rate( shot_error_rate=0.001 * (1 - 0.001)**2 * 3 + 0.001**3, pieces=3, ), 0.001, rtol=1e-5) # Extremely low error rates. np.testing.assert_allclose( shot_error_rate_to_piece_error_rate( shot_error_rate=1e-100, pieces=100, ), 1e-102, rtol=1e-5) def test_shot_error_rate_to_piece_error_rate_unions(): np.testing.assert_allclose( shot_error_rate_to_piece_error_rate( shot_error_rate=0.75, pieces=1, values=2, ), 0.75, rtol=1e-5) np.testing.assert_allclose( shot_error_rate_to_piece_error_rate( shot_error_rate=0.2, pieces=1, values=2, ), 0.2, rtol=1e-5) np.testing.assert_allclose( shot_error_rate_to_piece_error_rate( shot_error_rate=0.001, pieces=1000000, values=2, ), 0.001 / 1000000, rtol=1e-2) np.testing.assert_allclose( shot_error_rate_to_piece_error_rate( shot_error_rate=0.0975, pieces=10, values=2, ), 0.010453280306648605, rtol=1e-5) def test_fit_repr(): v = sinter.Fit(low=0.25, best=1, high=10) assert eval(repr(v), {"sinter": sinter}) == v ================================================ FILE: glue/zx/README.md ================================================ # Stim ZX Stim ZX is an example of using Stim as a library for implementing other tools. Stim ZX implements utilities for analyzing stabilizer ZX calculus graphs. # How to Install stimzx is not a published pypi package. You have to install it from source using `pip install -e`. For example: ``` git clone git@github.com:quantumlib/stim.git cd stim pip install -e glue/zx ``` # How to Use StimZX implements one key method for getting the stabilizers of a stabilizer ZX graph: ``` stimzx.zx_graph_to_external_stabilizers( graph: Union[nx.Graph, nx.MultiGraph] ) -> List[stimzx.ExternalStabilizer] ``` and one fun helper method for creating the graphs from text diagrams: ``` stimzx.text_diagram_to_zx_graph( text_diagram: str ) -> nx.MultiGraph ``` For example: ```python import stimzx print("CNOT graph") cnot_graph = stimzx.text_diagram_to_zx_graph(r""" in---Z---out | in---X---out """) for e in stimzx.zx_graph_to_external_stabilizers(cnot_graph): print(e) # prints: # CNOT graph # +X_ -> +XX # +Z_ -> +Z_ # +_X -> +_X # +_Z -> +ZZ print("SQRT_X graph") sqrt_x_graph = stimzx.text_diagram_to_zx_graph(r""" in---X(pi/2)---out """) for e in stimzx.zx_graph_to_external_stabilizers(sqrt_x_graph): print(e) # prints: # SQRT_X graph # +X -> +X # +Z -> +Y print("mystery graph") graph = stimzx.text_diagram_to_zx_graph(r""" in---X | H *---Z(-pi/2)---out |/ in---X """) for e in stimzx.zx_graph_to_external_stabilizers(graph): print(e) # prints: # mystery graph # +ZX -> +_ # +XZ -> +Z # +Z_ -> +Y print("S distillation graph") s_distill_graph = stimzx.text_diagram_to_zx_graph(r""" * *---------------Z--------------------Z-------Z(pi/2) / \ | | | *-----* *------------Z---+---------------+---Z----------------+-------Z(pi/2) | | | | | | X---X---Z(pi/2) X---X---Z(pi/2) X---X---Z(pi/2) X---X---Z(pi/2) | | | | | | *---+------------------Z-------------------+--------------------+---Z---Z(pi/2) | | | in-------Z--------------------------------------Z-------------------Z(pi)--------out """) for e in stimzx.zx_graph_to_external_stabilizers(s_distill_graph): print(e) # prints: # S distillation graph # +X -> +Y # +Z -> +Z ``` ================================================ FILE: glue/zx/setup.py ================================================ # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from setuptools import setup with open('README.md', encoding='UTF-8') as f: long_description = f.read() __version__ = '1.16.dev0' setup( name='stimzx', version=__version__, author='Craig Gidney', author_email='craig.gidney@gmail.com', url='https://github.com/quantumlib/stim', license='Apache 2', packages=['stimzx'], description='Implements utilities for analyzing ZX calculus graphs using Stim.', long_description=long_description, long_description_content_type='text/markdown', python_requires='>=3.6.0', data_files=['README.md'], install_requires=['stim', 'networkx~=3.0'], tests_require=['pytest', 'python3-distutils'], ) ================================================ FILE: glue/zx/stimzx/__init__.py ================================================ __version__ = '1.16.dev0' from ._external_stabilizer import ( ExternalStabilizer, ) from ._text_diagram_parsing import ( text_diagram_to_networkx_graph, ) from ._zx_graph_solver import ( zx_graph_to_external_stabilizers, text_diagram_to_zx_graph, ZxType, ) ================================================ FILE: glue/zx/stimzx/_external_stabilizer.py ================================================ from typing import List, Any import stim class ExternalStabilizer: """An input-to-output relationship enforced by a stabilizer circuit.""" def __init__(self, *, input: stim.PauliString, output: stim.PauliString): self.input = input self.output = output @staticmethod def from_dual(dual: stim.PauliString, num_inputs: int) -> 'ExternalStabilizer': sign = dual.sign # Transpose input. Ys get negated. for k in range(num_inputs): if dual[k] == 2: sign *= -1 return ExternalStabilizer( input=dual[:num_inputs], output=dual[num_inputs:] * sign, ) @staticmethod def canonicals_from_duals(duals: List[stim.PauliString], num_inputs: int) -> List['ExternalStabilizer']: if not duals: return [] duals = [e.copy() for e in duals] num_qubits = len(duals[0]) num_outputs = num_qubits - num_inputs id_out = stim.PauliString(num_outputs) # Pivot on output qubits, to potentially isolate input-only stabilizers. _eliminate_stabilizers(duals, range(num_inputs, num_qubits)) # Separate input-only stabilizers from the rest. input_only_stabilizers = [] output_using_stabilizers = [] for dual in duals: if dual[num_inputs:] == id_out: input_only_stabilizers.append(dual) else: output_using_stabilizers.append(dual) # Separately canonicalize the output-using and input-only stabilizers. _eliminate_stabilizers(output_using_stabilizers, range(num_qubits)) _eliminate_stabilizers(input_only_stabilizers, range(num_inputs)) duals = input_only_stabilizers + output_using_stabilizers return [ExternalStabilizer.from_dual(e, num_inputs) for e in duals] def __mul__(self, other: 'ExternalStabilizer') -> 'ExternalStabilizer': return ExternalStabilizer(input=other.input * self.input, output=self.output * other.output) def __str__(self) -> str: return str(self.input) + ' -> ' + str(self.output) def __eq__(self, other: Any) -> bool: if not isinstance(other, ExternalStabilizer): return NotImplemented return self.output == other.output and self.input == other.input def __ne__(self, other: Any) -> bool: return not self == other def __repr__(self): return f'stimzx.ExternalStabilizer(input={self.input!r}, output={self.output!r})' def _eliminate_stabilizers(stabilizers: List[stim.PauliString], elimination_indices: range): """Performs partial Gaussian elimination on the list of stabilizers.""" min_pivot = 0 for q in elimination_indices: for b in [1, 3]: for pivot in range(min_pivot, len(stabilizers)): p = stabilizers[pivot][q] if p == 2 or p == b: break else: continue for k, stabilizer in enumerate(stabilizers): p = stabilizer[q] if k != pivot and (p == 2 or p == b): stabilizer *= stabilizers[pivot] if min_pivot != pivot: stabilizers[min_pivot], stabilizers[pivot] = stabilizers[pivot], stabilizers[min_pivot] min_pivot += 1 ================================================ FILE: glue/zx/stimzx/_external_stabilizer_test.py ================================================ import stim import stimzx def test_repr(): e = stimzx.ExternalStabilizer(input=stim.PauliString("XX"), output=stim.PauliString("Y")) assert eval(repr(e), {'stimzx': stimzx, 'stim': stim}) == e ================================================ FILE: glue/zx/stimzx/_text_diagram_parsing.py ================================================ import re from typing import Dict, Tuple, TypeVar, List, Set, Callable import networkx as nx K = TypeVar("K") def text_diagram_to_networkx_graph(text_diagram: str, *, value_func: Callable[[str], K] = str) -> nx.MultiGraph: r"""Converts a text diagram into a networkx multi graph. Args: text_diagram: An ascii text diagram of the graph, linking nodes together with edges. Edges can be horizontal (-), vertical (|), diagonal (/\), crossing (+), or changing direction (*). Nodes can be alphanumeric with parentheses. It is assumed that all text is shown with a fixed-width font. value_func: An optional transformation to apply to the node text in order to get the node's value. Otherwise the node's value is just its text. Example: >>> import stimzx >>> import networkx as nx >>> actual = stimzx.text_diagram_to_networkx_graph(r''' ... ... A ... | ... NODE1--+--NODE2----------* ... | | / ... B | / ... *------NODE4 ... ... ''') >>> expected = nx.MultiGraph() >>> expected.add_node(0, value='A') >>> expected.add_node(1, value='NODE1') >>> expected.add_node(2, value='NODE2') >>> expected.add_node(3, value='B') >>> expected.add_node(4, value='NODE4') >>> _ = expected.add_edge(0, 3) >>> _ = expected.add_edge(1, 2) >>> _ = expected.add_edge(2, 4) >>> _ = expected.add_edge(2, 4) >>> nx.utils.graphs_equal(actual, expected) True Returns: A networkx multi graph containing the graph from the text diagram. Nodes in the graph are integers (the ordering of nodes is in the natural string ordering from left to right then top to bottom in the diagram), and have a "value" attribute containing either the node's string from the diagram or else a function of that string if value_func was specified. """ char_map = _text_to_char_map(text_diagram) node_ids, nodes = _find_nodes(char_map, value_func) edges = _find_all_edges(char_map, node_ids) result = nx.MultiGraph() for k, v in enumerate(nodes): result.add_node(k, value=v) for a, b in edges: result.add_edge(a, b) return result def _text_to_char_map(text: str) -> Dict[complex, str]: char_map = {} x = 0 y = 0 for c in text: if c == '\n': x = 0 y += 1 continue if c != ' ': char_map[x + 1j*y] = c x += 1 return char_map DIR_TO_CHARS = { -1 - 1j: '\\', 0 - 1j: '|+', 1 - 1j: '/', -1: '-+', 1: '-+', -1 + 1j: '/', 1j: '|+', 1 + 1j: '\\', } CHAR_TO_DIR = { '\\': 1 + 1j, '-': 1, '|': 1j, '/': -1 + 1j, } def _find_all_edges(char_map: Dict[complex, str], terminal_map: Dict[complex, K]) -> List[Tuple[K, K]]: edges = [] already_travelled = set() for xy, c in char_map.items(): x = int(xy.real) y = int(xy.imag) if xy in terminal_map or xy in already_travelled or c in '*+': continue already_travelled.add(xy) dxy = CHAR_TO_DIR.get(c) if dxy is None: raise ValueError(f"Character {x+1} ('{c}') in line {y+1} isn't part in a node or an edge") n1 = _find_end_of_edge(xy + dxy, dxy, char_map, terminal_map, already_travelled) n2 = _find_end_of_edge(xy - dxy, -dxy, char_map, terminal_map, already_travelled) edges.append((n2, n1)) return edges def _find_end_of_edge(xy: complex, dxy: complex, char_map: Dict[complex, str], terminal_map: Dict[complex, K], already_travelled: Set[complex]): while True: c = char_map[xy] if xy in terminal_map: return terminal_map[xy] if c != '+': if xy in already_travelled: raise ValueError("Edge used twice.") already_travelled.add(xy) next_deltas: List[complex] = [] if c == '*': for dx2 in [-1, 0, 1]: for dy2 in [-1, 0, 1]: dxy2 = dx2 + dy2 * 1j c2 = char_map.get(xy + dxy2) if dxy2 != 0 and dxy2 != -dxy and c2 is not None and c2 in DIR_TO_CHARS[dxy2]: next_deltas.append(dxy2) if len(next_deltas) != 1: raise ValueError(f"Edge junction ('*') at character {int(xy.real)+1}$ in line {int(xy.imag)+1} doesn't have exactly 2 legs.") dxy, = next_deltas else: expected = DIR_TO_CHARS[dxy] if c not in expected: raise ValueError(f"Dangling edge at character {int(xy.real)+1} in line {int(xy.imag)+1} travelling dx=${int(dxy.real)},dy={int(dxy.imag)}.") xy += dxy def _find_nodes(char_map: Dict[complex, str], value_func: Callable[[str], K]) -> Tuple[Dict[complex, int], List[K]]: node_ids = {} nodes = [] node_chars = re.compile("^[a-zA-Z0-9()]$") next_node_id = 0 for xy, lead_char in char_map.items(): if xy in node_ids: continue if not node_chars.match(lead_char): continue n = 0 nested = 0 full_name = '' while True: c = char_map.get(xy + n, ' ') if c == ' ' and nested > 0: raise ValueError("Label ended before ')' to go with '(' was found.") if nested == 0 and not node_chars.match(c): break full_name += c if c == '(': nested += 1 elif c == ')': nested -= 1 n += 1 nodes.append(value_func(full_name)) node_id = next_node_id next_node_id += 1 for k in range(n): node_ids[xy + k] = node_id return node_ids, nodes ================================================ FILE: glue/zx/stimzx/_text_diagram_parsing_test.py ================================================ import networkx as nx import pytest from ._text_diagram_parsing import _find_nodes, _text_to_char_map, _find_end_of_edge, _find_all_edges, text_diagram_to_networkx_graph def test_text_to_char_map(): assert _text_to_char_map(""" ABC DEF G HI """) == { 0 + 1j: 'A', 1 + 1j: 'B', 2 + 1j: 'C', 4 + 1j: 'D', 5 + 1j: 'E', 6 + 1j: 'F', 0 + 2j: 'G', 1 + 3j: 'H', 2 + 3j: 'I', } def test_find_nodes(): assert _find_nodes(_text_to_char_map(''), lambda e: e) == ({}, []) with pytest.raises(ValueError, match="base 10"): _find_nodes(_text_to_char_map('NOTANINT'), int) with pytest.raises(ValueError, match=r"ended before '\)'"): _find_nodes(_text_to_char_map('X(run_off'), str) assert _find_nodes(_text_to_char_map('X'), str) == ( { 0j: 0, }, ['X'], ) assert _find_nodes(_text_to_char_map('\n X'), str) == ( { 3 + 1j: 0, }, ['X'], ) assert _find_nodes(_text_to_char_map('X(pi)'), str) == ( { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, }, ['X(pi)'], ) assert _find_nodes(_text_to_char_map('X--Z'), str) == ( { 0: 0, 3: 1, }, ['X', 'Z'], ) assert _find_nodes(_text_to_char_map(""" X--* / Z """), str) == ( { 1j: 0, 1 + 3j: 1, }, ['X', 'Z'], ) assert _find_nodes(_text_to_char_map(""" X(pi)--Z """), str) == ( { 0 + 1j: 0, 1 + 1j: 0, 2 + 1j: 0, 3 + 1j: 0, 4 + 1j: 0, 7 + 1j: 1, }, ["X(pi)", "Z"], ) def test_find_end_of_edge(): c = _text_to_char_map(r""" 1--------* \ 2 | 5 \ *--++-* *-----+-* |/ | | / 2 |/ * """) terminal = {1: 'ONE', 18 + 6j: 'TWO'} seen = set() assert _find_end_of_edge(1 + 1j, 1, c, terminal, seen) == 'TWO' assert len(seen) == 31 def test_find_all_edges(): c = _text_to_char_map(r""" X---Z H----X(pi/2) / Z(pi/2) """) node_ids, _ = _find_nodes(c, str) assert _find_all_edges(c, node_ids) == [ (0, 1), (2, 3), (2, 4), ] def test_from_text_diagram(): actual = text_diagram_to_networkx_graph(""" in---Z---H---------out | in---X---Z(-pi/2)---out """) expected = nx.MultiGraph() expected.add_node(0, value='in'), expected.add_node(1, value='Z'), expected.add_node(2, value='H'), expected.add_node(3, value='out'), expected.add_node(4, value='in'), expected.add_node(5, value='X'), expected.add_node(6, value='Z(-pi/2)'), expected.add_node(7, value='out'), expected.add_edge(0, 1) expected.add_edge(1, 2) expected.add_edge(2, 3) expected.add_edge(1, 5) expected.add_edge(4, 5) expected.add_edge(5, 6) expected.add_edge(6, 7) assert nx.utils.graphs_equal(actual, expected) actual = text_diagram_to_networkx_graph(""" Z-* | | X-* """) expected = nx.MultiGraph() expected.add_node(0, value='Z') expected.add_node(1, value='X') expected.add_edge(0, 1) expected.add_edge(0, 1) assert nx.utils.graphs_equal(actual, expected) ================================================ FILE: glue/zx/stimzx/_zx_graph_solver.py ================================================ from typing import Dict, Tuple, List, Any, Union import stim import networkx as nx from ._text_diagram_parsing import text_diagram_to_networkx_graph from ._external_stabilizer import ExternalStabilizer class ZxType: """Data describing a ZX node.""" def __init__(self, kind: str, quarter_turns: int = 0): self.kind = kind self.quarter_turns = quarter_turns def __eq__(self, other): if not isinstance(other, ZxType): return NotImplemented return self.kind == other.kind and self.quarter_turns == other.quarter_turns def __ne__(self, other): return not self == other def __hash__(self): return hash((ZxType, self.kind, self.quarter_turns)) def __repr__(self): return f'ZxType(kind={self.kind!r}, quarter_turns={self.quarter_turns!r})' ZX_TYPES = { "X": ZxType("X"), "X(pi/2)": ZxType("X", 1), "X(pi)": ZxType("X", 2), "X(-pi/2)": ZxType("X", 3), "Z": ZxType("Z"), "Z(pi/2)": ZxType("Z", 1), "Z(pi)": ZxType("Z", 2), "Z(-pi/2)": ZxType("Z", 3), "H": ZxType("H"), "in": ZxType("in"), "out": ZxType("out"), } def text_diagram_to_zx_graph(text_diagram: str) -> nx.MultiGraph: """Converts an ASCII text diagram into a ZX graph (represented as a networkx MultiGraph). Supported node types: "X": X spider with angle set to 0. "Z": Z spider with angle set to 0. "X(pi/2)": X spider with angle set to pi/2. "X(pi)": X spider with angle set to pi. "X(-pi/2)": X spider with angle set to -pi/2. "Z(pi/2)": X spider with angle set to pi/2. "Z(pi)": X spider with angle set to pi. "Z(-pi/2)": X spider with angle set to -pi/2. "H": Hadamard node. Must have degree 2. "in": Input node. Must have degree 1. "out": Output node. Must have degree 1. Args: text_diagram: A text diagram containing ZX nodes (e.g. "X(pi)") and edges (e.g. "------") connecting them. Example: >>> import stimzx >>> import networkx >>> actual: networkx.MultiGraph = stimzx.text_diagram_to_zx_graph(r''' ... in----X------out ... | ... in---Z(pi)---out ... ''') >>> expected = networkx.MultiGraph() >>> expected.add_node(0, value=stimzx.ZxType("in")) >>> expected.add_node(1, value=stimzx.ZxType("X")) >>> expected.add_node(2, value=stimzx.ZxType("out")) >>> expected.add_node(3, value=stimzx.ZxType("in")) >>> expected.add_node(4, value=stimzx.ZxType("Z", quarter_turns=2)) >>> expected.add_node(5, value=stimzx.ZxType("out")) >>> _ = expected.add_edge(0, 1) >>> _ = expected.add_edge(1, 2) >>> _ = expected.add_edge(1, 4) >>> _ = expected.add_edge(3, 4) >>> _ = expected.add_edge(4, 5) >>> networkx.utils.graphs_equal(actual, expected) True Returns: A networkx MultiGraph containing the nodes and edges from the diagram. Nodes are numbered 0, 1, 2, etc in reading ordering from the diagram, and have a "value" attribute of type `stimzx.ZxType`. """ return text_diagram_to_networkx_graph(text_diagram, value_func=ZX_TYPES.__getitem__) def _reduced_zx_graph(graph: Union[nx.Graph, nx.MultiGraph]) -> nx.Graph: """Return an equivalent graph without self edges or repeated edges.""" reduced_graph = nx.Graph() odd_parity_edges = set() for n1, n2 in graph.edges(): if n1 == n2: continue odd_parity_edges ^= {frozenset([n1, n2])} for n, value in graph.nodes('value'): reduced_graph.add_node(n, value=value) for n1, n2 in odd_parity_edges: reduced_graph.add_edge(n1, n2) return reduced_graph def zx_graph_to_external_stabilizers(graph: Union[nx.Graph, nx.MultiGraph]) -> List[ExternalStabilizer]: """Computes the external stabilizers of a ZX graph; generators of Paulis that leave it unchanged including sign. Args: graph: A non-contradictory connected ZX graph with nodes annotated by 'type' and optionally by 'angle'. Allowed types are 'x', 'z', 'h', and 'out'. Allowed angles are multiples of `math.pi/2`. Only 'x' and 'z' node types can have angles. 'out' nodes must have degree 1. 'h' nodes must have degree 2. Returns: A list of canonicalized external stabilizer generators for the graph. """ graph = _reduced_zx_graph(graph) sim = stim.TableauSimulator() # Interpret each edge as a cup producing an EPR pair. # - The qubits of the EPR pair fly away from the center of the edge, towards their respective nodes. # - The qubit keyed by (a, b) is the qubit heading towards b from the edge between a and b. qubit_ids: Dict[Tuple[Any, Any], int] = {} for n1, n2 in graph.edges: qubit_ids[(n1, n2)] = len(qubit_ids) qubit_ids[(n2, n1)] = len(qubit_ids) sim.h(qubit_ids[(n1, n2)]) sim.cnot(qubit_ids[(n1, n2)], qubit_ids[(n2, n1)]) # Interpret each internal node as a family of post-selected parity measurements. for n, node_type in graph.nodes('value'): if node_type.kind in 'XZ': # Surround X type node with Hadamards so it can be handled as if it were Z type. if node_type.kind == 'X': for neighbor in graph.neighbors(n): sim.h(qubit_ids[(neighbor, n)]) elif node_type.kind == 'H': # Hadamard one input so the H node can be handled as if it were Z type. neighbor, _ = graph.neighbors(n) sim.h(qubit_ids[(neighbor, n)]) elif node_type.kind in ['out', 'in']: continue # Don't measure qubits leaving the system. else: raise ValueError(f"Unknown node type {node_type!r}") # Handle Z type node. # - Postselects the ZZ observable over each pair of incoming qubits. # - Postselects the (S**quarter_turns X S**-quarter_turns)XX..X observable over all incoming qubits. neighbors = [n2 for n2 in graph.neighbors(n) if n2 != n] center = qubit_ids[(neighbors[0], n)] # Pick one incoming qubit to be the common control for the others. # Handle node angle using a phasing operation. [id, sim.s, sim.z, sim.s_dag][node_type.quarter_turns](center) # Use multi-target CNOT and Hadamard to transform postselected observables into single-qubit Z observables. for n2 in neighbors[1:]: sim.cnot(center, qubit_ids[(n2, n)]) sim.h(center) # Postselect the observables. for n2 in neighbors: _pseudo_postselect(sim, qubit_ids[(n2, n)]) # Find output qubits. in_nodes = sorted(n for n, value in graph.nodes('value') if value.kind == 'in') out_nodes = sorted(n for n, value in graph.nodes('value') if value.kind == 'out') ext_nodes = in_nodes + out_nodes out_qubits = [] for out in ext_nodes: (neighbor,) = graph.neighbors(out) out_qubits.append(qubit_ids[(neighbor, out)]) # Remove qubits corresponding to non-external edges. for i, q in enumerate(out_qubits): sim.swap(q, len(qubit_ids) + i) for i, q in enumerate(out_qubits): sim.swap(i, len(qubit_ids) + i) sim.set_num_qubits(len(out_qubits)) # Stabilizers of the simulator state are the external stabilizers of the graph. dual_stabilizers = sim.canonical_stabilizers() return ExternalStabilizer.canonicals_from_duals(dual_stabilizers, len(in_nodes)) def _pseudo_postselect(sim: stim.TableauSimulator, target: int): """Pretend to postselect by using classical feedback to consistently get into the measurement-was-false state.""" measurement_result, kickback = sim.measure_kickback(target) if kickback is not None: for qubit, pauli in enumerate(kickback): feedback_op = [None, sim.cnot, sim.cy, sim.cz][pauli] if feedback_op is not None: feedback_op(stim.target_rec(-1), qubit) assert kickback is not None or not measurement_result, "Impossible postselection. Graph contained a contradiction." ================================================ FILE: glue/zx/stimzx/_zx_graph_solver_test.py ================================================ from typing import List import stim from ._zx_graph_solver import zx_graph_to_external_stabilizers, text_diagram_to_zx_graph, ExternalStabilizer def test_disconnected(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X X---out """)) == [ ExternalStabilizer(input=stim.PauliString("Z"), output=stim.PauliString("_")), ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), ] assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z---out | X """)) == [ ExternalStabilizer(input=stim.PauliString("Z"), output=stim.PauliString("_")), ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), ] assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z---X---out | | *---* """)) == [ ExternalStabilizer(input=stim.PauliString("X"), output=stim.PauliString("_")), ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), ] def test_cnot(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X---out | in---Z---out """)) == external_stabilizers_of_circuit(stim.Circuit("CNOT 1 0")) assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z---out | in---X---out """)) == external_stabilizers_of_circuit(stim.Circuit("CNOT 0 1")) def test_cz(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z---out | H | in---Z---out """)) == external_stabilizers_of_circuit(stim.Circuit("CZ 0 1")) def test_s(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z(pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("S 0")) def test_s_dag(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z(-pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("S_DAG 0")) def test_sqrt_x(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X(pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("SQRT_X 0")) def test_sqrt_x_sqrt_x(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X(pi/2)---X(pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("X 0")) def test_sqrt_z_sqrt_z(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z(pi/2)---Z(pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("Z 0")) def test_sqrt_x_dag(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X(-pi/2)---out """)) == external_stabilizers_of_circuit(stim.Circuit("SQRT_X_DAG 0")) def test_x(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X(pi)---out """)) == external_stabilizers_of_circuit(stim.Circuit("X 0")) def test_z(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---Z(pi)---out """)) == external_stabilizers_of_circuit(stim.Circuit("Z 0")) def test_id(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" in---X---Z---out """)) == external_stabilizers_of_circuit(stim.Circuit("I 0")) def test_s_state_distill(): assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(r""" * *---------------Z--------------------Z-------Z(pi/2) / \ | | | *-----* *------------Z---+---------------+---Z----------------+-------Z(pi/2) | | | | | | X---X---Z(pi/2) X---X---Z(pi/2) X---X---Z(pi/2) X---X---Z(pi/2) | | | | | | *---+------------------Z-------------------+--------------------+---Z---Z(pi/2) | | | in-------Z--------------------------------------Z-------------------Z(pi)--------out """)) == external_stabilizers_of_circuit(stim.Circuit("S 0")) def external_stabilizers_of_circuit(circuit: stim.Circuit) -> List[ExternalStabilizer]: n = circuit.num_qubits s = stim.TableauSimulator() s.do(circuit) t = s.current_inverse_tableau()**-1 stabilizers = [] for k in range(n): p = [0] * n p[k] = 1 stabilizers.append(stim.PauliString(p) + t.x_output(k)) p[k] = 3 stabilizers.append(stim.PauliString(p) + t.z_output(k)) return [ExternalStabilizer.from_dual(e, circuit.num_qubits) for e in stabilizers] def test_sign(): x, z = external_stabilizers_of_circuit(stim.Circuit("X 0")) assert x.input == stim.PauliString("+X") assert x.output == stim.PauliString("+X") assert z.input == stim.PauliString("+Z") assert z.output == stim.PauliString("-Z") assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(r""" in---X---out """)) == external_stabilizers_of_circuit(stim.Circuit("I 0")) assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(r""" in---X(pi)---out """)) == external_stabilizers_of_circuit(stim.Circuit("X 0")) assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(r""" in---Z(pi)---out """)) == external_stabilizers_of_circuit(stim.Circuit("Z 0")) assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(r""" in---X(pi)---Z(pi)---out """)) == external_stabilizers_of_circuit(stim.Circuit("Y 0")) ================================================ FILE: package.json ================================================ { "name": "stim", "title": "Stim", "description": "A quantum stabilizer circuit simulator.", "license": "Apache-2.0", "version": "1.0.0", "homepage": "https://github.com/quantumlib/stim", "bugs": { "url": "https://github.com/quantumlib/stim/issues" }, "devDependencies": { "puppeteer": "^5.3.0" }, "repository": { "type": "git", "url": "https://github.com/quantumlib/stim.git" } } ================================================ FILE: puppeteer_run_tests.js ================================================ const puppeteer = require('puppeteer'); (async () => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); let caughtPageError = false; page.on('console', message => console.log(message.text())); page.on('pageerror', ({message}) => { caughtPageError = true; console.error("Page error bubbled into PuppeteerRunTests.js: " + message); }); const outDirUrl = 'file:///' + __dirname.split('\\').join('/') + '/out/'; await page.goto(outDirUrl + 'all_stim_tests.html#blocking'); await page.waitForSelector('#done', {timeout: 5 * 60 * 1000}); let anyFailures = await page.evaluate('__any_failures'); await browser.close(); if (anyFailures || caughtPageError) { process.exit(1); } } catch (ex) { console.error("Error bubbled up into PuppeteerRunTests.js: " + ex); process.exit(1); } })(); ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools", "wheel", "pybind11~=2.11.1"] build-backend = "setuptools.build_meta" ================================================ FILE: setup.py ================================================ # 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. import platform from setuptools import setup, Extension import glob import pybind11 ALL_SOURCE_FILES = glob.glob("src/**/*.cc", recursive=True) MUX_SOURCE_FILES = glob.glob("src/**/march.pybind.cc", recursive=True) TEST_FILES = glob.glob("src/**/*.test.cc", recursive=True) PERF_FILES = glob.glob("src/**/*.perf.cc", recursive=True) MAIN_FILES = glob.glob("src/**/main.cc", recursive=True) HEADER_FILES = glob.glob("src/**/*.h", recursive=True) + glob.glob("src/**/*.inl", recursive=True) RELEVANT_SOURCE_FILES = sorted(set(ALL_SOURCE_FILES) - set(TEST_FILES + PERF_FILES + MAIN_FILES + MUX_SOURCE_FILES)) __version__ = '1.16.dev0' if platform.system().startswith('Win'): common_compile_args = [ '/std:c++20', '/O2', f'/DVERSION_INFO={__version__}', ] arch_avx = ['/arch:AVX2'] arch_sse = ['/arch:SSE2'] arch_basic = [] else: common_compile_args = [ '-std=c++20', '-fno-strict-aliasing', '-O3', '-g0', f'-DVERSION_INFO={__version__}', ] arch_avx = ['-mavx2'] arch_sse = ['-msse2', '-mno-avx2'] arch_basic = [] stim_detect_machine_architecture = Extension( 'stim._detect_machine_architecture', sources=MUX_SOURCE_FILES, include_dirs=[pybind11.get_include(), "src"], language='c++', extra_compile_args=[ *common_compile_args, *arch_basic, ], ) stim_polyfill = Extension( 'stim._stim_polyfill', sources=RELEVANT_SOURCE_FILES, include_dirs=[pybind11.get_include(), "src"], language='c++', extra_compile_args=[ *common_compile_args, *arch_basic, '-DSTIM_PYBIND11_MODULE_NAME=_stim_polyfill', ], ) stim_sse2 = Extension( 'stim._stim_sse2', sources=RELEVANT_SOURCE_FILES, include_dirs=[pybind11.get_include(), "src"], language='c++', extra_compile_args=[ *common_compile_args, *arch_sse, '-DSTIM_PYBIND11_MODULE_NAME=_stim_sse2', ], ) # NOTE: disabled until https://github.com/quantumlib/Stim/issues/432 is fixed # stim_avx2 = Extension( # 'stim._stim_avx2', # sources=RELEVANT_SOURCE_FILES, # include_dirs=[pybind11.get_include(), "src"], # language='c++', # extra_compile_args=[ # *common_compile_args, # *arch_avx, # '-DSTIM_PYBIND11_MODULE_NAME=_stim_avx2', # ], # ) with open('glue/python/README.md', encoding='UTF-8') as f: long_description = f.read() def _get_extensions(): archs=["x86", "i686", "i386", "amd64"] if any(_ext in platform.processor().lower() for _ext in archs): # NOTE: disabled until https://github.com/quantumlib/Stim/issues/432 is fixed # stim_avx2, return [stim_detect_machine_architecture, stim_polyfill, # stim_avx2, stim_sse2] else: return [stim_detect_machine_architecture, stim_polyfill] setup( name='stim', version=__version__, author='Craig Gidney', author_email='craig.gidney@gmail.com', url='https://github.com/quantumlib/stim', license='Apache 2', description='A fast library for analyzing with quantum stabilizer circuits.', long_description=long_description, long_description_content_type='text/markdown', ext_modules=_get_extensions(), python_requires='>=3.6.0', packages=['stim'], package_dir={'stim': 'glue/python/src/stim'}, package_data={'': [*HEADER_FILES, 'glue/python/src/stim/__init__.pyi', 'glue/python/README.md', 'pyproject.toml']}, include_package_data=True, install_requires=['numpy'], entry_points={ 'console_scripts': ['stim=stim._main_argv:main_argv'], }, # Needed on Windows to avoid the default `build` colliding with Bazel's `BUILD`. options={'build': {'build_base': 'python_build_stim'}}, ) ================================================ FILE: src/main.cc ================================================ // 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. #include "stim/main_namespaced.h" int main(int argc, const char **argv) { return stim::main(argc, argv); } ================================================ FILE: src/stim/circuit/circuit.cc ================================================ // 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. #include "stim/circuit/circuit.h" #include #include #include #include "stim/circuit/gate_target.h" #include "stim/gates/gates.h" #include "stim/util_bot/str_util.h" using namespace stim; enum class READ_CONDITION { READ_AS_LITTLE_AS_POSSIBLE, READ_UNTIL_END_OF_BLOCK, READ_UNTIL_END_OF_FILE, }; /// Concatenates the second pointer range's data into the first. /// Typically, the two ranges are contiguous and so this only requires advancing the end of the destination region. /// In cases where that doesn't occur, space is created in the given monotonic buffer to store the result and both /// the start and end of the destination range move. void fuse_data(SpanRef &dst, SpanRef src, MonotonicBuffer &buf) { if (dst.ptr_end != src.ptr_start) { buf.ensure_available(src.size() + dst.size()); dst = buf.take_copy(dst); src = buf.take_copy(src); } assert(dst.ptr_end == src.ptr_start); dst.ptr_end = src.ptr_end; } Circuit::Circuit() : target_buf(), arg_buf(), tag_buf(), operations(), blocks() { } Circuit::Circuit(const Circuit &circuit) : target_buf(circuit.target_buf.total_allocated()), arg_buf(circuit.arg_buf.total_allocated()), tag_buf(circuit.tag_buf.total_allocated()), operations(circuit.operations), blocks(circuit.blocks) { // Keep local copy of operation data. for (auto &op : operations) { op.targets = target_buf.take_copy(op.targets); op.args = arg_buf.take_copy(op.args); op.tag = tag_buf.take_copy(op.tag); } } Circuit::Circuit(Circuit &&circuit) noexcept : target_buf(std::move(circuit.target_buf)), arg_buf(std::move(circuit.arg_buf)), tag_buf(std::move(circuit.tag_buf)), operations(std::move(circuit.operations)), blocks(std::move(circuit.blocks)) { } Circuit &Circuit::operator=(const Circuit &circuit) { if (&circuit != this) { blocks = circuit.blocks; operations = circuit.operations; // Keep local copy of operation data. target_buf = MonotonicBuffer(circuit.target_buf.total_allocated()); arg_buf = MonotonicBuffer(circuit.arg_buf.total_allocated()); tag_buf = MonotonicBuffer(circuit.tag_buf.total_allocated()); for (auto &op : operations) { op.targets = target_buf.take_copy(op.targets); op.args = arg_buf.take_copy(op.args); op.tag = tag_buf.take_copy(op.tag); } } return *this; } Circuit &Circuit::operator=(Circuit &&circuit) noexcept { if (&circuit != this) { operations = std::move(circuit.operations); blocks = std::move(circuit.blocks); target_buf = std::move(circuit.target_buf); arg_buf = std::move(circuit.arg_buf); tag_buf = std::move(circuit.tag_buf); } return *this; } bool Circuit::operator==(const Circuit &other) const { if (operations.size() != other.operations.size() || blocks.size() != other.blocks.size()) { return false; } for (size_t k = 0; k < operations.size(); k++) { if (operations[k].gate_type == GateType::REPEAT && other.operations[k].gate_type == GateType::REPEAT) { if (operations[k].repeat_block_rep_count() != other.operations[k].repeat_block_rep_count()) { return false; } const auto &b1 = operations[k].repeat_block_body(*this); const auto &b2 = other.operations[k].repeat_block_body(other); if (b1 != b2) { return false; } } else if (operations[k] != other.operations[k]) { return false; } } return true; } bool Circuit::operator!=(const Circuit &other) const { return !(*this == other); } bool Circuit::approx_equals(const Circuit &other, double atol) const { if (operations.size() != other.operations.size() || blocks.size() != other.blocks.size()) { return false; } for (size_t k = 0; k < operations.size(); k++) { if (operations[k].gate_type == GateType::REPEAT && other.operations[k].gate_type == GateType::REPEAT) { if (operations[k].repeat_block_rep_count() != other.operations[k].repeat_block_rep_count()) { return false; } const auto &b1 = operations[k].repeat_block_body(*this); const auto &b2 = other.operations[k].repeat_block_body(other); if (!b1.approx_equals(b2, atol)) { return false; } } else if (!operations[k].approx_equals(other.operations[k], atol)) { return false; } } return true; } inline bool is_name_char(int c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; } template inline const Gate &read_gate_name(int &c, SOURCE read_char) { char name_buf[32]; size_t n = 0; while (is_name_char(c) && n < sizeof(name_buf)) { name_buf[n] = (char)c; c = read_char(); n++; } // Note: in the name-too-long case, the full buffer name won't match any gate and an exception will fire. try { return GATE_DATA.at(std::string_view{&name_buf[0], n}); } catch (const std::out_of_range &ex) { throw std::invalid_argument(ex.what()); } } template uint64_t read_uint63_t(int &c, SOURCE read_char) { if (!(c >= '0' && c <= '9')) { throw std::invalid_argument("Expected a digit but got '" + std::string(1, c) + "'"); } uint64_t result = 0; do { result *= 10; result += c - '0'; if (result >= uint64_t{1} << 63) { throw std::invalid_argument("Number too large."); } c = read_char(); } while (c >= '0' && c <= '9'); return result; } template inline void read_arbitrary_targets_into(int &c, SOURCE read_char, Circuit &circuit) { bool need_space = true; while (read_until_next_line_arg(c, read_char, need_space)) { GateTarget t = read_single_gate_target(c, read_char); circuit.target_buf.append_tail(t); need_space = !t.is_combiner(); } } template inline void read_result_targets64_into(int &c, SOURCE read_char, Circuit &circuit) { while (read_until_next_line_arg(c, read_char)) { uint64_t q = read_uint63_t(c, read_char); circuit.target_buf.append_tail(GateTarget{(uint32_t)(q & 0xFFFFFFFFULL)}); circuit.target_buf.append_tail(GateTarget{(uint32_t)(q >> 32)}); } } template void circuit_read_single_operation(Circuit &circuit, char lead_char, SOURCE read_char) { int c = (int)lead_char; const auto &gate = read_gate_name(c, read_char); std::string_view tail_tag; try { read_tag(c, gate.name, read_char, circuit.tag_buf); if (!circuit.tag_buf.tail.empty()) { tail_tag = std::string_view(circuit.tag_buf.tail.ptr_start, circuit.tag_buf.tail.size()); } read_parens_arguments(c, gate.name, read_char, circuit.arg_buf); if (gate.flags & GATE_IS_BLOCK) { read_result_targets64_into(c, read_char, circuit); if (c != '{') { throw std::invalid_argument("Missing '{' at start of " + std::string(gate.name) + " block."); } } else { read_arbitrary_targets_into(c, read_char, circuit); if (c == '{') { throw std::invalid_argument("Unexpected '{'."); } CircuitInstruction(gate.id, circuit.arg_buf.tail, circuit.target_buf.tail, tail_tag).validate(); } } catch (const std::invalid_argument &ex) { circuit.target_buf.discard_tail(); circuit.arg_buf.discard_tail(); throw ex; } circuit.tag_buf.commit_tail(); circuit.operations.push_back( CircuitInstruction(gate.id, circuit.arg_buf.commit_tail(), circuit.target_buf.commit_tail(), tail_tag)); } void Circuit::try_fuse_last_two_ops() { if (operations.size() >= 2) { try_fuse_after(operations.size() - 2); } } void Circuit::try_fuse_after(size_t index) { if (index + 1 >= operations.size()) { return; } if (operations[index].can_fuse(operations[index + 1])) { fuse_data(operations[index].targets, operations[index + 1].targets, target_buf); operations.erase(operations.begin() + index + 1); } } template void circuit_read_operations(Circuit &circuit, SOURCE read_char, READ_CONDITION read_condition) { auto &ops = circuit.operations; do { int c = read_char(); read_past_dead_space_between_commands(c, read_char); if (c == EOF) { if (read_condition == READ_CONDITION::READ_UNTIL_END_OF_BLOCK) { throw std::invalid_argument("Unterminated block. Got a '{' without an eventual '}'."); } return; } if (c == '}') { if (read_condition != READ_CONDITION::READ_UNTIL_END_OF_BLOCK) { throw std::invalid_argument("Uninitiated block. Got a '}' without a '{'."); } return; } circuit_read_single_operation(circuit, c, read_char); CircuitInstruction &new_op = ops.back(); if (new_op.gate_type == GateType::REPEAT) { if (new_op.targets.size() != 2) { throw std::invalid_argument("Invalid instruction. Expected one repetition arg like `REPEAT 100 {`."); } uint32_t rep_count_low = new_op.targets[0].data; uint32_t rep_count_high = new_op.targets[1].data; uint32_t block_id = (uint32_t)circuit.blocks.size(); if (rep_count_low == 0 && rep_count_high == 0) { throw std::invalid_argument("Repeating 0 times is not supported."); } // Read block. circuit.blocks.emplace_back(); circuit_read_operations(circuit.blocks.back(), read_char, READ_CONDITION::READ_UNTIL_END_OF_BLOCK); // Rewrite target data to reference the parsed block. circuit.target_buf.ensure_available(3); circuit.target_buf.append_tail(GateTarget{block_id}); circuit.target_buf.append_tail(GateTarget{rep_count_low}); circuit.target_buf.append_tail(GateTarget{rep_count_high}); new_op.targets = circuit.target_buf.commit_tail(); } // Fuse operations. circuit.try_fuse_last_two_ops(); } while (read_condition != READ_CONDITION::READ_AS_LITTLE_AS_POSSIBLE); } void Circuit::append_from_text(std::string_view text) { size_t k = 0; circuit_read_operations( *this, [&]() { return k < text.size() ? text[k++] : EOF; }, READ_CONDITION::READ_UNTIL_END_OF_FILE); } void Circuit::safe_append(CircuitInstruction operation, bool block_fusion) { auto flags = GATE_DATA[operation.gate_type].flags; if (flags & GATE_IS_BLOCK) { throw std::invalid_argument("Can't append a block like a normal operation."); } operation.validate(); // Ensure arg/target data is backed by coping it into this circuit's buffers. operation.args = arg_buf.take_copy(operation.args); operation.targets = target_buf.take_copy(operation.targets); operation.tag = tag_buf.take_copy(operation.tag); if (!block_fusion && !operations.empty() && operations.back().can_fuse(operation)) { // Extend targets of last gate. fuse_data(operations.back().targets, operation.targets, target_buf); } else { // Add a fresh new operation with its own target data. operations.push_back(operation); } } void Circuit::safe_append_ua( std::string_view gate_name, const std::vector &targets, double singleton_arg, std::string_view tag) { const auto &gate = GATE_DATA.at(gate_name); std::vector converted; converted.reserve(targets.size()); for (auto e : targets) { converted.push_back({e}); } safe_append(CircuitInstruction(gate.id, &singleton_arg, converted, tag)); } void Circuit::safe_append_u( std::string_view gate_name, const std::vector &targets, const std::vector &args, std::string_view tag) { const auto &gate = GATE_DATA.at(gate_name); std::vector converted; converted.reserve(targets.size()); for (auto e : targets) { converted.push_back({e}); } safe_append(CircuitInstruction(gate.id, args, converted, tag)); } void Circuit::safe_insert(size_t index, const CircuitInstruction &instruction) { if (index > operations.size()) { throw std::invalid_argument("index > operations.size()"); } auto flags = GATE_DATA[instruction.gate_type].flags; if (flags & GATE_IS_BLOCK) { throw std::invalid_argument("Can't insert a block like a normal operation."); } instruction.validate(); // Copy arg/target data into this circuit's buffers. CircuitInstruction copy = instruction; copy.args = arg_buf.take_copy(copy.args); copy.targets = target_buf.take_copy(copy.targets); copy.tag = tag_buf.take_copy(copy.tag); operations.insert(operations.begin() + index, copy); // Fuse at boundaries. try_fuse_after(index); if (index > 0) { try_fuse_after(index - 1); } } void Circuit::safe_insert(size_t index, const Circuit &circuit) { if (index > operations.size()) { throw std::invalid_argument("index > operations.size()"); } operations.insert(operations.begin() + index, circuit.operations.begin(), circuit.operations.end()); // Copy backing data over into this circuit. for (size_t k = index; k < index + circuit.operations.size(); k++) { if (operations[k].gate_type == GateType::REPEAT) { blocks.push_back(operations[k].repeat_block_body(circuit)); auto repeat_count = operations[k].repeat_block_rep_count(); target_buf.append_tail(GateTarget{(uint32_t)(blocks.size() - 1)}); target_buf.append_tail(GateTarget{(uint32_t)(repeat_count & 0xFFFFFFFFULL)}); target_buf.append_tail(GateTarget{(uint32_t)(repeat_count >> 32)}); operations[k].targets = target_buf.commit_tail(); } else { operations[k].targets = target_buf.take_copy(operations[k].targets); operations[k].args = arg_buf.take_copy(operations[k].args); operations[k].tag = tag_buf.take_copy(operations[k].tag); } } // Fuse at boundaries. if (!circuit.operations.empty()) { try_fuse_after(index + circuit.operations.size() - 1); if (index > 0) { try_fuse_after(index - 1); } } } void Circuit::safe_insert_repeat_block( size_t index, uint64_t repeat_count, const Circuit &block, std::string_view tag) { if (repeat_count == 0) { throw std::invalid_argument("Can't repeat 0 times."); } if (index > operations.size()) { throw std::invalid_argument("index > operations.size()"); } target_buf.append_tail(GateTarget{(uint32_t)blocks.size()}); target_buf.append_tail(GateTarget{(uint32_t)(repeat_count & 0xFFFFFFFFULL)}); target_buf.append_tail(GateTarget{(uint32_t)(repeat_count >> 32)}); blocks.push_back(block); auto targets = target_buf.commit_tail(); operations.insert(operations.begin() + index, CircuitInstruction(GateType::REPEAT, {}, targets, tag)); } void Circuit::safe_append_reversed_targets(CircuitInstruction instruction, bool reverse_in_pairs) { if (reverse_in_pairs) { if (instruction.targets.size() % 2 != 0) { throw std::invalid_argument("targets.size() % 2 != 0"); } for (size_t k = instruction.targets.size(); k;) { k -= 2; target_buf.append_tail(instruction.targets[k]); target_buf.append_tail(instruction.targets[k + 1]); } } else { for (size_t k = instruction.targets.size(); k-- > 0;) { target_buf.append_tail(instruction.targets[k]); } } CircuitInstruction to_add = instruction; try { to_add.validate(); } catch (const std::invalid_argument &ex) { target_buf.discard_tail(); throw; } // Commit reversed tail data. to_add.targets = target_buf.commit_tail(); // Ensure arg/tag data is backed by copying it into this circuit's buffers. to_add.args = arg_buf.take_copy(to_add.args); to_add.tag = tag_buf.take_copy(to_add.tag); if (!operations.empty() && operations.back().can_fuse(to_add)) { // Extend targets of last gate. fuse_data(operations.back().targets, to_add.targets, target_buf); } else { // Add a fresh new operation with its own target data. operations.push_back(to_add); } } void Circuit::append_from_file(FILE *file, bool stop_asap) { circuit_read_operations( *this, [&]() { return getc(file); }, stop_asap ? READ_CONDITION::READ_AS_LITTLE_AS_POSSIBLE : READ_CONDITION::READ_UNTIL_END_OF_FILE); } void stim::print_circuit(std::ostream &out, const Circuit &c, size_t indentation) { bool first = true; for (const auto &op : c.operations) { if (first) { first = false; } else { out << "\n"; } // Recurse on repeat blocks. if (op.gate_type == GateType::REPEAT) { if (op.targets.size() == 3 && op.targets[0].data < c.blocks.size()) { for (size_t k = 0; k < indentation; k++) { out << ' '; } out << "REPEAT"; if (!op.tag.empty()) { out << '['; write_tag_escaped_string_to(op.tag, out); out << ']'; } out << " " << op.repeat_block_rep_count() << " {\n"; print_circuit(out, c.blocks[op.targets[0].data], indentation + 4); out << '\n'; for (size_t k = 0; k < indentation; k++) { out << ' '; } out << '}'; continue; } } for (size_t k = 0; k < indentation; k++) { out << ' '; } out << op; } } std::ostream &stim::operator<<(std::ostream &out, const Circuit &c) { print_circuit(out, c, 0); return out; } void Circuit::clear() { target_buf.clear(); arg_buf.clear(); operations.clear(); blocks.clear(); } Circuit Circuit::operator+(const Circuit &other) const { Circuit result = *this; result += other; return result; } Circuit Circuit::operator*(uint64_t repetitions) const { if (repetitions == 0) { return Circuit(); } if (repetitions == 1) { return *this; } // If the entire circuit is a repeat block, just adjust its repeat count. if (operations.size() == 1 && operations[0].gate_type == GateType::REPEAT) { uint64_t old_reps = operations[0].repeat_block_rep_count(); uint64_t new_reps = old_reps * repetitions; if (old_reps != new_reps / repetitions) { throw std::invalid_argument("Fused repetition count is too large."); } Circuit copy; copy.append_repeat_block(new_reps, operations[0].repeat_block_body(*this), ""); return copy; } Circuit result; result.append_repeat_block(repetitions, *this, ""); return result; } /// Helper method for fusing during concatenation. If the data being extended is at the end of /// the monotonic buffer and there's space for the additional data, put it there in place. /// Otherwise it needs to be copied to the new location. /// /// CAUTION: This violates the usual guarantee that once data is committed to a monotonic /// buffer it cannot be moved. The old data is still readable in its original location, but /// the caller is responsible for guaranteeing that no dangling writeable pointers remain /// that point to the old location (since they will write data that is no longer read by /// other parts of the code). template SpanRef mono_extend(MonotonicBuffer &cur, SpanRef original, SpanRef additional) { if (original.ptr_end == cur.tail.ptr_start) { // Try to append new data right after the original data. cur.ensure_available(additional.size()); if (original.ptr_end == cur.tail.ptr_start) { cur.append_tail(additional); auto added = cur.commit_tail(); return {original.ptr_start, added.ptr_end}; } } // Ensure necessary space is available, plus some padding to avoid quadratic behavior when repeatedly extending. cur.ensure_available((int)(1.1 * (original.size() + additional.size())) + 10); cur.append_tail(original); cur.append_tail(additional); return cur.commit_tail(); } Circuit &Circuit::operator+=(const Circuit &other) { SpanRef ops_to_add = other.operations; if (!operations.empty() && !ops_to_add.empty() && operations.back().can_fuse(ops_to_add[0])) { operations.back().targets = mono_extend(target_buf, operations.back().targets, ops_to_add[0].targets); ops_to_add.ptr_start++; } if (&other == this) { operations.insert(operations.end(), ops_to_add.begin(), ops_to_add.end()); return *this; } uint32_t block_offset = (uint32_t)blocks.size(); blocks.insert(blocks.end(), other.blocks.begin(), other.blocks.end()); for (const auto &op : ops_to_add) { SpanRef target_data = target_buf.take_copy(op.targets); if (op.gate_type == GateType::REPEAT) { assert(op.targets.size() == 3); target_data[0].data += block_offset; } SpanRef arg_data = arg_buf.take_copy(op.args); std::string_view tag_data = tag_buf.take_copy(op.tag); operations.push_back(CircuitInstruction(op.gate_type, arg_data, target_data, tag_data)); } return *this; } Circuit &Circuit::operator*=(uint64_t repetitions) { if (repetitions == 0) { clear(); } else { *this = *this * repetitions; } return *this; } std::string Circuit::str() const { std::stringstream s; s << *this; return s.str(); } Circuit Circuit::from_file(FILE *file) { Circuit result; result.append_from_file(file, false); return result; } Circuit::Circuit(std::string_view text) { append_from_text(text); } size_t Circuit::count_qubits() const { return (uint32_t)max_operation_property([](const CircuitInstruction &op) -> uint32_t { uint32_t r = 0; for (auto t : op.targets) { if (!(t.data & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { r = std::max(r, t.qubit_value() + uint32_t{1}); } } return r; }); } size_t Circuit::max_lookback() const { return max_operation_property([](const CircuitInstruction &op) -> uint32_t { uint32_t r = 0; for (auto t : op.targets) { if (t.data & TARGET_RECORD_BIT) { r = std::max(r, t.qubit_value()); } } return r; }); } uint64_t stim::add_saturate(uint64_t a, uint64_t b) { uint64_t r = a + b; if (r < a) { return UINT64_MAX; } return r; } uint64_t stim::mul_saturate(uint64_t a, uint64_t b) { if (b && a > UINT64_MAX / b) { return UINT64_MAX; } return a * b; } uint64_t Circuit::count_measurements() const { return flat_count_operations([=](const CircuitInstruction &op) -> uint64_t { return op.count_measurement_results(); }); } uint64_t Circuit::count_detectors() const { return flat_count_operations([=](const CircuitInstruction &op) -> uint64_t { return op.gate_type == GateType::DETECTOR; }); } uint64_t Circuit::count_ticks() const { return flat_count_operations([=](const CircuitInstruction &op) -> uint64_t { return op.gate_type == GateType::TICK; }); } uint64_t Circuit::count_observables() const { return max_operation_property([=](const CircuitInstruction &op) -> uint64_t { return op.gate_type == GateType::OBSERVABLE_INCLUDE ? (size_t)op.args[0] + 1 : 0; }); } size_t Circuit::count_sweep_bits() const { return max_operation_property([](const CircuitInstruction &op) -> uint32_t { uint32_t r = 0; for (auto t : op.targets) { if (t.data & TARGET_SWEEP_BIT) { r = std::max(r, t.qubit_value() + 1); } } return r; }); } CircuitStats Circuit::compute_stats() const { CircuitStats total; for (const auto &op : operations) { op.add_stats_to(total, this); } return total; } Circuit Circuit::py_get_slice(int64_t start, int64_t step, int64_t slice_length) const { assert(slice_length >= 0); assert(slice_length == 0 || start >= 0); Circuit result; for (size_t k = 0; k < (size_t)slice_length; k++) { const auto &op = operations[start + step * k]; if (op.gate_type == GateType::REPEAT) { result.target_buf.append_tail(GateTarget{(uint32_t)result.blocks.size()}); result.target_buf.append_tail(op.targets[1]); result.target_buf.append_tail(op.targets[2]); auto targets = result.target_buf.commit_tail(); auto tag = result.tag_buf.take_copy(op.tag); result.blocks.push_back(op.repeat_block_body(*this)); result.operations.push_back(CircuitInstruction(op.gate_type, {}, targets, tag)); } else { auto args = result.arg_buf.take_copy(op.args); auto targets = result.target_buf.take_copy(op.targets); auto tag = result.tag_buf.take_copy(op.tag); result.operations.push_back({op.gate_type, args, targets, tag}); } } return result; } void Circuit::append_repeat_block(uint64_t repeat_count, Circuit &&body, std::string_view tag) { if (repeat_count == 0) { throw std::invalid_argument("Can't repeat 0 times."); } target_buf.append_tail(GateTarget{(uint32_t)blocks.size()}); target_buf.append_tail(GateTarget{(uint32_t)(repeat_count & 0xFFFFFFFFULL)}); target_buf.append_tail(GateTarget{(uint32_t)(repeat_count >> 32)}); blocks.push_back(std::move(body)); auto targets = target_buf.commit_tail(); operations.push_back(CircuitInstruction(GateType::REPEAT, {}, targets, tag_buf.take_copy(tag))); } void Circuit::append_repeat_block(uint64_t repeat_count, const Circuit &body, std::string_view tag) { if (repeat_count == 0) { throw std::invalid_argument("Can't repeat 0 times."); } target_buf.append_tail(GateTarget{(uint32_t)blocks.size()}); target_buf.append_tail(GateTarget{(uint32_t)(repeat_count & 0xFFFFFFFFULL)}); target_buf.append_tail(GateTarget{(uint32_t)(repeat_count >> 32)}); blocks.push_back(body); auto targets = target_buf.commit_tail(); operations.push_back(CircuitInstruction(GateType::REPEAT, {}, targets, tag_buf.take_copy(tag))); } const Circuit Circuit::aliased_noiseless_circuit() const { // HACK: result has pointers into `circuit`! Circuit result; for (const auto &op : operations) { auto flags = GATE_DATA[op.gate_type].flags; if (flags & GATE_PRODUCES_RESULTS) { if (op.gate_type == GateType::HERALDED_ERASE || op.gate_type == GateType::HERALDED_PAULI_CHANNEL_1) { // Replace heralded errors with fixed MPAD. result.target_buf.ensure_available(op.targets.size()); auto &tail = result.target_buf.tail; tail.ptr_end = tail.ptr_start + op.targets.size(); memset(tail.ptr_start, 0, (tail.ptr_end - tail.ptr_start) * sizeof(GateTarget)); result.operations.push_back( CircuitInstruction(GateType::MPAD, {}, result.target_buf.commit_tail(), op.tag)); result.try_fuse_last_two_ops(); } else { // Drop result flip probability. result.operations.push_back(CircuitInstruction(op.gate_type, {}, op.targets, op.tag)); } } else if (!(flags & GATE_IS_NOISY)) { // Keep noiseless operations. result.operations.push_back(op); } // Because some operations are rewritten into others, and some become fusable due to // arguments getting removed, just keep trying to fuse things. result.try_fuse_last_two_ops(); } for (const auto &block : blocks) { result.blocks.push_back(block.aliased_noiseless_circuit()); } return result; } Circuit Circuit::without_tags() const { Circuit result; for (CircuitInstruction inst : operations) { if (inst.gate_type == GateType::REPEAT) { result.append_repeat_block(inst.repeat_block_rep_count(), inst.repeat_block_body(*this).without_tags(), ""); } else { inst.tag = ""; result.safe_append(inst); } } return result; } Circuit Circuit::without_noise() const { Circuit result; for (const auto &op : operations) { auto flags = GATE_DATA[op.gate_type].flags; if (flags & GATE_PRODUCES_RESULTS) { if (op.gate_type == GateType::HERALDED_ERASE || op.gate_type == GateType::HERALDED_PAULI_CHANNEL_1) { // Replace heralded errors with fixed MPAD. result.target_buf.ensure_available(op.targets.size()); auto &tail = result.target_buf.tail; tail.ptr_end = tail.ptr_start + op.targets.size(); memset(tail.ptr_start, 0, (tail.ptr_end - tail.ptr_start) * sizeof(GateTarget)); auto tag = result.tag_buf.take_copy(op.tag); result.operations.push_back( CircuitInstruction(GateType::MPAD, {}, result.target_buf.commit_tail(), tag)); } else { // Drop result flip probabilities. auto targets = result.target_buf.take_copy(op.targets); auto tag = result.tag_buf.take_copy(op.tag); result.safe_append(CircuitInstruction(op.gate_type, {}, targets, tag)); } } else if (op.gate_type == GateType::REPEAT) { auto args = result.arg_buf.take_copy(op.args); auto targets = result.target_buf.take_copy(op.targets); auto tag = result.tag_buf.take_copy(op.tag); result.operations.push_back({op.gate_type, args, targets, tag}); } else if (!(flags & GATE_IS_NOISY)) { // Keep noiseless operations. auto args = result.arg_buf.take_copy(op.args); auto targets = result.target_buf.take_copy(op.targets); auto tag = result.tag_buf.take_copy(op.tag); result.safe_append(CircuitInstruction(op.gate_type, args, targets, tag)); } // Because some operations are rewritten into others, and some become fusable due to // arguments getting removed, just keep trying to fuse things. result.try_fuse_last_two_ops(); } for (const auto &block : blocks) { result.blocks.push_back(block.without_noise()); } return result; } void flattened_helper( const Circuit &body, std::vector &cur_coordinate_shift, std::vector &coord_buffer, Circuit &out) { for (const auto &op : body.operations) { GateType id = op.gate_type; if (id == GateType::SHIFT_COORDS) { while (cur_coordinate_shift.size() < op.args.size()) { cur_coordinate_shift.push_back(0); } for (size_t k = 0; k < op.args.size(); k++) { cur_coordinate_shift[k] += op.args[k]; } } else if (id == GateType::REPEAT) { uint64_t reps = op.repeat_block_rep_count(); const auto &loop_body = op.repeat_block_body(body); for (uint64_t k = 0; k < reps; k++) { flattened_helper(loop_body, cur_coordinate_shift, coord_buffer, out); } } else { coord_buffer.clear(); coord_buffer.insert(coord_buffer.end(), op.args.begin(), op.args.end()); if (id == GateType::QUBIT_COORDS || id == GateType::DETECTOR) { for (size_t k = 0; k < coord_buffer.size() && k < cur_coordinate_shift.size(); k++) { coord_buffer[k] += cur_coordinate_shift[k]; } } out.safe_append(CircuitInstruction(op.gate_type, coord_buffer, op.targets, op.tag)); } } } Circuit Circuit::flattened() const { Circuit result; std::vector shift; std::vector coord_buffer; flattened_helper(*this, shift, coord_buffer, result); return result; } Circuit Circuit::inverse(bool allow_weak_inverse) const { Circuit result; result.operations.reserve(operations.size()); result.target_buf.ensure_available(target_buf.total_allocated()); result.arg_buf.ensure_available(arg_buf.total_allocated()); result.tag_buf.ensure_available(tag_buf.total_allocated()); size_t skip_reversing = 0; std::vector args_buf; for (size_t k = 0; k < operations.size(); k++) { const auto &op = operations[k]; if (op.gate_type == GateType::REPEAT) { const auto &block = op.repeat_block_body(*this); uint64_t reps = op.repeat_block_rep_count(); result.append_repeat_block(reps, block.inverse(allow_weak_inverse), op.tag); continue; } SpanRef args = op.args; const auto &gate_data = GATE_DATA[op.gate_type]; auto flags = gate_data.flags; if (flags & GATE_IS_UNITARY) { // Unitary gates always have an inverse. } else if (op.gate_type == GateType::TICK) { // Ticks are self-inverse. } else if (flags & GATE_IS_NOISY) { // Noise isn't invertible, but it is weakly invertible. // ELSE_CORRELATED_ERROR isn't implemented due to complex order dependencies. if (!allow_weak_inverse || op.gate_type == GateType::ELSE_CORRELATED_ERROR) { throw std::invalid_argument( "The circuit has no well-defined inverse because it contains noise.\n" "For example it contains a '" + op.str() + "' instruction."); } } else if (flags & (GATE_IS_RESET | GATE_PRODUCES_RESULTS)) { // Dissipative operations aren't invertible, but they are weakly invertible. if (!allow_weak_inverse) { throw std::invalid_argument( "The circuit has no well-defined inverse because it contains resets or measurements.\n" "For example it contains a '" + op.str() + "' instruction."); } } else if (op.gate_type == GateType::QUBIT_COORDS) { // Qubit coordinate headers are kept at the beginning. if (k > skip_reversing) { throw std::invalid_argument( "Inverting QUBIT_COORDS is not implemented except at the start of the circuit."); } skip_reversing++; } else if (op.gate_type == GateType::SHIFT_COORDS) { // Coordinate shifts reverse. args_buf.clear(); for (const auto &a : op.args) { args_buf.push_back(-a); } args = args_buf; } else if (op.gate_type == GateType::DETECTOR || op.gate_type == GateType::OBSERVABLE_INCLUDE) { if (allow_weak_inverse) { // If strong inverse for these gets implemented, they should be included in the weak inverse. // But for now it's sufficient to just drop them for the weak inverse. continue; } throw std::invalid_argument("Inverse not implemented: " + op.str()); } else { throw std::invalid_argument("Inverse not implemented: " + op.str()); } // Add inverse operation to inverse circuit. result.safe_append_reversed_targets( CircuitInstruction(gate_data.best_candidate_inverse_id, args, op.targets, op.tag), gate_data.flags & GATE_TARGETS_PAIRS); } // Put the qubit coordinates in the original order. std::reverse(result.operations.begin() + skip_reversing, result.operations.end()); return result; } void stim::vec_pad_add_mul(std::vector &target, SpanRef offset, uint64_t mul) { while (target.size() < offset.size()) { target.push_back(0); } for (size_t k = 0; k < offset.size(); k++) { target[k] += offset[k] * mul; } } void get_final_qubit_coords_helper( const Circuit &circuit, uint64_t repetitions, std::vector &out_coord_shift, std::map> &out_qubit_coords) { auto initial_shift = out_coord_shift; std::map> new_qubit_coords; for (const auto &op : circuit.operations) { if (op.gate_type == GateType::REPEAT) { const auto &block = circuit.blocks[op.targets[0].data]; uint64_t block_repeats = op.repeat_block_rep_count(); get_final_qubit_coords_helper(block, block_repeats, out_coord_shift, new_qubit_coords); } else if (op.gate_type == GateType::SHIFT_COORDS) { vec_pad_add_mul(out_coord_shift, op.args); } else if (op.gate_type == GateType::QUBIT_COORDS) { while (out_coord_shift.size() < op.args.size()) { out_coord_shift.push_back(0); } for (const auto &t : op.targets) { if (t.is_qubit_target()) { auto &vec = new_qubit_coords[t.qubit_value()]; for (size_t k = 0; k < op.args.size(); k++) { vec.push_back(op.args[k] + out_coord_shift[k]); } } } } } // Handle additional iterations by computing the total coordinate shift instead of iterating instructions. if (repetitions > 1 && out_coord_shift != initial_shift) { // Determine how much each coordinate shifts in each iteration. auto gain_per_iteration = out_coord_shift; for (size_t k = 0; k < initial_shift.size(); k++) { gain_per_iteration[k] -= initial_shift[k]; } // Shift in-loop qubit coordinates forward to the last iteration's values. for (auto &kv : new_qubit_coords) { auto &qc = kv.second; for (size_t k = 0; k < qc.size(); k++) { qc[k] += gain_per_iteration[k] * (repetitions - 1); } } // Advance the coordinate shifts to account for all iterations. vec_pad_add_mul(out_coord_shift, gain_per_iteration, repetitions - 1); } // Output updated values. for (const auto &kv : new_qubit_coords) { out_qubit_coords[kv.first] = kv.second; } } std::map> Circuit::get_final_qubit_coords() const { std::vector coord_shift; std::map> qubit_coords; get_final_qubit_coords_helper(*this, 1, coord_shift, qubit_coords); return qubit_coords; } std::vector Circuit::final_coord_shift() const { std::vector coord_shift; for (const auto &op : operations) { if (op.gate_type == GateType::SHIFT_COORDS) { vec_pad_add_mul(coord_shift, op.args); } else if (op.gate_type == GateType::REPEAT) { const auto &block = op.repeat_block_body(*this); uint64_t reps = op.repeat_block_rep_count(); vec_pad_add_mul(coord_shift, block.final_coord_shift(), reps); } } return coord_shift; } void get_detector_coordinates_helper( const Circuit &circuit, const std::set &included_detector_indices, std::set::const_iterator &iter_desired_detector_index, const std::vector &initial_coord_shift, uint64_t &next_detector_index, std::map> &out) { if (iter_desired_detector_index == included_detector_indices.end()) { return; } std::vector coord_shift = initial_coord_shift; for (const auto &op : circuit.operations) { if (op.gate_type == GateType::SHIFT_COORDS) { vec_pad_add_mul(coord_shift, op.args); } else if (op.gate_type == GateType::REPEAT) { const auto &block = op.repeat_block_body(circuit); auto block_shift = block.final_coord_shift(); uint64_t per = block.count_detectors(); uint64_t reps = op.repeat_block_rep_count(); uint64_t used_reps = 0; while (used_reps < reps) { uint64_t skip = per == 0 ? reps : std::min(reps, (*iter_desired_detector_index - next_detector_index) / per); used_reps += skip; next_detector_index += per * skip; vec_pad_add_mul(coord_shift, block_shift, skip); if (used_reps < reps) { get_detector_coordinates_helper( block, included_detector_indices, iter_desired_detector_index, coord_shift, next_detector_index, out); used_reps += 1; vec_pad_add_mul(coord_shift, block_shift); if (iter_desired_detector_index == included_detector_indices.end()) { return; } } } } else if (op.gate_type == GateType::DETECTOR) { if (next_detector_index == *iter_desired_detector_index) { std::vector det_coords; for (size_t k = 0; k < op.args.size(); k++) { det_coords.push_back(op.args[k]); if (k < coord_shift.size()) { det_coords[k] += coord_shift[k]; } } out[next_detector_index] = det_coords; iter_desired_detector_index++; if (iter_desired_detector_index == included_detector_indices.end()) { return; } } next_detector_index++; } } } std::vector Circuit::coords_of_detector(uint64_t detector_index) const { return get_detector_coordinates({detector_index})[detector_index]; } std::map> Circuit::get_detector_coordinates( const std::set &included_detector_indices) const { std::map> out; uint64_t next_coordinate_index = 0; std::set::const_iterator iter = included_detector_indices.begin(); get_detector_coordinates_helper(*this, included_detector_indices, iter, {}, next_coordinate_index, out); if (iter != included_detector_indices.end()) { std::stringstream msg; msg << "Detector index " << *iter << " is too big. The circuit has "; msg << count_detectors() << " detectors)"; throw std::invalid_argument(msg.str()); } return out; } std::string Circuit::describe_instruction_location(size_t instruction_offset) const { std::stringstream out; out << " at instruction #" << (instruction_offset + 1); const auto &op = operations[instruction_offset]; if (op.gate_type == GateType::REPEAT) { out << " [which is a REPEAT " << op.repeat_block_rep_count() << " block]"; } else { out << " [which is " << op << "]"; } return out.str(); } ================================================ FILE: src/stim/circuit/circuit.h ================================================ /* * 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. */ #ifndef _STIM_CIRCUIT_CIRCUIT_H #define _STIM_CIRCUIT_CIRCUIT_H #include #include #include #include #include #include #include #include #include #include "stim/circuit/circuit_instruction.h" #include "stim/circuit/gate_target.h" #include "stim/gates/gates.h" #include "stim/mem/monotonic_buffer.h" #include "stim/mem/span_ref.h" namespace stim { uint64_t add_saturate(uint64_t a, uint64_t b); uint64_t mul_saturate(uint64_t a, uint64_t b); /// A description of a quantum computation. struct Circuit { /// Backing data stores for variable-sized target data referenced by operations. MonotonicBuffer target_buf; MonotonicBuffer arg_buf; MonotonicBuffer tag_buf; /// Operations in the circuit, from earliest to latest. std::vector operations; std::vector blocks; // Returns one more than the largest `k` from any qubit target `k` or `!k` or `{X,Y,Z}k`. size_t count_qubits() const; // Returns the number of measurement bits produced by the circuit. uint64_t count_measurements() const; // Returns the number of detection event bits produced by the circuit. uint64_t count_detectors() const; // Returns one more than the largest `k` from any `OBSERVABLE_INCLUDE(k)` instruction. uint64_t count_observables() const; // Returns the number of ticks performed when running the circuit. uint64_t count_ticks() const; // Returns the largest `k` from any `rec[-k]` target. size_t max_lookback() const; // Returns one more than the largest `k` from any `sweep[k]` target. size_t count_sweep_bits() const; CircuitStats compute_stats() const; /// Constructs an empty circuit. Circuit(); /// Copy constructor. Circuit(const Circuit &circuit); /// Move constructor. Circuit(Circuit &&circuit) noexcept; /// Copy assignment. Circuit &operator=(const Circuit &circuit); /// Move assignment. Circuit &operator=(Circuit &&circuit) noexcept; /// Parse constructor. Creates a circuit from text with operations like "H 0 \n CNOT 0 1 \n M 0 1". /// /// Note: operations are automatically fused. explicit Circuit(std::string_view text); /// Parses a circuit from a file containing operations. /// /// Note: operations are automatically fused. static Circuit from_file(FILE *file); /// Grows the circuit using operations from a file. /// /// Note: operations are automatically fused. /// /// Args: /// file: The opened file to read from. /// stop_asap: When set to true, the reading process stops after the next operation is read. This is used for /// interactive (repl) mode, where measurements should produce results immediately instead of only after the /// circuit is entirely specified. *This has significantly worse performance. It prevents measurement /// batching.* void append_from_file(FILE *file, bool stop_asap = false); /// Grows the circuit using operations from a string. /// /// Note: operations are automatically fused. void append_from_text(std::string_view text); Circuit operator+(const Circuit &other) const; Circuit operator*(uint64_t repetitions) const; Circuit &operator+=(const Circuit &other); Circuit &operator*=(uint64_t repetitions); /// Safely adds an operation at the end of the circuit, copying its data into the circuit's jagged data as needed. void safe_append(CircuitInstruction operation, bool block_fusion = false); /// Safely adds an operation at the end of the circuit, copying its data into the circuit's jagged data as needed. void safe_append_ua( std::string_view gate_name, const std::vector &targets, double singleton_arg, std::string_view tag = ""); /// Safely adds an operation at the end of the circuit, copying its data into the circuit's jagged data as needed. void safe_append_u( std::string_view gate_name, const std::vector &targets, const std::vector &args = {}, std::string_view tag = ""); /// Safely copies a repeat block to the end of the circuit. void append_repeat_block(uint64_t repeat_count, const Circuit &body, std::string_view tag); /// Safely moves a repeat block to the end of the circuit. void append_repeat_block(uint64_t repeat_count, Circuit &&body, std::string_view tag); void safe_insert(size_t index, const CircuitInstruction &instruction); void safe_insert_repeat_block(size_t index, uint64_t repeat_count, const Circuit &block, std::string_view tag); void safe_insert(size_t index, const Circuit &circuit); /// Appends the given gate, but with targets reversed. void safe_append_reversed_targets(CircuitInstruction instruction, bool reverse_in_pairs); /// Resets the circuit back to an empty circuit. void clear(); /// Returns a text description of the circuit. std::string str() const; /// Equality. bool operator==(const Circuit &other) const; /// Inequality. bool operator!=(const Circuit &other) const; /// Approximate equality. bool approx_equals(const Circuit &other, double atol) const; /// Gets a python-style slice of the circuit's instructions. Circuit py_get_slice(int64_t start, int64_t step, int64_t slice_length) const; /// Returns a noiseless version of the given circuit. The result must live for less time than the given circuit. /// /// CAUTION: for performance, the returned circuit contains pointers into the given circuit! /// The result's lifetime must be shorter than the given circuit's lifetime! const Circuit aliased_noiseless_circuit() const; /// Returns a copy of the circuit with all noise processes removed. Circuit without_noise() const; /// Returns a copy of the circuit with all tags removed. Circuit without_tags() const; /// Returns an equivalent circuit without REPEAT or SHIFT_COORDS instructions. Circuit flattened() const; /// Returns a circuit that implements the inverse Clifford operation. /// /// Args: /// allow_weak_inverse: When this is set to true, the inverse of the circuit doesn't need /// to be exact. In particular, noise and measurement becomes self-inverse. Examples: /// - The weak inverse of MX is MX. /// - The weak inverse of MRX is MRX. /// - The weak inverse of RX is MRX. /// - The weak inverse of X_ERROR(0.1) is X_ERROR(0.1). /// - The weak inverse of DETECTOR is [discard the operation]. /// - The weak inverse of OBSERVABLE_INCLUDE is [discard the operation]. /// /// Returns: /// The inverted circuit. Circuit inverse(bool allow_weak_inverse = false) const; /// Helper method for executing the circuit, e.g. repeating REPEAT blocks. template void for_each_operation(const CALLBACK &callback) const { for (const auto &op : operations) { if (op.gate_type == GateType::REPEAT) { uint64_t repeats = op.repeat_block_rep_count(); const auto &block = op.repeat_block_body(*this); for (uint64_t k = 0; k < repeats; k++) { block.for_each_operation(callback); } } else { callback(op); } } } /// Helper method for reverse-executing the circuit, e.g. repeating REPEAT blocks. template void for_each_operation_reverse(const CALLBACK &callback) const { for (size_t p = operations.size(); p-- > 0;) { const auto &op = operations[p]; if (op.gate_type == GateType::REPEAT) { uint64_t repeats = op.repeat_block_rep_count(); const auto &block = op.repeat_block_body(*this); for (uint64_t k = 0; k < repeats; k++) { block.for_each_operation_reverse(callback); } } else { callback(op); } } } /// Helper method for counting measurements, detectors, etc. template uint64_t flat_count_operations(const COUNT &count) const { uint64_t n = 0; for (const auto &op : operations) { if (op.gate_type == GateType::REPEAT) { assert(op.targets.size() == 3); auto b = op.targets[0].data; assert(b < blocks.size()); auto sub = blocks[b].flat_count_operations(count); n = add_saturate(n, mul_saturate(sub, op.repeat_block_rep_count())); } else { n = add_saturate(n, count(op)); } } return n; } /// Helper method for finding the largest observable, etc. template uint64_t max_operation_property(const MAP &map) const { uint64_t n = 0; for (const auto &block : blocks) { n = std::max(n, block.max_operation_property(map)); } for (const auto &op : operations) { if (op.gate_type == GateType::REPEAT) { // Handled in block case. continue; } n = std::max(n, (uint64_t)map(op)); } return n; } /// Looks for QUBIT_COORDS instructions in the circuit, and returns a map from qubit index to qubit coordinate. /// /// This method efficiently handles REPEAT blocks. It finishes in time proportional to the size of the circuit /// file, not time proportional to the amount of data produced by the circuit. std::map> get_final_qubit_coords() const; /// Looks up the coordinate data of a detector. /// /// Args: /// detector_index: The index of the detector to get coordinate data for. /// Detectors are indexed by the order they appear in the circuit (accounting for repeat blocks). /// /// Returns: /// The coordinate data for the detector. /// If the detector has no coordinate data, an empty vector is returned. /// /// Throws: /// std::invalid_argument: The detector index is greater than or equal to circuit.count_detectors(). std::vector coords_of_detector(uint64_t detector_index) const; /// Looks up the coordinate data of a given set of detectors. /// /// Args: /// included_detector_indices: An ordered set of the indices of detectors to get coordinate data for. /// /// Returns: /// A map from detector index to coordinate data. /// Every index from included_detector_indices will be in this map as a key. /// Detectors with no coordinate data are mapped to an empty vector. /// /// Throws: /// std::invalid_argument: A detector index is greater than or equal to circuit.count_detectors(). std::map> get_detector_coordinates( const std::set &included_detector_indices) const; /// Returns the total coordinate shift accumulated over the entire circuit, accounting for REPEAT blocks. std::vector final_coord_shift() const; /// Helper method for building up human readable descriptions of circuit locations. std::string describe_instruction_location(size_t instruction_offset) const; void try_fuse_last_two_ops(); void try_fuse_after(size_t index); }; void vec_pad_add_mul(std::vector &target, SpanRef offset, uint64_t mul = 1); template inline void read_past_within_line_whitespace(int &c, SOURCE read_char) { while (c == ' ' || c == '\t') { c = read_char(); } } template bool read_until_next_line_arg(int &c, SOURCE read_char, bool space_required = true) { if (c == '*') { return true; } if (space_required) { if (c != ' ' && c != '#' && c != '\t' && c != '\n' && c != '\r' && c != '{' && c != EOF) { throw std::invalid_argument("Targets must be separated by spacing."); } } while (c == ' ' || c == '\t' || c == '\r') { c = read_char(); } if (c == '#') { do { c = read_char(); } while (c != '\n' && c != EOF); } return c != '\n' && c != '{' && c != EOF; } template void read_past_dead_space_between_commands(int &c, SOURCE read_char) { while (true) { while (isspace(c)) { c = read_char(); } if (c == EOF) { break; } if (c != '#') { break; } while (c != '\n' && c != EOF) { c = read_char(); } } } inline bool is_double_char(int c) { return (c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-'; } template double read_normal_double(int &c, SOURCE read_char) { char buf[64]; size_t n = 0; while (n < sizeof(buf) - 1 && is_double_char(c)) { buf[n] = (char)c; c = read_char(); n++; } buf[n] = '\0'; char *end; double result = strtod(buf, &end); if (end != buf + n || std::isinf(result) || std::isnan(result)) { throw std::invalid_argument("Not a real number: " + std::string(buf)); } return result; } template void read_tag(int &c, std::string_view name, SOURCE read_char, MonotonicBuffer &out) { if (c != '[') { return; } c = read_char(); while (c != ']') { if (c == '\r' || c == '\n') { std::stringstream ss; ss << "A tag wasn't closed with ']' before the end of the line.\n"; ss << "Hit a "; if (c == '\r') { ss << "carriage return character (0x0D)"; } else { ss << "line feed character (0x0A)"; } ss << " while trying to parse the tag of "; if (name.empty()) { ss << "an instruction.\n"; } else { ss << "a '" << name << "' instruction.\n"; } ss << "In tags, use the escape sequence '\\r' for carriage returns and '\\n' for line feeds."; throw std::invalid_argument(ss.str()); } else if (c == '\\') { c = read_char(); switch (c) { case 'n': out.append_tail('\n'); break; case 'r': out.append_tail('\r'); break; case 'B': out.append_tail('\\'); break; case 'C': out.append_tail(']'); break; default: std::stringstream ss; ss << "Unrecognized escape sequence '\\" << c << "'."; ss << "\nKnown escape sequences are:"; ss << "\n \\n: 0x0A (line feed)"; ss << "\n \\r: 0x0D (carriage return)"; ss << "\n \\B: 0x5C (backslash '\\')"; ss << "\n \\C: 0x5D (closing square bracket ']')"; throw std::invalid_argument(ss.str()); } } else { out.append_tail(c); } c = read_char(); } c = read_char(); } template void read_parens_arguments(int &c, std::string_view name, SOURCE read_char, MonotonicBuffer &out) { if (c != '(') { return; } c = read_char(); read_past_within_line_whitespace(c, read_char); while (true) { out.append_tail(read_normal_double(c, read_char)); read_past_within_line_whitespace(c, read_char); if (c != ',') { break; } c = read_char(); read_past_within_line_whitespace(c, read_char); } read_past_within_line_whitespace(c, read_char); if (c != ')') { throw std::invalid_argument("Parens arguments for '" + std::string(name) + "' didn't end with a ')'."); } c = read_char(); } std::ostream &operator<<(std::ostream &out, const Circuit &c); void print_circuit(std::ostream &out, const Circuit &c, size_t indentation); } // namespace stim #endif ================================================ FILE: src/stim/circuit/circuit.perf.cc ================================================ // 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. #include "stim/circuit/circuit.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(circuit_parse) { Circuit c; benchmark_go([&]() { c = Circuit(R"input( H 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 CNOT 4 5 6 7 M 1 2 3 4 5 6 7 8 9 10 11 )input"); }).goal_nanos(950); if (c.count_qubits() == 0) { std::cerr << "impossible"; } } BENCHMARK(circuit_parse_sparse) { Circuit c; for (auto k = 0; k < 1000; k++) { c.safe_append_u("H", {0}); c.safe_append_u("CNOT", {1, 2}); c.safe_append_u("M", {0}); } auto text = c.str(); benchmark_go([&]() { c = Circuit(text.data()); }).goal_micros(150); if (c.count_qubits() == 0) { std::cerr << "impossible"; } } ================================================ FILE: src/stim/circuit/circuit.pybind.cc ================================================ // 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. #include "stim/circuit/circuit.pybind.h" #include #include "stim/circuit/circuit_instruction.pybind.h" #include "stim/circuit/circuit_repeat_block.pybind.h" #include "stim/circuit/gate_target.pybind.h" #include "stim/cmd/command_diagram.pybind.h" #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_color_code.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/io/raii_file.h" #include "stim/py/base.pybind.h" #include "stim/py/compiled_detector_sampler.pybind.h" #include "stim/py/compiled_measurement_sampler.pybind.h" #include "stim/py/numpy.pybind.h" #include "stim/search/search.h" #include "stim/simulators/error_analyzer.h" #include "stim/simulators/error_matcher.h" #include "stim/simulators/measurements_to_detection_events.pybind.h" #include "stim/simulators/tableau_simulator.h" #include "stim/stabilizers/flow.h" using namespace stim; using namespace stim_pybind; std::string circuit_repr(const Circuit &self) { if (self.operations.empty()) { return "stim.Circuit()"; } std::stringstream ss; ss << "stim.Circuit('''\n"; print_circuit(ss, self, 4); ss << "\n''')"; return ss.str(); } std::vector circuit_shortest_graphlike_error( const Circuit &self, bool ignore_ungraphlike_errors, bool reduce_to_representative) { DetectorErrorModel dem = ErrorAnalyzer::circuit_to_detector_error_model(self, !ignore_ungraphlike_errors, true, false, 1, false, false); DetectorErrorModel filter = shortest_graphlike_undetectable_logical_error(dem, ignore_ungraphlike_errors); return ErrorMatcher::explain_errors_from_circuit(self, &filter, reduce_to_representative); } std::vector py_find_undetectable_logical_error( const Circuit &self, size_t dont_explore_detection_event_sets_with_size_above, size_t dont_explore_edges_with_degree_above, bool dont_explore_edges_increasing_symptom_degree, bool reduce_to_representative) { DetectorErrorModel dem = ErrorAnalyzer::circuit_to_detector_error_model(self, false, true, false, 1, false, false); DetectorErrorModel filter = stim::find_undetectable_logical_error( dem, dont_explore_detection_event_sets_with_size_above, dont_explore_edges_with_degree_above, dont_explore_edges_increasing_symptom_degree); return ErrorMatcher::explain_errors_from_circuit(self, &filter, reduce_to_representative); } std::string py_shortest_error_sat_problem(const Circuit &self, std::string_view format) { DetectorErrorModel dem = ErrorAnalyzer::circuit_to_detector_error_model(self, false, true, false, 1, false, false); return stim::shortest_error_sat_problem(dem, format); } std::string py_likeliest_error_sat_problem(const Circuit &self, int quantization, std::string_view format) { DetectorErrorModel dem = ErrorAnalyzer::circuit_to_detector_error_model(self, false, true, false, 1, false, false); return stim::likeliest_error_sat_problem(dem, quantization, format); } pybind11::object circuit_get_item(const Circuit &self, const pybind11::object &index_or_slice) { pybind11::ssize_t index, step, slice_length; if (normalize_index_or_slice(index_or_slice, self.operations.size(), &index, &step, &slice_length)) { return pybind11::cast(self.py_get_slice(index, step, slice_length)); } auto &op = self.operations[index]; if (op.gate_type == GateType::REPEAT) { return pybind11::cast( CircuitRepeatBlock{op.repeat_block_rep_count(), op.repeat_block_body(self), pybind11::str(op.tag)}); } std::vector targets; for (const auto &e : op.targets) { targets.push_back(GateTarget(e)); } std::vector args; for (const auto &e : op.args) { args.push_back(e); } return pybind11::cast(PyCircuitInstruction(op.gate_type, targets, args, op.tag)); } pybind11::object circuit_pop(Circuit &self, pybind11::ssize_t index) { if (index < -(pybind11::ssize_t)self.operations.size() || index >= (pybind11::ssize_t)self.operations.size()) { std::stringstream ss; ss << "not -len(circuit) < index=" << index << " < len(circuit)=" << self.operations.size(); throw std::out_of_range(ss.str()); } if (index < 0) { index += self.operations.size(); } pybind11::object result = circuit_get_item(self, pybind11::cast(index)); self.operations.erase(self.operations.begin() + (size_t)index); return result; } void circuit_insert(Circuit &self, pybind11::ssize_t &index, pybind11::object &operation) { if (index < 0) { index += self.operations.size(); } if (index < 0 || (uint64_t)index > self.operations.size()) { std::stringstream ss; ss << "Index is out of range. Need -len(circuit) <= index <= len(circuit)."; ss << "\n index=" << index; ss << "\n len(circuit)=" << self.operations.size(); throw std::invalid_argument(ss.str()); } if (pybind11::isinstance(operation)) { const PyCircuitInstruction &v = pybind11::cast(operation); self.safe_insert(index, v.as_operation_ref()); } else if (pybind11::isinstance(operation)) { const CircuitRepeatBlock &v = pybind11::cast(operation); self.safe_insert_repeat_block(index, v.repeat_count, v.body, pybind11::cast(v.tag)); } else if (pybind11::isinstance(operation)) { const Circuit &v = pybind11::cast(operation); self.safe_insert(index, v); } else { std::stringstream ss; ss << "Don't know how to insert an object of type "; ss << pybind11::str(pybind11::module_::import("builtins").attr("type")(operation)); ss << "\nExpected a stim.CircuitInstruction, stim.CircuitRepeatBlock, or stim.Circuit."; throw std::invalid_argument(ss.str()); } } void handle_to_gate_targets(const pybind11::handle &obj, std::vector &out, bool can_iterate) { try { FlexPauliString ps = pybind11::cast(obj); bool first = true; ps.value.ref().for_each_active_pauli([&](size_t q) { if (!first) { out.push_back(GateTarget::combiner().data); } first = false; out.push_back(GateTarget::pauli_xz(q, ps.value.xs[q], ps.value.zs[q]).data); }); if (first) { throw std::invalid_argument("Don't know how to target an empty stim.PauliString"); } return; } catch (const pybind11::cast_error &ex) { } try { std::string_view text = pybind11::cast(obj); out.push_back(GateTarget::from_target_str(text).data); return; } catch (const pybind11::cast_error &ex) { } try { out.push_back(pybind11::cast(obj).data); return; } catch (const pybind11::cast_error &ex) { } try { out.push_back(GateTarget{pybind11::cast(obj)}.data); return; } catch (const pybind11::cast_error &ex) { } if (can_iterate) { pybind11::module collections = pybind11::module::import("collections.abc"); pybind11::object iterable_type = collections.attr("Iterable"); if (pybind11::isinstance(obj, iterable_type)) { for (const auto &t : obj) { handle_to_gate_targets(t, out, false); } return; } } std::stringstream ss; ss << "Don't know how to target the object `"; ss << pybind11::cast(pybind11::repr(obj)); ss << "`."; throw std::invalid_argument(ss.str()); } void circuit_append( Circuit &self, const pybind11::object &obj, const pybind11::object &targets, const pybind11::object &arg, std::string_view tag, bool backwards_compat) { // Extract single target or list of targets. std::vector raw_targets; handle_to_gate_targets(targets, raw_targets, true); if (pybind11::isinstance(obj)) { std::string_view gate_name = pybind11::cast(obj); // Maintain backwards compatibility to when there was always exactly one argument. pybind11::object used_arg; if (!arg.is_none()) { used_arg = arg; } else if (backwards_compat && GATE_DATA.at(gate_name).arg_count == 1) { used_arg = pybind11::make_tuple(0.0); } else { used_arg = pybind11::make_tuple(); } // Extract single argument or list of arguments. try { auto d = pybind11::cast(used_arg); self.safe_append_ua(gate_name, raw_targets, d, tag); return; } catch (const pybind11::cast_error &) { } try { auto args = pybind11::cast>(used_arg); self.safe_append_u(gate_name, raw_targets, args, tag); return; } catch (const pybind11::cast_error &) { } throw std::invalid_argument("Arg must be a double or sequence of doubles."); } else if (pybind11::isinstance(obj)) { if (!raw_targets.empty() || !arg.is_none() || !tag.empty()) { throw std::invalid_argument( "Can't specify `targets` or `arg` or `tag` when appending a stim.CircuitInstruction."); } const PyCircuitInstruction &instruction = pybind11::cast(obj); self.safe_append( CircuitInstruction{ instruction.gate_type, instruction.gate_args, instruction.targets, pybind11::cast(instruction.tag), }); } else if (pybind11::isinstance(obj)) { if (!raw_targets.empty() || !arg.is_none() || !tag.empty()) { throw std::invalid_argument( "Can't specify `targets` or `arg` or `tag` when appending a stim.CircuitRepeatBlock."); } const CircuitRepeatBlock &block = pybind11::cast(obj); self.append_repeat_block(block.repeat_count, block.body, pybind11::cast(block.tag)); } else if (pybind11::isinstance(obj)) { self += pybind11::cast(obj); } else { throw std::invalid_argument( "First argument of append_operation must be a str (a gate name), " "a stim.CircuitInstruction, " "or a stim.CircuitRepeatBlock"); } } void circuit_append_backwards_compat( Circuit &self, const pybind11::object &obj, const pybind11::object &targets, const pybind11::object &arg, std::string_view tag) { circuit_append(self, obj, targets, arg, tag, true); } void circuit_append_strict( Circuit &self, const pybind11::object &obj, const pybind11::object &targets, const pybind11::object &arg, std::string_view tag) { circuit_append(self, obj, targets, arg, tag, false); } pybind11::class_ stim_pybind::pybind_circuit(pybind11::module &m) { auto c = pybind11::class_( m, "Circuit", clean_doc_string(R"DOC( A mutable stabilizer circuit. The stim.Circuit class is arguably the most important object in the entire library. It is the interface through which you explain a noisy quantum computation to Stim, in order to do fast bulk sampling or fast error analysis. For example, suppose you want to use a matching-based decoder on a new quantum error correction construction. Stim can help you do this but the very first step is to create a circuit implementing the construction. Once you have the circuit you can then use methods like stim.Circuit.detector_error_model() to create an object that can be used to configure the decoder, or like stim.Circuit.compile_detector_sampler() to produce problems for the decoder to solve, or like stim.Circuit.shortest_graphlike_error() to check for mistakes in the implementation of the code. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append("X", 0) >>> c.append("M", 0) >>> c.compile_sampler().sample(shots=1) array([[ True]]) >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''').compile_detector_sampler().sample(shots=1) array([[False]]) )DOC") .data()); return c; } uint64_t obj_to_abs_detector_id(const pybind11::handle &obj, bool fail) { try { return obj.cast(); } catch (const pybind11::cast_error &) { } try { ExposedDemTarget t = obj.cast(); if (t.is_relative_detector_id()) { return t.data; } } catch (const pybind11::cast_error &) { } if (!fail) { return UINT64_MAX; } std::stringstream ss; ss << "Expected a detector id but didn't get a stim.DemTarget or a uint64_t."; ss << " Got " << pybind11::repr(obj); throw std::invalid_argument(ss.str()); } std::set obj_to_abs_detector_id_set( const pybind11::object &obj, const std::function &get_num_detectors) { std::set filter; if (obj.is_none()) { size_t n = get_num_detectors(); for (size_t k = 0; k < n; k++) { filter.insert(k); } } else { uint64_t single = obj_to_abs_detector_id(obj, false); if (single != UINT64_MAX) { filter.insert(single); } else { for (const auto &e : obj) { filter.insert(obj_to_abs_detector_id(e, true)); } } } return filter; } void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ &c) { c.def( pybind11::init([](std::string_view stim_program_text) { Circuit self; self.append_from_text(stim_program_text); return self; }), pybind11::arg("stim_program_text") = "", clean_doc_string(R"DOC( Creates a stim.Circuit. Args: stim_program_text: Defaults to empty. Describes operations to append into the circuit. Examples: >>> import stim >>> empty = stim.Circuit() >>> not_empty = stim.Circuit(''' ... X 0 ... CNOT 0 1 ... M 1 ... ''') )DOC") .data()); c.def_property_readonly( "num_measurements", &Circuit::count_measurements, clean_doc_string(R"DOC( Counts the number of bits produced when sampling the circuit's measurements. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... REPEAT 100 { ... M 0 1 ... } ... ''') >>> c.num_measurements 201 )DOC") .data()); c.def_property_readonly( "num_detectors", &Circuit::count_detectors, clean_doc_string(R"DOC( Counts the number of bits produced when sampling the circuit's detectors. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... DETECTOR rec[-1] ... REPEAT 100 { ... M 0 1 2 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... } ... ''') >>> c.num_detectors 201 )DOC") .data()); c.def_property_readonly( "num_ticks", &Circuit::count_ticks, clean_doc_string(R"DOC( Counts the number of TICK instructions executed when running the circuit. TICKs in loops are counted once per iteration. Returns: The number of ticks executed by the circuit. Examples: >>> import stim >>> stim.Circuit().num_ticks 0 >>> stim.Circuit(''' ... TICK ... ''').num_ticks 1 >>> stim.Circuit(''' ... H 0 ... TICK ... CX 0 1 ... TICK ... ''').num_ticks 2 >>> stim.Circuit(''' ... H 0 ... TICK ... REPEAT 100 { ... CX 0 1 ... TICK ... } ... ''').num_ticks 101 )DOC") .data()); c.def_property_readonly( "num_observables", &Circuit::count_observables, clean_doc_string(R"DOC( Counts the number of logical observables defined by the circuit. This is one more than the largest index that appears as an argument to an OBSERVABLE_INCLUDE instruction. Examples: >>> import stim >>> c = stim.Circuit(''' ... M 0 ... OBSERVABLE_INCLUDE(2) rec[-1] ... OBSERVABLE_INCLUDE(5) rec[-1] ... ''') >>> c.num_observables 6 )DOC") .data()); c.def_property_readonly( "num_qubits", &Circuit::count_qubits, clean_doc_string(R"DOC( Counts the number of qubits used when simulating the circuit. This is always one more than the largest qubit index used by the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... X 0 ... M 0 1 ... ''').num_qubits 2 >>> stim.Circuit(''' ... X 0 ... M 0 1 ... H 100 ... ''').num_qubits 101 )DOC") .data()); c.def_property_readonly( "num_sweep_bits", &Circuit::count_sweep_bits, clean_doc_string(R"DOC( Returns the number of sweep bits needed to completely configure the circuit. This is always one more than the largest sweep bit index used by the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... CX sweep[2] 0 ... ''').num_sweep_bits 3 >>> stim.Circuit(''' ... CZ sweep[5] 0 ... CX sweep[2] 0 ... ''').num_sweep_bits 6 )DOC") .data()); c.def( "compile_sampler", &py_init_compiled_sampler, pybind11::kw_only(), pybind11::arg("skip_reference_sample") = false, pybind11::arg("seed") = pybind11::none(), pybind11::arg("reference_sample") = pybind11::none(), clean_doc_string(R"DOC( @signature def compile_sampler(self, *, skip_reference_sample: bool = False, seed: Optional[int] = None, reference_sample: Optional[np.ndarray] = None) -> stim.CompiledMeasurementSampler: Returns an object that can quickly batch sample measurements from the circuit. Args: skip_reference_sample: Defaults to False. When set to True, the reference sample used by the sampler is initialized to all-zeroes instead of being collected from the circuit. This means that the results returned by the sampler are actually whether or not each measurement was *flipped*, instead of true measurement results. Forcing an all-zero reference sample is useful when you are only interested in error propagation and don't want to have to deal with the fact that some measurements want to be On when no errors occur. It is also useful when you know for sure that the all-zero result is actually a possible result from the circuit (under noiseless execution), meaning it is a valid reference sample as good as any other. Computing the reference sample is the most time consuming and memory intensive part of simulating the circuit, so promising that the simulator can safely skip that step is an effective optimization. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. reference_sample: The data to xor into the measurement flips produced by the frame simulator, in order to produce proper measurement results. This can either be specified as an `np.bool_` array or a bit packed `np.uint8` array (little endian). Under normal conditions, the reference sample should be a valid noiseless sample of the circuit, such as the one returned by `circuit.reference_sample()`. If this argument is not provided, the reference sample will be set to `circuit.reference_sample()`, unless `skip_reference_sample=True` is used, in which case it will be set to all-zeros. Raises: ValueError: skip_reference_sample is True and reference_sample is not None. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 2 ... M 0 1 2 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[False, False, True]]) )DOC") .data()); c.def( "reference_sample", [](const Circuit &self, bool bit_packed) { auto ref = TableauSimulator::reference_sample_circuit(self); simd_bits_range_ref reference_sample(ref.ptr_simd, ref.num_simd_words); size_t num_measure = self.count_measurements(); return simd_bits_to_numpy(reference_sample, num_measure, bit_packed); }, pybind11::kw_only(), pybind11::arg("bit_packed") = false, clean_doc_string(R"DOC( @signature def reference_sample(self, *, bit_packed: bool = False) -> np.ndarray: Samples the given circuit in a deterministic fashion. Discards all noisy operations, and biases all collapse events towards +Z instead of randomly +Z/-Z. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: A numpy array containing the reference sample. if bit_packed: shape == (math.ceil(num_measurements / 8),) dtype == np.uint8 else: shape == (num_measurements,) dtype == np.bool_ Examples: >>> import stim >>> stim.Circuit(''' ... X 1 ... M 0 1 ... ''').reference_sample() array([False, True]) )DOC") .data()); c.def( "reference_detector_and_observable_signs", [](const Circuit &self, bool bit_packed) -> pybind11::object { simd_bits ref = TableauSimulator::reference_sample_circuit(self); size_t num_det = self.count_detectors(); size_t num_obs = self.count_observables(); simd_bits dets(num_det); simd_bits obs(num_obs); size_t offset = 0; size_t k_det = 0; self.for_each_operation([&](CircuitInstruction inst) { if (inst.gate_type == GateType::DETECTOR || inst.gate_type == GateType::OBSERVABLE_INCLUDE) { bit_ref d = inst.gate_type == GateType::DETECTOR ? dets[k_det++] : obs[(int)inst.args[0]]; for (const auto &t : inst.targets) { if (t.is_measurement_record_target()) { d ^= ref[offset + t.value()]; } } } else { offset += inst.count_measurement_results(); } }); auto det_py = simd_bits_to_numpy(dets, num_det, bit_packed); auto obs_py = simd_bits_to_numpy(obs, num_obs, bit_packed); return pybind11::make_tuple(det_py, obs_py); }, pybind11::kw_only(), pybind11::arg("bit_packed") = false, clean_doc_string(R"DOC( @signature def reference_detector_and_observable_signs(self, *, bit_packed: bool = False) -> tuple[np.ndarray, np.ndarray]: Determines noiseless parities of the measurement sets of detectors/observables. BEWARE: the returned values are NOT the "expected value of the detector/observable". Stim consistently defines the value of a detector/observable as whether or not it flipped, so the expected value of a detector/observable is vacuously always 0 (not flipped). This method instead returns the "sign"; the expected parity of the measurement set declared by the detector/observable. The sign is the baseline used to determine if a flip occurred. A detector/observable's value is whether its sign disagrees with the measured parity of its measurement set. Note that this method doesn't account for sweep bits. It will effectively ignore instructions like `CX sweep[0] 0`. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: A (det, obs) tuple with numpy arrays containing the reference parities. if bit_packed: det.shape == (math.ceil(num_detectors / 8),) det.dtype == np.uint8 obs.shape == (math.ceil(num_observables / 8),) obs.dtype == np.uint8 else: det.shape == (num_detectors,) det.dtype == np.bool_ obs.shape == (num_observables,) obs.dtype == np.bool_ Examples: >>> import stim >>> stim.Circuit(''' ... X 1 ... M 0 1 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... OBSERVABLE_INCLUDE(3) rec[-1] rec[-2] ... ''').reference_detector_and_observable_signs() (array([ True, False]), array([False, False, False, True])) )DOC") .data()); c.def( "compile_m2d_converter", &py_init_compiled_measurements_to_detection_events_converter, pybind11::kw_only(), pybind11::arg("skip_reference_sample") = false, clean_doc_string(R"DOC( Creates a measurement-to-detection-event converter for the given circuit. The converter can efficiently compute detection events and observable flips from raw measurement data. The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is. Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit. Args: skip_reference_sample: Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution). Returns: An initialized stim.CompiledMeasurementsToDetectionEventsConverter. Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> converter.convert( ... measurements=np.array([[0], [1]], dtype=np.bool_), ... append_observables=False, ... ) array([[ True], [False]]) )DOC") .data()); c.def( "compile_detector_sampler", &py_init_compiled_detector_sampler, pybind11::kw_only(), pybind11::arg("seed") = pybind11::none(), clean_doc_string(R"DOC( Returns an object that can batch sample detection events from the circuit. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[False]]) )DOC") .data()); c.def( "clear", &Circuit::clear, clean_doc_string(R"DOC( Clears the contents of the circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c.clear() >>> c stim.Circuit() )DOC") .data()); c.def( "flattened_operations", [](Circuit &self) { pybind11::list result; self.for_each_operation([&](const CircuitInstruction &op) { pybind11::list args; pybind11::list targets; for (auto t : op.targets) { auto v = t.qubit_value(); if (t.data & TARGET_INVERTED_BIT) { targets.append(pybind11::make_tuple("inv", v)); } else if (t.data & (TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT)) { if (!(t.data & TARGET_PAULI_Z_BIT)) { targets.append(pybind11::make_tuple("X", v)); } else if (!(t.data & TARGET_PAULI_X_BIT)) { targets.append(pybind11::make_tuple("Z", v)); } else { targets.append(pybind11::make_tuple("Y", v)); } } else if (t.data & TARGET_RECORD_BIT) { targets.append(pybind11::make_tuple("rec", -(long long)v)); } else if (t.data & TARGET_SWEEP_BIT) { targets.append(pybind11::make_tuple("sweep", v)); } else { targets.append(pybind11::int_(v)); } } for (auto t : op.args) { args.append(t); } const auto &gate_data = GATE_DATA[op.gate_type]; if (op.args.empty()) { // Backwards compatibility. result.append(pybind11::make_tuple(gate_data.name, targets, 0)); } else if (op.args.size() == 1) { // Backwards compatibility. result.append(pybind11::make_tuple(gate_data.name, targets, op.args[0])); } else { result.append(pybind11::make_tuple(gate_data.name, targets, args)); } }); return result; }, clean_doc_string(R"DOC( [DEPRECATED] Returns a list of tuples encoding the contents of the circuit. Instead of this method, use `for instruction in circuit` or, to avoid REPEAT blocks, `for instruction in circuit.flattened()`. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... X_ERROR(0.125) 1 ... M 0 !1 ... ''').flattened_operations() [('H', [0], 0), ('X_ERROR', [1], 0.125), ('M', [0, ('inv', 1)], 0)] >>> stim.Circuit(''' ... REPEAT 2 { ... H 6 ... } ... ''').flattened_operations() [('H', [6], 0), ('H', [6], 0)] )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two circuits have identical contents."); c.def(pybind11::self != pybind11::self, "Determines if two circuits have non-identical contents."); c.def( "__add__", &Circuit::operator+, pybind11::arg("second"), clean_doc_string(R"DOC( Creates a circuit by appending two circuits. Examples: >>> import stim >>> c1 = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c2 = stim.Circuit(''' ... M 0 1 2 ... ''') >>> c1 + c2 stim.Circuit(''' X 0 Y 1 2 M 0 1 2 ''') )DOC") .data()); c.def( "__iadd__", &Circuit::operator+=, pybind11::arg("second"), clean_doc_string(R"DOC( Appends a circuit into the receiving circuit (mutating it). Examples: >>> import stim >>> c1 = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c2 = stim.Circuit(''' ... M 0 1 2 ... ''') >>> c1 += c2 >>> print(repr(c1)) stim.Circuit(''' X 0 Y 1 2 M 0 1 2 ''') )DOC") .data()); c.def( "__imul__", &Circuit::operator*=, pybind11::arg("repetitions"), clean_doc_string(R"DOC( Mutates the circuit by putting its contents into a REPEAT block. Special case: if the repetition count is 0, the circuit is cleared. Special case: if the repetition count is 1, nothing happens. Args: repetitions: The number of times the REPEAT block should repeat. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c *= 3 >>> print(repr(c)) stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') )DOC") .data()); c.def( "__mul__", &Circuit::operator*, pybind11::arg("repetitions"), clean_doc_string(R"DOC( Repeats the circuit using a REPEAT block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the REPEAT block should repeat. Returns: repetitions=0: An empty circuit. repetitions=1: A copy of this circuit. repetitions>=2: A circuit with a single REPEAT block, where the contents of that repeat block are this circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> c * 3 stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') )DOC") .data()); c.def( "__rmul__", &Circuit::operator*, pybind11::arg("repetitions"), clean_doc_string(R"DOC( Repeats the circuit using a REPEAT block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the REPEAT block should repeat. Returns: repetitions=0: An empty circuit. repetitions=1: A copy of this circuit. repetitions>=2: A circuit with a single REPEAT block, where the contents of that repeat block are this circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 ... Y 1 2 ... ''') >>> 3 * c stim.Circuit(''' REPEAT 3 { X 0 Y 1 2 } ''') )DOC") .data()); for (size_t k = 0; k < 2; k++) { c.def( k == 0 ? "append_operation" : "append", k == 0 ? &circuit_append_backwards_compat : &circuit_append_strict, pybind11::arg("name"), pybind11::arg("targets") = pybind11::make_tuple(), pybind11::arg("arg") = pybind11::none(), pybind11::kw_only(), pybind11::arg("tag") = "", k == 0 ? "[DEPRECATED] use stim.Circuit.append instead" : clean_doc_string(R"DOC( @overload def append(self, name: str, targets: Union[int, stim.GateTarget, stim.PauliString, Iterable[Union[int, stim.GateTarget, stim.PauliString]]], arg: Union[float, Iterable[float], None] = None, *, tag: str = "") -> None: @overload def append(self, name: Union[stim.CircuitInstruction, stim.CircuitRepeatBlock, stim.Circuit]) -> None: Appends an operation into the circuit. Note: `stim.Circuit.append_operation` is an alias of `stim.Circuit.append`. Args: name: The name of the operation's gate (e.g. "H" or "M" or "CNOT"). This argument can also be set to a `stim.CircuitInstruction` or `stim.CircuitInstructionBlock`, which results in the instruction or block being appended to the circuit. The other arguments (targets and arg) can't be specified when doing so. (The argument being called `name` is no longer quite right, but is being kept for backwards compatibility.) targets: The objects operated on by the gate. This can be either a single target or an iterable of multiple targets. Each target can be: An int: The index of a targeted qubit. A `stim.GateTarget`: Could be a variety of things. Methods like `stim.target_rec`, `stim.target_sweet`, `stim.target_x`, and `stim.CircuitInstruction.__getitem__` all return this type. A `stim.PauliString`: This will automatically be expanded into a product of pauli targets like `X1*Y2*Z3`. arg: The "parens arguments" for the gate, such as the probability for a noise operation. A double or list of doubles parameterizing the gate. Different gates take different parens arguments. For example, X_ERROR takes a probability, OBSERVABLE_INCLUDE takes an observable index, and PAULI_CHANNEL_1 takes three disjoint probabilities. Note: Defaults to no parens arguments. Except, for backwards compatibility reasons, `cirq.append_operation` (but not `cirq.append`) will default to a single 0.0 argument for gates that take exactly one argument. tag: A customizable string attached to the instruction. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append("X", 0) >>> c.append("H", [0, 1]) >>> c.append("M", [0, stim.target_inv(1)]) >>> c.append("CNOT", [stim.target_rec(-1), 0]) >>> c.append("X_ERROR", [0], 0.125) >>> c.append("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(2)], 0.25) >>> c.append("MPP", [stim.PauliString("X1*Y2"), stim.GateTarget("Z3")]) >>> print(repr(c)) stim.Circuit(''' X 0 H 0 1 M 0 !1 CX rec[-1] 0 X_ERROR(0.125) 0 E(0.25) X0 Y2 MPP X1*Y2 Z3 ''') )DOC") .data()); } c.def( "insert", &circuit_insert, pybind11::arg("index"), pybind11::arg("operation"), clean_doc_string(R"DOC( Inserts an operation at the given index, pushing existing operations forward. @signature def insert(self, index: int, operation: Union[stim.CircuitInstruction, stim.Circuit]) -> None: Beware that inserted operations are automatically fused with the preceding and following operations, if possible. This can make it complex to reason about how the indices of operations change in response to insertions. Args: index: The index to insert at. Must satisfy -len(circuit) <= index < len(circuit). Negative indices are made non-negative by adding len(circuit) to them, so they refer to indices relative to the end of the circuit instead of the start. Instructions before the index are not shifted. Instructions that were at or after the index are shifted forwards as needed. operation: The object to insert. This can be a single stim.CircuitInstruction or an entire stim.Circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... S 1 ... X 2 ... ''') >>> c.insert(1, stim.CircuitInstruction("Y", [3, 4, 5])) >>> c stim.Circuit(''' H 0 Y 3 4 5 S 1 X 2 ''') >>> c.insert(-1, stim.Circuit("S 999\nCX 0 1\nCZ 2 3")) >>> c stim.Circuit(''' H 0 Y 3 4 5 S 1 999 CX 0 1 CZ 2 3 X 2 ''') )DOC") .data()); c.def( "pop", &circuit_pop, pybind11::arg("index") = -1, clean_doc_string(R"DOC( @signature def pop(self, index: int = -1) -> Union[stim.CircuitInstruction, stim.CircuitRepeatBlock]: Pops an operation from the end of the circuit, or at the given index. Args: index: Defaults to -1 (end of circuit). The index to pop from. Returns: The popped instruction. Raises: IndexError: The given index is outside the bounds of the circuit. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... S 1 ... X 2 ... Y 3 ... ''') >>> c.pop() stim.CircuitInstruction('Y', [stim.GateTarget(3)], []) >>> c.pop(1) stim.CircuitInstruction('S', [stim.GateTarget(1)], []) >>> c stim.Circuit(''' H 0 X 2 ''') )DOC") .data()); c.def( "append_from_stim_program_text", [](Circuit &self, const char *text) { self.append_from_text(text); }, pybind11::arg("stim_program_text"), clean_doc_string(R"DOC( Appends operations described by a STIM format program into the circuit. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append_from_stim_program_text(''' ... H 0 # comment ... CNOT 0 2 ... ... M 2 ... CNOT rec[-1] 1 ... ''') >>> print(c) H 0 CX 0 2 M 2 CX rec[-1] 1 Args: stim_program_text: The STIM program text containing the circuit operations to append. )DOC") .data()); c.def( "__str__", &Circuit::str, "Returns stim instructions (that can be saved to a file and parsed by stim) for the current circuit."); c.def( "__repr__", &circuit_repr, "Returns text that is a valid python expression evaluating to an equivalent `stim.Circuit`."); c.def( "copy", [](Circuit &self) { Circuit copy = self; return copy; }, clean_doc_string(R"DOC( Returns a copy of the circuit. An independent circuit with the same contents. Examples: >>> import stim >>> c1 = stim.Circuit("H 0") >>> c2 = c1.copy() >>> c2 is c1 False >>> c2 == c1 True )DOC") .data()); c.def_static( "generated", [](std::string_view type, size_t distance, size_t rounds, double after_clifford_depolarization, double before_round_data_depolarization, double before_measure_flip_probability, double after_reset_flip_probability) { auto r = type.find(':'); std::string_view code; std::string_view task; if (r == std::string::npos) { code = ""; task = type; } else { code = type.substr(0, r); task = type.substr(r + 1); } CircuitGenParameters params(rounds, distance, std::string(task)); params.after_clifford_depolarization = after_clifford_depolarization; params.after_reset_flip_probability = after_reset_flip_probability; params.before_measure_flip_probability = before_measure_flip_probability; params.before_round_data_depolarization = before_round_data_depolarization; params.validate_params(); if (code == "surface_code") { return generate_surface_code_circuit(params).circuit; } else if (code == "repetition_code") { return generate_rep_code_circuit(params).circuit; } else if (code == "color_code") { return generate_color_code_circuit(params).circuit; } else { throw std::invalid_argument( "Unrecognized circuit type. Expected type to start with " "'surface_code:', 'repetition_code:', or 'color_code:'."); } }, pybind11::arg("code_task"), pybind11::kw_only(), pybind11::arg("distance"), pybind11::arg("rounds"), pybind11::arg("after_clifford_depolarization") = 0.0, pybind11::arg("before_round_data_depolarization") = 0.0, pybind11::arg("before_measure_flip_probability") = 0.0, pybind11::arg("after_reset_flip_probability") = 0.0, clean_doc_string(R"DOC( Generates common circuits. The generated circuits can include configurable noise. The generated circuits include DETECTOR and OBSERVABLE_INCLUDE annotations so that their detection events and logical observables can be sampled. The generated circuits include TICK annotations to mark the progression of time. (E.g. so that converting them using `stimcirq.stim_circuit_to_cirq_circuit` will produce a `cirq.Circuit` with the intended moment structure.) Args: code_task: A string identifying the type of circuit to generate. Available code tasks are: - "repetition_code:memory" - "surface_code:rotated_memory_x" - "surface_code:rotated_memory_z" - "surface_code:unrotated_memory_x" - "surface_code:unrotated_memory_z" - "color_code:memory_xyz" distance: The desired code distance of the generated circuit. The code distance is the minimum number of physical errors needed to cause a logical error. This parameter indirectly determines how many qubits the generated circuit uses. rounds: How many times the measurement qubits in the generated circuit will be measured. Indirectly determines the duration of the generated circuit. after_clifford_depolarization: Defaults to 0. The probability (p) of `DEPOLARIZE1(p)` operations to add after every single-qubit Clifford operation and `DEPOLARIZE2(p)` operations to add after every two-qubit Clifford operation. The after-Clifford depolarizing operations are only included if this probability is not 0. before_round_data_depolarization: Defaults to 0. The probability (p) of `DEPOLARIZE1(p)` operations to apply to every data qubit at the start of a round of stabilizer measurements. The start-of-round depolarizing operations are only included if this probability is not 0. before_measure_flip_probability: Defaults to 0. The probability (p) of `X_ERROR(p)` operations applied to qubits before each measurement (X basis measurements use `Z_ERROR(p)` instead). The before-measurement flips are only included if this probability is not 0. after_reset_flip_probability: Defaults to 0. The probability (p) of `X_ERROR(p)` operations applied to qubits after each reset (X basis resets use `Z_ERROR(p)` instead). The after-reset flips are only included if this probability is not 0. Returns: The generated circuit. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... distance=4, ... rounds=10000, ... after_clifford_depolarization=0.0125) >>> print(circuit) R 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.0125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.0125) 2 1 4 3 6 5 TICK MR 1 3 5 DETECTOR(1, 0) rec[-3] DETECTOR(3, 0) rec[-2] DETECTOR(5, 0) rec[-1] REPEAT 9999 { TICK CX 0 1 2 3 4 5 DEPOLARIZE2(0.0125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.0125) 2 1 4 3 6 5 TICK MR 1 3 5 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-3] rec[-6] DETECTOR(3, 0) rec[-2] rec[-5] DETECTOR(5, 0) rec[-1] rec[-4] } M 0 2 4 6 DETECTOR(1, 1) rec[-3] rec[-4] rec[-7] DETECTOR(3, 1) rec[-2] rec[-3] rec[-6] DETECTOR(5, 1) rec[-1] rec[-2] rec[-5] OBSERVABLE_INCLUDE(0) rec[-1] )DOC") .data()); c.def_static( "from_file", [](pybind11::object &obj) { if (pybind11::isinstance(obj)) { std::string_view path = pybind11::cast(obj); RaiiFile f(path, "rb"); return Circuit::from_file(f.f); } pybind11::object py_path = pybind11::module::import("pathlib").attr("Path"); if (pybind11::isinstance(obj, py_path)) { pybind11::object obj_str = pybind11::str(obj); std::string_view path = pybind11::cast(obj_str); RaiiFile f(path, "rb"); return Circuit::from_file(f.f); } pybind11::object py_text_io_base = pybind11::module::import("io").attr("TextIOBase"); if (pybind11::isinstance(obj, py_text_io_base)) { pybind11::object contents = obj.attr("read")(); return Circuit(pybind11::cast(contents)); } std::stringstream ss; ss << "Don't know how to read from "; ss << pybind11::repr(obj); throw std::invalid_argument(ss.str()); }, pybind11::arg("file"), clean_doc_string(R"DOC( @signature def from_file(file: Union[io.TextIOBase, str, pathlib.Path]) -> stim.Circuit: Reads a stim circuit from a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md Args: file: A file path or open file object to read from. Returns: The circuit parsed from the file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('H 5', file=f) ... circuit = stim.Circuit.from_file(path) >>> circuit stim.Circuit(''' H 5 ''') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('CNOT 4 5', file=f) ... with open(path) as f: ... circuit = stim.Circuit.from_file(f) >>> circuit stim.Circuit(''' CX 4 5 ''') )DOC") .data()); c.def( "to_file", [](const Circuit &self, pybind11::object &obj) { if (pybind11::isinstance(obj)) { std::string path = pybind11::cast(obj); std::ofstream out(path, std::ofstream::out); if (!out.is_open()) { throw std::invalid_argument("Failed to open " + path); } out << self << '\n'; return; } auto py_path = pybind11::module::import("pathlib").attr("Path"); if (pybind11::isinstance(obj, py_path)) { auto path = pybind11::cast(pybind11::str(obj)); std::ofstream out(path, std::ofstream::out); if (!out.is_open()) { throw std::invalid_argument("Failed to open " + path); } out << self << '\n'; return; } auto py_text_io_base = pybind11::module::import("io").attr("TextIOBase"); if (pybind11::isinstance(obj, py_text_io_base)) { obj.attr("write")(pybind11::str(self.str())); obj.attr("write")(pybind11::str("\n")); return; } std::stringstream ss; ss << "Don't know how to write to "; ss << pybind11::repr(obj); throw std::invalid_argument(ss.str()); }, pybind11::arg("file"), clean_doc_string(R"DOC( @signature def to_file(self, file: Union[io.TextIOBase, str, pathlib.Path]) -> None: Writes the stim circuit to a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md Args: file: A file path or an open file to write to. Examples: >>> import stim >>> import tempfile >>> c = stim.Circuit('H 5\nX 0') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... c.to_file(f) ... with open(path) as f: ... contents = f.read() >>> contents 'H 5\nX 0\n' >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... c.to_file(path) ... with open(path) as f: ... contents = f.read() >>> contents 'H 5\nX 0\n' )DOC") .data()); c.def( "__len__", [](const Circuit &self) { return self.operations.size(); }, clean_doc_string(R"DOC( Returns the number of top-level instructions and blocks in the circuit. Instructions inside of blocks are not included in this count. Examples: >>> import stim >>> len(stim.Circuit()) 0 >>> len(stim.Circuit(''' ... X 0 ... X_ERROR(0.5) 1 2 ... TICK ... M 0 ... DETECTOR rec[-1] ... ''')) 5 >>> len(stim.Circuit(''' ... REPEAT 100 { ... X 0 ... Y 1 2 ... } ... ''')) 1 )DOC") .data()); c.def( "__getitem__", &circuit_get_item, pybind11::arg("index_or_slice"), clean_doc_string(R"DOC( Returns copies of instructions from the circuit. @overload def __getitem__(self, index_or_slice: int) -> Union[stim.CircuitInstruction, stim.CircuitRepeatBlock]: @overload def __getitem__(self, index_or_slice: slice) -> stim.Circuit: Args: index_or_slice: An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a circuit. Returns: If the index was an integer, then an instruction from the circuit. If the index was a slice, then a circuit made up of the instructions in that slice. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X 0 ... X_ERROR(0.5) 2 ... REPEAT 100 { ... X 0 ... Y 1 2 ... } ... TICK ... M 0 ... DETECTOR rec[-1] ... ''') >>> circuit[1] stim.CircuitInstruction('X_ERROR', [stim.GateTarget(2)], [0.5]) >>> circuit[2] stim.CircuitRepeatBlock(100, stim.Circuit(''' X 0 Y 1 2 ''')) >>> circuit[1::2] stim.Circuit(''' X_ERROR(0.5) 2 TICK DETECTOR rec[-1] ''') )DOC") .data()); c.def( "detector_error_model", [](const Circuit &self, bool decompose_errors, bool flatten_loops, bool allow_gauge_detectors, double approximate_disjoint_errors, bool ignore_decomposition_failures, bool block_decomposition_from_introducing_remnant_edges) -> DetectorErrorModel { return ErrorAnalyzer::circuit_to_detector_error_model( self, decompose_errors, !flatten_loops, allow_gauge_detectors, approximate_disjoint_errors, ignore_decomposition_failures, block_decomposition_from_introducing_remnant_edges); }, pybind11::kw_only(), pybind11::arg("decompose_errors") = false, pybind11::arg("flatten_loops") = false, pybind11::arg("allow_gauge_detectors") = false, pybind11::arg("approximate_disjoint_errors") = false, pybind11::arg("ignore_decomposition_failures") = false, pybind11::arg("block_decomposition_from_introducing_remnant_edges") = false, clean_doc_string(R"DOC( Returns a stim.DetectorErrorModel describing the error processes in the circuit. Args: decompose_errors: Defaults to false. When set to true, the error analysis attempts to decompose the components of composite error mechanisms (such as depolarization errors) into simpler errors, and suggest this decomposition via `stim.target_separator()` between the components. For example, in an XZ surface code, single qubit depolarization has a Y error term which can be decomposed into simpler X and Z error terms. Decomposition fails (causing this method to throw) if it's not possible to decompose large errors into simple errors that affect at most two detectors. flatten_loops: Defaults to false. When set to True, the output will not contain any `repeat` blocks. When set to False, the error analysis watches for loops in the circuit reaching a periodic steady state with respect to the detectors being introduced, the error mechanisms that affect them, and the locations of the logical observables. When it identifies such a steady state, it outputs a repeat block. This is massively more efficient than flattening for circuits that contain loops, but creates a more complex output. allow_gauge_detectors: Defaults to false. When set to false, the error analysis verifies that detectors in the circuit are actually deterministic under noiseless execution of the circuit. When set to True, these detectors are instead considered to be part of degrees freedom that can be removed from the error model. For example, if detectors D1 and D3 both anti-commute with a reset, then the error model has a gauge `error(0.5) D1 D3`. When gauges are identified, one of the involved detectors is removed from the system using Gaussian elimination. Note that logical observables are still verified to be deterministic, even if this option is set. approximate_disjoint_errors: Defaults to false. When set to false, composite error mechanisms with disjoint components (such as `PAULI_CHANNEL_1(0.1, 0.2, 0.0)`) can cause the error analysis to throw exceptions (because detector error models can only contain independent error mechanisms). When set to true, the probabilities of the disjoint cases are instead assumed to be independent probabilities. For example, a `PAULI_CHANNEL_1(0.1, 0.2, 0.0)` becomes equivalent to an `X_ERROR(0.1)` followed by a `Y_ERROR(0.2)`. This assumption is an approximation, but it is a good approximation for small probabilities. This argument can also be set to a probability between 0 and 1, setting a threshold below which the approximation is acceptable. Any error mechanisms that have a component probability above the threshold will cause an exception to be thrown. ignore_decomposition_failures: Defaults to False. When this is set to True, circuit errors that fail to decompose into graphlike detector error model errors no longer cause the conversion process to abort. Instead, the undecomposed error is inserted into the output. Whatever tool the detector error model is then given to is responsible for dealing with the undecomposed errors (e.g. a tool may choose to simply ignore them). Irrelevant unless decompose_errors=True. block_decomposition_from_introducing_remnant_edges: Defaults to False. Requires that both A B and C D be present elsewhere in the detector error model in order to decompose A B C D into A B ^ C D. Normally, only one of A B or C D needs to appear to allow this decomposition. Remnant edges can be a useful feature for ensuring decomposition succeeds, but they can also reduce the effective code distance by giving the decoder single edges that actually represent multiple errors in the circuit (resulting in the decoder making misinformed choices when decoding). Irrelevant unless decompose_errors=True. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model() stim.DetectorErrorModel(''' error(0.125) D0 error(0.375) D0 D1 error(0.25) D1 ''') )DOC") .data()); c.def( "approx_equals", [](const Circuit &self, const pybind11::object &obj, double atol) -> bool { try { return self.approx_equals(pybind11::cast(obj), atol); } catch (const pybind11::cast_error &) { return false; } }, pybind11::arg("other"), pybind11::kw_only(), pybind11::arg("atol"), clean_doc_string(R"DOC( Checks if a circuit is approximately equal to another circuit. Two circuits are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example, `X_ERROR(0.100) 0` is approximately equal to `X_ERROR(0.099)` within an absolute tolerance of 0.002. All other details of the circuits (such as the ordering of instructions and targets) must be exactly the same. Args: other: The circuit, or other object, to compare to this one. atol: The absolute error tolerance. The maximum amount each probability may have been perturbed by. Returns: True if the given object is a circuit approximately equal up to the receiving circuit up to the given tolerance, otherwise False. Examples: >>> import stim >>> base = stim.Circuit(''' ... X_ERROR(0.099) 0 1 2 ... M 0 1 2 ... ''') >>> base.approx_equals(base, atol=0) True >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0) False >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0.0001) False >>> base.approx_equals(stim.Circuit(''' ... X_ERROR(0.101) 0 1 2 ... M 0 1 2 ... '''), atol=0.01) True >>> base.approx_equals(stim.Circuit(''' ... DEPOLARIZE1(0.099) 0 1 2 ... MRX 0 1 2 ... '''), atol=9999) False )DOC") .data()); c.def( "get_detector_coordinates", [](const Circuit &self, const pybind11::object &obj) { return self.get_detector_coordinates(obj_to_abs_detector_id_set(obj, [&]() { return self.count_detectors(); })); }, pybind11::arg("only") = pybind11::none(), clean_doc_string(R"DOC( Returns the coordinate metadata of detectors in the circuit. Args: only: Defaults to None (meaning include all detectors). A list of detector indices to include in the result. Detector indices beyond the end of the detector error model of the circuit cause an error. Returns: A dictionary mapping integers (detector indices) to lists of floats (coordinates). Detectors with no specified coordinate data are mapped to an empty tuple. If `only` is specified, then `set(result.keys()) == set(only)`. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... M 0 ... DETECTOR rec[-1] ... DETECTOR(1, 2, 3) rec[-1] ... REPEAT 3 { ... DETECTOR(42) rec[-1] ... SHIFT_COORDS(100) ... } ... ''') >>> circuit.get_detector_coordinates() {0: [], 1: [1.0, 2.0, 3.0], 2: [42.0], 3: [142.0], 4: [242.0]} >>> circuit.get_detector_coordinates(only=[1]) {1: [1.0, 2.0, 3.0]} )DOC") .data()); c.def( "get_final_qubit_coordinates", &Circuit::get_final_qubit_coords, clean_doc_string(R"DOC( Returns the coordinate metadata of qubits in the circuit. If a qubit's coordinates are specified multiple times, only the last specified coordinates are returned. Returns: A dictionary mapping qubit indices (integers) to coordinates (lists of floats). Qubits that never had their coordinates specified are not included in the result. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... QUBIT_COORDS(1, 2, 3) 1 ... ''') >>> circuit.get_final_qubit_coordinates() {1: [1.0, 2.0, 3.0]} )DOC") .data()); c.def( pybind11::pickle( [](const Circuit &self) -> pybind11::str { return self.str(); }, [](const pybind11::str &text) { return Circuit(pybind11::cast(text)); })); c.def( "shortest_graphlike_error", &circuit_shortest_graphlike_error, pybind11::kw_only(), pybind11::arg("ignore_ungraphlike_errors") = true, pybind11::arg("canonicalize_circuit_errors") = false, clean_doc_string(R"DOC( Finds a minimum set of graphlike errors to produce an undetected logical error. A "graphlike error" is an error that creates at most two detection events (causes a change in the parity of the measurement sets of at most two DETECTOR annotations). Note that this method does not pay attention to error probabilities (other than ignoring errors with probability 0). It searches for a logical error with the minimum *number* of physical errors, not the maximum probability of those physical errors all occurring. This method works by converting the circuit into a `stim.DetectorErrorModel` using `circuit.detector_error_model(...)`, computing the shortest graphlike error of the error model, and then converting the physical errors making up that logical error back into representative circuit errors. Args: ignore_ungraphlike_errors: False: Attempt to decompose any ungraphlike errors in the circuit into graphlike parts. If this fails, raise an exception instead of continuing. Note: in some cases, graphlike errors only appear as parts of decomposed ungraphlike errors. This can produce a result that lists DEM errors with zero matching circuit errors, because the only way to achieve those errors is by combining a decomposed error with a graphlike error. As a result, when using this option it is NOT guaranteed that the length of the result is an upper bound on the true code distance. That is only the case if every item in the result lists at least one matching circuit error. True (default): Ungraphlike errors are simply skipped as if they weren't present, even if they could become graphlike if decomposed. This guarantees the length of the result is an upper bound on the true code distance. canonicalize_circuit_errors: Whether or not to use one representative for equal-symptom circuit errors. False (default): Each DEM error lists every possible circuit error that single handedly produces those symptoms as a potential match. This is verbose but gives complete information. True: Each DEM error is matched with one possible circuit error that single handedly produces those symptoms, with a preference towards errors that are simpler (e.g. apply Paulis to fewer qubits). This discards mostly-redundant information about different ways to produce the same symptoms in order to give a succinct result. Returns: A list of error mechanisms that cause an undetected logical error. Each entry in the list is a `stim.ExplainedError` detailing the location and effects of a single physical error. The effects of the entire list combine to produce a logical frame change without any detection events. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... before_round_data_depolarization=0.01) >>> len(circuit.shortest_graphlike_error()) 7 )DOC") .data()); c.def( "search_for_undetectable_logical_errors", &py_find_undetectable_logical_error, pybind11::kw_only(), pybind11::arg("dont_explore_detection_event_sets_with_size_above"), pybind11::arg("dont_explore_edges_with_degree_above"), pybind11::arg("dont_explore_edges_increasing_symptom_degree"), pybind11::arg("canonicalize_circuit_errors") = false, clean_doc_string(R"DOC( Searches for small sets of errors that form an undetectable logical error. THIS IS A HEURISTIC METHOD. It does not guarantee that it will find errors of particular sizes, or with particular properties. The errors it finds are a tangled combination of the truncation parameters you specify, internal optimizations which are correct when not truncating, and minutia of the circuit being considered. If you want a well behaved method that does provide guarantees of finding errors of a particular type, use `stim.Circuit.shortest_graphlike_error`. This method is more thorough than that (assuming you don't truncate so hard you omit graphlike edges), but exactly how thorough is difficult to describe. It's also not guaranteed that the behavior of this method will not be changed in the future in a way that permutes which logical errors are found and which are missed. This search method considers hyper errors, so it has worst case exponential runtime. It is important to carefully consider the arguments you are providing, which truncate the search space and trade cost for quality. The search progresses by starting from each error that crosses a logical observable, noting which detection events each error produces, and then iteratively adding in errors touching those detection events attempting to cancel out the detection event with the lowest index. Beware that the choice of logical observable can interact with the truncation options. Using different observables can change whether or not the search succeeds, even if those observables are equal modulo the stabilizers of the code. This is because the edges crossing logical observables are used as starting points for the search, and starting from different places along a path will result in different numbers of symptoms in intermediate states as the search progresses. For example, if the logical observable is next to a boundary, then the starting edges are likely boundary edges (degree 1) with 'room to grow', whereas if the observable was running through the bulk then the starting edges will have degree at least 2. Args: dont_explore_detection_event_sets_with_size_above: Truncates the search space by refusing to cross an edge (i.e. add an error) when doing so would produce an intermediate state that has more detection events than this limit. dont_explore_edges_with_degree_above: Truncates the search space by refusing to consider errors that cause a lot of detection events. For example, you may only want to consider graphlike errors which have two or fewer detection events. dont_explore_edges_increasing_symptom_degree: Truncates the search space by refusing to cross an edge (i.e. add an error) when doing so would produce an intermediate state that has more detection events that the previous intermediate state. This massively improves the efficiency of the search because instead of, for example, exploring all n^4 possible detection event sets with 4 symptoms, the search will attempt to cancel out symptoms one by one. canonicalize_circuit_errors: Whether or not to use one representative for equal-symptom circuit errors. False (default): Each DEM error lists every possible circuit error that single handedly produces those symptoms as a potential match. This is verbose but gives complete information. True: Each DEM error is matched with one possible circuit error that single handedly produces those symptoms, with a preference towards errors that are simpler (e.g. apply Paulis to fewer qubits). This discards mostly-redundant information about different ways to produce the same symptoms in order to give a succinct result. Returns: A list of error mechanisms that cause an undetected logical error. Each entry in the list is a `stim.ExplainedError` detailing the location and effects of a single physical error. The effects of the entire list combine to produce a logical frame change without any detection events. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... rounds=5, ... distance=5, ... after_clifford_depolarization=0.001) >>> print(len(circuit.search_for_undetectable_logical_errors( ... dont_explore_detection_event_sets_with_size_above=4, ... dont_explore_edges_with_degree_above=4, ... dont_explore_edges_increasing_symptom_degree=True, ... ))) 5 )DOC") .data()); c.def( "shortest_error_sat_problem", &py_shortest_error_sat_problem, pybind11::kw_only(), pybind11::arg("format") = "WDIMACS", clean_doc_string(R"DOC( Makes a maxSAT problem of the circuit's distance, that other tools can solve. The output is a string describing the maxSAT problem in WDIMACS format (see https://jix.github.io/varisat/manual/0.2.0/formats/dimacs.html). The optimal solution to the problem is the fault distance of the circuit (the minimum number of error mechanisms that combine to flip any logical observable while producing no detection events). This method ignores the probabilities of the error mechanisms since it only cares about minimizing the number of errors triggered. There are many tools that can solve maxSAT problems in WDIMACS format. One quick way to get started is to install pysat by running this BASH terminal command: pip install python-sat Afterwards, you can run the included maxSAT solver "RC2" with this Python code: from pysat.examples.rc2 import RC2 from pysat.formula import WCNF wcnf = WCNF(from_string="p wcnf 1 2 3\n3 -1 0\n3 1 0\n") with RC2(wcnf) as rc2: print(rc2.compute()) print(rc2.cost) Much faster solvers are available online. For example, you can download one of the entries in the 2023 maxSAT competition (see https://maxsat-evaluations.github.io/2023) and run it on your problem by running these BASH terminal commands: wget https://maxsat-evaluations.github.io/2023/mse23-solver-src/exact/CASHWMaxSAT-CorePlus.zip unzip CASHWMaxSAT-CorePlus.zip ./CASHWMaxSAT-CorePlus/bin/cashwmaxsatcoreplus -bm -m your_problem.wcnf Args: format: Defaults to "WDIMACS", corresponding to WDIMACS format which is described here: http://www.maxhs.org/docs/wdimacs.html Returns: A string corresponding to the contents of a maxSAT problem file in the requested format. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X_ERROR(0.1) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... X_ERROR(0.4) 0 ... M 0 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.shortest_error_sat_problem(), end='') p wcnf 2 4 5 1 -1 0 1 -2 0 5 -1 0 5 2 0 )DOC") .data()); c.def( "likeliest_error_sat_problem", &py_likeliest_error_sat_problem, pybind11::kw_only(), pybind11::arg("quantization") = 100, pybind11::arg("format") = "WDIMACS", clean_doc_string(R"DOC( Makes a maxSAT problem for the circuit's likeliest undetectable logical error. The output is a string describing the maxSAT problem in WDIMACS format (see https://jix.github.io/varisat/manual/0.2.0/formats/dimacs.html). The optimal solution to the problem is the highest likelihood set of error mechanisms that combine to flip any logical observable while producing no detection events). If there are any errors with probability p > 0.5, they are inverted so that the resulting weight ends up being positive. If there are errors with weight close or equal to 0.5, they can end up with 0 weight meaning that they can be included or not in the solution with no affect on the likelihood. There are many tools that can solve maxSAT problems in WDIMACS format. One quick way to get started is to install pysat by running this BASH terminal command: pip install python-sat Afterwards, you can run the included maxSAT solver "RC2" with this Python code: from pysat.examples.rc2 import RC2 from pysat.formula import WCNF wcnf = WCNF(from_string="p wcnf 1 2 3\n3 -1 0\n3 1 0\n") with RC2(wcnf) as rc2: print(rc2.compute()) print(rc2.cost) Much faster solvers are available online. For example, you can download one of the entries in the 2023 maxSAT competition (see https://maxsat-evaluations.github.io/2023) and run it on your problem by running these BASH terminal commands: wget https://maxsat-evaluations.github.io/2023/mse23-solver-src/exact/CASHWMaxSAT-CorePlus.zip unzip CASHWMaxSAT-CorePlus.zip ./CASHWMaxSAT-CorePlus/bin/cashwmaxsatcoreplus -bm -m your_problem.wcnf Args: format: Defaults to "WDIMACS", corresponding to WDIMACS format which is described here: http://www.maxhs.org/docs/wdimacs.html quantization: Defaults to 10. Error probabilities are converted to log-odds and scaled/rounded to be positive integers at most this large. Setting this argument to a larger number results in more accurate quantization such that the returned error set should have a likelihood closer to the true most likely solution. This comes at the cost of making some maxSAT solvers slower. Returns: A string corresponding to the contents of a maxSAT problem file in the requested format. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... X_ERROR(0.1) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... X_ERROR(0.4) 0 ... M 0 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.likeliest_error_sat_problem( ... quantization=1000 ... ), end='') p wcnf 2 4 4001 185 -1 0 1000 -2 0 4001 -1 0 4001 2 0 )DOC") .data()); c.def( "explain_detector_error_model_errors", [](const Circuit &self, const pybind11::object &dem_filter, bool reduce_to_one_representative_error) -> std::vector { if (dem_filter.is_none()) { return ErrorMatcher::explain_errors_from_circuit(self, nullptr, reduce_to_one_representative_error); } else { const DetectorErrorModel &model = dem_filter.cast(); return ErrorMatcher::explain_errors_from_circuit(self, &model, reduce_to_one_representative_error); } }, pybind11::kw_only(), pybind11::arg("dem_filter") = pybind11::none(), pybind11::arg("reduce_to_one_representative_error") = false, clean_doc_string(R"DOC( Explains how detector error model errors are produced by circuit errors. Args: dem_filter: Defaults to None (unused). When used, the output will only contain detector error model errors that appear in the given `stim.DetectorErrorModel`. Any error mechanisms from the detector error model that can't be reproduced using one error from the circuit will also be included in the result, but with an empty list of associated circuit error mechanisms. reduce_to_one_representative_error: Defaults to False. When True, the items in the result will contain at most one circuit error mechanism. Returns: A `List[stim.ExplainedError]` (see `stim.ExplainedError` for more information). Each item in the list describes how a detector error model error can be produced by individual circuit errors. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... # Create Bell pair. ... H 0 ... CNOT 0 1 ... ... # Noise. ... DEPOLARIZE1(0.01) 0 ... ... # Bell basis measurement. ... CNOT 0 1 ... H 0 ... M 0 1 ... ... # Both measurements should be False under noiseless execution. ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... ''') >>> explained_errors = circuit.explain_detector_error_model_errors( ... dem_filter=stim.DetectorErrorModel('error(1) D0 D1'), ... reduce_to_one_representative_error=True, ... ) >>> print(explained_errors[0].circuit_error_locations[0]) CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 0 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.01) 0 } )DOC") .data()); c.def( "without_noise", &Circuit::without_noise, clean_doc_string(R"DOC( Returns a copy of the circuit with all noise processes removed. Pure noise instructions, such as X_ERROR and DEPOLARIZE2, are not included in the result. Noisy measurement instructions, like `M(0.001)`, have their noise parameter removed. Returns: A `stim.Circuit` with the same instructions except all noise processes have been removed. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.25) 0 ... CNOT 0 1 ... M(0.125) 0 ... ''').without_noise() stim.Circuit(''' CX 0 1 M 0 ''') )DOC") .data()); c.def( "without_tags", &Circuit::without_tags, clean_doc_string(R"DOC( Returns a copy of the circuit with all tags removed. Returns: A `stim.Circuit` with the same instructions except all tags have been removed. Examples: >>> import stim >>> stim.Circuit(''' ... X[test-tag] 0 ... M[test-tag-2](0.125) 0 ... ''').without_tags() stim.Circuit(''' X 0 M(0.125) 0 ''') )DOC") .data()); c.def( "flattened", &Circuit::flattened, clean_doc_string(R"DOC( Creates an equivalent circuit without REPEAT or SHIFT_COORDS. Returns: A `stim.Circuit` with the same instructions in the same order, but with loops flattened into repeated instructions and with all coordinate shifts inlined. Examples: >>> import stim >>> stim.Circuit(''' ... REPEAT 5 { ... MR 0 1 ... DETECTOR(0, 0) rec[-2] ... DETECTOR(1, 0) rec[-1] ... SHIFT_COORDS(0, 1) ... } ... ''').flattened() stim.Circuit(''' MR 0 1 DETECTOR(0, 0) rec[-2] DETECTOR(1, 0) rec[-1] MR 0 1 DETECTOR(0, 1) rec[-2] DETECTOR(1, 1) rec[-1] MR 0 1 DETECTOR(0, 2) rec[-2] DETECTOR(1, 2) rec[-1] MR 0 1 DETECTOR(0, 3) rec[-2] DETECTOR(1, 3) rec[-1] MR 0 1 DETECTOR(0, 4) rec[-2] DETECTOR(1, 4) rec[-1] ''') )DOC") .data()); c.def( "inverse", [](const Circuit &self) -> Circuit { return self.inverse(); }, clean_doc_string(R"DOC( Returns a circuit that applies the same operations but inverted and in reverse. If circuit starts with QUBIT_COORDS instructions, the returned circuit will still have the same QUBIT_COORDS instructions in the same order at the start. Returns: A `stim.Circuit` that applies inverted operations in the reverse order. Raises: ValueError: The circuit contains operations that don't have an inverse, such as measurements. There are also some unsupported operations such as SHIFT_COORDS. Examples: >>> import stim >>> stim.Circuit(''' ... S 0 1 ... ISWAP 0 1 1 2 ... ''').inverse() stim.Circuit(''' ISWAP_DAG 1 2 0 1 S_DAG 1 0 ''') >>> stim.Circuit(''' ... QUBIT_COORDS(1, 2) 0 ... QUBIT_COORDS(4, 3) 1 ... QUBIT_COORDS(9, 5) 2 ... H 0 1 ... REPEAT 100 { ... CX 0 1 1 2 ... TICK ... S 1 2 ... } ... ''').inverse() stim.Circuit(''' QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(4, 3) 1 QUBIT_COORDS(9, 5) 2 REPEAT 100 { S_DAG 2 1 TICK CX 1 2 0 1 } H 1 0 ''') )DOC") .data()); c.def( "diagram", &circuit_diagram, pybind11::arg("type") = "timeline-text", pybind11::kw_only(), pybind11::arg("tick") = pybind11::none(), pybind11::arg("rows") = pybind11::none(), pybind11::arg("filter_coords") = pybind11::none(), clean_doc_string(R"DOC( @signature def diagram(self, type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "interactive", "interactive-html"] = 'timeline-text', *, tick: Union[None, int, range] = None, filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), rows: int | None = None) -> 'stim._DiagramHelper': Returns a diagram of the circuit, from a variety of options. Args: type: The type of diagram. Available types are: "timeline-text" (default): An ASCII diagram of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-svg": An SVG image of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-svg-html": A resizable SVG image viewer of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeline-3d": A 3d model, in GLTF format, of the operations applied by the circuit over time. "timeline-3d-html": Same 3d model as 'timeline-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. "detslice-text": An ASCII diagram of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. "detslice-svg": An SVG image of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. For example, a detector slice diagram of a CSS surface code circuit during the TICK between a measurement layer and a reset layer will produce the usual diagram of a surface code. Uses the Pauli color convention XYZ=RGB. "detslice-svg-html": Same as detslice-svg but the SVG image is inside a resizable HTML iframe. "matchgraph-svg": An SVG image of the match graph extracted from the circuit by stim.Circuit.detector_error_model. "matchgraph-svg-html": Same as matchgraph-svg but the SVG image is inside a resizable HTML iframe. "matchgraph-3d": An 3D model of the match graph extracted from the circuit by stim.Circuit.detector_error_model. "matchgraph-3d-html": Same 3d model as 'match-graph-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. "timeslice-svg": An SVG image of the operations applied between two TICK instructions in the circuit, with the operations laid out in 2d. "timeslice-svg-html": Same as timeslice-svg but the SVG image is inside a resizable HTML iframe. "detslice-with-ops-svg": A combination of timeslice-svg and detslice-svg, with the operations overlaid over the detector slices taken from the TICK after the operations were applied. "detslice-with-ops-svg-html": Same as detslice-with-ops-svg but the SVG image is inside a resizable HTML iframe. "interactive" or "interactive-html": An HTML web page containing Crumble (an interactive editor for 2D stabilizer circuits) initialized with the given circuit as its default contents. tick: Required for detector and time slice diagrams. Specifies which TICK instruction, or range of TICK instructions, to slice at. Note that the first TICK instruction in the circuit corresponds tick=1. The value tick=0 refers to the very start of the circuit. Passing `range(A, B)` for a detector slice will show the slices for ticks A through B including A but excluding B. Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. rows: In diagrams that have multiple separate pieces, such as timeslice diagrams and detslice diagrams, this controls how many rows of pieces there will be. If not specified, a number of rows that creates a roughly square layout will be chosen. filter_coords: A list of things to include in the diagram. Different effects depending on the diagram. For detslice diagrams, the filter defaults to showing all detectors and no observables. When specified, each list entry can be a collection of floats (detectors whose coordinates start with the same numbers will be included), a stim.DemTarget (specifying a detector or observable to include), a string like "D5" or "L0" specifying a detector or observable to include. Returns: An object whose `__str__` method returns the diagram, so that writing the diagram to a file works correctly. The returned object may also define methods such as `_repr_html_`, so that ipython notebooks recognize it can be shown using a specialized viewer instead of as raw text. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... CNOT 0 1 1 2 ... ''') >>> print(circuit.diagram()) q0: -H-@--- | q1: ---X-@- | q2: -----X- >>> circuit = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... TICK ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> print(circuit.diagram("detslice-text", tick=1)) q0: -Z:D0- | q1: -Z:D0- )DOC") .data()); } ================================================ FILE: src/stim/circuit/circuit.pybind.h ================================================ // 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. #ifndef _STIM_CIRCUIT_CIRCUIT_PYBIND_H #define _STIM_CIRCUIT_CIRCUIT_PYBIND_H #include #include "stim/circuit/circuit.h" namespace stim_pybind { pybind11::class_ pybind_circuit(pybind11::module &m); void pybind_circuit_methods(pybind11::module &m, pybind11::class_ &c); void pybind_circuit_methods_extra(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind std::set obj_to_abs_detector_id_set( const pybind11::object &obj, const std::function &get_num_detectors); std::string circuit_repr(const stim::Circuit &self); #endif ================================================ FILE: src/stim/circuit/circuit.test.cc ================================================ // 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. #include "stim/circuit/circuit.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" using namespace stim; TEST(circuit, from_text) { Circuit expected; const auto &f = [](const char *c) { return Circuit(c); }; ASSERT_EQ(f("# not an operation"), expected); expected.clear(); expected.safe_append_u("H", {0}); ASSERT_EQ(f("H 0"), expected); ASSERT_EQ(f("h 0"), expected); ASSERT_EQ(f("H 0 "), expected); ASSERT_EQ(f(" H 0 "), expected); ASSERT_EQ(f("\tH 0\t\t"), expected); ASSERT_EQ(f("H 0 # comment"), expected); expected.clear(); expected.safe_append_u("H", {23}); ASSERT_EQ(f("H 23"), expected); expected.clear(); expected.safe_append_ua("DEPOLARIZE1", {4, 5}, 0.125); ASSERT_EQ(f("DEPOLARIZE1(0.125) 4 5 # comment"), expected); expected.clear(); expected.safe_append_u("ZCX", {5, 6}); ASSERT_EQ(f(" \t Cnot 5 6 # comment "), expected); ASSERT_THROW({ f("H a"); }, std::invalid_argument); ASSERT_THROW({ f("H(1)"); }, std::invalid_argument); ASSERT_THROW({ f("X_ERROR 1"); }, std::invalid_argument); ASSERT_THROW({ f("H 9999999999999999999999999999999999999999999"); }, std::invalid_argument); ASSERT_THROW({ f("H -1"); }, std::invalid_argument); ASSERT_THROW({ f("CNOT 0 a"); }, std::invalid_argument); ASSERT_THROW({ f("CNOT 0 99999999999999999999999999999999"); }, std::invalid_argument); ASSERT_THROW({ f("CNOT 0 -1"); }, std::invalid_argument); ASSERT_THROW({ f("DETECTOR 1 2"); }, std::invalid_argument); ASSERT_THROW({ f("CX 1 1"); }, std::invalid_argument); ASSERT_THROW({ f("SWAP 1 1"); }, std::invalid_argument); ASSERT_THROW({ f("DEPOLARIZE2(1) 1 1"); }, std::invalid_argument); ASSERT_THROW({ f("DETEstdCTOR rec[-1]"); }, std::invalid_argument); ASSERT_THROW({ f("DETECTOR rec[0]"); }, std::invalid_argument); ASSERT_THROW({ f("DETECTOR rec[1]"); }, std::invalid_argument); ASSERT_THROW({ f("DETECTOR rec[-999999999999]"); }, std::invalid_argument); ASSERT_THROW({ f("OBSERVABLE_INCLUDE rec[-1]"); }, std::invalid_argument); ASSERT_THROW({ f("OBSERVABLE_INCLUDE(-1) rec[-1]"); }, std::invalid_argument); ASSERT_THROW({ f("CORRELATED_ERROR(1) B1"); }, std::invalid_argument); ASSERT_THROW({ f("CORRELATED_ERROR(1) X 1"); }, std::invalid_argument); ASSERT_THROW({ f("CORRELATED_ERROR(1) X\n"); }, std::invalid_argument); ASSERT_THROW({ f("CORRELATED_ERROR(1) 1"); }, std::invalid_argument); ASSERT_THROW({ f("ELSE_CORRELATED_ERROR(1) 1 2"); }, std::invalid_argument); ASSERT_THROW({ f("CORRELATED_ERROR(1) 1 2"); }, std::invalid_argument); ASSERT_THROW({ f("CORRELATED_ERROR(1) A"); }, std::invalid_argument); ASSERT_THROW({ f("CNOT 0\nCNOT 1"); }, std::invalid_argument); expected.clear(); ASSERT_EQ(f(""), expected); ASSERT_EQ(f("# Comment\n\n\n# More"), expected); expected.clear(); expected.safe_append_u("H_XZ", {0}); ASSERT_EQ(f("H 0"), expected); expected.clear(); expected.safe_append_u("H_XZ", {0, 1}); ASSERT_EQ(f("H 0 \n H 1"), expected); expected.clear(); expected.safe_append_u("H_XZ", {1}); ASSERT_EQ(f("H 1"), expected); expected.clear(); expected.safe_append_u("H", {0}); expected.safe_append_u("ZCX", {0, 1}); ASSERT_EQ( f("# EPR\n" "H 0\n" "CNOT 0 1"), expected); expected.clear(); expected.safe_append_u("M", {0, 0 | TARGET_INVERTED_BIT, 1, 1 | TARGET_INVERTED_BIT}); ASSERT_EQ(f("M 0 !0 1 !1"), expected); // Measurement fusion. expected.clear(); expected.safe_append_u("H", {0}); expected.safe_append_u("M", {0, 1, 2}); expected.safe_append_u("SWAP", {0, 1}); expected.safe_append_u("M", {0, 10}); ASSERT_EQ( f(R"CIRCUIT( H 0 M 0 M 1 M 2 SWAP 0 1 M 0 M 10 )CIRCUIT"), expected); expected.clear(); expected.safe_append_u("X", {0}); expected += Circuit("Y 1 2") * 2; ASSERT_EQ( f(R"CIRCUIT( X 0 REPEAT 2 { Y 1 Y 2 #####" } #####" )CIRCUIT"), expected); expected.clear(); expected.safe_append_u("DETECTOR", {5 | TARGET_RECORD_BIT}); ASSERT_EQ(f("DETECTOR rec[-5]"), expected); expected.clear(); expected.safe_append_u("DETECTOR", {6 | TARGET_RECORD_BIT}); ASSERT_EQ(f("DETECTOR rec[-6]"), expected); Circuit parsed = f("M 0\n" "REPEAT 5 {\n" " M 1 2\n" " M 3\n" "} #####"); ASSERT_EQ(parsed.operations.size(), 2); ASSERT_EQ(parsed.blocks.size(), 1); ASSERT_EQ(parsed.blocks[0].operations.size(), 1); ASSERT_EQ(parsed.blocks[0].operations[0].targets.size(), 3); expected.clear(); expected.safe_append_ua( "CORRELATED_ERROR", {90 | TARGET_PAULI_X_BIT, 91 | TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT, 92 | TARGET_PAULI_Z_BIT, 93 | TARGET_PAULI_X_BIT}, 0.125); ASSERT_EQ( f(R"circuit( CORRELATED_ERROR(0.125) X90 Y91 Z92 X93 )circuit"), expected); } TEST(circuit, parse_mpp) { ASSERT_THROW({ Circuit("H *"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP 0"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP *"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP * X1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP * X1 *"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP X1 *"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP X1 * * Y2"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP X1**Y2"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP(1.1) X1**Y2"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP(-0.5) X1**Y2"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP X1*rec[-1]"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP rec[-1]"); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP sweep[0]"); }, std::invalid_argument); auto c = Circuit("MPP X1*Y2 Z3 * Z4\nMPP Z5"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].args.size(), 0); ASSERT_EQ(c.operations[0].targets.size(), 7); std::vector expected{ GateTarget::x(1), GateTarget::combiner(), GateTarget::y(2), GateTarget::z(3), GateTarget::combiner(), GateTarget::z(4), GateTarget::z(5), }; ASSERT_EQ(c.operations[0].targets, (SpanRef)expected); c = Circuit("MPP(0.125) X1*Y2 Z3 * Z4\nMPP Z5"); ASSERT_EQ(c.operations[0].args.size(), 1); ASSERT_EQ(c.operations[0].args[0], 0.125); c = Circuit("MPP X1*X1"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].targets.size(), 3); } TEST(circuit, parse_spp) { ASSERT_THROW({ Circuit("SPP 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("SPP rec[-1]"); }, std::invalid_argument); ASSERT_THROW({ Circuit("SPP sweep[0]"); }, std::invalid_argument); ASSERT_THROW({ Circuit("SPP rec[-1]*X0"); }, std::invalid_argument); Circuit c; c = Circuit("SPP"); ASSERT_EQ(c.operations.size(), 1); c = Circuit("SPP X0 X1*Y2*Z3"); ASSERT_EQ(c.operations.size(), 1); c = Circuit("SPP X1 Z2"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].targets.size(), 2); ASSERT_EQ( c.operations[0].targets, ((SpanRef)std::vector{GateTarget::x(1), GateTarget::z(2)})); } TEST(circuit, parse_spp_dag) { ASSERT_THROW({ Circuit("SPP_DAG 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("SPP_DAG rec[-1]"); }, std::invalid_argument); ASSERT_THROW({ Circuit("SPP_DAG sweep[0]"); }, std::invalid_argument); ASSERT_THROW({ Circuit("SPP_DAG rec[-1]*X0"); }, std::invalid_argument); Circuit c; c = Circuit("SPP_DAG"); ASSERT_EQ(c.operations.size(), 1); c = Circuit("SPP_DAG X0 X1*Y2*Z3"); ASSERT_EQ(c.operations.size(), 1); c = Circuit("SPP_DAG X1 Z2"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].targets.size(), 2); ASSERT_EQ( c.operations[0].targets, ((SpanRef)std::vector{GateTarget::x(1), GateTarget::z(2)})); } TEST(circuit, parse_tag) { Circuit c; c = Circuit("H[test] 3 5"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].gate_type, GateType::H); ASSERT_EQ(c.operations[0].tag, "test"); ASSERT_EQ(c.operations[0].targets.size(), 2); ASSERT_EQ(c.operations[0].targets[0], GateTarget::qubit(3)); ASSERT_EQ(c.operations[0].targets[1], GateTarget::qubit(5)); c = Circuit("H[] 3 5"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].gate_type, GateType::H); ASSERT_EQ(c.operations[0].tag, ""); ASSERT_EQ(c.operations[0].targets.size(), 2); c = Circuit("H 3 5"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].gate_type, GateType::H); ASSERT_EQ(c.operations[0].tag, ""); ASSERT_EQ(c.operations[0].targets.size(), 2); c = Circuit("H[test \\B\\C\\r\\n] 3 5"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].gate_type, GateType::H); ASSERT_EQ(c.operations[0].tag, "test \\]\r\n"); ASSERT_EQ(c.operations[0].targets.size(), 2); ASSERT_EQ(c.operations[0].str(), "H[test \\B\\C\\r\\n] 3 5"); c = Circuit(R"CIRCUIT( X_ERROR[test](0.125) X_ERROR[no_fuse](0.125) 1 X_ERROR(0.125) 2 X_ERROR[](0.125) 3 REPEAT[looper] 5 { CX[within] 0 1 } )CIRCUIT"); ASSERT_EQ(c.operations.size(), 4); ASSERT_EQ(c.operations[0].gate_type, GateType::X_ERROR); ASSERT_EQ(c.operations[1].gate_type, GateType::X_ERROR); ASSERT_EQ(c.operations[2].gate_type, GateType::X_ERROR); ASSERT_EQ(c.operations[0].tag, "test"); ASSERT_EQ(c.operations[1].tag, "no_fuse"); ASSERT_EQ(c.operations[2].tag, ""); ASSERT_EQ(c.operations[0].targets.size(), 0); ASSERT_EQ(c.operations[1].targets.size(), 1); ASSERT_EQ(c.operations[2].targets.size(), 2); ASSERT_EQ(c.operations[1].targets[0], GateTarget::qubit(1)); ASSERT_EQ(c.operations[2].targets[0], GateTarget::qubit(2)); ASSERT_EQ(c.operations[2].targets[1], GateTarget::qubit(3)); ASSERT_EQ(c.operations[3].gate_type, GateType::REPEAT); ASSERT_EQ(c.operations[3].tag, "looper"); ASSERT_EQ(c.operations[3].repeat_block_rep_count(), 5); ASSERT_EQ(c.str(), R"CIRCUIT(X_ERROR[test](0.125) X_ERROR[no_fuse](0.125) 1 X_ERROR(0.125) 2 3 REPEAT[looper] 5 { CX[within] 0 1 })CIRCUIT"); } TEST(circuit, parse_sweep_bits) { ASSERT_THROW({ Circuit("H sweep[0]"); }, std::invalid_argument); ASSERT_THROW({ Circuit("X sweep[0]"); }, std::invalid_argument); std::vector expected{ GateTarget::sweep_bit(2), GateTarget::qubit(5), }; Circuit c("CNOT sweep[2] 5"); ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.operations[0].targets, (SpanRef)expected); ASSERT_TRUE(c.operations[0].args.empty()); } TEST(circuit, append_circuit) { Circuit c1; c1.safe_append_u("X", {0, 1}); c1.safe_append_u("M", {0, 1, 2, 4}); Circuit c2; c2.safe_append_u("M", {7}); Circuit actual = c1; actual += c2; ASSERT_EQ(actual.operations.size(), 2); ASSERT_EQ(actual, Circuit("X 0 1\nM 0 1 2 4 7")); actual *= 4; ASSERT_EQ(actual.str(), R"CIRCUIT(REPEAT 4 { X 0 1 M 0 1 2 4 7 })CIRCUIT"); } TEST(circuit, append_op_fuse) { Circuit expected; Circuit actual; actual.clear(); expected.safe_append_u("H", {1, 2, 3}); actual.safe_append_u("H", {1}); actual.safe_append_u("H", {2, 3}); ASSERT_EQ(actual, expected); actual.safe_append_u("R", {0}); actual.safe_append_u("R", {}); expected.safe_append_u("R", {0}); ASSERT_EQ(actual, expected); actual.clear(); actual.safe_append_u("DETECTOR", {2 | TARGET_RECORD_BIT, 2 | TARGET_RECORD_BIT}); actual.safe_append_u("DETECTOR", {1 | TARGET_RECORD_BIT, 1 | TARGET_RECORD_BIT}); ASSERT_EQ(actual.operations.size(), 2); actual.clear(); actual.safe_append_u("TICK", {}); actual.safe_append_u("TICK", {}); ASSERT_EQ(actual.operations.size(), 2); actual.clear(); expected.clear(); actual.safe_append_u("M", {0, 1}); actual.safe_append_u("M", {2, 3}); expected.safe_append_u("M", {0, 1, 2, 3}); ASSERT_EQ(actual, expected); ASSERT_THROW({ actual.safe_append_u("CNOT", {0}); }, std::invalid_argument); ASSERT_THROW({ actual.safe_append_ua("X", {0}, 0.5); }, std::invalid_argument); } TEST(circuit, str) { Circuit c; c.safe_append_u("tick", {}); c.safe_append_u("CNOT", {2, 3}); c.safe_append_u("CNOT", {5 | TARGET_RECORD_BIT, 3}); c.safe_append_u("CY", {6 | TARGET_SWEEP_BIT, 4}); c.safe_append_u("M", {1, 3, 2}); c.safe_append_u("DETECTOR", {7 | TARGET_RECORD_BIT}); c.safe_append_ua("OBSERVABLE_INCLUDE", {11 | TARGET_RECORD_BIT, 1 | TARGET_RECORD_BIT}, 17); c.safe_append_ua("X_ERROR", {19}, 0.5); c.safe_append_ua( "CORRELATED_ERROR", { 23 | TARGET_PAULI_X_BIT, 27 | TARGET_PAULI_Z_BIT, 29 | TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT, }, 0.25); ASSERT_EQ(c.str(), R"circuit(TICK CX 2 3 rec[-5] 3 CY sweep[6] 4 M 1 3 2 DETECTOR rec[-7] OBSERVABLE_INCLUDE(17) rec[-11] rec[-1] X_ERROR(0.5) 19 E(0.25) X23 Z27 Y29)circuit"); } TEST(circuit, append_op_validation) { Circuit c; ASSERT_THROW({ c.safe_append_u("CNOT", {0}); }, std::invalid_argument); c.safe_append_u("CNOT", {0, 1}); ASSERT_THROW({ c.safe_append_u("REPEAT", {100}); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_u("X", {0 | TARGET_PAULI_X_BIT}); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_u("X", {0 | TARGET_PAULI_Z_BIT}); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_u("X", {0 | TARGET_INVERTED_BIT}); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_ua("X", {0}, 0.5); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_u("M", {0 | TARGET_PAULI_X_BIT}); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_u("M", {0 | TARGET_PAULI_Z_BIT}); }, std::invalid_argument); c.safe_append_u("M", {0 | TARGET_INVERTED_BIT}); c.safe_append_ua("M", {0 | TARGET_INVERTED_BIT}, 0.125); ASSERT_THROW({ c.safe_append_ua("M", {0}, 1.5); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_ua("M", {0}, -1.5); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_u("M", {0}, {0.125, 0.25}); }, std::invalid_argument); c.safe_append_ua("CORRELATED_ERROR", {0 | TARGET_PAULI_X_BIT}, 0.1); c.safe_append_ua("CORRELATED_ERROR", {0 | TARGET_PAULI_Z_BIT}, 0.1); ASSERT_THROW({ c.safe_append_u("CORRELATED_ERROR", {0 | TARGET_PAULI_X_BIT}); }, std::invalid_argument); ASSERT_THROW({ c.safe_append_u("CORRELATED_ERROR", {0 | TARGET_PAULI_X_BIT}, {0.1, 0.2}); }, std::invalid_argument); ASSERT_THROW( { c.safe_append_u("CORRELATED_ERROR", {0 | TARGET_PAULI_X_BIT | TARGET_INVERTED_BIT}); }, std::invalid_argument); c.safe_append_ua("X_ERROR", {0}, 0.5); ASSERT_THROW({ c.safe_append_u("CNOT", {0, 0}); }, std::invalid_argument); } TEST(circuit, repeat_validation) { ASSERT_THROW({ Circuit("REPEAT 100 {"); }, std::invalid_argument); ASSERT_THROW({ Circuit("REPEAT 100 {{\n}"); }, std::invalid_argument); ASSERT_THROW({ Circuit("REPEAT {\n}"); }, std::invalid_argument); ASSERT_THROW({ Circuit().append_from_text("REPEAT 100 {"); }, std::invalid_argument); ASSERT_THROW({ Circuit().append_from_text("REPEAT {\n}"); }, std::invalid_argument); ASSERT_THROW({ Circuit("H {"); }, std::invalid_argument); ASSERT_THROW({ Circuit("H {\n}"); }, std::invalid_argument); } TEST(circuit, tick_validation) { ASSERT_THROW({ Circuit("TICK 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit().safe_append_u("TICK", {1}); }, std::invalid_argument); } TEST(circuit, detector_validation) { ASSERT_THROW({ Circuit("DETECTOR 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit().safe_append_u("DETECTOR", {1}); }, std::invalid_argument); } TEST(circuit, x_error_validation) { Circuit("X_ERROR(0) 1"); Circuit("X_ERROR(0.1) 1"); Circuit("X_ERROR(1) 1"); ASSERT_THROW({ Circuit("X_ERROR 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("X_ERROR(-0.1) 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("X_ERROR(1.1) 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("X_ERROR(0.1, 0.1) 1"); }, std::invalid_argument); } TEST(circuit, pauli_err_1_validation) { Circuit("PAULI_CHANNEL_1(0,0,0) 1"); Circuit("PAULI_CHANNEL_1(0.1,0.2,0.6) 1"); Circuit("PAULI_CHANNEL_1(1,0,0) 1"); Circuit("PAULI_CHANNEL_1(0.33333333334,0.33333333334,0.33333333334) 1"); ASSERT_THROW({ Circuit("PAULI_CHANNEL_1 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("PAULI_CHANNEL_1(0.1) 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("PAULI_CHANNEL_1(0.1,0.1) 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("PAULI_CHANNEL_1(0.1,0.1,0.1,0.1) 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("PAULI_CHANNEL_1(-1,0,0) 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("PAULI_CHANNEL_1(0.1,0.5,0.6) 1"); }, std::invalid_argument); } TEST(circuit, pauli_err_2_validation) { Circuit("PAULI_CHANNEL_2(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) 1 2"); Circuit("PAULI_CHANNEL_2(0.1,0,0,0,0,0,0,0,0,0,0.1,0,0,0,0.1) 1 2"); ASSERT_THROW({ Circuit("PAULI_CHANNEL_2(0.1,0,0,0,0,0,0,0,0,0,0.1,0,0,0,0.1) 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit("PAULI_CHANNEL_2(0.4,0,0,0,0,0.4,0,0,0,0,0,0,0,0,0.4) 1 2"); }, std::invalid_argument); ASSERT_THROW({ Circuit("PAULI_CHANNEL_2(0,0,0,0,0,0,0,0,0,0,0,0,0,0) 1 2"); }, std::invalid_argument); ASSERT_THROW({ Circuit("PAULI_CHANNEL_2(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) 1 2"); }, std::invalid_argument); } TEST(circuit, qubit_coords) { ASSERT_THROW({ Circuit("TICK 1"); }, std::invalid_argument); ASSERT_THROW({ Circuit().safe_append_u("TICK", {1}); }, std::invalid_argument); } TEST(circuit, classical_controls) { ASSERT_THROW( { Circuit(R"circuit( M 0 XCX rec[-1] 1 )circuit"); }, std::invalid_argument); Circuit expected; expected.safe_append_u("CX", {0, 1, 1 | TARGET_RECORD_BIT, 1}); expected.safe_append_u("CY", {2 | TARGET_RECORD_BIT, 1}); expected.safe_append_u("CZ", {4 | TARGET_RECORD_BIT, 1}); ASSERT_EQ( Circuit(R"circuit(ZCX 0 1 ZCX rec[-1] 1 ZCY rec[-2] 1 ZCZ rec[-4] 1)circuit"), expected); } TEST(circuit, for_each_operation) { Circuit c; c.append_from_text(R"CIRCUIT( H 0 M 0 1 REPEAT 2 { X 1 REPEAT 3 { Y 2 } } )CIRCUIT"); Circuit flat; flat.safe_append_u("H", {0}); flat.safe_append_u("M", {0, 1}); flat.safe_append_u("X", {1}); flat.safe_append_u("Y", {2}); flat.operations.push_back(flat.operations.back()); flat.operations.push_back(flat.operations.back()); flat.safe_append_u("X", {1}); flat.safe_append_u("Y", {2}); flat.operations.push_back(flat.operations.back()); flat.operations.push_back(flat.operations.back()); std::vector ops; c.for_each_operation([&](const CircuitInstruction &op) { ops.push_back(op); }); ASSERT_EQ(ops, flat.operations); } TEST(circuit, for_each_operation_reverse) { Circuit c; c.append_from_text(R"CIRCUIT( H 0 M 0 1 REPEAT 2 { X 1 REPEAT 3 { Y 2 } } )CIRCUIT"); Circuit flat; flat.safe_append_u("Y", {2}); flat.operations.push_back(flat.operations.back()); flat.operations.push_back(flat.operations.back()); flat.safe_append_u("X", {1}); flat.safe_append_u("Y", {2}); flat.operations.push_back(flat.operations.back()); flat.operations.push_back(flat.operations.back()); flat.safe_append_u("X", {1}); flat.safe_append_u("M", {0, 1}); flat.safe_append_u("H", {0}); std::vector ops; c.for_each_operation_reverse([&](const CircuitInstruction &op) { ops.push_back(op); }); ASSERT_EQ(ops, flat.operations); } TEST(circuit, count_qubits) { ASSERT_EQ(Circuit().count_qubits(), 0); auto c = Circuit(R"CIRCUIT( H 0 M 0 1 REPEAT 2 { X 1 REPEAT 3 { Y 2 M 2 } } )CIRCUIT"); ASSERT_EQ(c.count_qubits(), 3); ASSERT_EQ(c.compute_stats().num_qubits, 3); // Ensure not unrolling to compute. ASSERT_EQ( Circuit(R"CIRCUIT( H 0 M 0 1 REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { X 1 REPEAT 999999 { Y 2 M 2 } } } } } )CIRCUIT") .count_qubits(), 3); } TEST(circuit, count_sweep_bits) { ASSERT_EQ(Circuit().count_sweep_bits(), 0); ASSERT_EQ( Circuit(R"CIRCUIT( H 0 M 0 1 REPEAT 2 { X 1 REPEAT 3 { CY 100 3 CY 80 3 M 2 } } )CIRCUIT") .count_sweep_bits(), 0); ASSERT_EQ( Circuit(R"CIRCUIT( H 0 M 0 1 REPEAT 2 { X 1 REPEAT 3 { CY sweep[100] 3 CY sweep[80] 3 M 2 } } )CIRCUIT") .count_sweep_bits(), 101); // Ensure not unrolling to compute. auto c = Circuit(R"CIRCUIT( H 0 M 0 1 REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { X 1 REPEAT 999999 { CY sweep[77] 3 M 2 } } } } } )CIRCUIT"); ASSERT_EQ(c.count_sweep_bits(), 78); ASSERT_EQ(c.compute_stats().num_sweep_bits, 78); } TEST(circuit, count_detectors_num_observables) { ASSERT_EQ(Circuit().count_detectors(), 0); ASSERT_EQ(Circuit().count_observables(), 0); auto c = Circuit(R"CIRCUIT( M 0 1 2 DETECTOR rec[-1] OBSERVABLE_INCLUDE(5) rec[-1] )CIRCUIT"); ASSERT_EQ(c.count_detectors(), 1); ASSERT_EQ(c.count_observables(), 6); ASSERT_EQ(c.compute_stats().num_detectors, 1); ASSERT_EQ(c.compute_stats().num_observables, 6); // Ensure not unrolling to compute. c = Circuit(R"CIRCUIT( M 0 1 REPEAT 1000 { REPEAT 1000 { REPEAT 1000 { REPEAT 1000 { DETECTOR rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] } } } } )CIRCUIT"); ASSERT_EQ(c.count_detectors(), 1000000000000ULL); ASSERT_EQ(c.count_observables(), 3); ASSERT_EQ(c.compute_stats().num_detectors, 1000000000000ULL); ASSERT_EQ(c.compute_stats().num_observables, 3); c = Circuit(R"CIRCUIT( M 0 1 REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { DETECTOR rec[-1] } } } } } } } } } } } } } )CIRCUIT"); ASSERT_EQ(c.count_detectors(), UINT64_MAX); ASSERT_EQ(c.count_observables(), 0); ASSERT_EQ(c.compute_stats().num_detectors, UINT64_MAX); ASSERT_EQ(c.compute_stats().num_observables, 0); } TEST(circuit, max_lookback) { ASSERT_EQ(Circuit().max_lookback(), 0); ASSERT_EQ( Circuit(R"CIRCUIT( M 0 1 2 3 4 5 6 )CIRCUIT") .max_lookback(), 0); ASSERT_EQ( Circuit(R"CIRCUIT( M 0 1 2 3 4 5 6 REPEAT 2 { CNOT rec[-4] 0 REPEAT 3 { CNOT rec[-1] 0 } } )CIRCUIT") .max_lookback(), 4); // Ensure not unrolling to compute. ASSERT_EQ( Circuit(R"CIRCUIT( M 0 1 2 3 4 5 REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { CNOT rec[-5] 0 } } } } } )CIRCUIT") .max_lookback(), 5); } TEST(circuit, count_measurements) { ASSERT_EQ(Circuit().count_measurements(), 0); auto c = Circuit(R"CIRCUIT( H 0 M 0 1 REPEAT 2 { X 1 REPEAT 3 { Y 2 M 2 } } )CIRCUIT"); ASSERT_EQ(c.count_measurements(), 8); ASSERT_EQ(c.compute_stats().num_measurements, 8); // Ensure not unrolling to compute. ASSERT_EQ( Circuit(R"CIRCUIT( REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { M 0 } } } )CIRCUIT") .count_measurements(), 999999ULL * 999999ULL * 999999ULL); c = Circuit(R"CIRCUIT( REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { REPEAT 999999 { M 0 } } } } } } } } } )CIRCUIT"); ASSERT_EQ(c.count_measurements(), UINT64_MAX); ASSERT_EQ(c.compute_stats().num_measurements, UINT64_MAX); ASSERT_EQ( Circuit(R"CIRCUIT( MPP X0 Z1 Y2 )CIRCUIT") .count_measurements(), 3); ASSERT_EQ( Circuit(R"CIRCUIT( MPP X0 Z1*Y2 )CIRCUIT") .count_measurements(), 2); ASSERT_EQ( Circuit(R"CIRCUIT( MPP X0*X1*X2*X3*X4 Z5 Z6 )CIRCUIT") .count_measurements(), 3); ASSERT_EQ( Circuit(R"CIRCUIT( MPP X0*X1*X2*X3*X4 Z5 Z6 )CIRCUIT") .operations[0] .count_measurement_results(), 3); ASSERT_EQ(Circuit("MXX 0 1 2 3").operations[0].count_measurement_results(), 2); ASSERT_EQ(Circuit("MYY 0 1 2 3").operations[0].count_measurement_results(), 2); ASSERT_EQ(Circuit("MZZ 0 1 2 3").operations[0].count_measurement_results(), 2); ASSERT_EQ( Circuit(R"CIRCUIT( MPP X0*X1 Z0*Z1 Y0*Y1 )CIRCUIT") .count_measurements(), 3); ASSERT_EQ( Circuit(R"CIRCUIT( HERALDED_ERASE(0.01) 0 1 2 )CIRCUIT") .count_measurements(), 3); } TEST(circuit, preserves_repetition_blocks) { Circuit c = Circuit(R"CIRCUIT( H 0 M 0 1 REPEAT 2 { X 1 REPEAT 3 { Y 2 M 2 X 0 } } )CIRCUIT"); ASSERT_EQ(c.operations.size(), 3); ASSERT_EQ(c.blocks.size(), 1); ASSERT_EQ(c.blocks[0].operations.size(), 2); ASSERT_EQ(c.blocks[0].blocks.size(), 1); ASSERT_EQ(c.blocks[0].blocks[0].operations.size(), 3); ASSERT_EQ(c.blocks[0].blocks[0].blocks.size(), 0); } TEST(circuit, multiplication_repeats) { Circuit c = Circuit(R"CIRCUIT( H 0 M 0 1 )CIRCUIT"); ASSERT_EQ(c * 2, Circuit(R"CIRCUIT( REPEAT 2 { H 0 M 0 1 } )CIRCUIT")); ASSERT_EQ((c * 2) * 3, Circuit(R"CIRCUIT( REPEAT 6 { H 0 M 0 1 } )CIRCUIT")); ASSERT_EQ(c * 0, Circuit()); ASSERT_EQ(c * 1, c); Circuit copy = c; c *= 1; ASSERT_EQ(c, copy); c *= 0; ASSERT_EQ(c, Circuit()); } TEST(circuit, self_addition) { Circuit c = Circuit(R"CIRCUIT( X 0 )CIRCUIT"); c += c; ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c.blocks.size(), 0); ASSERT_EQ(c, Circuit("X 0 0")); c = Circuit(R"CIRCUIT( X 0 Y 0 )CIRCUIT"); c += c; ASSERT_EQ(c.operations.size(), 4); ASSERT_EQ(c.blocks.size(), 0); ASSERT_EQ(c.operations[0], c.operations[2]); ASSERT_EQ(c.operations[1], c.operations[3]); ASSERT_EQ(c, Circuit("X 0\nY 0\nX 0\nY 0")); c = Circuit(R"CIRCUIT( X 0 REPEAT 2 { Y 0 } )CIRCUIT"); c += c; ASSERT_EQ(c.operations.size(), 4); ASSERT_EQ(c.blocks.size(), 1); ASSERT_EQ(c.operations[0], c.operations[2]); ASSERT_EQ(c.operations[1], c.operations[3]); } TEST(circuit, addition_shares_blocks) { Circuit c1 = Circuit(R"CIRCUIT( X 0 REPEAT 2 { X 1 } )CIRCUIT"); Circuit c2 = Circuit(R"CIRCUIT( X 2 REPEAT 2 { X 3 } )CIRCUIT"); Circuit c3 = Circuit(R"CIRCUIT( X 0 REPEAT 2 { X 1 } X 2 REPEAT 2 { X 3 } )CIRCUIT"); ASSERT_EQ(c1 + c2, c3); c1 += c2; ASSERT_EQ(c1, c3); } TEST(circuit, big_rep_count) { Circuit c = Circuit(R"CIRCUIT( REPEAT 1234567890123456789 { M 1 } )CIRCUIT"); ASSERT_EQ(c.operations[0].targets.size(), 3); ASSERT_EQ(c.operations[0].targets[0].data, 0); ASSERT_EQ(c.operations[0].targets[1].data, 1234567890123456789ULL & 0xFFFFFFFFULL); ASSERT_EQ(c.operations[0].targets[2].data, 1234567890123456789ULL >> 32); ASSERT_EQ(c.str(), "REPEAT 1234567890123456789 {\n M 1\n}"); ASSERT_EQ(c.count_measurements(), 1234567890123456789ULL); ASSERT_THROW({ c * 12345ULL; }, std::invalid_argument); } TEST(circuit, zero_repetitions_not_allowed) { ASSERT_ANY_THROW({ Circuit(R"CIRCUIT( REPEAT 0 { M 0 OBSERVABLE_INCLUDE(0) rec[-1] } )CIRCUIT"); }); } TEST(circuit, negative_float_coordinates) { auto c = Circuit(R"CIRCUIT( SHIFT_COORDS(-1, -2, -3) QUBIT_COORDS(1, -2) 1 QUBIT_COORDS(-3.5) 1 )CIRCUIT"); ASSERT_EQ(c.operations[0].args[2], -3); ASSERT_EQ(c.operations[2].args[0], -3.5); ASSERT_ANY_THROW({ Circuit("M(-0.1) 0"); }); c = Circuit("QUBIT_COORDS(1e20) 0"); ASSERT_EQ(c.operations[0].args[0], 1e20); c = Circuit("QUBIT_COORDS(1E+20) 0"); ASSERT_EQ(c.operations[0].args[0], 1E+20); ASSERT_ANY_THROW({ Circuit("QUBIT_COORDS(1e10000) 0"); }); } TEST(circuit, py_get_slice) { Circuit c(R"CIRCUIT( H 0 CNOT 0 1 M(0.125) 0 1 REPEAT 100 { E(0.25) X0 Y5 REPEAT 20 { H 0 } } H 1 REPEAT 999 { H 2 } )CIRCUIT"); ASSERT_EQ(c.py_get_slice(0, 1, 6), c); ASSERT_EQ(c.py_get_slice(0, 1, 4), Circuit(R"CIRCUIT( H 0 CNOT 0 1 M(0.125) 0 1 REPEAT 100 { E(0.25) X0 Y5 REPEAT 20 { H 0 } } )CIRCUIT")); ASSERT_EQ(c.py_get_slice(2, 1, 3), Circuit(R"CIRCUIT( M(0.125) 0 1 REPEAT 100 { E(0.25) X0 Y5 REPEAT 20 { H 0 } } H 1 )CIRCUIT")); ASSERT_EQ(c.py_get_slice(4, -1, 3), Circuit(R"CIRCUIT( H 1 REPEAT 100 { E(0.25) X0 Y5 REPEAT 20 { H 0 } } M(0.125) 0 1 )CIRCUIT")); ASSERT_EQ(c.py_get_slice(5, -2, 3), Circuit(R"CIRCUIT( REPEAT 999 { H 2 } REPEAT 100 { E(0.25) X0 Y5 REPEAT 20 { H 0 } } CNOT 0 1 )CIRCUIT")); Circuit c2 = c; Circuit c3 = c2.py_get_slice(0, 1, 6); c2.clear(); ASSERT_EQ(c, c3); } TEST(circuit, append_repeat_block) { Circuit c; Circuit b("X 0"); Circuit a("Y 0"); c.append_repeat_block(100, b, ""); ASSERT_EQ(c, Circuit(R"CIRCUIT( REPEAT 100 { X 0 } )CIRCUIT")); c.append_repeat_block(200, a, ""); ASSERT_EQ(c, Circuit(R"CIRCUIT( REPEAT 100 { X 0 } REPEAT 200 { Y 0 } )CIRCUIT")); c.append_repeat_block(400, std::move(b), ""); ASSERT_TRUE(b.operations.empty()); ASSERT_FALSE(a.operations.empty()); ASSERT_EQ(c, Circuit(R"CIRCUIT( REPEAT 100 { X 0 } REPEAT 200 { Y 0 } REPEAT 400 { X 0 } )CIRCUIT")); ASSERT_THROW({ c.append_repeat_block(0, a, ""); }, std::invalid_argument); ASSERT_THROW({ c.append_repeat_block(0, std::move(a), ""); }, std::invalid_argument); } TEST(circuit, aliased_noiseless_circuit) { Circuit initial(R"CIRCUIT( H 0 X_ERROR(0.1) 0 M(0.05) 0 M(0.15) 1 REPEAT 100 { CNOT 0 1 DEPOLARIZE2(0.1) 0 1 MPP(0.1) X0*X1 Z0 Z1 } )CIRCUIT"); Circuit noiseless = initial.aliased_noiseless_circuit(); ASSERT_EQ(noiseless, Circuit(R"CIRCUIT( H 0 M 0 1 REPEAT 100 { CNOT 0 1 MPP X0*X1 Z0 Z1 } )CIRCUIT")); Circuit c1 = initial.without_noise(); Circuit c2 = c1; ASSERT_EQ(c1, noiseless); initial.clear(); ASSERT_EQ(c1, c2); ASSERT_EQ(Circuit("H 0\nX_ERROR(0.01) 0\nH 0").without_noise(), Circuit("H 0 0")); } TEST(circuit, noiseless_heralded_erase) { Circuit noisy(R"CIRCUIT( M 0 1 MPAD 1 HERALDED_ERASE(0.01) 2 3 0 1 MPAD 1 M 2 0 DETECTOR rec[-1] rec[-8] )CIRCUIT"); Circuit noiseless(R"CIRCUIT( M 0 1 MPAD 1 0 0 0 0 1 M 2 0 DETECTOR rec[-1] rec[-8] )CIRCUIT"); ASSERT_EQ(noisy.aliased_noiseless_circuit(), noiseless); ASSERT_EQ(noisy.without_noise(), noiseless); } TEST(circuit, validate_nan_probability) { Circuit c; ASSERT_THROW({ c.safe_append_ua("X_ERROR", {0}, NAN); }, std::invalid_argument); } TEST(circuit, validate_mpad) { Circuit c; c.append_from_text("MPAD 0 1"); ASSERT_THROW({ c.append_from_text("MPAD 2"); }, std::invalid_argument); ASSERT_THROW({ c.append_from_text("MPAD sweep[-1]"); }, std::invalid_argument); } TEST(circuit, get_final_qubit_coords) { ASSERT_EQ(Circuit().get_final_qubit_coords(), (std::map>{})); ASSERT_EQ( Circuit(R"CIRCUIT( QUBIT_COORDS(1, 2) 3 )CIRCUIT") .get_final_qubit_coords(), (std::map>{ {3, {1, 2}}, })); ASSERT_EQ( Circuit(R"CIRCUIT( SHIFT_COORDS(10, 20, 30) QUBIT_COORDS(4, 5) 3 )CIRCUIT") .get_final_qubit_coords(), (std::map>{ {3, {14, 25}}, })); ASSERT_EQ( Circuit(R"CIRCUIT( QUBIT_COORDS(1, 2, 3, 4) 1 REPEAT 100 { SHIFT_COORDS(10, 20, 30) } QUBIT_COORDS(4, 5) 3 QUBIT_COORDS(6) 4 )CIRCUIT") .get_final_qubit_coords(), (std::map>{ {1, {1, 2, 3, 4}}, {3, {1004, 2005}}, {4, {1006}}, })); } TEST(circuit, get_final_qubit_coords_huge_repetition_count_efficiency) { auto actual = Circuit(R"CIRCUIT( QUBIT_COORDS(0) 0 REPEAT 1000 { QUBIT_COORDS(1, 1) 1 REPEAT 2000 { QUBIT_COORDS(2, 0.5) 2 REPEAT 4000 { QUBIT_COORDS(3) 3 REPEAT 8000 { QUBIT_COORDS(4) 4 SHIFT_COORDS(100) QUBIT_COORDS(5) 5 } SHIFT_COORDS(10) QUBIT_COORDS(6) 6 } QUBIT_COORDS(7) 7 } QUBIT_COORDS(8) 8 } QUBIT_COORDS(9) 9 )CIRCUIT") .get_final_qubit_coords(); ASSERT_EQ( actual, (std::map>{ {0, {0}}, {1, {6400080000000001 - 6400080000000, 1}}, {2, {6400080000000002 - 3200040000, 0.5}}, {3, {6400080000000003 - 800010}}, {4, {6400080000000004 - 110}}, {5, {6400080000000005 - 10}}, {6, {6400080000000006}}, {7, {6400080000000007}}, {8, {6400080000000008}}, {9, {6400080000000009}}, })); // Precision sanity check. ASSERT_EQ(6400080000000009, (uint64_t)(double)6400080000000009); } TEST(circuit, count_ticks) { ASSERT_EQ(Circuit().count_ticks(), 0); ASSERT_EQ( Circuit(R"CIRCUIT( TICK )CIRCUIT") .count_ticks(), 1); ASSERT_EQ( Circuit(R"CIRCUIT( TICK TICK )CIRCUIT") .count_ticks(), 2); ASSERT_EQ( Circuit(R"CIRCUIT( TICK H 0 TICK )CIRCUIT") .count_ticks(), 2); auto c = Circuit(R"CIRCUIT( TICK REPEAT 1000 { REPEAT 2000 { REPEAT 1000 { TICK } TICK TICK TICK } } TICK )CIRCUIT"); ASSERT_EQ(c.count_ticks(), 2006000002); ASSERT_EQ(c.compute_stats().num_ticks, 2006000002); } TEST(circuit, coords_of_detector) { Circuit c(R"CIRCUIT( TICK REPEAT 1000 { REPEAT 2000 { REPEAT 1000 { DETECTOR(0, 0, 0, 4) SHIFT_COORDS(1, 0, 0) } DETECTOR(0, 0, 0, 3) SHIFT_COORDS(0, 1, 0) } DETECTOR(0, 0, 0, 2) SHIFT_COORDS(0, 0, 1) } DETECTOR(0, 0, 0, 1) )CIRCUIT"); ASSERT_EQ(c.coords_of_detector(0), (std::vector{0, 0, 0, 4})); ASSERT_EQ(c.coords_of_detector(1), (std::vector{1, 0, 0, 4})); ASSERT_EQ(c.coords_of_detector(999), (std::vector{999, 0, 0, 4})); ASSERT_EQ(c.coords_of_detector(1000), (std::vector{1000, 0, 0, 3})); ASSERT_EQ(c.coords_of_detector(1001), (std::vector{1000, 1, 0, 4})); ASSERT_EQ(c.coords_of_detector(1002), (std::vector{1001, 1, 0, 4})); ASSERT_THROW({ c.get_detector_coordinates({4000000000}); }, std::invalid_argument); auto result = c.get_detector_coordinates({ 0, 1, 999, 1000, 1001, 1002, }); ASSERT_EQ( result, (std::map>{ {0, {0, 0, 0, 4}}, {1, {1, 0, 0, 4}}, {999, {999, 0, 0, 4}}, {1000, {1000, 0, 0, 3}}, {1001, {1000, 1, 0, 4}}, {1002, {1001, 1, 0, 4}}, })); } TEST(circuit, final_coord_shift) { Circuit c(R"CIRCUIT( REPEAT 1000 { REPEAT 2000 { REPEAT 3000 { SHIFT_COORDS(0, 0, 1) } SHIFT_COORDS(1) } SHIFT_COORDS(0, 1) } )CIRCUIT"); ASSERT_EQ(c.final_coord_shift(), (std::vector{2000000, 1000, 6000000000})); } TEST(circuit, concat_fuse) { Circuit c1("H 0"); Circuit c2("H 1"); Circuit c3 = c1 + c2; ASSERT_EQ(c3.operations.size(), 1); ASSERT_EQ(c3, Circuit("H 0 1")); } TEST(circuit, concat_self_fuse) { Circuit c("H 0"); c += c; ASSERT_EQ(c.operations.size(), 1); ASSERT_EQ(c, Circuit("H 0 0")); } TEST(circuit, assignment_copies_operations) { Circuit c1("H 0 1\nS 0"); Circuit c2("TICK"); ASSERT_EQ(c1.operations.size(), 2); ASSERT_EQ(c2.operations.size(), 1); c2 = c1; ASSERT_EQ(c2.operations.size(), 2); ASSERT_EQ(c1, c2); } TEST(circuit, flattened) { ASSERT_EQ(Circuit().flattened(), Circuit()); ASSERT_EQ(Circuit("SHIFT_COORDS(1, 2)").flattened(), Circuit()); ASSERT_EQ(Circuit("H 1").flattened(), Circuit("H 1")); ASSERT_EQ(Circuit("REPEAT 100 {\n}").flattened(), Circuit()); ASSERT_EQ(Circuit("REPEAT 3 {\nH 0\n}").flattened(), Circuit("H 0 0 0")); } TEST(circuit, approx_equals) { Circuit ref(R"CIRCUIT( H 0 X_ERROR(0.02) 0 QUBIT_COORDS(0.08, 0.06) 0 )CIRCUIT"); ASSERT_TRUE(ref.approx_equals(ref, 1e-4)); ASSERT_TRUE(ref.approx_equals(ref, 0)); ASSERT_FALSE(ref.approx_equals( Circuit(R"CIRCUIT( H 0 X_ERROR(0.021) 0 QUBIT_COORDS(0.08, 0.06) 0 )CIRCUIT"), 1e-4)); ASSERT_FALSE(ref.approx_equals( Circuit(R"CIRCUIT( H 0 X_ERROR(0.02) 0 QUBIT_COORDS(0.081, 0.06) 0 )CIRCUIT"), 1e-4)); ASSERT_FALSE(ref.approx_equals( Circuit(R"CIRCUIT( H 0 X_ERROR(0.02) 0 QUBIT_COORDS(0.08, 0.06) 0 TICK )CIRCUIT"), 1e-4)); ASSERT_FALSE(ref.approx_equals( Circuit(R"CIRCUIT( H 0 X_ERROR(0.02) 0 REPEAT 1 { QUBIT_COORDS(0.08, 0.06) 0 } )CIRCUIT"), 1e-4)); ASSERT_TRUE(ref.approx_equals( Circuit(R"CIRCUIT( H 0 X_ERROR(0.021) 0 QUBIT_COORDS(0.081, 0.06) 0 )CIRCUIT"), 1e-2)); ASSERT_FALSE(ref.approx_equals( Circuit(R"CIRCUIT( H 1 X_ERROR(0.02) 0 QUBIT_COORDS(0.08, 0.06) 0 )CIRCUIT"), 100)); ASSERT_FALSE(ref.approx_equals( Circuit(R"CIRCUIT( H 0 QUBIT_COORDS(0.08, 0.06) 0 X_ERROR(0.02) 0 )CIRCUIT"), 100)); Circuit rep2(R"CIRCUIT( H 0 X_ERROR(0.02) 0 REPEAT 1 { QUBIT_COORDS(0.08, 0.06) 0 } )CIRCUIT"); ASSERT_FALSE(ref.approx_equals(rep2, 100)); ASSERT_TRUE(rep2.approx_equals(rep2, 0)); } TEST(circuit, equality) { Circuit a(R"CIRCUIT( H 0 REPEAT 100 { X_ERROR(0.25) 1 } )CIRCUIT"); Circuit b(R"CIRCUIT( H 1 REPEAT 100 { X_ERROR(0.25) 1 } )CIRCUIT"); Circuit c(R"CIRCUIT( H 0 REPEAT 100 { X_ERROR(0.125) 1 } )CIRCUIT"); ASSERT_FALSE(a == b); ASSERT_FALSE(a == c); ASSERT_FALSE(b == c); ASSERT_TRUE(a == Circuit(a)); ASSERT_TRUE(b == Circuit(b)); ASSERT_TRUE(c == Circuit(c)); ASSERT_TRUE(a != b); ASSERT_TRUE(a != c); ASSERT_TRUE(b != c); ASSERT_FALSE(a != Circuit(a)); ASSERT_FALSE(b != Circuit(b)); ASSERT_FALSE(c != Circuit(c)); } TEST(circuit, parse_windows_newlines) { ASSERT_EQ(Circuit("H 0\r\nCX 0 1\r\n"), Circuit("H 0\nCX 0 1\n")); } TEST(circuit, inverse) { ASSERT_EQ( Circuit(R"CIRCUIT( H 0 CX 0 1 S 1 )CIRCUIT") .inverse(), Circuit(R"CIRCUIT( S_DAG 1 CX 0 1 H 0 )CIRCUIT")); ASSERT_EQ( Circuit(R"CIRCUIT( TICK )CIRCUIT") .inverse(), Circuit(R"CIRCUIT( TICK )CIRCUIT")); ASSERT_EQ( Circuit(R"CIRCUIT( QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(1, 3) 1 CY 0 1 SQRT_X_DAG 1 )CIRCUIT") .inverse(), Circuit(R"CIRCUIT( QUBIT_COORDS(1, 2) 0 QUBIT_COORDS(1, 3) 1 SQRT_X 1 CY 0 1 )CIRCUIT")); ASSERT_EQ( Circuit(R"CIRCUIT( CX 0 1 2 3 4 5 C_XYZ 6 7 8 9 )CIRCUIT") .inverse(), Circuit(R"CIRCUIT( C_ZYX 9 8 7 6 CX 4 5 2 3 0 1 )CIRCUIT")); ASSERT_EQ( Circuit(R"CIRCUIT( S_DAG 0 REPEAT 100 { ISWAP 0 1 1 2 TICK CZ CX 0 1 REPEAT 50 { } } H 1 2 )CIRCUIT") .inverse(), Circuit(R"CIRCUIT( H 2 1 REPEAT 100 { REPEAT 50 { } CX 0 1 CZ TICK ISWAP_DAG 1 2 0 1 } S 0 )CIRCUIT")); ASSERT_EQ( Circuit(R"CIRCUIT( SHIFT_COORDS(-2, 3) TICK TICK SHIFT_COORDS(4) TICK )CIRCUIT") .inverse(), Circuit(R"CIRCUIT( TICK SHIFT_COORDS(-4) TICK TICK SHIFT_COORDS(2, -3) )CIRCUIT")); ASSERT_EQ( Circuit(R"CIRCUIT( X_ERROR(0.125) 0 1 Y_ERROR(0.125) 1 2 Z_ERROR(0.125) 2 3 PAULI_CHANNEL_1(0.125, 0.25, 0) 0 1 0 0 PAULI_CHANNEL_2(0, 0.125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 5 7 2 3 DEPOLARIZE1(0.25) 4 5 6 DEPOLARIZE2(0.25) 1 2 3 4 REPEAT 2 { MX(0.125) 3 4 MY(0.125) 5 6 M(0.125) 7 8 } MRX(0.125) 9 10 MRY(0.125) 11 12 MR(0.125) 13 14 RX 15 16 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] RY 17 18 R 19 20 MPP(0.125) X0*X1 Y2*Y3*Y4 Z5*Y6 )CIRCUIT") .inverse(true), Circuit(R"CIRCUIT( MPP(0.125) Y6*Z5 Y4*Y3*Y2 X1*X0 M 20 19 MY 18 17 MX 16 15 MR(0.125) 14 13 MRY(0.125) 12 11 MRX(0.125) 10 9 REPEAT 2 { M(0.125) 8 7 MY(0.125) 6 5 MX(0.125) 4 3 } DEPOLARIZE2(0.25) 3 4 1 2 DEPOLARIZE1(0.25) 6 5 4 PAULI_CHANNEL_2(0, 0.125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 2 3 5 7 PAULI_CHANNEL_1(0.125, 0.25, 0) 0 0 1 0 Z_ERROR(0.125) 3 2 Y_ERROR(0.125) 2 1 X_ERROR(0.125) 1 0 )CIRCUIT")); ASSERT_THROW({ Circuit("X_ERROR(0.125) 0").inverse(); }, std::invalid_argument); ASSERT_THROW({ Circuit("M(0.125) 0").inverse(); }, std::invalid_argument); ASSERT_THROW({ Circuit("M 0").inverse(); }, std::invalid_argument); ASSERT_THROW({ Circuit("R 0").inverse(); }, std::invalid_argument); ASSERT_THROW({ Circuit("MR 0").inverse(); }, std::invalid_argument); ASSERT_THROW({ Circuit("MPP X0*X1").inverse(); }, std::invalid_argument); ASSERT_THROW({ Circuit("DETECTOR").inverse(); }, std::invalid_argument); ASSERT_THROW({ Circuit("OBSERVABLE_INCLUDE").inverse(); }, std::invalid_argument); ASSERT_THROW({ Circuit("ELSE_CORRELATED_ERROR(0.125) X0").inverse(true); }, std::invalid_argument); } Circuit stim::generate_test_circuit_with_all_operations() { return Circuit(R"CIRCUIT( QUBIT_COORDS(1, 2, 3) 0 # Pauli gates I 0 X 1 Y 2 Z 3 TICK # Single Qubit Clifford Gates C_XYZ 0 C_NXYZ 1 C_XNYZ 2 C_XYNZ 3 C_ZYX 4 C_NZYX 5 C_ZNYX 6 C_ZYNX 7 H_XY 0 H_XZ 1 H_YZ 2 H_NXY 3 H_NXZ 4 H_NYZ 5 SQRT_X 0 SQRT_X_DAG 1 SQRT_Y 2 SQRT_Y_DAG 3 SQRT_Z 4 SQRT_Z_DAG 5 TICK # Two Qubit Clifford Gates CXSWAP 0 1 ISWAP 2 3 ISWAP_DAG 4 5 SWAP 6 7 SWAPCX 8 9 CZSWAP 10 11 SQRT_XX 0 1 SQRT_XX_DAG 2 3 SQRT_YY 4 5 SQRT_YY_DAG 6 7 SQRT_ZZ 8 9 SQRT_ZZ_DAG 10 11 II 12 13 XCX 0 1 XCY 2 3 XCZ 4 5 YCX 6 7 YCY 8 9 YCZ 10 11 ZCX 12 13 ZCY 14 15 ZCZ 16 17 TICK # Noise Channels CORRELATED_ERROR(0.01) X1 Y2 Z3 ELSE_CORRELATED_ERROR(0.02) X4 Y7 Z6 DEPOLARIZE1(0.02) 0 DEPOLARIZE2(0.03) 1 2 PAULI_CHANNEL_1(0.01, 0.02, 0.03) 3 PAULI_CHANNEL_2(0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009, 0.010, 0.011, 0.012, 0.013, 0.014, 0.015) 4 5 X_ERROR(0.01) 0 Y_ERROR(0.02) 1 Z_ERROR(0.03) 2 HERALDED_ERASE(0.04) 3 HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.03, 0.04) 6 I_ERROR(0.06) 7 II_ERROR(0.07) 8 9 TICK # Pauli Product Gates MPP X0*Y1*Z2 Z0*Z1 SPP X0*Y1*Z2 X3 SPP_DAG X0*Y1*Z2 X2 TICK # Collapsing Gates MRX 0 MRY 1 MRZ 2 MX 3 MY 4 MZ 5 6 RX 7 RY 8 RZ 9 TICK # Pair Measurement Gates MXX 0 1 2 3 MYY 4 5 MZZ 6 7 TICK # Control Flow REPEAT 3 { H 0 CX 0 1 S 1 TICK } TICK # Annotations MR 0 X_ERROR(0.1) 0 MR(0.01) 0 SHIFT_COORDS(1, 2, 3) DETECTOR(1, 2, 3) rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] MPAD 0 1 0 OBSERVABLE_INCLUDE(1) Z2 Z3 TICK # Inverted measurements. MRX !0 MY !1 MZZ !2 3 OBSERVABLE_INCLUDE(1) rec[-1] MYY !4 !5 MPP X6*!Y7*Z8 TICK # Feedback CX rec[-1] 0 CY sweep[0] 1 CZ 2 rec[-1] )CIRCUIT"); } TEST(circuit, generate_test_circuit_with_all_operations) { auto c = generate_test_circuit_with_all_operations(); std::set seen{GateType::NOT_A_GATE}; for (const auto &instruction : c.operations) { seen.insert(instruction.gate_type); } ASSERT_EQ(seen.size(), NUM_DEFINED_GATES); } TEST(circuit, insert_circuit) { Circuit c(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT"); c.safe_insert(2, Circuit(R"CIRCUIT( H 1 X 3 S 2 )CIRCUIT")); ASSERT_EQ(c.operations.size(), 5); ASSERT_EQ(c, Circuit(R"CIRCUIT( CX 0 1 H 0 1 X 3 S 2 0 CX 0 1 )CIRCUIT")); c = Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT"); c.safe_insert(0, Circuit(R"CIRCUIT( H 1 X 3 S 2 )CIRCUIT")); ASSERT_EQ(c, Circuit(R"CIRCUIT( H 1 X 3 S 2 CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT")); c = Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT"); c.safe_insert(4, Circuit(R"CIRCUIT( H 1 X 3 S 2 )CIRCUIT")); ASSERT_EQ(c, Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 H 1 X 3 S 2 )CIRCUIT")); } TEST(circuit, insert_instruction) { Circuit c = Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT"); c.safe_insert(2, Circuit("H 1").operations[0]); ASSERT_EQ(c, Circuit(R"CIRCUIT( CX 0 1 H 0 1 S 0 CX 0 1 )CIRCUIT")); c = Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT"); c.safe_insert(2, Circuit("S 1").operations[0]); ASSERT_EQ(c, Circuit(R"CIRCUIT( CX 0 1 H 0 S 1 0 CX 0 1 )CIRCUIT")); c = Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT"); c.safe_insert(2, Circuit("X 1").operations[0]); ASSERT_EQ(c, Circuit(R"CIRCUIT( CX 0 1 H 0 X 1 S 0 CX 0 1 )CIRCUIT")); c = Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT"); c.safe_insert(0, Circuit("X 1").operations[0]); ASSERT_EQ(c, Circuit(R"CIRCUIT( X 1 CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT")); c = Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 )CIRCUIT"); c.safe_insert(4, Circuit("X 1").operations[0]); ASSERT_EQ(c, Circuit(R"CIRCUIT( CX 0 1 H 0 S 0 CX 0 1 X 1 )CIRCUIT")); } TEST(circuit, without_tags) { Circuit initial(R"CIRCUIT( H[test-1] 0 REPEAT[test-2] 100 { REPEAT[test-3] 100 { M[test-4](0.125) 0 } } )CIRCUIT"); ASSERT_EQ(initial.without_tags(), Circuit(R"CIRCUIT( H 0 REPEAT 100 { REPEAT 100 { M(0.125) 0 } } )CIRCUIT")); } ================================================ FILE: src/stim/circuit/circuit.test.h ================================================ /* * 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. */ #ifndef _STIM_CIRCUIT_TEST_H #define _STIM_CIRCUIT_TEST_H #include "stim/circuit/circuit.h" namespace stim { stim::Circuit generate_test_circuit_with_all_operations(); } // namespace stim #endif ================================================ FILE: src/stim/circuit/circuit2.pybind.cc ================================================ // 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. #include #include "stim/circuit/circuit.pybind.h" #include "stim/cmd/command_help.h" #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/util_top/circuit_flow_generators.h" #include "stim/util_top/circuit_inverse_qec.h" #include "stim/util_top/circuit_to_detecting_regions.h" #include "stim/util_top/circuit_vs_tableau.h" #include "stim/util_top/count_determined_measurements.h" #include "stim/util_top/export_crumble_url.h" #include "stim/util_top/export_qasm.h" #include "stim/util_top/export_quirk_url.h" #include "stim/util_top/has_flow.h" #include "stim/util_top/missing_detectors.h" #include "stim/util_top/simplified_circuit.h" #include "stim/util_top/transform_without_feedback.h" using namespace stim; using namespace stim_pybind; static std::set py_dem_filter_to_dem_target_set( const Circuit &circuit, const CircuitStats &stats, const pybind11::object &included_targets_filter) { std::set result; auto add_all_dets = [&]() { for (uint64_t k = 0; k < stats.num_detectors; k++) { result.insert(DemTarget::relative_detector_id(k)); } }; auto add_all_obs = [&]() { for (uint64_t k = 0; k < stats.num_observables; k++) { result.insert(DemTarget::observable_id(k)); } }; bool has_coords = false; std::map> cached_coords; auto get_coords_cached = [&]() -> const std::map> & { std::set all_dets; for (uint64_t k = 0; k < stats.num_detectors; k++) { all_dets.insert(k); } if (!has_coords) { cached_coords = circuit.get_detector_coordinates(all_dets); has_coords = true; } return cached_coords; }; if (included_targets_filter.is_none()) { add_all_dets(); add_all_obs(); return result; } for (const auto &filter : included_targets_filter) { bool fail = false; if (pybind11::isinstance(filter)) { result.insert(pybind11::cast(filter)); } else if (pybind11::isinstance(filter)) { std::string_view s = pybind11::cast(filter); if (s == "D") { add_all_dets(); } else if (s == "L") { add_all_obs(); } else if (s.starts_with("D") || s.starts_with("L")) { result.insert(DemTarget::from_text(s)); } else { fail = true; } } else { std::vector prefix; for (auto e : filter) { if (pybind11::isinstance(e) || pybind11::isinstance(e)) { prefix.push_back(pybind11::cast(e)); } else { fail = true; break; } } if (!fail) { for (const auto &[target, coord] : get_coords_cached()) { if (coord.size() >= prefix.size()) { bool match = true; for (size_t k = 0; k < prefix.size(); k++) { match &= prefix[k] == coord[k]; } if (match) { result.insert(DemTarget::relative_detector_id(target)); } } } } } if (fail) { std::stringstream ss; ss << "Don't know how to interpret '"; ss << pybind11::cast(pybind11::repr(filter)); ss << "' as a dem target filter."; throw std::invalid_argument(ss.str()); } } return result; } void stim_pybind::pybind_circuit_methods_extra(pybind11::module &, pybind11::class_ &c) { c.def( "detecting_regions", [](const Circuit &self, const pybind11::object &included_targets, const pybind11::object &included_ticks, bool ignore_anticommutation_errors) -> std::map> { auto stats = self.compute_stats(); auto included_target_set = py_dem_filter_to_dem_target_set(self, stats, included_targets); std::set included_tick_set; if (included_ticks.is_none()) { for (uint64_t k = 0; k < stats.num_ticks; k++) { included_tick_set.insert(k); } } else { for (const auto &t : included_ticks) { included_tick_set.insert(pybind11::cast(t)); } } auto result = circuit_to_detecting_regions( self, included_target_set, included_tick_set, ignore_anticommutation_errors); std::map> exposed_result; for (const auto &[k, v] : result) { exposed_result.insert({ExposedDemTarget(k), std::move(v)}); } return exposed_result; }, pybind11::kw_only(), pybind11::arg("targets") = pybind11::none(), pybind11::arg("ticks") = pybind11::none(), pybind11::arg("ignore_anticommutation_errors") = false, clean_doc_string(R"DOC( @signature def detecting_regions(self, *, targets: Optional[Iterable[stim.DemTarget | str | Iterable[float]]] = None, ticks: Optional[Iterable[int]] = None) -> Dict[stim.DemTarget, Dict[int, stim.PauliString]]: Records where detectors and observables are sensitive to errors over time. The result of this method is a nested dictionary, mapping detectors/observables and ticks to Pauli sensitivities for that detector/observable at that time. For example, if observable 2 has Z-type sensitivity on qubits 5 and 6 during tick 3, then `result[stim.target_logical_observable_id(2)][3]` will be equal to `stim.PauliString("Z5*Z6")`. If you want sensitivities from more places in the circuit, besides just at the TICK instructions, you can work around this by making a version of the circuit with more TICKs. Args: targets: Defaults to everything (None). When specified, this should be an iterable of filters where items matching any one filter are included. A variety of filters are supported: stim.DemTarget: Includes the targeted detector or observable. Iterable[float]: Coordinate prefix match. Includes detectors whose coordinate data begins with the same floats. "D": Includes all detectors. "L": Includes all observables. "D#" (e.g. "D5"): Includes the detector with the specified index. "L#" (e.g. "L5"): Includes the observable with the specified index. ticks: Defaults to everything (None). When specified, this should be a list of integers corresponding to the tick indices to report sensitivities for. ignore_anticommutation_errors: Defaults to False. When set to False, invalid detecting regions that anticommute with a reset will cause the method to raise an exception. When set to True, the offending component will simply be silently dropped. This can result in broken detectors having apparently enormous detecting regions. Returns: Nested dictionaries keyed first by a `stim.DemTarget` identifying the detector or observable, then by the index of the tick, leading to a PauliString with that target's error sensitivity at that tick. Note you can use `stim.PauliString.pauli_indices` to quickly get to the non-identity terms in the sensitivity. Examples: >>> import stim >>> detecting_regions = stim.Circuit(''' ... R 0 ... TICK ... H 0 ... TICK ... CX 0 1 ... TICK ... MX 0 1 ... DETECTOR rec[-1] rec[-2] ... ''').detecting_regions() >>> for target, tick_regions in detecting_regions.items(): ... print("target", target) ... for tick, sensitivity in tick_regions.items(): ... print(" tick", tick, "=", sensitivity) target D0 tick 0 = +Z_ tick 1 = +X_ tick 2 = +XX >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... rounds=5, ... distance=4, ... ) >>> detecting_regions = circuit.detecting_regions( ... targets=["L0", (2, 4), stim.DemTarget.relative_detector_id(5)], ... ticks=range(5, 15), ... ) >>> for target, tick_regions in detecting_regions.items(): ... print("target", target) ... for tick, sensitivity in tick_regions.items(): ... print(" tick", tick, "=", sensitivity) target D1 tick 5 = +____________________X______________________ tick 6 = +____________________Z______________________ target D5 tick 5 = +______X____________________________________ tick 6 = +______Z____________________________________ target D14 tick 5 = +__________X_X______XXX_____________________ tick 6 = +__________X_X______XZX_____________________ tick 7 = +__________X_X______XZX_____________________ tick 8 = +__________X_X______XXX_____________________ tick 9 = +__________XXX_____XXX______________________ tick 10 = +__________XXX_______X______________________ tick 11 = +__________X_________X______________________ tick 12 = +____________________X______________________ tick 13 = +____________________Z______________________ target D29 tick 7 = +____________________Z______________________ tick 8 = +____________________X______________________ tick 9 = +____________________XX_____________________ tick 10 = +___________________XXX_______X_____________ tick 11 = +____________X______XXXX______X_____________ tick 12 = +__________X_X______XXX_____________________ tick 13 = +__________X_X______XZX_____________________ tick 14 = +__________X_X______XZX_____________________ target D44 tick 14 = +____________________Z______________________ target L0 tick 5 = +_X________X________X________X______________ tick 6 = +_X________X________X________X______________ tick 7 = +_X________X________X________X______________ tick 8 = +_X________X________X________X______________ tick 9 = +_X________X_______XX________X______________ tick 10 = +_X________X________X________X______________ tick 11 = +_X________XX_______X________XX_____________ tick 12 = +_X________X________X________X______________ tick 13 = +_X________X________X________X______________ tick 14 = +_X________X________X________X______________ )DOC") .data()); c.def( "count_determined_measurements", &count_determined_measurements, pybind11::kw_only(), pybind11::arg("unknown_input") = false, clean_doc_string(R"DOC( @signature def count_determined_measurements(self, *, unknown_input: bool = False) -> int: Counts the number of predictable measurements in the circuit. This method ignores any noise in the circuit. This method works by performing a tableau stabilizer simulation of the circuit and, before each measurement is simulated, checking if its expectation is non-zero. A measurement is predictable if its result can be predicted by using other measurements that have already been performed, assuming the circuit is executed without any noise. Note that, when multiple measurements occur at the same time, re-ordering the order they are resolved can change which specific measurements are predictable but won't change how many of them were predictable in total. The number of predictable measurements is a useful quantity because it's related to the number of detectors and observables that a circuit should declare. If circuit.num_detectors + circuit.num_observables is less than circuit.count_determined_measurements(), this is a warning sign that you've missed some detector declarations. The exact relationship between the number of determined measurements and the number of detectors and observables can differ from code to code. For example, the toric code has an extra redundant measurement compared to the surface code because in the toric code the last X stabilizer to be measured is equal to the product of all other X stabilizers even in the first round when initializing in the Z basis. Typically this relationship is not declared as a detector, because it's not local, or as an observable, because it doesn't store a qubit. Args: unknown_input: Defaults to False (inputs assumed to be in the |0> state). When set to True, the inputs are instead treated as being in unknown random states. For example, this means that Z-basis measurements at the very beginning of the circuit will be considered random rather than determined. Returns: The number of measurements that were predictable. Examples: >>> import stim >>> stim.Circuit(''' ... R 0 ... M 0 ... ''').count_determined_measurements() 1 >>> stim.Circuit(''' ... R 0 ... H 0 ... M 0 ... ''').count_determined_measurements() 0 >>> stim.Circuit(''' ... M 0 ... ''').count_determined_measurements() 1 >>> stim.Circuit(''' ... M 0 ... ''').count_determined_measurements(unknown_input=True) 0 >>> stim.Circuit(''' ... M 0 ... M 0 1 ... M 0 1 2 ... M 0 1 2 3 ... ''').count_determined_measurements(unknown_input=True) 6 >>> stim.Circuit(''' ... R 0 1 ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... ''').count_determined_measurements() 2 >>> circuit = stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... distance=5, ... rounds=9, ... ) >>> circuit.count_determined_measurements() 217 >>> circuit.num_detectors + circuit.num_observables 217 )DOC") .data()); c.def( "missing_detectors", &missing_detectors, pybind11::kw_only(), pybind11::arg("unknown_input") = false, clean_doc_string(R"DOC( @signature def missing_detectors(self, *, unknown_input: bool = False) -> int: Finds deterministic measurements independent of declared detectors/observables. This method is useful for debugging missing detectors in a circuit, because it identifies generators for uncovered degrees of freedom. It's not recommended to use this method to solve for the detectors of a circuit. The returned detectors are not guaranteed to be stable across versions, and aren't optimized to be "good" (e.g. form a low weight basis or be matchable if possible). It will also identify things that are technically determined but that the user may not want to use as a detector, such as the fact that in the first round after transversal Z basis initialization of a toric code the product of all X stabilizer measurements is deterministic even though the individual measurements are all random. Args: unknown_input: Defaults to False (inputs assumed to be in the |0> state). When set to True, the inputs are instead treated as being in unknown random states. For example, this means that Z-basis measurements at the very beginning of the circuit will be considered random rather than determined. Returns: A circuit containing DETECTOR instructions that specify the uncovered degrees of freedom in the deterministic measurement sets of the input circuit. The returned circuit can be appended to the input circuit to get a circuit with no missing detectors. Examples: >>> import stim >>> stim.Circuit(''' ... R 0 ... M 0 ... ''').missing_detectors() stim.Circuit(''' DETECTOR rec[-1] ''') >>> stim.Circuit(''' ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... DEPOLARIZE1(0.1) 0 1 ... MZZ 0 1 ... MYY 0 1 ... MXX 0 1 ... DETECTOR rec[-1] rec[-4] ... DETECTOR rec[-2] rec[-5] ... DETECTOR rec[-3] rec[-6] ... ''').missing_detectors(unknown_input=True) stim.Circuit(''' DETECTOR rec[-3] rec[-2] rec[-1] ''') )DOC") .data()); c.def( "to_tableau", [](const Circuit &circuit, bool ignore_noise, bool ignore_measurement, bool ignore_reset) { return circuit_to_tableau(circuit, ignore_noise, ignore_measurement, ignore_reset); }, pybind11::kw_only(), pybind11::arg("ignore_noise") = false, pybind11::arg("ignore_measurement") = false, pybind11::arg("ignore_reset") = false, clean_doc_string(R"DOC( @signature def to_tableau(self, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False) -> stim.Tableau: Converts the circuit into an equivalent stabilizer tableau. Args: ignore_noise: Defaults to False. When False, any noise operations in the circuit will cause the conversion to fail with an exception. When True, noise operations are skipped over as if they weren't even present in the circuit. ignore_measurement: Defaults to False. When False, any measurement operations in the circuit will cause the conversion to fail with an exception. When True, measurement operations are skipped over as if they weren't even present in the circuit. ignore_reset: Defaults to False. When False, any reset operations in the circuit will cause the conversion to fail with an exception. When True, reset operations are skipped over as if they weren't even present in the circuit. Returns: A tableau equivalent to the circuit (up to global phase). Raises: ValueError: The circuit contains noise operations but ignore_noise=False. OR The circuit contains measurement operations but ignore_measurement=False. OR The circuit contains reset operations but ignore_reset=False. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''').to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) )DOC") .data()); c.def( "to_qasm", [](const Circuit &self, int open_qasm_version, bool skip_dets_and_obs) -> std::string { std::stringstream out; export_open_qasm(self, out, open_qasm_version, skip_dets_and_obs); return out.str(); }, pybind11::kw_only(), pybind11::arg("open_qasm_version"), pybind11::arg("skip_dets_and_obs") = false, clean_doc_string(R"DOC( @signature def to_qasm(self, *, open_qasm_version: int, skip_dets_and_obs: bool = False) -> str: Creates an equivalent OpenQASM implementation of the circuit. Args: open_qasm_version: The version of OpenQASM to target. This should be set to 2 or to 3. Differences between the versions are: - Support for operations on classical bits (only version 3). This means DETECTOR and OBSERVABLE_INCLUDE only work with version 3. - Support for feedback operations (only version 3). - Support for subroutines (only version 3). Without subroutines, non-standard dissipative gates like MR and RX need to decompose inline every single time they're used. - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc). skip_dets_and_obs: Defaults to False. When set to False, the output will include a `dets` register and an `obs` register (assuming the circuit has detectors and observables). These registers will be computed as part of running the circuit. This requires performing a simulation of the circuit, in order to correctly account for the expected value of measurements. When set to True, the `dets` and `obs` registers are not included in the output, and no simulation of the circuit is performed. Returns: The OpenQASM code as a string. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... R 0 1 ... X 1 ... H 0 ... CX 0 1 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... '''); >>> qasm = circuit.to_qasm(open_qasm_version=3); >>> print(qasm.strip().replace('\n\n', '\n')) OPENQASM 3.0; include "stdgates.inc"; qreg q[2]; creg rec[2]; creg dets[1]; reset q[0]; reset q[1]; x q[1]; h q[0]; cx q[0], q[1]; measure q[0] -> rec[0]; measure q[1] -> rec[1]; dets[0] = rec[1] ^ rec[0] ^ 1; )DOC") .data()); c.def( "has_flow", [](const Circuit &self, const Flow &flow, bool unsigned_only) -> bool { std::span> flows = {&flow, &flow + 1}; if (unsigned_only) { return check_if_circuit_has_unsigned_stabilizer_flows(self, flows)[0]; } else { auto rng = externally_seeded_rng(); return sample_if_circuit_has_stabilizer_flows(256, rng, self, flows)[0]; } }, pybind11::arg("flow"), pybind11::kw_only(), pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( @signature def has_flow(self, flow: stim.Flow, *, unsigned: bool = False) -> bool: Determines if the circuit has the given stabilizer flow or not. A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer P at the start of the circuit to the instantaneous stabilizer Q at the end of the circuit. The flow may be mediated by certain measurements. For example, a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and the CNOT flows implemented by the circuit involve these measurements. A flow like P -> Q means the circuit transforms P into Q. A flow like 1 -> P means the circuit prepares P. A flow like P -> 1 means the circuit measures P. A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). This method ignores any noise in the circuit. Args: flow: The flow to check for. unsigned: Defaults to False. When False, the flows must be correct including the sign of the Pauli strings. When True, only the Pauli terms need to be correct; the signs are permitted to be inverted. In effect, this requires the circuit to be correct up to Pauli gates. Returns: True if the circuit has the given flow; False otherwise. Examples: >>> import stim >>> m = stim.Circuit('M 0') >>> m.has_flow(stim.Flow('Z -> Z')) True >>> m.has_flow(stim.Flow('X -> X')) False >>> m.has_flow(stim.Flow('Z -> I')) False >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) True >>> m.has_flow(stim.Flow('Z -> rec[-1]')) True >>> cx58 = stim.Circuit('CX 5 8') >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) True >>> cx58.has_flow(stim.Flow('X_ -> XX')) False >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) True >>> stim.Circuit(''' ... RY 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("Y"), ... )) True >>> stim.Circuit(''' ... RY 0 ... X_ERROR(0.1) 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("Y"), ... )) True >>> stim.Circuit(''' ... RY 0 ... ''').has_flow(stim.Flow( ... output=stim.PauliString("X"), ... )) False >>> stim.Circuit(''' ... CX 0 1 ... ''').has_flow(stim.Flow( ... input=stim.PauliString("+X_"), ... output=stim.PauliString("+XX"), ... )) True >>> stim.Circuit(''' ... # Lattice surgery CNOT ... R 1 ... MXX 0 1 ... MZZ 1 2 ... MX 1 ... ''').has_flow(stim.Flow( ... input=stim.PauliString("+X_X"), ... output=stim.PauliString("+__X"), ... measurements=[0, 2], ... )) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( ... stim.Flow("Y -> Y"), ... unsigned=True, ... ) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( ... stim.Flow("Y -> Y"), ... unsigned=False, ... ) False Caveats: Currently, the unsigned=False version of this method is implemented by performing 256 randomized tests. Each test has a 50% chance of a false positive, and a 0% chance of a false negative. So, when the method returns True, there is technically still a 2^-256 chance the circuit doesn't have the flow. This is lower than the chance of a cosmic ray flipping the result. )DOC") .data()); c.def( "has_all_flows", [](const Circuit &self, const std::vector> &flows, bool unsigned_only) -> bool { std::vector results; if (unsigned_only) { results = check_if_circuit_has_unsigned_stabilizer_flows(self, flows); } else { auto rng = externally_seeded_rng(); results = sample_if_circuit_has_stabilizer_flows(256, rng, self, flows); } for (bool b : results) { if (!b) { return false; } } return true; }, pybind11::arg("flows"), pybind11::kw_only(), pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( @signature def has_all_flows(self, flows: Iterable[stim.Flow], *, unsigned: bool = False) -> bool: Determines if the circuit has all the given stabilizer flow or not. This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster because, behind the scenes, the circuit can be iterated once instead of once per flow. This method ignores any noise in the circuit. Args: flows: An iterable of `stim.Flow` instances representing the flows to check. unsigned: Defaults to False. When False, the flows must be correct including the sign of the Pauli strings. When True, only the Pauli terms need to be correct; the signs are permitted to be inverted. In effect, this requires the circuit to be correct up to Pauli gates. Returns: True if the circuit has the given flow; False otherwise. Examples: >>> import stim >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> Y'), ... stim.Flow('Z -> X'), ... ]) False >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> -Y'), ... stim.Flow('Z -> X'), ... ]) True >>> stim.Circuit('H 0').has_all_flows([ ... stim.Flow('X -> Z'), ... stim.Flow('Y -> Y'), ... stim.Flow('Z -> X'), ... ], unsigned=True) True Caveats: Currently, the unsigned=False version of this method is implemented by performing 256 randomized tests. Each test has a 50% chance of a false positive, and a 0% chance of a false negative. So, when the method returns True, there is technically still a 2^-256 chance the circuit doesn't have the flow. This is lower than the chance of a cosmic ray flipping the result. )DOC") .data()); c.def( "flow_generators", &circuit_flow_generators, clean_doc_string(R"DOC( @signature def flow_generators(self) -> List[stim.Flow]: Returns a list of flows that generate all of the circuit's flows. Every stabilizer flow that the circuit implements is a product of some subset of the returned generators. Every returned flow will be a flow of the circuit. Returns: A list of flow generators for the circuit. Examples: >>> import stim >>> stim.Circuit("H 0").flow_generators() [stim.Flow("X -> Z"), stim.Flow("Z -> X")] >>> stim.Circuit("M 0").flow_generators() [stim.Flow("1 -> Z xor rec[0]"), stim.Flow("Z -> rec[0]")] >>> stim.Circuit("RX 0").flow_generators() [stim.Flow("1 -> X")] >>> for flow in stim.Circuit("MXX 0 1").flow_generators(): ... print(flow) 1 -> XX xor rec[0] _X -> _X X_ -> _X xor rec[0] ZZ -> ZZ >>> for flow in stim.Circuit.generated( ... "repetition_code:memory", ... rounds=2, ... distance=3, ... after_clifford_depolarization=1e-3, ... ).flow_generators(): ... print(flow) 1 -> rec[0] 1 -> rec[1] 1 -> rec[2] 1 -> rec[3] 1 -> rec[4] 1 -> rec[5] 1 -> rec[6] 1 -> ____Z 1 -> ___Z_ 1 -> __Z__ 1 -> _Z___ 1 -> Z____ )DOC") .data()); c.def( "solve_flow_measurements", [](const Circuit &self, const std::vector> &flows) -> pybind11::object { std::span> flows_span = flows; auto solution = solve_for_flow_measurements(self, flows_span); std::vector result; for (const auto &e : solution) { if (e.has_value()) { result.push_back(pybind11::cast(*e)); } else { result.push_back(pybind11::none()); } } return pybind11::cast(result); }, clean_doc_string(R"DOC( @signature def solve_flow_measurements(self, flows: List[stim.Flow]) -> List[Optional[List[int]]]: Finds measurements to explain the starts/ends of the given flows, ignoring sign. CAUTION: it's not guaranteed that the solutions returned by this method are minimal. It may use 20 measurements when only 2 are needed. The method applies some simple heuristics that attempt to reduce the size, but these heuristics aren't perfect and don't make any strong guarantees. The recommended way to use this method is on small parts of a circuit, such as a single surface code round. The ideal use case is when there is exactly one solution for each flow, because then the method behaves predictably and consistently. When there are multiple solutions, the method has no real way to pick out a "good" solution rather than a "cataclysmic trash fire of a" solution. For example, if you have a multi-round surface code circuit with open time boundaries and solve the flow 1 -> Z1*Z2*Z3*Z4, then there's a good solution (the Z1*Z2*Z3*Z4 measurement from the last round), various mediocre solutions (a Z1*Z2*Z3*Z4 measurement from a different round), and lots of terrible solutions (a combination of multiple Z1*Z2*Z3*Z4 measurements from an odd number of rounds, times a random combination of unrelated detectors). The method is permitted to return any of those solutions. Args: flows: A list of flows, each of which to be solved. Measurements and signs are entirely ignored. An error is raised if one of the given flows has an identity pauli string as its input and as its output, despite the fact that this case has a vacuous solution (no measurements). This error is only present as a safety check that catches some possible bugs in the calling code, such as accidentally applying this method to detector flows. This error may be removed in the future, so that the vacuous case succeeds vacuously. Returns: A list of solutions for each given flow. If no solution exists for flows[k], then solutions[k] is None. Otherwise, solutions[k] is a list of measurement indices for flows[k]. When solutions[k] is not None, it's guaranteed that circuit.has_flow(stim.Flow( input=flows[k].input, output=flows[k].output, measurements=solutions[k], ), unsigned=True) Raises: ValueError: A flow had an empty input and output. Examples: >>> import stim >>> stim.Circuit(''' ... M 2 ... ''').solve_flow_measurements([ ... stim.Flow("Z2 -> 1"), ... ]) [[0]] >>> stim.Circuit(''' ... M 2 ... ''').solve_flow_measurements([ ... stim.Flow("X2 -> X2"), ... ]) [None] >>> stim.Circuit(''' ... MXX 0 1 ... ''').solve_flow_measurements([ ... stim.Flow("YY -> ZZ"), ... ]) [[0]] >>> # Rep code cycle >>> stim.Circuit(''' ... R 1 3 ... CX 0 1 2 3 ... CX 4 3 2 1 ... M 1 3 ... ''').solve_flow_measurements([ ... stim.Flow("1 -> Z0*Z4"), ... stim.Flow("Z0 -> Z2"), ... stim.Flow("X0*X2*X4 -> X0*X2*X4"), ... stim.Flow("Y0 -> Y0"), ... ]) [[0, 1], [0], [], None] )DOC") .data()); c.def( "time_reversed_for_flows", [](const Circuit &self, const std::vector> &flows, bool dont_turn_measurements_into_resets) -> pybind11::object { auto [inv_circuit, inv_flows] = circuit_inverse_qec(self, flows, dont_turn_measurements_into_resets); return pybind11::make_tuple(inv_circuit, inv_flows); }, pybind11::arg("flows"), pybind11::kw_only(), pybind11::arg("dont_turn_measurements_into_resets") = false, clean_doc_string(R"DOC( @signature def time_reversed_for_flows(self, flows: Iterable[stim.Flow], *, dont_turn_measurements_into_resets: bool = False) -> Tuple[stim.Circuit, List[stim.Flow]]: Time-reverses the circuit while preserving error correction structure. This method returns a circuit that has the same internal detecting regions as the given circuit, as well as the same internal-to-external flows given in the `flows` argument, except they are all time-reversed. For example, if you pass a fault tolerant preparation circuit into this method (1 -> Z), the result will be a fault tolerant *measurement* circuit (Z -> 1). Or, if you pass a fault tolerant C_XYZ circuit into this method (X->Y, Y->Z, and Z->X), the result will be a fault tolerant C_ZYX circuit (X->Z, Y->X, and Z->Y). Note that this method doesn't guarantee that it will preserve the *sign* of the detecting regions or stabilizer flows. For example, inverting a memory circuit that preserves a logical observable (X->X and Z->Z) may produce a memory circuit that always bit flips the logical observable (X->X and Z->-Z) or that dynamically adjusts the logical observable in response to measurements (like "X -> X xor rec[-1]" and "Z -> Z xor rec[-2]"). This method will turn time-reversed resets into measurements, and attempts to turn time-reversed measurements into resets. A measurement will time-reverse into a reset if some annotated detectors, annotated observables, or given flows have detecting regions with sensitivity just before the measurement but none have detecting regions with sensitivity after the measurement. In some cases this method will have to introduce new operations. In particular, when a measurement-reset operation has a noisy result, time-reversing this measurement noise produces reset noise. But the measure-reset operations don't have built-in reset noise, so the reset noise is specified by adding an X_ERROR or Z_ERROR noise instruction after the time-reversed measure-reset operation. Args: flows: Flows you care about, that reach past the start/end of the given circuit. The result will contain an inverted flow for each of these given flows. You need this information because it reveals the measurements needed to produce the inverted flows that you care about. An exception will be raised if the circuit doesn't have all these flows. The inverted circuit will have the inverses of these flows (ignoring sign). dont_turn_measurements_into_resets: Defaults to False. When set to True, measurements will time-reverse into measurements even if nothing is sensitive to the measured qubit after the measurement completes. This guarantees the output circuit has *all* flows that the input circuit has (up to sign and feedback), even ones that aren't annotated. Returns: An (inverted_circuit, inverted_flows) tuple. inverted_circuit is the qec inverse of the given circuit. inverted_flows is a list of flows, matching up by index with the flows given as arguments to the method. The input, output, and sign fields of these flows are boring. The useful field is measurement_indices, because it's difficult to predict which measurements are needed for the inverted flow due to effects such as implicitly-included resets inverting into explicitly-included measurements. Caveats: Currently, this method doesn't compute the sign of the inverted flows. It unconditionally sets the sign to False. Examples: >>> import stim >>> inv_circuit, inv_flows = stim.Circuit(''' ... R 0 ... H 0 ... S 0 ... MY 0 ... DETECTOR rec[-1] ... ''').time_reversed_for_flows([]) >>> inv_circuit stim.Circuit(''' RY 0 S_DAG 0 H 0 M 0 DETECTOR rec[-1] ''') >>> inv_flows [] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows([ ... stim.Flow("Z -> rec[-1]"), ... ]) >>> inv_circuit stim.Circuit(''' R 0 ''') >>> inv_flows [stim.Flow("1 -> Z")] >>> inv_circuit.has_all_flows(inv_flows, unsigned=True) True >>> inv_circuit, inv_flows = stim.Circuit(''' ... R 0 ... ''').time_reversed_for_flows([ ... stim.Flow("1 -> Z"), ... ]) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("Z -> rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows([ ... stim.Flow("1 -> Z xor rec[-1]"), ... ]) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("Z -> rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... M 0 ... ''').time_reversed_for_flows( ... flows=[stim.Flow("Z -> rec[-1]")], ... dont_turn_measurements_into_resets=True, ... ) >>> inv_circuit stim.Circuit(''' M 0 ''') >>> inv_flows [stim.Flow("1 -> Z xor rec[-1]")] >>> inv_circuit, inv_flows = stim.Circuit(''' ... MR(0.125) 0 ... ''').time_reversed_for_flows([]) >>> inv_circuit stim.Circuit(''' MR 0 X_ERROR(0.125) 0 ''') >>> inv_flows [] >>> inv_circuit, inv_flows = stim.Circuit(''' ... MXX 0 1 ... H 0 ... ''').time_reversed_for_flows([ ... stim.Flow("ZZ -> YY xor rec[-1]"), ... stim.Flow("ZZ -> XZ"), ... ]) >>> inv_circuit stim.Circuit(''' H 0 MXX 0 1 ''') >>> inv_flows [stim.Flow("YY -> ZZ xor rec[-1]"), stim.Flow("XZ -> ZZ")] >>> stim.Circuit.generated( ... "surface_code:rotated_memory_x", ... distance=2, ... rounds=1, ... ).time_reversed_for_flows([])[0] stim.Circuit(''' QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 1) 3 QUBIT_COORDS(1, 3) 6 QUBIT_COORDS(2, 2) 7 QUBIT_COORDS(3, 3) 8 QUBIT_COORDS(2, 4) 12 RX 8 6 3 1 MR 12 7 2 TICK H 12 2 TICK CX 1 7 12 6 TICK CX 6 7 12 8 TICK CX 3 7 2 1 TICK CX 8 7 2 3 TICK H 12 2 TICK M 12 7 2 DETECTOR(2, 0, 1) rec[-1] DETECTOR(2, 4, 1) rec[-3] MX 8 6 3 1 DETECTOR(2, 0, 0) rec[-5] rec[-2] rec[-1] DETECTOR(2, 4, 0) rec[-7] rec[-4] rec[-3] OBSERVABLE_INCLUDE(0) rec[-3] rec[-1] ''') )DOC") .data()); c.def( "to_crumble_url", [](const Circuit &self, bool skip_detectors, pybind11::object &obj_mark) { std::map> mark; if (!obj_mark.is_none()) { mark = pybind11::cast>>(obj_mark); } return export_crumble_url(self, skip_detectors, mark); }, pybind11::kw_only(), pybind11::arg("skip_detectors") = false, pybind11::arg("mark") = pybind11::none(), clean_doc_string(R"DOC( @signature def to_crumble_url(self, *, skip_detectors: bool = False, mark: Optional[dict[int, list[stim.ExplainedError]]] = None) -> str: Returns a URL that opens up crumble and loads this circuit into it. Crumble is a tool for editing stabilizer circuits, and visualizing their stabilizer flows. Its source code is in the `glue/crumble` directory of the stim code repository on github. A prebuilt version is made available at https://algassert.com/crumble, which is what the URL returned by this method will point to. Args: skip_detectors: Defaults to False. If set to True, detectors from the circuit aren't included in the crumble URL. This can reduce visual clutter in crumble, and improve its performance, since it doesn't need to indicate or track the sensitivity regions of detectors. mark: Defaults to None (no marks). If set to a dictionary from int to errors, such as `mark={1: circuit.shortest_graphlike_error()}`, then the errors will be highlighted and tracked forward by crumble. Returns: A URL that can be opened in a web browser. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... S 1 ... ''').to_crumble_url() 'https://algassert.com/crumble#circuit=H_0;CX_0_1;S_1_' >>> circuit = stim.Circuit(''' ... M(0.25) 0 1 2 ... DETECTOR rec[-1] rec[-2] ... DETECTOR rec[-2] rec[-3] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''') >>> err = circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) >>> circuit.to_crumble_url(skip_detectors=True, mark={1: err}) 'https://algassert.com/crumble#circuit=;TICK;MARKX(1)1;MARKX(1)2;MARKX(1)0;TICK;M(0.25)0_1_2;OI(0)rec[-1]_' )DOC") .data()); c.def( "to_quirk_url", &export_quirk_url, clean_doc_string(R"DOC( Returns a URL that opens up quirk and loads this circuit into it. Quirk is an open source drag and drop circuit editor with support for up to 16 qubits. Its source code is available at https://github.com/strilanc/quirk and a prebuilt version is available at https://algassert.com/quirk, which is what the URL returned by this method will point to. Quirk doesn't support features like noise, feedback, or detectors. This method will simply drop any unsupported operations from the circuit when producing the URL. Returns: A URL that can be opened in a web browser. Examples: >>> import stim >>> stim.Circuit(''' ... H 0 ... CNOT 0 1 ... S 1 ... ''').to_quirk_url() 'https://algassert.com/quirk#circuit={"cols":[["H"],["•","X"],[1,"Z^½"]]}' )DOC") .data()); c.def( "decomposed", &simplified_circuit, clean_doc_string(R"DOC( Recreates the circuit using (mostly) the {H,S,CX,M,R} gate set. The intent of this method is to simplify the circuit to use fewer gate types, so it's easier for other tools to consume. Currently, this method performs the following simplifications: - Single qubit cliffords are decomposed into {H,S}. - Multi-qubit cliffords are decomposed into {H,S,CX}. - Single qubit dissipative gates are decomposed into {H,S,M,R}. - Multi-qubit dissipative gates are decomposed into {H,S,CX,M,R}. Currently, the following types of gate *aren't* simplified, but they may be in the future: - Noise instructions (like X_ERROR, DEPOLARIZE2, and E). - Annotations (like TICK, DETECTOR, and SHIFT_COORDS). - The MPAD instruction. - Repeat blocks are not flattened. Returns: A `stim.Circuit` whose function is equivalent to the original circuit, but with most gates decomposed into the {H,S,CX,M,R} gate set. Examples: >>> import stim >>> stim.Circuit(''' ... SWAP 0 1 ... ''').decomposed() stim.Circuit(''' CX 0 1 1 0 0 1 ''') >>> stim.Circuit(''' ... ISWAP 0 1 2 1 ... TICK ... MPP !X1*Y2*Z3 ... ''').decomposed() stim.Circuit(''' H 0 CX 0 1 1 0 H 1 S 1 0 H 2 CX 2 1 1 2 H 1 S 1 2 TICK H 1 2 S 2 H 2 S 2 2 CX 2 1 3 1 M !1 CX 2 1 3 1 H 2 S 2 H 2 S 2 2 H 1 ''') )DOC") .data()); c.def( "with_inlined_feedback", &circuit_with_inlined_feedback, clean_doc_string(R"DOC( Returns a circuit without feedback with rewritten detectors/observables. When a feedback operation affects the expected parity of a detector or observable, the measurement controlling that feedback operation is implicitly part of the measurement set that defines the detector or observable. This method removes all feedback, but avoids changing the meaning of detectors or observables by turning these implicit measurement dependencies into explicit measurement dependencies added to the observable or detector. This method guarantees that the detector error model derived from the original circuit, and the transformed circuit, will be equivalent (modulo floating point rounding errors and variations in where loops are placed). Specifically, the following should be true for any circuit: dem1 = circuit.flattened().detector_error_model() dem2 = circuit.with_inlined_feedback().flattened().detector_error_model() assert dem1.approx_equals(dem2, 1e-5) Returns: A `stim.Circuit` with feedback operations removed, with rewritten DETECTOR instructions (as needed to avoid changing the meaning of each detector), and with additional OBSERVABLE_INCLUDE instructions (as needed to avoid changing the meaning of each observable). The circuit's function is permitted to differ from the original in that any feedback operation can be pushed to the end of the circuit and discarded. All non-feedback operations must stay where they are, preserving the structure of the circuit. Examples: >>> import stim >>> stim.Circuit(''' ... CX 0 1 # copy to measure qubit ... M 1 # measure first time ... CX rec[-1] 1 # use feedback to reset measurement qubit ... CX 0 1 # copy to measure qubit ... M 1 # measure second time ... DETECTOR rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').with_inlined_feedback() stim.Circuit(''' CX 0 1 M 1 OBSERVABLE_INCLUDE(0) rec[-1] CX 0 1 M 1 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''') )DOC") .data()); } ================================================ FILE: src/stim/circuit/circuit_instruction.cc ================================================ // 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. #include "stim/circuit/circuit_instruction.h" #include "stim/circuit/circuit.h" #include "stim/circuit/gate_target.h" #include "stim/gates/gates.h" #include "stim/util_bot/str_util.h" using namespace stim; uint64_t CircuitInstruction::repeat_block_rep_count() const { assert(targets.size() == 3); uint64_t low = targets[1].data; uint64_t high = targets[2].data; return low | (high << 32); } Circuit &CircuitInstruction::repeat_block_body(Circuit &host) const { assert(targets.size() == 3); auto b = targets[0].data; assert(b < host.blocks.size()); return host.blocks[b]; } CircuitStats CircuitInstruction::compute_stats(const Circuit *host) const { CircuitStats out; add_stats_to(out, host); return out; } void CircuitInstruction::add_stats_to(CircuitStats &out, const Circuit *host) const { if (gate_type == GateType::REPEAT) { if (host == nullptr) { throw std::invalid_argument("gate_type == REPEAT && host == nullptr"); } // Recurse into blocks. auto sub = repeat_block_body(*host).compute_stats(); auto reps = repeat_block_rep_count(); out.num_observables = std::max(out.num_observables, sub.num_observables); out.num_qubits = std::max(out.num_qubits, sub.num_qubits); out.max_lookback = std::max(out.max_lookback, sub.max_lookback); out.num_sweep_bits = std::max(out.num_sweep_bits, sub.num_sweep_bits); out.num_detectors = add_saturate(out.num_detectors, mul_saturate(sub.num_detectors, reps)); out.num_measurements = add_saturate(out.num_measurements, mul_saturate(sub.num_measurements, reps)); out.num_ticks = add_saturate(out.num_ticks, mul_saturate(sub.num_ticks, reps)); return; } for (auto t : targets) { auto v = t.data & TARGET_VALUE_MASK; // Qubit counting. if (gate_type != GateType::MPAD) { if (!(t.data & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { out.num_qubits = std::max(out.num_qubits, v + 1); } } // Lookback counting. if (t.data & TARGET_RECORD_BIT) { out.max_lookback = std::max(out.max_lookback, v); } // Sweep bit counting. if (t.data & TARGET_SWEEP_BIT) { out.num_sweep_bits = std::max(out.num_sweep_bits, v + 1); } } // Measurement counting. out.num_measurements += count_measurement_results(); switch (gate_type) { case GateType::DETECTOR: // Detector counting. out.num_detectors += out.num_detectors < UINT64_MAX; break; case GateType::OBSERVABLE_INCLUDE: // Observable counting. out.num_observables = std::max(out.num_observables, (uint64_t)args[0] + 1); break; case GateType::TICK: // Tick counting. out.num_ticks++; break; default: break; } } const Circuit &CircuitInstruction::repeat_block_body(const Circuit &host) const { assert(targets.size() == 3); auto b = targets[0].data; assert(b < host.blocks.size()); return host.blocks[b]; } CircuitInstruction::CircuitInstruction( GateType gate_type, SpanRef args, SpanRef targets, std::string_view tag) : gate_type(gate_type), args(args), targets(targets), tag(tag) { } void CircuitInstruction::validate() const { const Gate &gate = GATE_DATA[gate_type]; if (gate.flags == GateFlags::NO_GATE_FLAG) { throw std::invalid_argument("Unrecognized gate_type. Associated flag is NO_GATE_FLAG."); } if (gate.flags & GATE_TARGETS_PAIRS) { if (gate.flags & GATE_TARGETS_PAULI_STRING) { size_t term_count = targets.size(); for (auto t : targets) { if (t.is_combiner()) { term_count -= 2; } } if (term_count & 1) { throw std::invalid_argument( "The gate " + std::string(gate.name) + " requires an even number of products to target, but was given " "(" + comma_sep(args).str() + ")."); } } else { if (targets.size() & 1) { throw std::invalid_argument( "Two qubit gate " + std::string(gate.name) + " requires an even number of targets but was given " "(" + comma_sep(targets).str() + ")."); } for (size_t k = 0; k < targets.size(); k += 2) { if (targets[k] == targets[k + 1]) { throw std::invalid_argument( "The two qubit gate " + std::string(gate.name) + " was applied to a target pair with the same target (" + targets[k].target_str() + ") twice. Gates can't interact targets with themselves."); } } } } if (gate.arg_count == ARG_COUNT_SYGIL_ZERO_OR_ONE) { if (args.size() > 1) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " was given " + std::to_string(args.size()) + " parens arguments (" + comma_sep(args).str() + ") but takes 0 or 1 parens arguments."); } } else if (args.size() != gate.arg_count && gate.arg_count != ARG_COUNT_SYGIL_ANY) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " was given " + std::to_string(args.size()) + " parens arguments (" + comma_sep(args).str() + ") but takes " + std::to_string(gate.arg_count) + " parens arguments."); } if ((gate.flags & GATE_TAKES_NO_TARGETS) && !targets.empty()) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " takes no targets but was given targets" + targets_str(targets) + "."); } if (gate.flags & GATE_ARGS_ARE_DISJOINT_PROBABILITIES) { double total = 0; for (const auto p : args) { if (!(p >= 0 && p <= 1)) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " only takes probability arguments, but one of its arguments (" + comma_sep(args).str() + ") wasn't a probability."); } total += p; } if (total > 1.0000001) { throw std::invalid_argument( "The disjoint probability arguments (" + comma_sep(args).str() + ") given to gate " + std::string(gate.name) + " sum to more than 1."); } } else if (gate.flags & GATE_ARGS_ARE_UNSIGNED_INTEGERS) { for (const auto p : args) { if (p < 0 || p != round(p)) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " only takes non-negative integer arguments, but one of its arguments (" + comma_sep(args).str() + ") wasn't a non-negative integer."); } } } uint32_t valid_target_mask = TARGET_VALUE_MASK; // Check combiners. if (gate.flags & GATE_TARGETS_COMBINERS) { bool combiner_allowed = false; bool just_saw_combiner = false; bool failed = false; for (const auto p : targets) { if (p.is_combiner()) { failed |= !combiner_allowed; combiner_allowed = false; just_saw_combiner = true; } else { combiner_allowed = true; just_saw_combiner = false; } } failed |= just_saw_combiner; if (failed) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " given combiners ('*') that aren't between other targets: " + targets_str(targets) + "."); } valid_target_mask |= TARGET_COMBINER; } // Check that targets are in range. if (gate.flags & GATE_PRODUCES_RESULTS) { valid_target_mask |= TARGET_INVERTED_BIT; } if (gate.flags & GATE_CAN_TARGET_BITS) { valid_target_mask |= TARGET_RECORD_BIT | TARGET_SWEEP_BIT; } if (gate.flags & GATE_ONLY_TARGETS_MEASUREMENT_RECORD) { if (gate.flags & GATE_TARGETS_PAULI_STRING) { for (GateTarget q : targets) { if (!q.is_measurement_record_target() && !q.is_pauli_target()) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " only takes measurement record targets and Pauli targets (rec[-k], Xk, Yk, Zk)."); } } } else { for (GateTarget q : targets) { if (!q.is_measurement_record_target()) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " only takes measurement record targets (rec[-k])."); } } } } else if (gate.flags & GATE_TARGETS_PAULI_STRING) { if (gate.flags & GATE_CAN_TARGET_BITS) { for (GateTarget q : targets) { if (!(q.data & (TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT | TARGET_COMBINER | TARGET_SWEEP_BIT | TARGET_RECORD_BIT))) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " only takes Pauli targets or bit targets ('X2', 'Y3', 'Z5', 'rec[-1]', 'sweep[0]', etc)."); } } } else { for (GateTarget q : targets) { if (!(q.data & (TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT | TARGET_COMBINER))) { throw std::invalid_argument( "Gate " + std::string(gate.name) + " only takes Pauli targets ('X2', 'Y3', 'Z5', etc)."); } } } } else { for (GateTarget q : targets) { if (q.data != (q.data & valid_target_mask)) { std::stringstream ss; ss << "Target "; q.write_succinct(ss); ss << " has invalid modifiers for gate type '" << gate.name << "'."; throw std::invalid_argument(ss.str()); } } } if (gate_type == GateType::MPAD) { for (const auto &t : targets) { if (t.data > 1) { std::stringstream ss; ss << "Target "; t.write_succinct(ss); ss << " is not valid for gate type '" << gate.name << "'."; throw std::invalid_argument(ss.str()); } } } } uint64_t CircuitInstruction::count_measurement_results() const { auto flags = GATE_DATA[gate_type].flags; if (!(flags & GATE_PRODUCES_RESULTS)) { return 0; } uint64_t n = (uint64_t)targets.size(); if (flags & GATE_TARGETS_PAIRS) { return n >> 1; } else if (flags & GATE_TARGETS_COMBINERS) { for (auto e : targets) { if (e.is_combiner()) { n -= 2; } } } return n; } bool CircuitInstruction::can_fuse(const CircuitInstruction &other) const { auto flags = GATE_DATA[gate_type].flags; return gate_type == other.gate_type && args == other.args && !(flags & GATE_IS_NOT_FUSABLE) && tag == other.tag; } bool CircuitInstruction::operator==(const CircuitInstruction &other) const { return gate_type == other.gate_type && args == other.args && targets == other.targets && tag == other.tag; } bool CircuitInstruction::approx_equals(const CircuitInstruction &other, double atol) const { if (gate_type != other.gate_type || targets != other.targets || args.size() != other.args.size() || tag != other.tag) { return false; } for (size_t k = 0; k < args.size(); k++) { if (fabs(args[k] - other.args[k]) > atol) { return false; } } return true; } bool CircuitInstruction::operator!=(const CircuitInstruction &other) const { return !(*this == other); } std::ostream &stim::operator<<(std::ostream &out, const CircuitInstruction &instruction) { out << GATE_DATA[instruction.gate_type].name; if (!instruction.tag.empty()) { out << '['; write_tag_escaped_string_to(instruction.tag, out); out << ']'; } if (!instruction.args.empty()) { out << '('; bool first = true; for (auto e : instruction.args) { if (first) { first = false; } else { out << ", "; } if (e > (double)INT64_MIN && e < (double)INT64_MAX && (int64_t)e == e) { out << (int64_t)e; } else { out << e; } } out << ')'; } write_targets(out, instruction.targets); return out; } void stim::write_tag_escaped_string_to(std::string_view tag, std::ostream &out) { for (char c : tag) { switch (c) { case '\n': out << "\\n"; break; case '\r': out << "\\r"; break; case '\\': out << "\\B"; break; case ']': out << "\\C"; break; default: out << c; } } } std::string CircuitInstruction::str() const { std::stringstream s; s << *this; return s.str(); } ================================================ FILE: src/stim/circuit/circuit_instruction.h ================================================ /* * 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. */ #ifndef _STIM_CIRCUIT_INSTRUCTION_H #define _STIM_CIRCUIT_INSTRUCTION_H #include #include "stim/circuit/gate_target.h" #include "stim/mem/span_ref.h" namespace stim { struct Circuit; /// Stores a variety of circuit quantities relevant for sizing memory. struct CircuitStats { uint64_t num_detectors = 0; uint64_t num_observables = 0; uint64_t num_measurements = 0; uint32_t num_qubits = 0; uint64_t num_ticks = 0; uint32_t max_lookback = 0; uint32_t num_sweep_bits = 0; inline CircuitStats repeated(uint64_t repetitions) const { return CircuitStats{ num_detectors * repetitions, num_observables, num_measurements * repetitions, num_qubits, (uint32_t)(num_ticks * repetitions), max_lookback, num_sweep_bits, }; } }; /// The data that describes how a gate is being applied to qubits (or other targets). /// /// A gate applied to targets. /// /// This struct is not self-sufficient. It points into data stored elsewhere (e.g. in a Circuit's jagged_data). struct CircuitInstruction { /// The gate applied by the operation. GateType gate_type; /// Numeric arguments varying the functionality of the gate. /// /// The meaning of the numbers varies from gate to gate. /// Examples: /// X_ERROR(p) has a single argument: probability of X. /// PAULI_CHANNEL_1(px,py,pz) has multiple probability arguments. /// DETECTOR(c1,c2) has variable arguments: coordinate data. /// OBSERVABLE_INCLUDE(k) has a single argument: the observable index. SpanRef args; /// Encoded data indicating the qubits and other targets acted on by the gate. SpanRef targets; /// Arbitrary string associated with the instruction. /// No effect on simulations or analysis steps within stim, but user code may use it. std::string_view tag; CircuitInstruction() = delete; CircuitInstruction( GateType gate_type, SpanRef args, SpanRef targets, std::string_view tag); /// Computes number of qubits, number of measurements, etc. CircuitStats compute_stats(const Circuit *host) const; /// Computes number of qubits, number of measurements, etc and adds them into a target. void add_stats_to(CircuitStats &out, const Circuit *host) const; /// Determines if two operations can be combined into one operation (with combined targeting data). /// /// For example, `H 1` then `H 2 1` is equivalent to `H 1 2 1` so those instructions are fusable. bool can_fuse(const CircuitInstruction &other) const; /// Equality. bool operator==(const CircuitInstruction &other) const; /// Inequality. bool operator!=(const CircuitInstruction &other) const; /// Approximate equality. bool approx_equals(const CircuitInstruction &other, double atol) const; /// Returns a text description of the instruction, as would appear in a STIM circuit file. std::string str() const; /// Determines the number of entries added to the measurement record by the operation. /// /// Note: invalid to use this on REPEAT blocks. uint64_t count_measurement_results() const; uint64_t repeat_block_rep_count() const; Circuit &repeat_block_body(Circuit &host) const; const Circuit &repeat_block_body(const Circuit &host) const; /// Verifies complex invariants that circuit instructions are supposed to follow. /// /// For example: CNOT gates should have an even number of targets. /// For example: X_ERROR should have a single float argument between 0 and 1 inclusive. /// /// Raises: /// std::invalid_argument: Validation failed. void validate() const; template inline void for_combined_target_groups(CALLBACK callback) const { auto flags = GATE_DATA[gate_type].flags; size_t start = 0; while (start < targets.size()) { size_t end; if (flags & stim::GateFlags::GATE_TARGETS_COMBINERS) { end = start + 1; while (end < targets.size() && targets[end].is_combiner()) { end += 2; } } else if (flags & stim::GateFlags::GATE_IS_SINGLE_QUBIT_GATE) { end = start + 1; } else if (flags & stim::GateFlags::GATE_TARGETS_PAIRS) { end = start + 2; } else if ( (flags & stim::GateFlags::GATE_TARGETS_PAULI_STRING) && !(flags & stim::GateFlags::GATE_TARGETS_COMBINERS)) { // like CORRELATED_ERROR end = targets.size(); } else if (flags & stim::GateFlags::GATE_ONLY_TARGETS_MEASUREMENT_RECORD) { // like DETECTOR end = start + 1; } else if (gate_type == GateType::MPAD || gate_type == GateType::QUBIT_COORDS) { end = start + 1; } else { throw std::invalid_argument("Not implemented: splitting " + str()); } std::span group = targets.sub(start, end); callback(group); start = end; } } }; void write_tag_escaped_string_to(std::string_view tag, std::ostream &out); std::ostream &operator<<(std::ostream &out, const CircuitInstruction &op); } // namespace stim #endif ================================================ FILE: src/stim/circuit/circuit_instruction.pybind.cc ================================================ #include "stim/circuit/circuit_instruction.pybind.h" #include "stim/circuit/gate_target.pybind.h" #include "stim/gates/gates.h" #include "stim/py/base.pybind.h" #include "stim/util_bot/str_util.h" using namespace stim; using namespace stim_pybind; PyCircuitInstruction::PyCircuitInstruction( std::string_view name, std::span init_targets, std::span init_gate_args, pybind11::str tag) : gate_type(GATE_DATA.at(name).id), tag(tag) { for (const auto &obj : init_gate_args) { gate_args.push_back(obj); } for (const auto &obj : init_targets) { targets.push_back(obj_to_gate_target(obj)); } as_operation_ref().validate(); } PyCircuitInstruction::PyCircuitInstruction( GateType gate_type, std::vector targets, std::vector gate_args, pybind11::str tag) : gate_type(gate_type), targets(targets), gate_args(gate_args), tag(tag) { as_operation_ref().validate(); } PyCircuitInstruction PyCircuitInstruction::from_str(std::string_view text) { Circuit host; host.append_from_text(text); if (host.operations.size() != 1 || host.operations[0].gate_type == GateType::REPEAT) { throw std::invalid_argument("Given text didn't parse to a single CircuitInstruction."); } return PyCircuitInstruction::from_instruction(host.operations[0]); } PyCircuitInstruction PyCircuitInstruction::from_instruction(CircuitInstruction instruction) { std::vector arguments; std::vector targets; arguments.insert(arguments.begin(), instruction.args.begin(), instruction.args.end()); targets.insert(targets.begin(), instruction.targets.begin(), instruction.targets.end()); return PyCircuitInstruction{ instruction.gate_type, targets, arguments, instruction.tag, }; } bool PyCircuitInstruction::operator==(const PyCircuitInstruction &other) const { return gate_type == other.gate_type && targets == other.targets && gate_args == other.gate_args && pybind11::cast(tag) == pybind11::cast(other.tag); } bool PyCircuitInstruction::operator!=(const PyCircuitInstruction &other) const { return !(*this == other); } std::string PyCircuitInstruction::repr() const { std::stringstream result; result << "stim.CircuitInstruction('" << name() << "', ["; bool first = true; for (const auto &t : targets) { if (first) { first = false; } else { result << ", "; } result << t.repr(); } result << "], [" << comma_sep(gate_args) << "]"; if (pybind11::cast(pybind11::bool_(tag))) { result << ", tag="; result << pybind11::repr(tag); } result << ")"; return result.str(); } std::string PyCircuitInstruction::str() const { std::stringstream result; result << as_operation_ref(); return result.str(); } CircuitInstruction PyCircuitInstruction::as_operation_ref() const { return CircuitInstruction{gate_type, gate_args, targets, pybind11::cast(tag)}; } PyCircuitInstruction::operator CircuitInstruction() const { return as_operation_ref(); } std::string PyCircuitInstruction::name() const { return std::string(GATE_DATA[gate_type].name); } std::vector PyCircuitInstruction::raw_targets() const { std::vector result; for (const auto &t : targets) { result.push_back(t.data); } return result; } std::vector PyCircuitInstruction::targets_copy() const { return targets; } std::vector PyCircuitInstruction::gate_args_copy() const { return gate_args; } std::vector> PyCircuitInstruction::target_groups() const { std::vector> results; as_operation_ref().for_combined_target_groups([&](std::span group) { std::vector copy; for (auto g : group) { if (!g.is_combiner()) { copy.push_back(g); } } results.push_back(std::move(copy)); }); return results; } pybind11::class_ stim_pybind::pybind_circuit_instruction(pybind11::module &m) { return pybind11::class_( m, "CircuitInstruction", clean_doc_string(R"DOC( An instruction, like `H 0 1` or `CNOT rec[-1] 5`, from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... M 0 1 ... X_ERROR(0.125) 5 ... ''') >>> circuit[0] stim.CircuitInstruction('H', [stim.GateTarget(0)], []) >>> circuit[1] stim.CircuitInstruction('M', [stim.GateTarget(0), stim.GateTarget(1)], []) >>> circuit[2] stim.CircuitInstruction('X_ERROR', [stim.GateTarget(5)], [0.125]) )DOC") .data()); } void stim_pybind::pybind_circuit_instruction_methods(pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init( [](std::string_view name, pybind11::object targets, pybind11::object gate_args, pybind11::str tag) -> PyCircuitInstruction { if (targets.is_none() and gate_args.is_none() && !pybind11::cast(pybind11::bool_(tag))) { return PyCircuitInstruction::from_str(name); } std::vector conv_args; std::vector conv_targets; if (!gate_args.is_none()) { conv_args = pybind11::cast>(gate_args); } if (!targets.is_none()) { conv_targets = pybind11::cast>(targets); } return PyCircuitInstruction(name, conv_targets, conv_args, tag); }), pybind11::arg("name"), pybind11::arg("targets") = pybind11::none(), pybind11::arg("gate_args") = pybind11::none(), pybind11::kw_only(), pybind11::arg("tag") = "", clean_doc_string(R"DOC( @signature def __init__(self, name: str, targets: Optional[Iterable[Union[int, stim.GateTarget]]] = None, gate_args: Optional[Iterable[float]] = None, *, tag: str = "") -> None: Creates or parses a `stim.CircuitInstruction`. Args: name: The name of the instruction being applied. If `targets` and `gate_args` aren't specified, this can be a full instruction line from a stim Circuit file, like "CX 0 1". targets: The targets the instruction is being applied to. These can be raw values like `0` and `stim.target_rec(-1)`, or instances of `stim.GateTarget`. gate_args: The sequence of numeric arguments parameterizing a gate. For noise gates this is their probabilities. For `OBSERVABLE_INCLUDE` instructions it's the index of the logical observable to affect. tag: Defaults to "". A custom string attached to the instruction. For example, for a TICK instruction, this could a string specifying an amount of time which is used by custom code for adding noise to a circuit. In general, stim will attempt to propagate tags across circuit transformations but will otherwise completely ignore them. Examples: >>> import stim >>> print(stim.CircuitInstruction('DEPOLARIZE1', [5], [0.25])) DEPOLARIZE1(0.25) 5 >>> stim.CircuitInstruction('CX rec[-1] 5 # comment') stim.CircuitInstruction('CX', [stim.target_rec(-1), stim.GateTarget(5)], []) >>> print(stim.CircuitInstruction('I', [2], tag='100ns')) I[100ns] 2 )DOC") .data()); c.def_property_readonly( "name", &PyCircuitInstruction::name, clean_doc_string(R"DOC( The name of the instruction (e.g. `H` or `X_ERROR` or `DETECTOR`). )DOC") .data()); c.def_property_readonly( "tag", [](PyCircuitInstruction &self) -> pybind11::str { return self.tag; }, clean_doc_string(R"DOC( The custom tag attached to the instruction. The tag is an arbitrary string. The default tag, when none is specified, is the empty string. Examples: >>> import stim >>> stim.Circuit("H[test] 0")[0].tag 'test' >>> stim.Circuit("H 0")[0].tag '' )DOC") .data()); c.def( "target_groups", &PyCircuitInstruction::target_groups, clean_doc_string(R"DOC( @signature def target_groups(self) -> List[List[stim.GateTarget]]: Splits the instruction's targets into groups depending on the type of gate. Single qubit gates like H get one group per target. Two qubit gates like CX get one group per pair of targets. Pauli product gates like MPP get one group per combined product. Returns: A list of groups of targets. Examples: >>> import stim >>> for g in stim.Circuit('H 0 1 2')[0].target_groups(): ... print(repr(g)) [stim.GateTarget(0)] [stim.GateTarget(1)] [stim.GateTarget(2)] >>> for g in stim.Circuit('CX 0 1 2 3')[0].target_groups(): ... print(repr(g)) [stim.GateTarget(0), stim.GateTarget(1)] [stim.GateTarget(2), stim.GateTarget(3)] >>> for g in stim.Circuit('MPP X0*Y1*Z2 X5*X6')[0].target_groups(): ... print(repr(g)) [stim.target_x(0), stim.target_y(1), stim.target_z(2)] [stim.target_x(5), stim.target_x(6)] >>> for g in stim.Circuit('DETECTOR rec[-1] rec[-2]')[0].target_groups(): ... print(repr(g)) [stim.target_rec(-1)] [stim.target_rec(-2)] >>> for g in stim.Circuit('CORRELATED_ERROR(0.1) X0 Y1')[0].target_groups(): ... print(repr(g)) [stim.target_x(0), stim.target_y(1)] )DOC") .data()); c.def( "targets_copy", &PyCircuitInstruction::targets_copy, clean_doc_string(R"DOC( Returns a copy of the targets of the instruction. Examples: >>> import stim >>> instruction = stim.CircuitInstruction('X_ERROR', [2, 3], [0.125]) >>> instruction.targets_copy() [stim.GateTarget(2), stim.GateTarget(3)] >>> instruction.targets_copy() == instruction.targets_copy() True >>> instruction.targets_copy() is instruction.targets_copy() False )DOC") .data()); c.def( "gate_args_copy", &PyCircuitInstruction::gate_args_copy, clean_doc_string(R"DOC( Returns the gate's arguments (numbers parameterizing the instruction). For noisy gates this typically a list of probabilities. For OBSERVABLE_INCLUDE it's a singleton list containing the logical observable index. Examples: >>> import stim >>> instruction = stim.CircuitInstruction('X_ERROR', [2, 3], [0.125]) >>> instruction.gate_args_copy() [0.125] >>> instruction.gate_args_copy() == instruction.gate_args_copy() True >>> instruction.gate_args_copy() is instruction.gate_args_copy() False )DOC") .data()); c.def_property_readonly( "num_measurements", [](const PyCircuitInstruction &self) -> uint64_t { return self.as_operation_ref().count_measurement_results(); }, clean_doc_string(R"DOC( Returns the number of bits produced when running this instruction. Examples: >>> import stim >>> stim.CircuitInstruction('H', [0]).num_measurements 0 >>> stim.CircuitInstruction('M', [0]).num_measurements 1 >>> stim.CircuitInstruction('M', [2, 3, 5, 7, 11]).num_measurements 5 >>> stim.CircuitInstruction('MXX', [0, 1, 4, 5, 11, 13]).num_measurements 3 >>> stim.Circuit('MPP X0*X1 X0*Z1*Y2')[0].num_measurements 2 >>> stim.CircuitInstruction('HERALDED_ERASE', [0], [0.25]).num_measurements 1 )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two `stim.CircuitInstruction`s are identical."); c.def(pybind11::self != pybind11::self, "Determines if two `stim.CircuitInstruction`s are different."); c.def( "__repr__", &PyCircuitInstruction::repr, "Returns text that is a valid python expression evaluating to an equivalent `stim.CircuitInstruction`."); c.def( "__str__", &PyCircuitInstruction::str, "Returns a text description of the instruction as a stim circuit file line."); c.def("__hash__", [](const PyCircuitInstruction &self) { return pybind11::hash(pybind11::str(self.str())); }); } ================================================ FILE: src/stim/circuit/circuit_instruction.pybind.h ================================================ // 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. #ifndef _STIM_CIRCUIT_CIRCUIT_INSTRUCTION_PYBIND_H #define _STIM_CIRCUIT_CIRCUIT_INSTRUCTION_PYBIND_H #include #include "stim/circuit/circuit_instruction.h" #include "stim/circuit/gate_target.h" #include "stim/gates/gates.h" namespace stim_pybind { struct PyCircuitInstruction { stim::GateType gate_type; std::vector targets; std::vector gate_args; pybind11::str tag; PyCircuitInstruction() = delete; PyCircuitInstruction( std::string_view name, std::span targets, std::span gate_args, pybind11::str tag); PyCircuitInstruction( stim::GateType gate_type, std::vector targets, std::vector gate_args, pybind11::str tag); static PyCircuitInstruction from_str(std::string_view text); static PyCircuitInstruction from_instruction(stim::CircuitInstruction instruction); stim::CircuitInstruction as_operation_ref() const; operator stim::CircuitInstruction() const; std::string name() const; std::vector targets_copy() const; std::vector gate_args_copy() const; std::vector raw_targets() const; std::vector> target_groups() const; bool operator==(const PyCircuitInstruction &other) const; bool operator!=(const PyCircuitInstruction &other) const; std::string repr() const; std::string str() const; }; pybind11::class_ pybind_circuit_instruction(pybind11::module &m); void pybind_circuit_instruction_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/circuit/circuit_instruction.test.cc ================================================ #include "stim/circuit/circuit_instruction.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.h" #include "stim/circuit/circuit.test.h" using namespace stim; TEST(circuit_instruction, for_combined_targets) { Circuit circuit(R"CIRCUIT( X CX S 1 H 0 2 TICK CX 0 1 2 3 CY 3 5 SPP MPP X0*X1*Z2 Z7 X5*X9 SPP Z5 )CIRCUIT"); auto get_k = [&](size_t k) { std::vector> results; circuit.operations[k].for_combined_target_groups([&](std::span group) { std::vector items; for (auto g : group) { items.push_back(g); } results.push_back(items); }); return results; }; ASSERT_EQ(get_k(0), (std::vector>{})); ASSERT_EQ(get_k(1), (std::vector>{})); ASSERT_EQ( get_k(2), (std::vector>{ {GateTarget::qubit(1)}, })); ASSERT_EQ( get_k(3), (std::vector>{ {GateTarget::qubit(0)}, {GateTarget::qubit(2)}, })); ASSERT_EQ(get_k(4), (std::vector>{})); ASSERT_EQ( get_k(5), (std::vector>{ {GateTarget::qubit(0), GateTarget::qubit(1)}, {GateTarget::qubit(2), GateTarget::qubit(3)}, })); ASSERT_EQ( get_k(6), (std::vector>{ {GateTarget::qubit(3), GateTarget::qubit(5)}, })); ASSERT_EQ(get_k(7), (std::vector>{})); ASSERT_EQ( get_k(8), (std::vector>{ {GateTarget::x(0), GateTarget::combiner(), GateTarget::x(1), GateTarget::combiner(), GateTarget::z(2)}, {GateTarget::z(7)}, {GateTarget::x(5), GateTarget::combiner(), GateTarget::x(9)}, })); ASSERT_EQ( get_k(9), (std::vector>{ {GateTarget::z(5)}, })); } TEST(circuit_instruction, for_combined_targets_works_on_all) { Circuit c = generate_test_circuit_with_all_operations(); size_t count = 0; for (const auto &e : c.operations) { if (e.gate_type == GateType::REPEAT) { continue; } e.for_combined_target_groups([&](std::span group) { count += group.size(); }); } ASSERT_TRUE(count > 0); } ================================================ FILE: src/stim/circuit/circuit_instruction_pybind_test.py ================================================ # 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. import stim import pytest def test_init_and_equality(): i = stim.CircuitInstruction("X_ERROR", [stim.GateTarget(5)], [0.5]) assert i.name == "X_ERROR" assert i.targets_copy() == [stim.GateTarget(5)] assert i.gate_args_copy() == [0.5] i2 = stim.CircuitInstruction(name="X_ERROR", targets=[stim.GateTarget(5)], gate_args=[0.5]) assert i == i2 assert i == stim.CircuitInstruction("X_ERROR", [stim.GateTarget(5)], [0.5]) assert not (i == stim.CircuitInstruction("Z_ERROR", [stim.GateTarget(5)], [0.5])) assert i != stim.CircuitInstruction("Z_ERROR", [stim.GateTarget(5)], [0.5]) assert i != stim.CircuitInstruction("X_ERROR", [stim.GateTarget(5), stim.GateTarget(6)], [0.5]) assert i != stim.CircuitInstruction("X_ERROR", [stim.GateTarget(5)], [0.25]) @pytest.mark.parametrize("value", [ stim.CircuitInstruction("X_ERROR", [stim.GateTarget(5)], [0.5]), stim.CircuitInstruction("M", [stim.GateTarget(stim.target_inv(3))]), ]) def test_repr(value): assert eval(repr(value), {'stim': stim}) == value assert repr(eval(repr(value), {'stim': stim})) == repr(value) def test_str(): assert str(stim.CircuitInstruction("X_ERROR", [stim.GateTarget(5)], [0.5])) == "X_ERROR(0.5) 5" def test_hashable(): a = stim.CircuitInstruction("X_ERROR", [stim.GateTarget(5)], [0.5]) b = stim.CircuitInstruction("DEPOLARIZE1", [stim.GateTarget(5)], [0.5]) c = stim.CircuitInstruction("X_ERROR", [stim.GateTarget(5)], [0.5]) assert hash(a) == hash(c) assert len({a, b, c}) == 2 def test_num_measurements(): assert stim.CircuitInstruction("X", [1, 2, 3]).num_measurements == 0 assert stim.CircuitInstruction("MXX", [1, 2]).num_measurements == 1 assert stim.CircuitInstruction("M", [1, 2]).num_measurements == 2 assert stim.CircuitInstruction("MPAD", [0, 1, 0]).num_measurements == 3 def test_target_groups(): assert stim.CircuitInstruction("MPAD", [0, 1, 0]).target_groups() == [ [stim.GateTarget(0)], [stim.GateTarget(1)], [stim.GateTarget(0)], ] assert stim.CircuitInstruction("H", []).target_groups() == [] assert stim.CircuitInstruction("H", [1]).target_groups() == [[stim.GateTarget(1)]] assert stim.CircuitInstruction("H", [2, 3]).target_groups() == [[stim.GateTarget(2)], [stim.GateTarget(3)]] assert stim.CircuitInstruction("CX", []).target_groups() == [] assert stim.CircuitInstruction("CX", [0, 1]).target_groups() == [[stim.GateTarget(0), stim.GateTarget(1)]] assert stim.CircuitInstruction("CX", [2, 3, 5, 7]).target_groups() == [[stim.GateTarget(2), stim.GateTarget(3)], [stim.GateTarget(5), stim.GateTarget(7)]] assert stim.CircuitInstruction("DETECTOR", []).target_groups() == [] assert stim.CircuitInstruction("CORRELATED_ERROR", [], [0.001]).target_groups() == [] assert stim.CircuitInstruction("MPP", []).target_groups() == [] assert stim.CircuitInstruction("MPAD", []).target_groups() == [] assert stim.CircuitInstruction("QUBIT_COORDS", [1, 2]).target_groups() == [[stim.GateTarget(1)], [stim.GateTarget(2)]] def test_eager_validate(): with pytest.raises(ValueError, match="0, 1, 2"): stim.CircuitInstruction("CX", [0, 1, 2]) def test_init_from_str(): assert stim.CircuitInstruction("CX", [0, 1]) == stim.CircuitInstruction("CX 0 1") with pytest.raises(ValueError, match="single CircuitInstruction"): stim.CircuitInstruction("") with pytest.raises(ValueError, match="single CircuitInstruction"): stim.CircuitInstruction(""" REPEAT 5 { H 0 X 1 } """) with pytest.raises(ValueError, match="single CircuitInstruction"): stim.CircuitInstruction(""" H 0 X 1 """) ================================================ FILE: src/stim/circuit/circuit_pybind_test.py ================================================ # 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. import io import pathlib import tempfile from typing import cast import stim import pytest import numpy as np def test_circuit_init_num_measurements_num_qubits(): c = stim.Circuit() assert c.num_qubits == c.num_measurements == 0 assert str(c).strip() == "" c.append_operation("X", [3]) assert c.num_qubits == 4 assert c.num_measurements == 0 assert str(c).strip() == """ X 3 """.strip() c.append_operation("M", [0]) assert c.num_qubits == 4 assert c.num_measurements == 1 assert str(c).strip() == """ X 3 M 0 """.strip() def test_circuit_append_operation(): c = stim.Circuit() with pytest.raises(IndexError, match="Gate not found"): c.append_operation("NOT_A_GATE", [0]) with pytest.raises(ValueError, match="even number of targets"): c.append_operation("CNOT", [0]) with pytest.raises(ValueError, match="takes 0"): c.append_operation("X", [0], 0.5) with pytest.raises(ValueError, match="invalid modifiers"): c.append_operation("X", [stim.target_inv(0)]) with pytest.raises(ValueError, match="invalid modifiers"): c.append_operation("X", [stim.target_x(0)]) with pytest.raises(IndexError, match="lookback"): stim.target_rec(0) with pytest.raises(IndexError, match="lookback"): stim.target_rec(1) with pytest.raises(IndexError, match="lookback"): stim.target_rec(-2**30) assert stim.target_rec(-1) is not None assert stim.target_rec(-15) is not None c.append_operation("X", [0]) c.append_operation("X", [1, 2]) c.append_operation("X", [3]) c.append_operation("CNOT", [0, 1]) c.append_operation("M", [0, stim.target_inv(1)]) c.append_operation("X_ERROR", [0], 0.25) c.append_operation("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(1)], 0.5) c.append_operation("DETECTOR", [stim.target_rec(-1)]) c.append_operation("OBSERVABLE_INCLUDE", [stim.target_rec(-1), stim.target_rec(-2)], 5) assert str(c).strip() == """ X 0 1 2 3 CX 0 1 M 0 !1 X_ERROR(0.25) 0 E(0.5) X0 Y1 DETECTOR rec[-1] OBSERVABLE_INCLUDE(5) rec[-1] rec[-2] """.strip() def test_circuit_iadd(): c = stim.Circuit() alias = c c.append_operation("X", [1, 2]) c2 = stim.Circuit() c2.append_operation("Y", [3]) c2.append_operation("M", [4]) c += c2 assert c is alias assert str(c).strip() == """ X 1 2 Y 3 M 4 """.strip() c += c assert str(c).strip() == """ X 1 2 Y 3 M 4 X 1 2 Y 3 M 4 """.strip() assert c is alias def test_circuit_add(): c = stim.Circuit() c.append_operation("X", [1, 2]) c2 = stim.Circuit() c2.append_operation("Y", [3]) c2.append_operation("M", [4]) assert str(c + c2).strip() == """ X 1 2 Y 3 M 4 """.strip() assert str(c2 + c2).strip() == """ Y 3 M 4 Y 3 M 4 """.strip() def test_circuit_mul(): c = stim.Circuit() c.append_operation("Y", [3]) c.append_operation("M", [4]) assert str(c * 2) == str(2 * c) == """ REPEAT 2 { Y 3 M 4 } """.strip() assert str((c * 2) * 3) == """ REPEAT 6 { Y 3 M 4 } """.strip() expected = """ REPEAT 3 { Y 3 M 4 } """.strip() assert str(c * 3) == str(3 * c) == expected alias = c c *= 3 assert alias is c assert str(c) == expected c *= 1 assert str(c) == expected assert alias is c c *= 0 assert str(c) == "" assert alias is c def test_circuit_repr(): v = stim.Circuit(""" X 0 M 0 """) r = repr(v) assert r == """stim.Circuit(''' X 0 M 0 ''')""" assert eval(r, {'stim': stim}) == v def test_circuit_eq(): a = """ X 0 M 0 """ b = """ Y 0 M 0 """ assert stim.Circuit() == stim.Circuit() assert stim.Circuit() != stim.Circuit(a) assert not (stim.Circuit() != stim.Circuit()) assert not (stim.Circuit() == stim.Circuit(a)) assert stim.Circuit(a) == stim.Circuit(a) assert stim.Circuit(b) == stim.Circuit(b) assert stim.Circuit(a) != stim.Circuit(b) assert stim.Circuit() != None assert stim.Circuit != object() assert stim.Circuit != "another type" assert not (stim.Circuit == None) assert not (stim.Circuit == object()) assert not (stim.Circuit == "another type") def test_circuit_clear(): c = stim.Circuit(""" X 0 M 0 """) c.clear() assert c == stim.Circuit() def test_circuit_compile_sampler(): c = stim.Circuit() s = c.compile_sampler() c.append_operation("M", [0]) assert repr(s) == "stim.CompiledMeasurementSampler(stim.Circuit())" s = c.compile_sampler() assert repr(s) == """ stim.CompiledMeasurementSampler(stim.Circuit(''' M 0 ''')) """.strip() c.append_operation("H", [0, 1, 2, 3, 4]) c.append_operation("M", [0, 1, 2, 3, 4]) s = c.compile_sampler() r = repr(s) assert r == """ stim.CompiledMeasurementSampler(stim.Circuit(''' M 0 H 0 1 2 3 4 M 0 1 2 3 4 ''')) """.strip() == str(stim.CompiledMeasurementSampler(c)) # Check that expression can be evaluated. _ = eval(r, {"stim": stim}) def test_circuit_compile_detector_sampler(): c = stim.Circuit() s = c.compile_detector_sampler() c.append_operation("M", [0]) assert repr(s) == "stim.CompiledDetectorSampler(stim.Circuit())" c.append_operation("DETECTOR", [stim.target_rec(-1)]) s = c.compile_detector_sampler() r = repr(s) assert r == """ stim.CompiledDetectorSampler(stim.Circuit(''' M 0 DETECTOR rec[-1] ''')) """.strip() # Check that expression can be evaluated. _ = eval(r, {"stim": stim}) def test_circuit_flattened_operations(): assert stim.Circuit(''' H 0 REPEAT 3 { X_ERROR(0.125) 1 } CORRELATED_ERROR(0.25) X3 Y4 Z5 M 0 !1 DETECTOR rec[-1] ''').flattened_operations() == [ ("H", [0], 0), ("X_ERROR", [1], 0.125), ("X_ERROR", [1], 0.125), ("X_ERROR", [1], 0.125), ("E", [("X", 3), ("Y", 4), ("Z", 5)], 0.25), ("M", [0, ("inv", 1)], 0), ("DETECTOR", [("rec", -1)], 0), ] def test_copy(): c = stim.Circuit("H 0") c2 = c.copy() assert c == c2 assert c is not c2 def test_hash(): # stim.Circuit is mutable. It must not also be value-hashable. # Defining __hash__ requires defining a FrozenCircuit variant instead. with pytest.raises(TypeError, match="unhashable"): _ = hash(stim.Circuit()) def test_circuit_generation(): surface_code_circuit = stim.Circuit.generated( "surface_code:rotated_memory_z", distance=5, rounds=10) samples = surface_code_circuit.compile_detector_sampler().sample(5) assert samples.shape == (5, 24 * 10) assert np.count_nonzero(samples) == 0 def test_circuit_generation_errors(): with pytest.raises(ValueError, match="Known repetition_code tasks"): stim.Circuit.generated( "repetition_code:UNKNOWN", distance=3, rounds=1000) with pytest.raises(ValueError, match="Expected type to start with."): stim.Circuit.generated( "UNKNOWN:memory", distance=0, rounds=1000) with pytest.raises(ValueError, match="distance >= 2"): stim.Circuit.generated( "repetition_code:memory", distance=1, rounds=1000) with pytest.raises(ValueError, match="0 <= after_clifford_depolarization <= 1"): stim.Circuit.generated( "repetition_code:memory", distance=3, rounds=1000, after_clifford_depolarization=-1) with pytest.raises(ValueError, match="0 <= before_round_data_depolarization <= 1"): stim.Circuit.generated( "repetition_code:memory", distance=3, rounds=1000, before_round_data_depolarization=-1) with pytest.raises(ValueError, match="0 <= after_reset_flip_probability <= 1"): stim.Circuit.generated( "repetition_code:memory", distance=3, rounds=1000, after_reset_flip_probability=-1) with pytest.raises(ValueError, match="0 <= before_measure_flip_probability <= 1"): stim.Circuit.generated( "repetition_code:memory", distance=3, rounds=1000, before_measure_flip_probability=-1) def test_num_detectors(): assert stim.Circuit().num_detectors == 0 assert stim.Circuit("DETECTOR").num_detectors == 1 assert stim.Circuit(""" REPEAT 1000 { DETECTOR } """).num_detectors == 1000 assert stim.Circuit(""" DETECTOR REPEAT 1000000 { REPEAT 1000000 { M 0 DETECTOR rec[-1] } } """).num_detectors == 1000000**2 + 1 def test_num_observables(): assert stim.Circuit().num_observables == 0 assert stim.Circuit("OBSERVABLE_INCLUDE(0)").num_observables == 1 assert stim.Circuit("OBSERVABLE_INCLUDE(1)").num_observables == 2 assert stim.Circuit(""" M 0 OBSERVABLE_INCLUDE(2) REPEAT 1000000 { REPEAT 1000000 { M 0 OBSERVABLE_INCLUDE(3) rec[-1] } OBSERVABLE_INCLUDE(4) } """).num_observables == 5 def test_indexing_operations(): c = stim.Circuit() assert len(c) == 0 assert list(c) == [] with pytest.raises(IndexError): _ = c[0] with pytest.raises(IndexError): _ = c[-1] c = stim.Circuit('X 0') assert len(c) == 1 assert list(c) == [stim.CircuitInstruction('X', [stim.GateTarget(0)])] assert c[0] == c[-1] == stim.CircuitInstruction('X', [stim.GateTarget(0)]) with pytest.raises(IndexError): _ = c[1] with pytest.raises(IndexError): _ = c[-2] c = stim.Circuit(''' X 5 6 REPEAT 1000 { H 5 } M !0 ''') assert len(c) == 3 with pytest.raises(IndexError): _ = c[3] with pytest.raises(IndexError): _ = c[-4] assert list(c) == [ stim.CircuitInstruction('X', [stim.GateTarget(5), stim.GateTarget(6)]), stim.CircuitRepeatBlock(1000, stim.Circuit('H 5')), stim.CircuitInstruction('M', [stim.GateTarget(stim.target_inv(0))]), ] def test_slicing(): c = stim.Circuit(""" H 0 REPEAT 5 { X 1 } Y 2 Z 3 """) assert c[:] is not c assert c[:] == c assert c[1:-1] == stim.Circuit(""" REPEAT 5 { X 1 } Y 2 """) assert c[::2] == stim.Circuit(""" H 0 Y 2 """) assert c[1::2] == stim.Circuit(""" REPEAT 5 { X 1 } Z 3 """) def test_reappend_gate_targets(): expected = stim.Circuit(""" MPP !X0 * X1 CX rec[-1] 5 """) c = stim.Circuit() c.append_operation("MPP", cast(stim.CircuitInstruction, expected[0]).targets_copy()) c.append_operation("CX", cast(stim.CircuitInstruction, expected[1]).targets_copy()) assert c == expected def test_append_instructions_and_blocks(): c = stim.Circuit() c.append_operation("TICK") assert c == stim.Circuit("TICK") with pytest.raises(ValueError, match="no targets"): c.append_operation("TICK", [1, 2, 3]) c.append_operation(stim.Circuit("H 1")[0]) assert c == stim.Circuit("TICK\nH 1") c.append_operation(stim.Circuit("CX 1 2 3 4")[0]) assert c == stim.Circuit(""" TICK H 1 CX 1 2 3 4 """) c.append_operation((stim.Circuit("X 5") * 100)[0]) assert c == stim.Circuit(""" TICK H 1 CX 1 2 3 4 REPEAT 100 { X 5 } """) c.append_operation(stim.Circuit("PAULI_CHANNEL_1(0.125, 0.25, 0.325) 4 5 6")[0]) assert c == stim.Circuit(""" TICK H 1 CX 1 2 3 4 REPEAT 100 { X 5 } PAULI_CHANNEL_1(0.125, 0.25, 0.325) 4 5 6 """) with pytest.raises(ValueError, match="must be a"): c.append_operation(object()) with pytest.raises(ValueError, match="targets"): c.append_operation(stim.Circuit("H 1")[0], [2]) with pytest.raises(ValueError, match="arg"): c.append_operation(stim.Circuit("H 1")[0], [], 0.1) with pytest.raises(ValueError, match="targets"): c.append_operation((stim.Circuit("H 1") * 5)[0], [2]) with pytest.raises(ValueError, match="arg"): c.append_operation((stim.Circuit("H 1") * 5)[0], [], 0.1) with pytest.raises(ValueError, match="repeat 0"): c.append_operation(stim.CircuitRepeatBlock(0, stim.Circuit("H 1"))) def test_circuit_measurement_sampling_seeded(): c = stim.Circuit(""" H 0 M 0 """) with pytest.raises(ValueError, match="seed"): c.compile_sampler(seed=-1) with pytest.raises(ValueError, match="seed"): c.compile_sampler(seed=object()) s1 = c.compile_sampler().sample(256) s2 = c.compile_sampler().sample(256) assert not np.array_equal(s1, s2) s1 = c.compile_sampler(seed=None).sample(256) s2 = c.compile_sampler(seed=None).sample(256) assert not np.array_equal(s1, s2) s1 = c.compile_sampler(seed=5).sample(256) s2 = c.compile_sampler(seed=5).sample(256) s3 = c.compile_sampler(seed=6).sample(256) assert np.array_equal(s1, s2) assert not np.array_equal(s1, s3) def test_circuit_detector_sampling_seeded(): c = stim.Circuit(""" X_ERROR(0.5) 0 M 0 DETECTOR rec[-1] """) with pytest.raises(ValueError, match="seed"): c.compile_detector_sampler(seed=-1) with pytest.raises(ValueError, match="seed"): c.compile_detector_sampler(seed=object()) s1 = c.compile_detector_sampler().sample(256) s2 = c.compile_detector_sampler().sample(256) assert not np.array_equal(s1, s2) s1 = c.compile_detector_sampler(seed=None).sample(256) s2 = c.compile_detector_sampler(seed=None).sample(256) assert not np.array_equal(s1, s2) s1 = c.compile_detector_sampler(seed=5).sample(256) s2 = c.compile_detector_sampler(seed=5).sample(256) s3 = c.compile_detector_sampler(seed=6).sample(256) assert np.array_equal(s1, s2) assert not np.array_equal(s1, s3) def test_approx_equals(): base = stim.Circuit("X_ERROR(0.099) 0") assert not base.approx_equals(stim.Circuit("X_ERROR(0.101) 0"), atol=0) assert not base.approx_equals(stim.Circuit("X_ERROR(0.101) 0"), atol=0.00001) assert base.approx_equals(stim.Circuit("X_ERROR(0.101) 0"), atol=0.01) assert base.approx_equals(stim.Circuit("X_ERROR(0.101) 0"), atol=999) assert not base.approx_equals(stim.Circuit("DEPOLARIZE1(0.101) 0"), atol=999) assert not base.approx_equals(object(), atol=999) assert not base.approx_equals(stim.PauliString("XYZ"), atol=999) def test_append_extended_cases(): c = stim.Circuit() c.append("H", 5) c.append("CNOT", [0, 1]) c.append("H", c[0].targets_copy()[0]) c.append("X", (e + 1 for e in range(5))) assert c == stim.Circuit(""" H 5 CNOT 0 1 H 5 X 1 2 3 4 5 """) def test_pickle(): import pickle t = stim.Circuit(""" H 0 REPEAT 100 { M 0 CNOT rec[-1] 2 } """) a = pickle.dumps(t) assert pickle.loads(a) == t def test_backwards_compatibility_vs_safety_append_vs_append_operation(): c = stim.Circuit() with pytest.raises(ValueError, match="takes 1 parens argument"): c.append("X_ERROR", [5]) with pytest.raises(ValueError, match="takes 1 parens argument"): c.append("OBSERVABLE_INCLUDE", []) assert c == stim.Circuit() c.append_operation("X_ERROR", [5]) assert c == stim.Circuit("X_ERROR(0) 5") c.append_operation("Z_ERROR", [5], 0.25) assert c == stim.Circuit("X_ERROR(0) 5\nZ_ERROR(0.25) 5") def test_anti_commuting_mpp_error_message(): with pytest.raises(ValueError, match="while analyzing a Pauli product measurement"): stim.Circuit(""" MPP X0 Z0 DETECTOR rec[-1] """).detector_error_model() def test_blocked_remnant_edge_error(): circuit = stim.Circuit(""" X_ERROR(0.125) 0 CORRELATED_ERROR(0.25) X0 X1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-2] """) assert circuit.detector_error_model(decompose_errors=True) == stim.DetectorErrorModel(""" error(0.125) D2 D3 error(0.25) D2 D3 ^ D0 D1 """) with pytest.raises(ValueError, match="Failed to decompose"): circuit.detector_error_model( decompose_errors=True, block_decomposition_from_introducing_remnant_edges=True) assert circuit.detector_error_model( decompose_errors=True, block_decomposition_from_introducing_remnant_edges=True, ignore_decomposition_failures=True) == stim.DetectorErrorModel(""" error(0.25) D0 D1 D2 D3 error(0.125) D2 D3 """) def test_shortest_graphlike_error(): c = stim.Circuit(""" TICK X_ERROR(0.125) 0 Y_ERROR(0.125) 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] """) actual = c.shortest_graphlike_error() assert len(actual) == 1 assert isinstance(actual[0], stim.ExplainedError) assert str(actual[0]) == """ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #2 (X_ERROR) in the circuit at target #1 of the instruction resolving to X_ERROR(0.125) 0 } }""" actual = c.shortest_graphlike_error(canonicalize_circuit_errors=True) assert len(actual) == 1 assert isinstance(actual[0], stim.ExplainedError) assert str(actual[0]) == """ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #2 (X_ERROR) in the circuit at target #1 of the instruction resolving to X_ERROR(0.125) 0 } }""" def test_shortest_graphlike_error_empty(): with pytest.raises(ValueError, match="Failed to find"): stim.Circuit().shortest_graphlike_error() def test_shortest_graphlike_error_msgs(): with pytest.raises( ValueError, match="NO OBSERVABLES" ): stim.Circuit().shortest_graphlike_error() c = stim.Circuit(""" M 0 OBSERVABLE_INCLUDE(0) rec[-1] """) with pytest.raises(ValueError, match="NO DETECTORS"): c.shortest_graphlike_error() c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 """) with pytest.raises(ValueError, match=r"NO OBSERVABLES(.|\n)*NO DETECTORS"): c.shortest_graphlike_error() with pytest.raises(ValueError, match=""): c.shortest_graphlike_error() c = stim.Circuit(""" M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """) with pytest.raises(ValueError, match="NO ERRORS"): c.shortest_graphlike_error() c = stim.Circuit(""" M(0.1) 0 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """) with pytest.raises(ValueError, match="NO GRAPHLIKE ERRORS"): c.shortest_graphlike_error() c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 DETECTOR rec[-1] """) with pytest.raises(ValueError, match="NO OBSERVABLES"): c.shortest_graphlike_error() def test_search_for_undetectable_logical_errors_empty(): with pytest.raises(ValueError, match="Failed to find"): stim.Circuit().search_for_undetectable_logical_errors( dont_explore_edges_increasing_symptom_degree=True, dont_explore_edges_with_degree_above=4, dont_explore_detection_event_sets_with_size_above=4, ) def test_search_for_undetectable_logical_errors_msgs(): with pytest.raises(ValueError, match=r"NO OBSERVABLES(.|\n)*NO DETECTORS"): stim.Circuit().search_for_undetectable_logical_errors( dont_explore_edges_increasing_symptom_degree=True, dont_explore_edges_with_degree_above=4, dont_explore_detection_event_sets_with_size_above=4, ) c = stim.Circuit(""" M 0 OBSERVABLE_INCLUDE(0) rec[-1] """) with pytest.raises(ValueError, match=r"NO DETECTORS(.|\n)*NO ERRORS"): c.search_for_undetectable_logical_errors( dont_explore_edges_increasing_symptom_degree=True, dont_explore_edges_with_degree_above=4, dont_explore_detection_event_sets_with_size_above=4, ) c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 """) with pytest.raises(ValueError, match=r"NO OBSERVABLES(.|\n)*NO DETECTORS(.|\n)*NO ERRORS"): c.search_for_undetectable_logical_errors( dont_explore_edges_increasing_symptom_degree=True, dont_explore_edges_with_degree_above=4, dont_explore_detection_event_sets_with_size_above=4, ) c = stim.Circuit(""" M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """) with pytest.raises(ValueError, match="NO ERRORS"): c.search_for_undetectable_logical_errors( dont_explore_edges_increasing_symptom_degree=True, dont_explore_edges_with_degree_above=4, dont_explore_detection_event_sets_with_size_above=4, ) c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 DETECTOR rec[-1] """) with pytest.raises(ValueError, match="NO OBSERVABLES"): c.search_for_undetectable_logical_errors( dont_explore_edges_increasing_symptom_degree=True, dont_explore_edges_with_degree_above=4, dont_explore_detection_event_sets_with_size_above=4, ) def test_shortest_error_sat_problem_unrecognized_format(): c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] X_ERROR(0.4) 0 M 0 DETECTOR rec[-1] rec[-2] """) with pytest.raises(ValueError, match='Unsupported format'): _ = c.shortest_error_sat_problem(format='unsupported format name') def test_shortest_error_sat_problem(): c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] X_ERROR(0.4) 0 M 0 DETECTOR rec[-1] rec[-2] """) sat_str = c.shortest_error_sat_problem() assert sat_str == 'p wcnf 2 4 5\n1 -1 0\n1 -2 0\n5 -1 0\n5 2 0\n' def test_likeliest_error_sat_problem(): c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] X_ERROR(0.4) 0 M 0 DETECTOR rec[-1] rec[-2] """) sat_str = c.likeliest_error_sat_problem(quantization=100) assert sat_str == 'p wcnf 2 4 401\n18 -1 0\n100 -2 0\n401 -1 0\n401 2 0\n' def test_shortest_graphlike_error_ignore(): c = stim.Circuit(""" TICK X_ERROR(0.125) 0 M 0 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] """) with pytest.raises(ValueError, match="Failed to decompose errors"): c.shortest_graphlike_error(ignore_ungraphlike_errors=False) with pytest.raises(ValueError, match="Failed to find any graphlike logical errors"): c.shortest_graphlike_error(ignore_ungraphlike_errors=True) def test_coords(): circuit = stim.Circuit(""" QUBIT_COORDS(1, 2, 3) 0 QUBIT_COORDS(2) 1 SHIFT_COORDS(5) QUBIT_COORDS(3) 4 """) assert circuit.get_final_qubit_coordinates() == { 0: [1, 2, 3], 1: [2], 4: [8], } def test_explain_errors(): circuit = stim.Circuit(""" H 0 CNOT 0 1 DEPOLARIZE1(0.01) 0 CNOT 0 1 H 0 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] """) r = circuit.explain_detector_error_model_errors() assert len(r) == 3 assert str(r[0]) == """ExplainedError { dem_error_terms: D0 CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 0 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.01) 0 } }""" assert str(r[1]) == """ExplainedError { dem_error_terms: D0 D1 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 0 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.01) 0 } }""" assert str(r[2]) == """ExplainedError { dem_error_terms: D1 CircuitErrorLocation { flipped_pauli_product: Z0 Circuit location stack trace: (after 0 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.01) 0 } }""" r = circuit.explain_detector_error_model_errors( dem_filter=stim.DetectorErrorModel('error(1) D0 D1'), reduce_to_one_representative_error=True, ) assert len(r) == 1 assert str(r[0]) == """ExplainedError { dem_error_terms: D0 D1 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 0 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.01) 0 } }""" def test_without_noise(): assert stim.Circuit(""" X_ERROR(0.25) 0 CNOT 0 1 M(0.125) 0 REPEAT 50 { DEPOLARIZE1(0.25) 0 1 2 X 0 1 2 } """).without_noise() == stim.Circuit(""" CNOT 0 1 M 0 REPEAT 50 { X 0 1 2 } """) def test_flattened(): assert stim.Circuit(""" SHIFT_COORDS(5, 0) QUBIT_COORDS(1, 2, 3) 0 REPEAT 5 { MR 0 1 DETECTOR(0, 0) rec[-2] DETECTOR(1, 0) rec[-1] SHIFT_COORDS(0, 1) } """).flattened() == stim.Circuit(""" QUBIT_COORDS(6, 2, 3) 0 MR 0 1 DETECTOR(5, 0) rec[-2] DETECTOR(6, 0) rec[-1] MR 0 1 DETECTOR(5, 1) rec[-2] DETECTOR(6, 1) rec[-1] MR 0 1 DETECTOR(5, 2) rec[-2] DETECTOR(6, 2) rec[-1] MR 0 1 DETECTOR(5, 3) rec[-2] DETECTOR(6, 3) rec[-1] MR 0 1 DETECTOR(5, 4) rec[-2] DETECTOR(6, 4) rec[-1] """) def test_complex_slice_does_not_seg_fault(): with pytest.raises(TypeError): _ = stim.Circuit()[1j] def test_circuit_from_file(): with tempfile.TemporaryDirectory() as tmpdir: path = tmpdir + '/tmp.stim' with open(path, 'w') as f: print('H 5', file=f) assert stim.Circuit.from_file(path) == stim.Circuit('H 5') with tempfile.TemporaryDirectory() as tmpdir: path = pathlib.Path(tmpdir) / 'tmp.stim' with open(path, 'w') as f: print('H 5', file=f) assert stim.Circuit.from_file(path) == stim.Circuit('H 5') with tempfile.TemporaryDirectory() as tmpdir: path = tmpdir + '/tmp.stim' with open(path, 'w') as f: print('CNOT 4 5', file=f) with open(path) as f: assert stim.Circuit.from_file(f) == stim.Circuit('CX 4 5') with pytest.raises(ValueError, match="how to read"): stim.Circuit.from_file(object()) with pytest.raises(ValueError, match="how to read"): stim.Circuit.from_file(123) def test_circuit_to_file(): c = stim.Circuit('H 5\ncnot 0 1') with tempfile.TemporaryDirectory() as tmpdir: path = tmpdir + '/tmp.stim' c.to_file(path) with open(path) as f: assert f.read() == 'H 5\nCX 0 1\n' with tempfile.TemporaryDirectory() as tmpdir: path = pathlib.Path(tmpdir) / 'tmp.stim' c.to_file(path) with open(path) as f: assert f.read() == 'H 5\nCX 0 1\n' with tempfile.TemporaryDirectory() as tmpdir: path = tmpdir + '/tmp.stim' with open(path, 'w') as f: c.to_file(f) with open(path) as f: assert f.read() == 'H 5\nCX 0 1\n' with pytest.raises(ValueError, match="how to write"): c.to_file(object()) with pytest.raises(ValueError, match="how to write"): c.to_file(123) def test_diagram(): c = stim.Circuit(""" H 0 CX 0 1 """) assert str(c.diagram()).strip() == """ q0: -H-@- | q1: ---X- """.strip() assert str(c.diagram(type='timeline-text')) == str(c.diagram()) c = stim.Circuit(""" H 0 CNOT 0 1 TICK M 0 1 DETECTOR rec[-1] rec[-2] """) assert str(c.diagram(type='detector-slice-text', tick=1)).strip() == """ q0: -Z:D0- | q1: -Z:D0- """.strip() c = stim.Circuit(""" H 0 CNOT 0 1 0 2 TICK M 0 1 2 DETECTOR(4,5) rec[-1] rec[-2] DETECTOR(6) rec[-2] rec[-3] """) assert str(c.diagram(type='detector-slice-text', tick=1, filter_coords=[(5, 6, 7), (6,), (7, 8)])).strip() == """ q0: -Z:D1- | q1: -Z:D1- q2: ------ """.strip() assert c.diagram() is not None assert c.diagram(type="timeline-svg") is not None assert c.diagram(type="timeline-svg", tick=5) is not None assert c.diagram("timeline-svg") is not None assert c.diagram("timeline-3d") is not None assert c.diagram("timeline-3d-html") is not None assert c.diagram("matchgraph-svg") is not None assert c.diagram("matchgraph-3d") is not None assert c.diagram("matchgraph-3d-html") is not None assert c.diagram("match-graph-svg") is not None assert c.diagram("match-graph-3d") is not None assert c.diagram("match-graph-3d-html") is not None assert c.diagram("detslice-svg", tick=1) is not None assert c.diagram("detslice-text", tick=1) is not None assert c.diagram("detector-slice-svg", tick=1) is not None assert c.diagram("detector-slice-text", tick=1) is not None assert c.diagram("detslice-with-ops-svg", tick=1) is not None assert c.diagram("timeslice-svg", tick=1) is not None assert c.diagram("time-slice-svg", tick=1) is not None assert c.diagram("time+detector-slice-svg", tick=1) is not None assert c.diagram("time+detector-slice-svg", tick=range(1, 3)) is not None with pytest.raises(ValueError, match="step"): assert c.diagram("time+detector-slice-svg", tick=range(1, 3, 2)) is not None with pytest.raises(ValueError, match="stop"): assert c.diagram("time+detector-slice-svg", tick=range(3, 3)) is not None assert "iframe" in str(c.diagram(type="match-graph-svg-html")) assert "iframe" in str(c.diagram(type="detslice-svg-html")) assert "iframe" in str(c.diagram(type="timeslice-svg-html")) assert "iframe" in str(c.diagram(type="timeline-svg-html")) def test_circuit_inverse(): assert stim.Circuit(""" S 0 1 CX 0 1 0 2 """).inverse() == stim.Circuit(""" CX 0 2 0 1 S_DAG 1 0 """) def test_circuit_slice_reverse(): c = stim.Circuit() assert c[::-1] == stim.Circuit() c = stim.Circuit("X 1\nY 2\nZ 3") assert c[::-1] == stim.Circuit("Z 3\nY 2\nX 1") def test_with_inlined_feedback_bad_end_eats_into_loop(): assert stim.Circuit(""" CX 0 1 M 1 CX rec[-1] 1 CX 0 1 M 1 DETECTOR rec[-1] rec[-2] OBSERVABLE_INCLUDE(0) rec[-1] """).with_inlined_feedback() == stim.Circuit(""" CX 0 1 M 1 OBSERVABLE_INCLUDE(0) rec[-1] CX 0 1 M 1 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """) before = stim.Circuit(""" R 0 1 2 3 4 5 6 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 CX rec[-1] 5 rec[-2] 3 rec[-3] 1 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK REPEAT 10 { X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 CX rec[-1] 5 rec[-2] 3 rec[-3] 1 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] rec[-6] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK } X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 CX rec[-1] 5 rec[-2] 3 rec[-3] 1 DETECTOR rec[-1] rec[-2] rec[-3] DETECTOR rec[-3] rec[-4] rec[-5] DETECTOR rec[-5] rec[-6] rec[-7] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK OBSERVABLE_INCLUDE(0) rec[-1] """) after = before.with_inlined_feedback() assert after == stim.Circuit(""" R 0 1 2 3 4 5 6 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK REPEAT 8 { X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 DETECTOR rec[-7] rec[-1] DETECTOR rec[-8] rec[-2] DETECTOR rec[-9] rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK } X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 OBSERVABLE_INCLUDE(0) rec[-1] DETECTOR rec[-7] rec[-1] DETECTOR rec[-8] rec[-2] DETECTOR rec[-9] rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 DETECTOR rec[-6] rec[-5] rec[-4] rec[-3] rec[-2] rec[-1] DETECTOR rec[-8] rec[-7] rec[-6] rec[-5] rec[-4] rec[-3] DETECTOR rec[-10] rec[-9] rec[-8] rec[-7] rec[-6] rec[-5] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK OBSERVABLE_INCLUDE(0) rec[-1] """) dem1 = before.flattened().detector_error_model() dem2 = after.flattened().detector_error_model() assert dem1.approx_equals(dem2, atol=1e-5) def test_with_inlined_feedback(): assert stim.Circuit(""" CX 0 1 M 1 CX rec[-1] 1 CX 0 1 M 1 DETECTOR rec[-1] rec[-2] OBSERVABLE_INCLUDE(0) rec[-1] """).with_inlined_feedback() == stim.Circuit(""" CX 0 1 M 1 OBSERVABLE_INCLUDE(0) rec[-1] CX 0 1 M 1 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """) before = stim.Circuit(""" R 0 1 2 3 4 5 6 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 CX rec[-1] 5 rec[-2] 3 rec[-3] 1 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK REPEAT 10 { X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 CX rec[-1] 5 rec[-2] 3 rec[-3] 1 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] rec[-6] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK } X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 0 1 2 3 4 5 6 CX rec[-1] 5 rec[-2] 3 rec[-3] 1 DETECTOR rec[-1] rec[-2] rec[-3] DETECTOR rec[-3] rec[-4] rec[-5] DETECTOR rec[-5] rec[-6] rec[-7] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK OBSERVABLE_INCLUDE(0) rec[-1] """) after = before.with_inlined_feedback() assert str(after) == str(stim.Circuit(""" R 0 1 2 3 4 5 6 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK REPEAT 9 { X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 1 3 5 DETECTOR rec[-7] rec[-1] DETECTOR rec[-8] rec[-2] DETECTOR rec[-9] rec[-3] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK } X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 0 1 2 3 4 5 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK CX 6 5 4 3 2 1 X_ERROR(0.125) 0 1 2 3 4 5 6 TICK M(0.25) 0 1 2 3 4 5 6 DETECTOR rec[-8] rec[-3] rec[-2] rec[-1] DETECTOR rec[-9] rec[-5] rec[-4] rec[-3] DETECTOR rec[-10] rec[-7] rec[-6] rec[-5] X_ERROR(0.125) 0 1 2 3 4 5 6 TICK OBSERVABLE_INCLUDE(0) rec[-1] """)) dem1 = before.flattened().detector_error_model() dem2 = after.flattened().detector_error_model() assert dem1.approx_equals(dem2, atol=1e-5) def test_detslice_ops_diagram_no_ticks_does_not_hang(): assert stim.Circuit.generated("surface_code:rotated_memory_x", rounds=5, distance=5).diagram("detslice-svg") is not None def test_num_ticks(): assert stim.Circuit().num_ticks == 0 assert stim.Circuit("TICK").num_ticks == 1 assert stim.Circuit(""" TICK REPEAT 100 { TICK TICK REPEAT 10 { TICK } } """).num_ticks == 1201 assert stim.Circuit(""" H 0 TICK CX 0 1 TICK """).num_ticks == 2 def test_reference_sample(): circuit = stim.Circuit( """ H 0 CNOT 0 1 """ ) ref = circuit.reference_sample() assert len(ref) == 0 circuit = stim.Circuit( """ H 0 1 CX 0 2 1 3 MPP X0*X1 Y0*Y1 Z0*Z1 """ ) np.testing.assert_array_equal(circuit.reference_sample(), circuit.reference_sample()) assert np.sum(circuit.reference_sample()) % 2 == 1 circuit.append("X", (i for i in range(0, 100, 2))) circuit.append("M", (i for i in range(100))) ref_sample = circuit.reference_sample(bit_packed=True) unpacked = np.unpackbits(ref_sample, bitorder="little") expected = circuit.reference_sample(bit_packed=False) expected_padded = np.zeros_like(unpacked) expected_padded[:len(expected)] = expected np.testing.assert_array_equal(unpacked, expected_padded) def test_max_mix_depolarization_is_allowed_in_dem_conversion_without_args(): assert stim.Circuit(""" H 0 CX 0 1 DEPOLARIZE1(0.75) 0 CX 0 1 H 0 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] """).detector_error_model(approximate_disjoint_errors=True) == stim.DetectorErrorModel(""" error(0.5) D0 error(0.5) D0 D1 error(0.5) D1 """) assert stim.Circuit(""" H 0 1 CX 0 2 1 3 DEPOLARIZE2(0.9375) 0 1 CX 0 2 1 3 H 0 1 M 0 1 2 3 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] DETECTOR rec[-4] """).detector_error_model() == stim.DetectorErrorModel(""" error(0.5) D0 error(0.5) D0 D1 error(0.5) D0 D1 D2 error(0.5) D0 D1 D2 D3 error(0.5) D0 D1 D3 error(0.5) D0 D2 error(0.5) D0 D2 D3 error(0.5) D0 D3 error(0.5) D1 error(0.5) D1 D2 error(0.5) D1 D2 D3 error(0.5) D1 D3 error(0.5) D2 error(0.5) D2 D3 error(0.5) D3 """) def test_shortest_graphlike_error_many_obs(): c = stim.Circuit(""" MPP Z0*Z1 Z1*Z2 Z2*Z3 Z3*Z4 X_ERROR(0.1) 0 1 2 3 4 MPP Z0*Z1 Z1*Z2 Z2*Z3 Z3*Z4 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] M 4 OBSERVABLE_INCLUDE(1200) rec[-1] """) assert len(c.shortest_graphlike_error()) == 5 def test_detslice_filter_coords_flexibility(): c = stim.Circuit.generated("repetition_code:memory", distance=3, rounds=3) d1 = c.diagram("detslice", filter_coords=[stim.DemTarget.relative_detector_id(1)]) d2 = c.diagram("detslice-svg", filter_coords=stim.DemTarget.relative_detector_id(1)) d3 = c.diagram("detslice", filter_coords=["D1"]) d4 = c.diagram("detslice", filter_coords="D1") d5 = c.diagram("detector-slice-svg", filter_coords=[3, 0]) d6 = c.diagram("detslice-svg", filter_coords=[[3, 0]]) assert str(d1) == str(d2) assert str(d1) == str(d3) assert str(d1) == str(d4) assert str(d1) == str(d5) assert str(d1) == str(d6) assert str(d1) != str(c.diagram("detslice", filter_coords="L0")) d1 = c.diagram("detslice", filter_coords=[stim.DemTarget.relative_detector_id(1), stim.DemTarget.relative_detector_id(3), stim.DemTarget.relative_detector_id(5), "D7"]) d2 = c.diagram("detslice", filter_coords=["D1", "D3", "D5", "D7"]) d3 = c.diagram("detslice-svg", filter_coords=[3,]) d4 = c.diagram("detslice-svg", filter_coords=[[3,]]) d5 = c.diagram("detslice-svg", filter_coords=[[3, 0], [3, 1], [3, 2], [3, 3]]) assert str(d1) == str(d2) assert str(d1) == str(d3) assert str(d1) == str(d4) assert str(d1) == str(d5) def test_has_flow_ry(): c = stim.Circuit(""" RY 0 """) assert c.has_flow(stim.Flow("1 -> Y")) assert not c.has_flow(stim.Flow("1 -> -Y")) assert not c.has_flow(stim.Flow("1 -> X")) assert c.has_flow(stim.Flow("1 -> Y"), unsigned=True) assert not c.has_flow(stim.Flow("1 -> X"), unsigned=True) assert c.has_flow(stim.Flow("1 -> -Y"), unsigned=True) def test_has_flow_cxs(): c = stim.Circuit(""" CX 0 1 S 0 """) assert c.has_flow(stim.Flow("X_ -> YX")) assert c.has_flow(stim.Flow("Y_ -> -XX")) assert not c.has_flow(stim.Flow("X_ -> XX")) assert not c.has_flow(stim.Flow("X_ -> -XX")) assert c.has_flow(stim.Flow("X_ -> YX"), unsigned=True) assert c.has_flow(stim.Flow("Y_ -> -XX"), unsigned=True) assert not c.has_flow(stim.Flow("X_ -> XX"), unsigned=True) assert not c.has_flow(stim.Flow("X_ -> -XX"), unsigned=True) def test_has_flow_cxm(): c = stim.Circuit(""" CX 0 1 M 1 """) assert c.has_flow(stim.Flow("1 -> _Z xor rec[0]")) assert c.has_flow(stim.Flow("ZZ -> rec[0]")) assert c.has_flow(stim.Flow("ZZ -> _Z")) assert c.has_flow(stim.Flow("XX -> X_")) assert c.has_flow(stim.Flow("1 -> _Z xor rec[0]"), unsigned=True) assert c.has_flow(stim.Flow("ZZ -> rec[0]"), unsigned=True) assert c.has_flow(stim.Flow("ZZ -> _Z"), unsigned=True) assert c.has_flow(stim.Flow("XX -> X_"), unsigned=True) def test_has_flow_lattice_surgery(): c = stim.Circuit(""" # Lattice surgery CNOT with feedback. RX 2 MZZ 2 0 MXX 2 1 MZ 2 CX rec[-1] 1 rec[-3] 1 CZ rec[-2] 0 S 0 """) assert c.has_flow(stim.Flow("X_ -> YX")) assert c.has_flow(stim.Flow("Z_ -> Z_")) assert c.has_flow(stim.Flow("_X -> _X")) assert c.has_flow(stim.Flow("_Z -> ZZ")) assert not c.has_flow(stim.Flow("X_ -> XX")) assert not c.has_flow(stim.Flow("X_ -> XX")) assert not c.has_flow(stim.Flow("X_ -> -YX")) assert not c.has_flow(stim.Flow("X_ -> XX"), unsigned=True) assert c.has_flow(stim.Flow("X_ -> -YX"), unsigned=True) def test_has_flow_lattice_surgery_without_feedback(): c = stim.Circuit(""" # Lattice surgery CNOT without feedback. RX 2 MZZ 2 0 MXX 2 1 MZ 2 S 0 """) assert c.has_flow(stim.Flow("X_ -> YX xor rec[1]")) assert c.has_flow(stim.Flow("Z_ -> Z_")) assert c.has_flow(stim.Flow("_X -> _X")) assert c.has_flow(stim.Flow("_Z -> ZZ xor rec[0] xor rec[2]")) assert not c.has_flow(stim.Flow("X_ -> XX")) assert c.has_all_flows([]) assert c.has_all_flows([ stim.Flow("X_ -> YX xor rec[1]"), stim.Flow("Z_ -> Z_"), ]) assert not c.has_all_flows([ stim.Flow("X_ -> YX xor rec[1]"), stim.Flow("Z_ -> Z_"), stim.Flow("X_ -> XX"), ]) assert not c.has_flow(stim.Flow("X_ -> XX")) assert not c.has_flow(stim.Flow("X_ -> -YX")) assert not c.has_flow(stim.Flow("X_ -> XX"), unsigned=True) assert c.has_flow(stim.Flow("X_ -> -YX xor rec[1]"), unsigned=True) def test_has_flow_shorthands(): c = stim.Circuit(""" MZ 99 MXX 1 99 MZZ 0 99 MX 99 """) assert c.has_flow(stim.Flow("X_ -> XX xor rec[1] xor rec[3]")) assert c.has_flow(stim.Flow("Z_ -> Z_")) assert c.has_flow(stim.Flow("_X -> _X")) assert c.has_flow(stim.Flow("_Z -> ZZ xor rec[0] xor rec[2]")) assert c.has_flow(stim.Flow("X_ -> XX xor rec[1] xor rec[3]")) assert not c.has_flow(stim.Flow("Z_ -> -Z_")) assert not c.has_flow(stim.Flow("-Z_ -> Z_")) assert not c.has_flow(stim.Flow("Z_ -> X_")) assert c.has_flow(stim.Flow("iX_ -> iXX xor rec[1] xor rec[3]")) assert not c.has_flow(stim.Flow("-iX_ -> iXX xor rec[1] xor rec[3]")) assert c.has_flow(stim.Flow("-iX_ -> -iXX xor rec[1] xor rec[3]")) with pytest.raises(ValueError, match="Anti-Hermitian"): stim.Flow("iX_ -> XX") def test_decomposed(): assert stim.Circuit(""" ISWAP 0 1 2 1 TICK MPP X1*Z2*Y3 """).decomposed() == stim.Circuit(""" H 0 CX 0 1 1 0 H 1 S 1 0 H 2 CX 2 1 1 2 H 1 S 1 2 TICK H 1 3 S 3 H 3 S 3 3 CX 2 1 3 1 M 1 CX 2 1 3 1 H 3 S 3 H 3 S 3 3 H 1 """) def test_detecting_regions(): assert stim.Circuit(''' R 0 TICK H 0 TICK CX 0 1 TICK MX 0 1 DETECTOR rec[-1] rec[-2] ''').detecting_regions() == {stim.DemTarget.relative_detector_id(0): { 0: stim.PauliString("Z_"), 1: stim.PauliString("X_"), 2: stim.PauliString("XX"), }} def test_detecting_region_filters(): c = stim.Circuit.generated("repetition_code:memory", distance=3, rounds=3) assert len(c.detecting_regions(targets=["D"])) == c.num_detectors assert len(c.detecting_regions(targets=["L"])) == c.num_observables assert len(c.detecting_regions()) == c.num_observables + c.num_detectors assert len(c.detecting_regions(targets=["D0"])) == 1 assert len(c.detecting_regions(targets=["D0", "L0"])) == 2 assert len(c.detecting_regions(targets=[stim.target_relative_detector_id(0), "D0"])) == 1 def test_detecting_regions_mzz(): c = stim.Circuit(""" TICK MZZ 0 1 1 2 TICK M 2 DETECTOR rec[-1] """) assert c.detecting_regions() == { stim.target_relative_detector_id(0): { 0: stim.PauliString("__Z"), 1: stim.PauliString("__Z"), }, } def test_insert(): c = stim.Circuit() with pytest.raises(ValueError, match='type'): c.insert(0, object()) with pytest.raises(ValueError, match='index <'): c.insert(1, stim.CircuitInstruction("H", [1])) with pytest.raises(ValueError, match='index <'): c.insert(-1, stim.CircuitInstruction("H", [1])) c.insert(0, stim.CircuitInstruction("H", [1])) assert c == stim.Circuit(""" H 1 """) with pytest.raises(ValueError, match='index <'): c.insert(2, stim.CircuitInstruction("S", [2])) with pytest.raises(ValueError, match='index <'): c.insert(-2, stim.CircuitInstruction("S", [2])) c.insert(0, stim.CircuitInstruction("S", [2, 3])) assert c == stim.Circuit(""" S 2 3 H 1 """) c.insert(-1, stim.Circuit("H 5\nM 2")) assert c == stim.Circuit(""" S 2 3 H 5 M 2 H 1 """) c.insert(2, stim.Circuit(""" REPEAT 100 { M 3 } """)) assert c == stim.Circuit(""" S 2 3 H 5 REPEAT 100 { M 3 } M 2 H 1 """) c.insert(2, stim.Circuit(""" REPEAT 100 { M 3 } """)[0]) assert c == stim.Circuit(""" S 2 3 H 5 REPEAT 100 { M 3 } REPEAT 100 { M 3 } M 2 H 1 """) def test_pop(): with pytest.raises(IndexError, match='index'): stim.Circuit().pop() with pytest.raises(IndexError, match='index'): stim.Circuit().pop(-1) with pytest.raises(IndexError, match='index'): stim.Circuit().pop(0) c = stim.Circuit("H 0") with pytest.raises(IndexError, match='index'): c.pop(1) with pytest.raises(IndexError, match='index'): c.pop(-2) assert c.pop(0) == stim.CircuitInstruction("H", [0]) c = stim.Circuit("H 0\n X 1") assert c.pop() == stim.CircuitInstruction("X", [1]) assert c.pop() == stim.CircuitInstruction("H", [0]) def test_circuit_create_with_odd_cx(): with pytest.raises(ValueError, match="0, 1, 2"): stim.Circuit("CX 0 1 2") def test_to_tableau(): assert stim.Circuit().to_tableau() == stim.Tableau(0) assert stim.Circuit("QUBIT_COORDS 0").to_tableau() == stim.Tableau(1) assert stim.Circuit("I 0").to_tableau() == stim.Tableau(1) assert stim.Circuit("H 0").to_tableau() == stim.Tableau.from_named_gate("H") assert stim.Circuit("CX 0 1").to_tableau() == stim.Tableau.from_named_gate("CX") assert stim.Circuit("SPP Z0").to_tableau() == stim.Tableau.from_named_gate("S") assert stim.Circuit("SPP X0").to_tableau() == stim.Tableau.from_named_gate("SQRT_X") assert stim.Circuit("SPP_DAG Y0*Y1").to_tableau() == stim.Tableau.from_named_gate("SQRT_YY_DAG") def test_circuit_tags(): c = stim.Circuit(""" H[test] 0 """) assert str(c) == "H[test] 0" assert c[0].tag == 'test' c.append(stim.CircuitInstruction('CX', [0, 1], tag='test2')) assert c[1].tag == 'test2' assert c == stim.Circuit(""" H[test] 0 CX[test2] 0 1 """) assert c != stim.Circuit(""" H 0 CX 0 1 """) def test_circuit_add_tags(): assert stim.Circuit(""" H[test] 0 """) + stim.Circuit(""" CX[test2] 0 1 """) == stim.Circuit(""" H[test] 0 CX[test2] 0 1 """) def test_circuit_eq_tags(): assert stim.CircuitInstruction("TICK", tag="a") == stim.CircuitInstruction("TICK", tag="a") assert stim.CircuitInstruction("TICK", tag="a") != stim.CircuitInstruction("TICK", tag="b") assert stim.CircuitRepeatBlock(1, stim.Circuit(), tag="a") == stim.CircuitRepeatBlock(1, stim.Circuit(), tag="a") assert stim.CircuitRepeatBlock(1, stim.Circuit(), tag="a") != stim.CircuitRepeatBlock(1, stim.Circuit(), tag="b") assert stim.Circuit(""" H[test] 0 """) == stim.Circuit(""" H[test] 0 """) assert stim.Circuit(""" H[test] 0 """) != stim.Circuit(""" H[test2] 0 """) assert stim.Circuit(""" H[test] 0 """) != stim.Circuit(""" H 0 """) assert stim.Circuit(""" H[] 0 """) == stim.Circuit(""" H 0 """) def test_circuit_get_item_tags(): assert stim.Circuit(""" H[test] 0 CX[test2] 1 2 REPEAT[test3] 3 { M[test4](0.25) 4 } """)[1] == stim.CircuitInstruction("CX[test2] 1 2") assert stim.Circuit(""" H[test] 0 CX[test2] 1 2 REPEAT[test3] 3 { M[test4](0.25) 4 } """)[2] == stim.CircuitRepeatBlock(3, stim.Circuit("M[test4](0.25) 4"), tag="test3") assert stim.Circuit(""" H[test] 0 CX[test2] 1 2 REPEAT[test3] 3 { M[test4](0.25) 4 } """)[1:3] == stim.Circuit(""" CX[test2] 1 2 REPEAT[test3] 3 { M[test4](0.25) 4 } """) def test_tags_iadd(): c = stim.Circuit(""" H[test] 0 CX[test2] 1 2 """) c += stim.Circuit(""" REPEAT[test3] 3 { M[test4](0.25) 4 } """) assert c == stim.Circuit(""" H[test] 0 CX[test2] 1 2 REPEAT[test3] 3 { M[test4](0.25) 4 } """) def test_tags_imul(): c = stim.Circuit(""" H[test] 0 CX[test2] 1 2 """) c *= 2 assert c == stim.Circuit(""" REPEAT 2 { H[test] 0 CX[test2] 1 2 } """) def test_tags_mul(): c = stim.Circuit(""" H[test] 0 CX[test2] 1 2 """) assert c * 2 == stim.Circuit(""" REPEAT 2 { H[test] 0 CX[test2] 1 2 } """) def test_tags_append(): c = stim.Circuit(""" H[test] 0 CX[test2] 1 2 """) c.append(stim.CircuitRepeatBlock(3, stim.Circuit(""" M[test4](0.25) 4 """), tag="test3")) assert c == stim.Circuit(""" H[test] 0 CX[test2] 1 2 REPEAT[test3] 3 { M[test4](0.25) 4 } """) def test_tags_append_from_stim_program_text(): c = stim.Circuit() c.append_from_stim_program_text(""" H[test] 0 CX[test2] 1 2 """) assert c == stim.Circuit(""" H[test] 0 CX[test2] 1 2 """) def test_tag_approx_equals(): assert not stim.Circuit("H[test] 0").approx_equals(stim.Circuit("H[test2] 0"), atol=3) assert stim.Circuit("H[test] 0").approx_equals(stim.Circuit("H[test] 0"), atol=3) def test_tag_clear(): c = stim.Circuit("H[test] 0") c.clear() assert c == stim.Circuit() def test_tag_compile_samplers(): c = stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 M[test3](0.25) 0 DETECTOR[test4](1, 2) rec[-1] """) s = c.compile_detector_sampler() assert 200 < np.sum(s.sample(shots=1000)) < 600 s = c.compile_sampler() assert 200 < np.sum(s.sample(shots=1000)) < 600 _ = c.compile_m2d_converter() def test_tag_detector_error_model(): dem = stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 M[test3](0.25) 0 DETECTOR[test4](1, 2) rec[-1] """).detector_error_model() assert dem == stim.DetectorErrorModel(""" error[test2](0.25) D0 error[test3](0.25) D0 detector[test4](1, 2) D0 """) def test_tag_copy(): c = stim.Circuit(""" H[test] 0 CX[test2] 1 2 REPEAT[test3] 3 { M[test4](0.25) 4 } """).detector_error_model() cc = c.copy() assert cc is not c assert cc == c def test_tag_count_determined_measurements(): assert stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 M[test3](0.25) 0 DETECTOR[test4](1, 2) rec[-1] """).count_determined_measurements() == 1 def test_tag_decomposed(): assert stim.Circuit(""" RX[test1] 0 X_ERROR[test2](0.25) 0 MPP[test3](0.25) X0*Z1 DETECTOR[test4](1, 2) rec[-1] SPP[test5] Y0 """).decomposed() == stim.Circuit(""" R[test1] 0 H[test1] 0 X_ERROR[test2](0.25) 0 H[test3] 0 CX[test3] 1 0 M[test3] 0 CX[test3] 1 0 H[test3] 0 DETECTOR[test4](1, 2) rec[-1] H[test5] 0 S[test5] 0 H[test5] 0 S[test5] 0 0 0 H[test5] 0 S[test5] 0 H[test5] 0 S[test5] 0 0 """) def test_tag_detecting_regions(): assert stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 TICK M[test3](0.25) 0 DETECTOR[test4](1, 2) rec[-1] """).detecting_regions() == {stim.DemTarget('D0'): {0: stim.PauliString("Z")}} def test_tag_diagram(): # TODO: include tags in diagrams assert str(stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 M[test3](0.25) 0 DETECTOR[test4](1, 2) rec[-1] """).diagram()) == """ q0: -R-X_ERROR(0.25)-M(0.25):rec[0]-DETECTOR(1,2):D0=rec[0]- """.strip() def test_tag_flattened(): assert stim.Circuit(""" R[test1] 0 REPEAT[test1.5] 2 { H[test2] 0 } """).flattened() == stim.Circuit(""" R[test1] 0 H[test2] 0 H[test2] 0 """) def test_tag_from_file(): c = stim.Circuit.from_file(io.StringIO(""" R[test1] 0 REPEAT[test1.5] 2 { H[test2] 0 } """)) assert c == stim.Circuit(""" R[test1] 0 REPEAT[test1.5] 2 { H[test2] 0 } """) s = io.StringIO() c.to_file(s) s.seek(0) assert s.read() == str(c) + '\n' def test_tag_insert(): c = stim.Circuit(""" H[test1] 0 S[test2] 0 """) c.insert(1, stim.CircuitInstruction("CX[test3] 0 1")) assert c == stim.Circuit(""" H[test1] 0 CX[test3] 0 1 S[test2] 0 """) def test_tag_fuse(): c = stim.Circuit(""" H[test1] 0 H[test1] 0 H[test2] 0 H[test1] 0 """) assert len(c) == 3 assert c[0].tag == "test1" assert c[1].tag == "test2" assert c[2].tag == "test1" def test_tag_inverse(): assert stim.Circuit(""" S[test1] 0 CX[test2] 0 1 SPP[test3] X0*Y1 REPEAT[test4] 2 { H[test5] 0 } """).inverse() == stim.Circuit(""" REPEAT[test4] 2 { H[test5] 0 } SPP_DAG[test3] Y1*X0 CX[test2] 0 1 S_DAG[test1] 0 """) def test_tag_time_reversed_for_flows(): c, _ = stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 SQRT_X[test3] 0 MY[test4] 0 DETECTOR[test5] rec[-1] """).time_reversed_for_flows([]) assert c == stim.Circuit(""" RY[test4] 0 SQRT_X_DAG[test3] 0 X_ERROR[test2](0.25) 0 M[test1] 0 DETECTOR[test5] rec[-1] """) def test_tag_with_inlined_feedback(): assert stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 1 MR[test3] 0 CX[test4] rec[-1] 1 M[test5] 1 DETECTOR[test6] rec[-1] """).with_inlined_feedback() == stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 1 MR[test3] 0 M[test5] 1 DETECTOR[test6] rec[-2] rec[-1] """) def test_tag_without_noise(): assert stim.Circuit(""" R[test1] 0 X_ERROR[test2](0.25) 0 1 M[test3](0.25) 0 DETECTOR[test4] rec[-1] """).without_noise() == stim.Circuit(""" R[test1] 0 M[test3] 0 DETECTOR[test4] rec[-1] """) def test_append_tag(): c = stim.Circuit() c.append("H", [2, 3], tag="test") assert c == stim.Circuit("H[test] 2 3") with pytest.raises(ValueError, match="tag"): c.append(c[0], tag="newtag") with pytest.raises(ValueError, match="tag"): c.append(stim.CircuitRepeatBlock(10, stim.Circuit()), tag="newtag") assert c == stim.Circuit("H[test] 2 3") def test_append_pauli_string(): c = stim.Circuit() c.append("MPP", [ stim.PauliString("X1*Y2*Z3"), stim.target_y(4), stim.PauliString("Z5"), ]) assert c == stim.Circuit(""" MPP X1*Y2*Z3 Y4 Z5 """) c.append("MPP", stim.PauliString("X1*X2")) assert c == stim.Circuit(""" MPP X1*Y2*Z3 Y4 Z5 X1*X2 """) with pytest.raises(ValueError, match="empty stim.PauliString"): c.append("MPP", stim.PauliString("")) with pytest.raises(ValueError, match="empty stim.PauliString"): c.append("MPP", [stim.PauliString("")]) with pytest.raises(ValueError, match="empty stim.PauliString"): c.append("MPP", [stim.PauliString("X1"), stim.PauliString("")]) assert c == stim.Circuit(""" MPP X1*Y2*Z3 Y4 Z5 X1*X2 """) with pytest.raises(ValueError, match="Don't know how to target"): c.append("MPP", object()) with pytest.raises(ValueError, match="Don't know how to target"): c.append("MPP", object()) def test_without_tags(): circuit = stim.Circuit(""" H[tag] 5 """) assert circuit.without_tags() == stim.Circuit(""" H 5 """) def test_reference_detector_and_observable_signs(): det, obs = stim.Circuit(""" X 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] OBSERVABLE_INCLUDE(3) rec[-1] rec[-2] """).reference_detector_and_observable_signs() assert det.dtype == np.bool_ assert obs.dtype == np.bool_ np.testing.assert_array_equal(det, [True, False]) np.testing.assert_array_equal(obs, [False, False, False, True]) det, obs = stim.Circuit(""" X 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] OBSERVABLE_INCLUDE(3) rec[-1] rec[-2] """).reference_detector_and_observable_signs(bit_packed=True) assert det.dtype == np.uint8 assert obs.dtype == np.uint8 np.testing.assert_array_equal(det, [0b01]) np.testing.assert_array_equal(obs, [0b1000]) circuit = stim.Circuit.generated("surface_code:rotated_memory_x", rounds=3, distance=3) det, obs = circuit.reference_detector_and_observable_signs(bit_packed=True) assert det.dtype == np.uint8 assert obs.dtype == np.uint8 assert not np.any(det) assert not np.any(obs) assert len(det) == (circuit.num_detectors + 7) // 8 assert len(obs) == 1 def test_without_noise_removes_id_errors(): assert stim.Circuit(""" I_ERROR 0 I_ERROR(0.25) 1 II_ERROR 2 3 II_ERROR(0.125) 3 4 H 0 """).without_noise() == stim.Circuit(""" H 0 """) def test_append_circuit_to_circuit(): circuit = stim.Circuit(""" H 0 """) circuit.append(stim.Circuit(""" X 1 Z 2 """)) assert circuit == stim.Circuit(""" H 0 X 1 Z 2 """) ================================================ FILE: src/stim/circuit/circuit_repeat_block.pybind.cc ================================================ // 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. #include "stim/circuit/circuit_repeat_block.pybind.h" #include "stim/circuit/circuit.h" #include "stim/circuit/circuit.pybind.h" #include "stim/circuit/circuit_instruction.pybind.h" #include "stim/py/base.pybind.h" using namespace stim; using namespace stim_pybind; CircuitRepeatBlock::CircuitRepeatBlock(uint64_t repeat_count, stim::Circuit body, pybind11::str tag) : repeat_count(repeat_count), body(body), tag(tag) { if (repeat_count == 0) { throw std::invalid_argument("Can't repeat 0 times."); } } Circuit CircuitRepeatBlock::body_copy() { return body; } bool CircuitRepeatBlock::operator==(const CircuitRepeatBlock &other) const { return repeat_count == other.repeat_count && body == other.body && pybind11::cast(tag) == pybind11::cast(other.tag); } bool CircuitRepeatBlock::operator!=(const CircuitRepeatBlock &other) const { return !(*this == other); } std::string CircuitRepeatBlock::repr() const { return "stim.CircuitRepeatBlock(" + std::to_string(repeat_count) + ", " + circuit_repr(body) + ")"; } pybind11::class_ stim_pybind::pybind_circuit_repeat_block(pybind11::module &m) { return pybind11::class_( m, "CircuitRepeatBlock", clean_doc_string(R"DOC( A REPEAT block from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 >>> repeat_block.body_copy() stim.Circuit(''' CX 0 1 CZ 1 2 ''') )DOC") .data()); } void stim_pybind::pybind_circuit_repeat_block_methods(pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init(), pybind11::arg("repeat_count"), pybind11::arg("body"), pybind11::kw_only(), pybind11::arg("tag") = "", clean_doc_string(R"DOC( Initializes a `stim.CircuitRepeatBlock`. Args: repeat_count: The number of times to repeat the block. body: The body of the block, as a circuit. tag: Defaults to empty. A custom string attached to the REPEAT instruction. Examples: >>> import stim >>> c = stim.Circuit() >>> c.append(stim.CircuitRepeatBlock(100, stim.Circuit("M 0"))) >>> c stim.Circuit(''' REPEAT 100 { M 0 } ''') )DOC") .data()); c.def_readonly( "repeat_count", &CircuitRepeatBlock::repeat_count, clean_doc_string(R"DOC( The repetition count of the repeat block. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 )DOC") .data()); c.def_property_readonly( "name", [](const CircuitRepeatBlock &self) -> pybind11::str { return pybind11::cast("REPEAT"); }, clean_doc_string(R"DOC( Returns the name "REPEAT". This is a duck-typing convenience method. It exists so that code that doesn't know whether it has a `stim.CircuitInstruction` or a `stim.CircuitRepeatBlock` can check the object's name without having to do an `instanceof` check first. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 1 2 ... } ... S 1 ... ''') >>> [instruction.name for instruction in circuit] ['H', 'REPEAT', 'S'] )DOC") .data()); c.def_property_readonly( "num_measurements", [](const CircuitRepeatBlock &self) -> uint64_t { return self.body.count_measurements() * self.repeat_count; }, clean_doc_string(R"DOC( Returns the number of bits produced when running this loop. Examples: >>> import stim >>> stim.CircuitRepeatBlock( ... body=stim.Circuit("M 0 1"), ... repeat_count=25, ... ).num_measurements 50 )DOC") .data()); c.def_readonly( "repeat_count", &CircuitRepeatBlock::repeat_count, clean_doc_string(R"DOC( The repetition count of the repeat block. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.repeat_count 5 )DOC") .data()); c.def_property_readonly( "tag", [](CircuitRepeatBlock &self) -> pybind11::str { return self.tag; }, clean_doc_string(R"DOC( The custom tag attached to the REPEAT instruction. The tag is an arbitrary string. The default tag, when none is specified, is the empty string. Examples: >>> import stim >>> stim.Circuit(''' ... REPEAT[test] 5 { ... H 0 ... } ... ''')[0].tag 'test' >>> stim.Circuit(''' ... REPEAT 5 { ... H 0 ... } ... ''')[0].tag '' )DOC") .data()); c.def( "body_copy", &CircuitRepeatBlock::body_copy, clean_doc_string(R"DOC( Returns a copy of the body of the repeat block. (Making a copy is enforced to make it clear that editing the result won't change the block's body.) Examples: >>> import stim >>> circuit = stim.Circuit(''' ... H 0 ... REPEAT 5 { ... CX 0 1 ... CZ 1 2 ... } ... ''') >>> repeat_block = circuit[1] >>> repeat_block.body_copy() stim.Circuit(''' CX 0 1 CZ 1 2 ''') )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two `stim.CircuitRepeatBlock`s are identical."); c.def(pybind11::self != pybind11::self, "Determines if two `stim.CircuitRepeatBlock`s are different."); c.def( "__repr__", &CircuitRepeatBlock::repr, "Returns valid python code evaluating to an equivalent `stim.CircuitRepeatBlock`."); } ================================================ FILE: src/stim/circuit/circuit_repeat_block.pybind.h ================================================ // 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. #ifndef _STIM_CIRCUIT_CIRCUIT_REPEAT_BLOCK_PYBIND_H #define _STIM_CIRCUIT_CIRCUIT_REPEAT_BLOCK_PYBIND_H #include #include "stim/circuit/circuit.h" namespace stim_pybind { struct CircuitRepeatBlock { uint64_t repeat_count; stim::Circuit body; pybind11::str tag; CircuitRepeatBlock(uint64_t repeat_count, stim::Circuit body, pybind11::str tag); stim::Circuit body_copy(); bool operator==(const CircuitRepeatBlock &other) const; bool operator!=(const CircuitRepeatBlock &other) const; std::string repr() const; }; pybind11::class_ pybind_circuit_repeat_block(pybind11::module &m); void pybind_circuit_repeat_block_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/circuit/circuit_repeat_block_test.py ================================================ # 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. import stim import pytest def test_init_and_equality(): r = stim.CircuitRepeatBlock(500, stim.Circuit("X 0")) assert r.repeat_count == 500 assert r.body_copy() == stim.Circuit("X 0") assert stim.CircuitRepeatBlock(500, stim.Circuit("X 0")) == stim.CircuitRepeatBlock(500, stim.Circuit("X 0")) assert stim.CircuitRepeatBlock(500, stim.Circuit("X 0")) != stim.CircuitRepeatBlock(500, stim.Circuit()) assert stim.CircuitRepeatBlock(500, stim.Circuit("X 0")) != stim.CircuitRepeatBlock(101, stim.Circuit("X 0")) assert not (stim.CircuitRepeatBlock(500, stim.Circuit("X 0")) == stim.CircuitRepeatBlock(500, stim.Circuit())) assert not (stim.CircuitRepeatBlock(500, stim.Circuit("X 0")) != stim.CircuitRepeatBlock(500, stim.Circuit("X 0"))) r2 = stim.CircuitRepeatBlock(repeat_count=500, body=stim.Circuit("X 0")) assert r == r2 with pytest.raises(ValueError, match="repeat 0"): stim.CircuitRepeatBlock(0, stim.Circuit()) @pytest.mark.parametrize("value", [ stim.CircuitRepeatBlock(500, stim.Circuit("X 0")), stim.CircuitRepeatBlock(1, stim.Circuit("X 0\nREPEAT 100 {\nH 1\n}\n")), ]) def test_repr(value): assert eval(repr(value), {'stim': stim}) == value assert repr(eval(repr(value), {'stim': stim})) == repr(value) def test_name(): assert [e.name for e in stim.Circuit(''' H 0 REPEAT 5 { CX 1 2 } S 1 ''')] == ['H', 'REPEAT', 'S'] ================================================ FILE: src/stim/circuit/gate_decomposition.cc ================================================ /* * 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. */ #include "stim/circuit/gate_decomposition.h" #include #include "stim/stabilizers/pauli_string.h" using namespace stim; struct ConjugateBySelfInverse { CircuitInstruction inst; const std::function &do_instruction_callback; ConjugateBySelfInverse( CircuitInstruction inst, const std::function &do_instruction_callback) : inst(inst), do_instruction_callback(do_instruction_callback) { if (!inst.targets.empty()) { do_instruction_callback(inst); } } ~ConjugateBySelfInverse() { if (!inst.targets.empty()) { do_instruction_callback(inst); } } }; bool stim::accumulate_next_obs_terms_to_pauli_string_helper( CircuitInstruction instruction, size_t *start, PauliString<64> *obs, std::vector *bits, bool allow_imaginary) { if (*start >= instruction.targets.size()) { return false; } if (bits != nullptr) { bits->clear(); } obs->xs.clear(); obs->zs.clear(); obs->sign = false; bool imag = false; // Find end of current product. size_t end = *start + 1; while (end < instruction.targets.size() && instruction.targets[end].is_combiner()) { end += 2; } // Accumulate terms. for (size_t k = *start; k < end; k += 2) { GateTarget t = instruction.targets[k]; if (t.is_pauli_target()) { obs->left_mul_pauli(t, &imag); } else if (t.is_classical_bit_target() && bits != nullptr) { bits->push_back(t); } else { throw std::invalid_argument("Found an unsupported target `" + t.str() + "` in " + instruction.str()); } } if (imag && !allow_imaginary) { throw std::invalid_argument( "Acted on an anti-Hermitian operator (e.g. X0*Z0 instead of Y0) in " + instruction.str()); } *start = end; return true; } void stim::decompose_mpp_operation( const CircuitInstruction &mpp_op, size_t num_qubits, const std::function &do_instruction_callback) { PauliString<64> current(num_qubits); simd_bits<64> merged(num_qubits); std::vector h_xz; std::vector h_yz; std::vector cnot; std::vector meas; auto flush = [&]() { if (meas.empty()) { return; } { ConjugateBySelfInverse c1(CircuitInstruction(GateType::H, {}, h_xz, mpp_op.tag), do_instruction_callback); ConjugateBySelfInverse c2( CircuitInstruction(GateType::H_YZ, {}, h_yz, mpp_op.tag), do_instruction_callback); ConjugateBySelfInverse c3(CircuitInstruction(GateType::CX, {}, cnot, mpp_op.tag), do_instruction_callback); do_instruction_callback(CircuitInstruction(GateType::M, mpp_op.args, meas, mpp_op.tag)); } h_xz.clear(); h_yz.clear(); cnot.clear(); meas.clear(); merged.clear(); }; size_t start = 0; while (accumulate_next_obs_terms_to_pauli_string_helper(mpp_op, &start, ¤t, nullptr)) { // Products equal to +-I become MPAD instructions. if (current.ref().has_no_pauli_terms()) { flush(); GateTarget t = GateTarget::qubit((uint32_t)current.sign); do_instruction_callback(CircuitInstruction{GateType::MPAD, mpp_op.args, &t, mpp_op.tag}); continue; } // If there's overlap with previous groups, the previous groups need to be flushed. if (current.xs.intersects(merged) || current.zs.intersects(merged)) { flush(); } merged |= current.xs; merged |= current.zs; // Buffer operations to perform the desired measurement. bool first = true; current.ref().for_each_active_pauli([&](uint32_t q) { bool x = current.xs[q]; bool z = current.zs[q]; // Include single qubit gates transforming the Pauli into a Z. if (x) { if (z) { h_yz.push_back({q}); } else { h_xz.push_back({q}); } } // Include CNOT gates folding onto a single measured qubit. if (first) { meas.push_back(GateTarget::qubit(q, current.sign)); first = false; } else { cnot.push_back({q}); cnot.push_back({meas.back().qubit_value()}); } }); assert(!first); } // Flush remaining groups. flush(); } static void decompose_spp_or_spp_dag_operation_helper( PauliStringRef<64> observable, std::span classical_bits, bool invert_sign, const std::function &do_instruction_callback, std::vector *h_xz_buf, std::vector *h_yz_buf, std::vector *cnot_buf, std::string_view tag) { h_xz_buf->clear(); h_yz_buf->clear(); cnot_buf->clear(); // Assemble quantum terms from the observable. uint64_t focus_qubit = UINT64_MAX; observable.for_each_active_pauli([&](uint32_t q) { bool x = observable.xs[q]; bool z = observable.zs[q]; // Include single qubit gates transforming the Pauli into a Z. if (x) { if (z) { h_yz_buf->push_back({q}); } else { h_xz_buf->push_back({q}); } } // Include CNOT gates folding onto a single measured qubit. if (focus_qubit == UINT64_MAX) { focus_qubit = q; } else { cnot_buf->push_back({q}); cnot_buf->push_back({(uint32_t)focus_qubit}); } }); // Products need a quantum part to have an observable effect. if (focus_qubit == UINT64_MAX) { return; } for (const auto &t : classical_bits) { cnot_buf->push_back({t}); cnot_buf->push_back({(uint32_t)focus_qubit}); } GateTarget t = GateTarget::qubit(focus_qubit); bool sign = invert_sign ^ observable.sign; GateType g = sign ? GateType::S_DAG : GateType::S; { ConjugateBySelfInverse c1(CircuitInstruction(GateType::H, {}, *h_xz_buf, tag), do_instruction_callback); ConjugateBySelfInverse c2(CircuitInstruction(GateType::H_YZ, {}, *h_yz_buf, tag), do_instruction_callback); ConjugateBySelfInverse c3(CircuitInstruction(GateType::CX, {}, *cnot_buf, tag), do_instruction_callback); do_instruction_callback(CircuitInstruction(g, {}, &t, tag)); } } void stim::decompose_spp_or_spp_dag_operation( const CircuitInstruction &spp_op, size_t num_qubits, bool invert_sign, const std::function &do_instruction_callback) { PauliString<64> obs(num_qubits); std::vector h_xz_buf; std::vector h_yz_buf; std::vector cnot_buf; std::vector bits; if (spp_op.gate_type == GateType::SPP) { // No sign inversion needed. } else if (spp_op.gate_type == GateType::SPP_DAG) { invert_sign ^= true; } else { throw std::invalid_argument("Not an SPP or SPP_DAG instruction: " + spp_op.str()); } size_t start = 0; while (accumulate_next_obs_terms_to_pauli_string_helper(spp_op, &start, &obs, &bits)) { decompose_spp_or_spp_dag_operation_helper( obs, bits, invert_sign, do_instruction_callback, &h_xz_buf, &h_yz_buf, &cnot_buf, spp_op.tag); } } void stim::decompose_pair_instruction_into_disjoint_segments( const CircuitInstruction &inst, size_t num_qubits, const std::function &callback) { simd_bits<64> used_as_control(num_qubits); size_t num_flushed = 0; size_t cur_index = 0; auto flush = [&]() { callback( CircuitInstruction{ inst.gate_type, inst.args, inst.targets.sub(num_flushed, cur_index), inst.tag, }); used_as_control.clear(); num_flushed = cur_index; }; while (cur_index < inst.targets.size()) { size_t q0 = inst.targets[cur_index].qubit_value(); size_t q1 = inst.targets[cur_index + 1].qubit_value(); if (used_as_control[q0] || used_as_control[q1]) { flush(); } used_as_control[q0] = true; used_as_control[q1] = true; cur_index += 2; } if (num_flushed < inst.targets.size()) { flush(); } } void stim::for_each_disjoint_target_segment_in_instruction_reversed( const CircuitInstruction &inst, simd_bits_range_ref<64> workspace, const std::function &callback) { workspace.clear(); size_t cur_end = inst.targets.size(); size_t cur_start = inst.targets.size(); auto flush = [&]() { callback(CircuitInstruction(inst.gate_type, inst.args, inst.targets.sub(cur_start, cur_end), inst.tag)); workspace.clear(); cur_end = cur_start; }; while (cur_start > 0) { auto t = inst.targets[cur_start - 1]; if (t.has_qubit_value()) { if (workspace[t.qubit_value()]) { flush(); } workspace[t.qubit_value()] = true; } cur_start--; } if (cur_end > 0) { flush(); } } void stim::for_each_combined_targets_group( const CircuitInstruction &inst, const std::function &callback) { if (inst.targets.empty()) { return; } size_t start = 0; size_t next_start = 1; while (true) { if (next_start >= inst.targets.size() || !inst.targets[next_start].is_combiner()) { callback(CircuitInstruction(inst.gate_type, inst.args, inst.targets.sub(start, next_start), inst.tag)); start = next_start; next_start = start + 1; if (next_start > inst.targets.size()) { return; } } else { next_start += 2; } } } ================================================ FILE: src/stim/circuit/gate_decomposition.h ================================================ /* * 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. */ #ifndef _STIM_GATE_DECOMPOSITION_H #define _STIM_GATE_DECOMPOSITION_H #include #include "stim/circuit/circuit_instruction.h" #include "stim/circuit/gate_target.h" #include "stim/gates/gates.h" #include "stim/mem/simd_bits.h" namespace stim { /// Decomposes MPP operations into sequences of simpler operations with the same effect. /// /// The idea is that an instruction like /// /// MPP X0*Z1*Y2 X3*X4 Y0*Y1*Y2*Y3*Y4 /// /// can be decomposed into a sequence of instructions like /// /// H_XZ 0 3 4 /// H_YZ 2 /// CX 1 0 2 0 4 3 /// M 0 3 /// CX 1 0 2 0 4 3 /// H_YZ 2 /// H_XZ 0 3 4 /// /// H_YZ 0 1 2 3 4 /// CX 1 0 2 0 3 0 4 0 /// M 0 /// CX 1 0 2 0 3 0 4 0 /// H_YZ 0 1 2 3 4 /// /// This is tedious to do, so this method does it for you. /// /// Args: /// mpp_op: The operation to decompose. /// num_qubits: The number of qubits in the system. All targets must be less than this. /// callback: How to execute decomposed instructions. void decompose_mpp_operation( const CircuitInstruction &mpp_op, size_t num_qubits, const std::function &do_instruction_callback); /// Decomposes SPP operations into sequences of simpler operations with the same effect. void decompose_spp_or_spp_dag_operation( const CircuitInstruction &spp_op, size_t num_qubits, bool invert_sign, const std::function &do_instruction_callback); /// Finds contiguous segments where the first target of each pair is used once. /// /// This is used when decomposing operations like MXX into CX and MX. The CX /// gates can overlap on their targets, but the measurements can't overlap with /// each other and the measurements can't overlap with the controls of the CX /// gates. /// /// The idea is that an instruction like /// /// MXX 0 1 0 2 3 5 4 5 3 4 /// /// can be decomposed into a sequence of instructions like /// /// CX 0 1 /// MX 0 /// CX 0 1 /// /// CX 0 2 3 5 4 5 /// MX 0 3 4 /// CX 0 2 3 5 4 5 /// /// CX 3 4 /// MX 3 /// CX 3 4 /// /// Args: /// num_qubits: The number of qubits in the system. All targets in the circuit /// instruction must be less than this. /// inst: The circuit instruction to decompose. /// callback: The method called with each decomposed segment. void decompose_pair_instruction_into_disjoint_segments( const CircuitInstruction &inst, size_t num_qubits, const std::function &callback); bool accumulate_next_obs_terms_to_pauli_string_helper( CircuitInstruction instruction, size_t *start, PauliString<64> *obs, std::vector *bits, bool allow_imaginary = false); void for_each_disjoint_target_segment_in_instruction_reversed( const CircuitInstruction &inst, simd_bits_range_ref<64> workspace, const std::function &callback); void for_each_combined_targets_group( const CircuitInstruction &inst, const std::function &callback); } // namespace stim #endif ================================================ FILE: src/stim/circuit/gate_decomposition.test.cc ================================================ // 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. #include "stim/circuit/gate_decomposition.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.h" #include "stim/cmd/command_help.h" using namespace stim; TEST(gate_decomposition, decompose_mpp_operation) { Circuit out; auto append_into_circuit = [&](const CircuitInstruction &inst) { out.safe_append(inst); out.append_from_text("TICK"); }; decompose_mpp_operation( Circuit("MPP(0.125) X0*X1*X2 Z3*Z4*Z5 X2*Y4 Z3 Z3 Z4*Z5").operations[0], 10, append_into_circuit); ASSERT_EQ(out, Circuit(R"CIRCUIT( H 0 1 2 TICK CX 1 0 2 0 4 3 5 3 TICK M(0.125) 0 3 TICK CX 1 0 2 0 4 3 5 3 TICK H 0 1 2 TICK H 2 TICK H_YZ 4 TICK CX 4 2 TICK M(0.125) 2 3 TICK CX 4 2 TICK H_YZ 4 TICK H 2 TICK CX 5 4 TICK M(0.125) 3 4 TICK CX 5 4 TICK )CIRCUIT")); out.clear(); decompose_mpp_operation(Circuit("MPP X0*Z1*Y2 X3*X4 Y0*Y1*Y2*Y3*Y4").operations[0], 10, append_into_circuit); ASSERT_EQ(out, Circuit(R"CIRCUIT( H 0 3 4 TICK H_YZ 2 TICK CX 1 0 2 0 4 3 TICK M 0 3 TICK CX 1 0 2 0 4 3 TICK H_YZ 2 TICK H 0 3 4 TICK H_YZ 0 1 2 3 4 TICK CX 1 0 2 0 3 0 4 0 TICK M 0 TICK CX 1 0 2 0 3 0 4 0 TICK H_YZ 0 1 2 3 4 TICK )CIRCUIT")); } TEST(gate_decomposition, decompose_mpp_to_mpad) { Circuit out; auto append_into_circuit = [&](const CircuitInstruction &inst) { out.safe_append(inst); out.append_from_text("TICK"); }; decompose_mpp_operation( Circuit(R"CIRCUIT( MPP(0.125) X0*X0 X0*!X0 X0*Y0*Z0*X1*Y1*Z1 )CIRCUIT") .operations[0], 10, append_into_circuit); ASSERT_EQ(out, Circuit(R"CIRCUIT( MPAD(0.125) 0 TICK MPAD(0.125) 1 TICK MPAD(0.125) 1 TICK )CIRCUIT")); ASSERT_THROW( { decompose_mpp_operation(Circuit("MPP(0.125) X0*Y0*Z0").operations[0], 10, append_into_circuit); }, std::invalid_argument); } TEST(gate_decomposition, decompose_pair_instruction_into_disjoint_segments) { Circuit out; auto append_into_circuit = [&](const CircuitInstruction &segment) { out.safe_append(segment); out.append_from_text("TICK"); }; decompose_pair_instruction_into_disjoint_segments( Circuit("MXX(0.125) 0 1 0 2 3 5 4 5 3 4").operations[0], 10, append_into_circuit); ASSERT_EQ(out, Circuit(R"CIRCUIT( MXX(0.125) 0 1 TICK MXX(0.125) 0 2 3 5 TICK MXX(0.125) 4 5 TICK MXX(0.125) 3 4 TICK )CIRCUIT")); out.clear(); decompose_pair_instruction_into_disjoint_segments(Circuit("MZZ 0 1 1 2").operations[0], 10, append_into_circuit); ASSERT_EQ(out, Circuit(R"CIRCUIT( MZZ 0 1 TICK MZZ 1 2 TICK )CIRCUIT")); out.clear(); decompose_pair_instruction_into_disjoint_segments(Circuit("MZZ").operations[0], 10, append_into_circuit); ASSERT_EQ(out, Circuit(R"CIRCUIT( )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_simple) { Circuit out; decompose_spp_or_spp_dag_operation(Circuit("SPP Z0").operations[0], 10, false, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( S 0 )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_inverted) { Circuit out; decompose_spp_or_spp_dag_operation( Circuit("SPP !Z0").operations[0], 10, false, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( S_DAG 0 )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_inverted2) { Circuit out; decompose_spp_or_spp_dag_operation(Circuit("SPP Z0").operations[0], 10, true, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( S_DAG 0 )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_inverted3) { Circuit out; decompose_spp_or_spp_dag_operation( Circuit("SPP_DAG Z0").operations[0], 10, false, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( S_DAG 0 )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_double_inverted) { Circuit out; decompose_spp_or_spp_dag_operation(Circuit("SPP !Z0").operations[0], 10, true, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( S 0 )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_triple_inverted) { Circuit out; decompose_spp_or_spp_dag_operation( Circuit("SPP_DAG !Z0").operations[0], 10, true, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( S_DAG 0 )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_complex) { Circuit out; decompose_spp_or_spp_dag_operation( Circuit("SPP X0*Y1*Z2").operations[0], 10, false, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( H 0 H_YZ 1 CX 1 0 2 0 S 0 CX 1 0 2 0 H_YZ 1 H 0 )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_multiple) { Circuit out; decompose_spp_or_spp_dag_operation( Circuit("SPP X0 Y0*!Z2").operations[0], 10, false, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( H 0 S 0 H 0 H_YZ 0 CX 2 0 S_DAG 0 CX 2 0 H_YZ 0 )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_empty) { Circuit out; decompose_spp_or_spp_dag_operation(Circuit("SPP").operations[0], 10, false, [&](const CircuitInstruction &inst) { out.safe_append(inst); }); ASSERT_EQ(out, Circuit(R"CIRCUIT( )CIRCUIT")); } TEST(gate_decomposition, decompose_spp_or_spp_dag_operation_bad) { ASSERT_THROW( { decompose_spp_or_spp_dag_operation( Circuit("SPP X0*Z0").operations[0], 10, false, [](const CircuitInstruction &inst) { }); }, std::invalid_argument); ASSERT_THROW( { decompose_spp_or_spp_dag_operation( Circuit("MPP X0*Z0").operations[0], 10, false, [](const CircuitInstruction &inst) { }); }, std::invalid_argument); } TEST(gate_decomposition, for_each_disjoint_target_segment_in_instruction_reversed) { simd_bits<64> buf(100); Circuit out; auto append_into_circuit = [&](const CircuitInstruction &segment) { out.safe_append(segment); out.append_from_text("TICK"); }; for_each_disjoint_target_segment_in_instruction_reversed( Circuit("M(0.25) 0 1 2 3 2 5 6 7 1 5 6 6").operations[0], buf, append_into_circuit); ASSERT_EQ(out, Circuit(R"CIRCUIT( M(0.25) 6 TICK M(0.25) 7 1 5 6 TICK M(0.25) 3 2 5 6 TICK M(0.25) 0 1 2 TICK )CIRCUIT")); } TEST(gate_decomposition, for_each_combined_targets_group) { Circuit out; auto append_into_circuit = [&](const CircuitInstruction &segment) { out.safe_append(segment); out.append_from_text("TICK"); }; for_each_combined_targets_group(Circuit("MPP(0.25) X0 Z1 Y2*Z3 Y4*Z5*Z6 Z8").operations[0], append_into_circuit); for_each_combined_targets_group(Circuit("MPP(0.25) X0 Y1 Z2").operations[0], append_into_circuit); for_each_combined_targets_group(Circuit("MPP(0.25) X0*Y1 Z2*Z3").operations[0], append_into_circuit); ASSERT_EQ(out, Circuit(R"CIRCUIT( MPP(0.25) X0 TICK MPP(0.25) Z1 TICK MPP(0.25) Y2*Z3 TICK MPP(0.25) Y4*Z5*Z6 TICK MPP(0.25) Z8 TICK MPP(0.25) X0 TICK MPP(0.25) Y1 TICK MPP(0.25) Z2 TICK MPP(0.25) X0*Y1 TICK MPP(0.25) Z2*Z3 TICK )CIRCUIT")); } ================================================ FILE: src/stim/circuit/gate_target.cc ================================================ // 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. #include "stim/circuit/gate_target.h" using namespace stim; GateTarget GateTarget::pauli_xz(uint32_t qubit, bool x, bool z, bool inverted) { if (qubit != (qubit & TARGET_VALUE_MASK)) { throw std::invalid_argument("qubit target larger than " + std::to_string(TARGET_VALUE_MASK)); } return {qubit | (TARGET_INVERTED_BIT * inverted) | (TARGET_PAULI_X_BIT * x) | (TARGET_PAULI_Z_BIT * z)}; } GateTarget GateTarget::from_target_str(std::string_view text) { int c = text[0]; size_t k = 1; auto t = read_single_gate_target(c, [&]() { return k < text.size() ? text[k++] : EOF; }); if (c != EOF) { throw std::invalid_argument("Unparsed text at end of " + std::string(text)); } return t; } GateTarget GateTarget::x(uint32_t qubit, bool inverted) { if (qubit != (qubit & TARGET_VALUE_MASK)) { throw std::invalid_argument("qubit target larger than " + std::to_string(TARGET_VALUE_MASK)); } return {qubit | (TARGET_INVERTED_BIT * inverted) | TARGET_PAULI_X_BIT}; } GateTarget GateTarget::y(uint32_t qubit, bool inverted) { if (qubit != (qubit & TARGET_VALUE_MASK)) { throw std::invalid_argument("qubit target larger than " + std::to_string(TARGET_VALUE_MASK)); } return {qubit | (TARGET_INVERTED_BIT * inverted) | TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT}; } GateTarget GateTarget::z(uint32_t qubit, bool inverted) { if (qubit != (qubit & TARGET_VALUE_MASK)) { throw std::invalid_argument("qubit target larger than " + std::to_string(TARGET_VALUE_MASK)); } return {qubit | (TARGET_INVERTED_BIT * inverted) | TARGET_PAULI_Z_BIT}; } GateTarget GateTarget::qubit(uint32_t qubit, bool inverted) { if (qubit != (qubit & TARGET_VALUE_MASK)) { throw std::invalid_argument("qubit target larger than " + std::to_string(TARGET_VALUE_MASK)); } return {qubit | (TARGET_INVERTED_BIT * inverted)}; } GateTarget GateTarget::rec(int32_t lookback) { if (lookback >= 0 || lookback <= -(1 << 24)) { throw std::out_of_range("Need -16777215 <= lookback <= -1"); } return {((uint32_t)-lookback) | TARGET_RECORD_BIT}; } GateTarget GateTarget::sweep_bit(uint32_t index) { return {index | TARGET_SWEEP_BIT}; } GateTarget GateTarget::combiner() { return {TARGET_COMBINER}; } uint32_t GateTarget::qubit_value() const { return data & TARGET_VALUE_MASK; } int32_t GateTarget::value() const { int32_t result = (int32_t)(data & TARGET_VALUE_MASK); if (is_measurement_record_target()) { return -result; } return result; } int32_t GateTarget::rec_offset() const { assert(is_measurement_record_target()); return -(int32_t)(data & TARGET_VALUE_MASK); } bool GateTarget::is_x_target() const { return (data & TARGET_PAULI_X_BIT) && !(data & TARGET_PAULI_Z_BIT); } bool GateTarget::is_y_target() const { return (data & TARGET_PAULI_X_BIT) && (data & TARGET_PAULI_Z_BIT); } bool GateTarget::is_z_target() const { return !(data & TARGET_PAULI_X_BIT) && (data & TARGET_PAULI_Z_BIT); } bool GateTarget::is_inverted_result_target() const { return data & TARGET_INVERTED_BIT; } bool GateTarget::is_measurement_record_target() const { return data & TARGET_RECORD_BIT; } bool GateTarget::is_pauli_target() const { return data & (TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT); } bool GateTarget::has_qubit_value() const { return !(data & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT | TARGET_COMBINER)); } bool GateTarget::is_qubit_target() const { return !(data & (TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT | TARGET_RECORD_BIT | TARGET_SWEEP_BIT | TARGET_COMBINER)); } bool GateTarget::is_combiner() const { return data == TARGET_COMBINER; } bool GateTarget::is_sweep_bit_target() const { return data & TARGET_SWEEP_BIT; } bool GateTarget::is_classical_bit_target() const { return data & (TARGET_SWEEP_BIT | TARGET_RECORD_BIT); } bool GateTarget::operator==(const GateTarget &other) const { return data == other.data; } bool GateTarget::operator<(const GateTarget &other) const { return data < other.data; } bool GateTarget::operator!=(const GateTarget &other) const { return data != other.data; } std::ostream &stim::operator<<(std::ostream &out, const GateTarget &t) { if (t.is_combiner()) { return out << "stim.GateTarget.combiner()"; } if (t.is_qubit_target()) { if (t.is_inverted_result_target()) { return out << "stim.target_inv(" << t.value() << ")"; } return out << t.value(); } if (t.is_measurement_record_target()) { return out << "stim.target_rec(" << t.value() << ")"; } if (t.is_sweep_bit_target()) { return out << "stim.target_sweep_bit(" << t.value() << ")"; } if (t.is_x_target()) { out << "stim.target_x(" << t.value(); if (t.is_inverted_result_target()) { out << ", invert=True"; } return out << ")"; } if (t.is_y_target()) { out << "stim.target_y(" << t.value(); if (t.is_inverted_result_target()) { out << ", invert=True"; } return out << ")"; } if (t.is_z_target()) { out << "stim.target_z(" << t.value(); if (t.is_inverted_result_target()) { out << ", invert=True"; } return out << ")"; } throw std::invalid_argument("Malformed target."); } std::string GateTarget::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::string GateTarget::repr() const { std::stringstream ss; bool need_wrap = is_qubit_target() && !is_inverted_result_target(); if (need_wrap) { ss << "stim.GateTarget("; } ss << *this; if (need_wrap) { ss << ")"; } return ss.str(); } void GateTarget::write_succinct(std::ostream &out) const { if (data == TARGET_COMBINER) { out << "*"; return; } if (data & TARGET_INVERTED_BIT) { out << '!'; } if (data & (TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT)) { bool x = data & TARGET_PAULI_X_BIT; bool z = data & TARGET_PAULI_Z_BIT; out << "IXZY"[x + z * 2]; } if (data & TARGET_RECORD_BIT) { out << "rec[-" << (data & TARGET_VALUE_MASK) << "]"; } else if (data & TARGET_SWEEP_BIT) { out << "sweep[" << (data & TARGET_VALUE_MASK) << "]"; } else { out << (data & TARGET_VALUE_MASK); } } void stim::write_targets(std::ostream &out, SpanRef targets) { bool skip_space = false; for (auto t : targets) { if (t.is_combiner()) { skip_space = true; } else if (!skip_space) { out << ' '; } else { skip_space = false; } t.write_succinct(out); } } std::string stim::targets_str(SpanRef targets) { std::stringstream out; stim::write_targets(out, targets); return out.str(); } std::string GateTarget::target_str() const { std::stringstream out; write_succinct(out); return out.str(); } GateTarget GateTarget::operator!() const { if (data & (TARGET_COMBINER | TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) { throw std::invalid_argument("Target '" + str() + "' doesn't have a defined inverse."); } return GateTarget{data ^ TARGET_INVERTED_BIT}; } char GateTarget::pauli_type() const { assert(TARGET_PAULI_X_BIT == (1 << 30)); assert(TARGET_PAULI_Z_BIT == (1 << 29)); return "IZXY"[(data >> 29) & 3]; } ================================================ FILE: src/stim/circuit/gate_target.h ================================================ /* * 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. */ #ifndef _STIM_CIRCUIT_GATE_TARGET_H #define _STIM_CIRCUIT_GATE_TARGET_H #include #include "stim/gates/gates.h" #include "stim/mem/span_ref.h" namespace stim { constexpr uint32_t TARGET_VALUE_MASK = (uint32_t{1} << 24) - uint32_t{1}; constexpr uint32_t TARGET_INVERTED_BIT = uint32_t{1} << 31; constexpr uint32_t TARGET_PAULI_X_BIT = uint32_t{1} << 30; constexpr uint32_t TARGET_PAULI_Z_BIT = uint32_t{1} << 29; constexpr uint32_t TARGET_RECORD_BIT = uint32_t{1} << 28; constexpr uint32_t TARGET_COMBINER = uint32_t{1} << 27; constexpr uint32_t TARGET_SWEEP_BIT = uint32_t{1} << 26; struct GateTarget { uint32_t data; int32_t value() const; static GateTarget x(uint32_t qubit, bool inverted = false); static GateTarget y(uint32_t qubit, bool inverted = false); static GateTarget z(uint32_t qubit, bool inverted = false); static GateTarget pauli_xz(uint32_t qubit, bool x, bool z, bool inverted = false); static GateTarget qubit(uint32_t qubit, bool inverted = false); static GateTarget rec(int32_t lookback); static GateTarget sweep_bit(uint32_t index); static GateTarget combiner(); static GateTarget from_target_str(std::string_view text); GateTarget operator!() const; int32_t rec_offset() const; bool has_qubit_value() const; bool is_combiner() const; bool is_x_target() const; bool is_y_target() const; bool is_z_target() const; bool is_inverted_result_target() const; bool is_measurement_record_target() const; bool is_qubit_target() const; bool is_sweep_bit_target() const; bool is_classical_bit_target() const; bool is_pauli_target() const; uint32_t qubit_value() const; bool operator==(const GateTarget &other) const; bool operator!=(const GateTarget &other) const; bool operator<(const GateTarget &other) const; std::string str() const; std::string repr() const; char pauli_type() const; std::string target_str() const; void write_succinct(std::ostream &out) const; }; template uint32_t read_uint24_t(int &c, SOURCE read_char) { if (!(c >= '0' && c <= '9')) { throw std::invalid_argument("Expected a digit but got '" + std::string(1, c) + "'"); } uint32_t result = 0; do { result *= 10; result += c - '0'; if (result >= uint32_t{1} << 24) { throw std::invalid_argument("Number too large."); } c = read_char(); } while (c >= '0' && c <= '9'); return result; } template inline GateTarget read_raw_qubit_target(int &c, SOURCE read_char) { return GateTarget::qubit(read_uint24_t(c, read_char)); } template inline GateTarget read_measurement_record_target(int &c, SOURCE read_char) { if (c != 'r' || read_char() != 'e' || read_char() != 'c' || read_char() != '[' || read_char() != '-') { throw std::invalid_argument("Target started with 'r' but wasn't a record argument like 'rec[-1]'."); } c = read_char(); uint32_t lookback = read_uint24_t(c, read_char); if (c != ']') { throw std::invalid_argument("Target started with 'r' but wasn't a record argument like 'rec[-1]'."); } c = read_char(); return GateTarget{lookback | TARGET_RECORD_BIT}; } template inline GateTarget read_sweep_bit_target(int &c, SOURCE read_char) { if (c != 's' || read_char() != 'w' || read_char() != 'e' || read_char() != 'e' || read_char() != 'p' || read_char() != '[') { throw std::invalid_argument("Target started with 's' but wasn't a sweep bit argument like 'sweep[5]'."); } c = read_char(); uint32_t lookback = read_uint24_t(c, read_char); if (c != ']') { throw std::invalid_argument("Target started with 's' but wasn't a sweep bit argument like 'sweep[5]'."); } c = read_char(); return GateTarget{lookback | TARGET_SWEEP_BIT}; } template inline GateTarget read_single_gate_target(int &c, SOURCE read_char) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return read_raw_qubit_target(c, read_char); case 'r': return read_measurement_record_target(c, read_char); case '!': return read_inverted_target(c, read_char); case 'X': case 'Y': case 'Z': case 'x': case 'y': case 'z': return read_pauli_target(c, read_char); case '*': c = read_char(); return GateTarget::combiner(); case 's': return read_sweep_bit_target(c, read_char); default: throw std::invalid_argument("Unrecognized target prefix '" + std::string(1, c) + "'."); } } template inline GateTarget read_pauli_target(int &c, SOURCE read_char) { uint32_t m = 0; if (c == 'x' || c == 'X') { m = TARGET_PAULI_X_BIT; } else if (c == 'y' || c == 'Y') { m = TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT; } else if (c == 'z' || c == 'Z') { m = TARGET_PAULI_Z_BIT; } else { assert(false); } c = read_char(); if (c == ' ') { throw std::invalid_argument( "Pauli target '" + std::string(1, c) + "' followed by a space instead of a qubit index."); } uint32_t q = read_uint24_t(c, read_char); return {q | m}; } template inline GateTarget read_inverted_target(int &c, SOURCE read_char) { assert(c == '!'); c = read_char(); GateTarget t; if (c == 'X' || c == 'x' || c == 'Y' || c == 'y' || c == 'Z' || c == 'z') { t = read_pauli_target(c, read_char); } else { t = read_raw_qubit_target(c, read_char); } t.data ^= TARGET_INVERTED_BIT; return t; } void write_targets(std::ostream &out, SpanRef targets); std::string targets_str(SpanRef targets); std::ostream &operator<<(std::ostream &out, const GateTarget &t); } // namespace stim #endif ================================================ FILE: src/stim/circuit/gate_target.pybind.cc ================================================ // 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. #include "stim/circuit/gate_target.pybind.h" #include "stim/circuit/circuit.h" #include "stim/py/base.pybind.h" using namespace stim; using namespace stim_pybind; GateTarget handle_to_gate_target(const pybind11::handle &obj) { try { std::string_view text = pybind11::cast(obj); return GateTarget::from_target_str(text); } catch (const pybind11::cast_error &ex) { } try { return pybind11::cast(obj); } catch (const pybind11::cast_error &ex) { } try { return GateTarget{pybind11::cast(obj)}; } catch (const pybind11::cast_error &ex) { } throw std::invalid_argument( "target argument wasn't a qubit index, a result from a `stim.target_*` method, or a `stim.GateTarget`."); } GateTarget obj_to_gate_target(const pybind11::object &obj) { return handle_to_gate_target(obj); } pybind11::class_ stim_pybind::pybind_circuit_gate_target(pybind11::module &m) { return pybind11::class_( m, "GateTarget", clean_doc_string(R"DOC( Represents a gate target, like `0` or `rec[-1]`, from a circuit. Examples: >>> import stim >>> circuit = stim.Circuit(''' ... M 0 !1 ... ''') >>> circuit[0].targets_copy()[0] stim.GateTarget(0) >>> circuit[0].targets_copy()[1] stim.target_inv(1) )DOC") .data()); } void stim_pybind::pybind_circuit_gate_target_methods(pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init(&obj_to_gate_target), pybind11::arg("value"), clean_doc_string(R"DOC( Initializes a `stim.GateTarget`. Args: value: A value to convert into a gate target, like an integer to interpret as a qubit target or a string to parse. Examples: >>> import stim >>> stim.GateTarget(stim.GateTarget(5)) stim.GateTarget(5) >>> stim.GateTarget("X7") stim.target_x(7) >>> stim.GateTarget("rec[-3]") stim.target_rec(-3) >>> stim.GateTarget("!Z7") stim.target_z(7, invert=True) >>> stim.GateTarget("*") stim.GateTarget.combiner() )DOC") .data()); c.def_property_readonly( "value", &GateTarget::value, clean_doc_string(R"DOC( The numeric part of the target. This is non-negative integer for qubit targets, and a negative integer for measurement record targets. Examples: >>> import stim >>> stim.GateTarget(6).value 6 >>> stim.target_inv(7).value 7 >>> stim.target_x(8).value 8 >>> stim.target_y(2).value 2 >>> stim.target_z(3).value 3 >>> stim.target_sweep_bit(9).value 9 >>> stim.target_rec(-5).value -5 )DOC") .data()); c.def_property_readonly( "qubit_value", [](const GateTarget &self) -> pybind11::object { if (self.data & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT | TARGET_COMBINER)) { return pybind11::none(); } return pybind11::cast(self.qubit_value()); }, clean_doc_string(R"DOC( @signature def qubit_value(self) -> Optional[int]: Returns the integer value of the targeted qubit, or else None. Examples: >>> import stim >>> stim.GateTarget(6).qubit_value 6 >>> stim.target_inv(7).qubit_value 7 >>> stim.target_x(8).qubit_value 8 >>> stim.target_y(2).qubit_value 2 >>> stim.target_z(3).qubit_value 3 >>> print(stim.target_sweep_bit(9).qubit_value) None >>> print(stim.target_rec(-5).qubit_value) None )DOC") .data()); c.def_property_readonly( "is_qubit_target", &GateTarget::is_qubit_target, clean_doc_string(R"DOC( Returns whether or not this is a qubit target like `5` or `!6`. Examples: >>> import stim >>> stim.GateTarget(6).is_qubit_target True >>> stim.target_inv(7).is_qubit_target True >>> stim.target_x(8).is_qubit_target False >>> stim.target_y(2).is_qubit_target False >>> stim.target_z(3).is_qubit_target False >>> stim.target_sweep_bit(9).is_qubit_target False >>> stim.target_rec(-5).is_qubit_target False )DOC") .data()); c.def_property_readonly( "is_x_target", &GateTarget::is_x_target, clean_doc_string(R"DOC( Returns whether or not this is an X pauli target like `X2` or `!X7`. Examples: >>> import stim >>> stim.GateTarget(6).is_x_target False >>> stim.target_inv(7).is_x_target False >>> stim.target_x(8).is_x_target True >>> stim.target_y(2).is_x_target False >>> stim.target_z(3).is_x_target False >>> stim.target_sweep_bit(9).is_x_target False >>> stim.target_rec(-5).is_x_target False )DOC") .data()); c.def_property_readonly( "is_y_target", &GateTarget::is_y_target, clean_doc_string(R"DOC( Returns whether or not this is a Y pauli target like `Y2` or `!Y7`. Examples: >>> import stim >>> stim.GateTarget(6).is_y_target False >>> stim.target_inv(7).is_y_target False >>> stim.target_x(8).is_y_target False >>> stim.target_y(2).is_y_target True >>> stim.target_z(3).is_y_target False >>> stim.target_sweep_bit(9).is_y_target False >>> stim.target_rec(-5).is_y_target False )DOC") .data()); c.def_property_readonly( "is_z_target", &GateTarget::is_z_target, clean_doc_string(R"DOC( Returns whether or not this is a Z pauli target like `Z2` or `!Z7`. Examples: >>> import stim >>> stim.GateTarget(6).is_z_target False >>> stim.target_inv(7).is_z_target False >>> stim.target_x(8).is_z_target False >>> stim.target_y(2).is_z_target False >>> stim.target_z(3).is_z_target True >>> stim.target_sweep_bit(9).is_z_target False >>> stim.target_rec(-5).is_z_target False )DOC") .data()); c.def_property_readonly( "pauli_type", &GateTarget::pauli_type, clean_doc_string(R"DOC( Returns whether this is an 'X', 'Y', or 'Z' target. For non-pauli targets, this property evaluates to 'I'. Examples: >>> import stim >>> stim.GateTarget(6).pauli_type 'I' >>> stim.target_inv(7).pauli_type 'I' >>> stim.target_x(8).pauli_type 'X' >>> stim.target_y(2).pauli_type 'Y' >>> stim.target_z(3).pauli_type 'Z' >>> stim.target_sweep_bit(9).pauli_type 'I' >>> stim.target_rec(-5).pauli_type 'I' )DOC") .data()); c.def_property_readonly( "is_inverted_result_target", &GateTarget::is_inverted_result_target, clean_doc_string(R"DOC( Returns whether or not this is an inverted target like `!5` or `!X4`. Examples: >>> import stim >>> stim.GateTarget(6).is_inverted_result_target False >>> stim.target_inv(7).is_inverted_result_target True >>> stim.target_x(8).is_inverted_result_target False >>> stim.target_x(8, invert=True).is_inverted_result_target True >>> stim.target_y(2).is_inverted_result_target False >>> stim.target_z(3).is_inverted_result_target False >>> stim.target_sweep_bit(9).is_inverted_result_target False >>> stim.target_rec(-5).is_inverted_result_target False )DOC") .data()); c.def_property_readonly( "is_measurement_record_target", &GateTarget::is_measurement_record_target, clean_doc_string(R"DOC( Returns whether or not this is a measurement record target like `rec[-5]`. Examples: >>> import stim >>> stim.GateTarget(6).is_measurement_record_target False >>> stim.target_inv(7).is_measurement_record_target False >>> stim.target_x(8).is_measurement_record_target False >>> stim.target_y(2).is_measurement_record_target False >>> stim.target_z(3).is_measurement_record_target False >>> stim.target_sweep_bit(9).is_measurement_record_target False >>> stim.target_rec(-5).is_measurement_record_target True )DOC") .data()); c.def_property_readonly( "is_combiner", &GateTarget::is_combiner, clean_doc_string(R"DOC( Returns whether or not this is a combiner target like `*`. Examples: >>> import stim >>> stim.GateTarget(6).is_combiner False >>> stim.target_inv(7).is_combiner False >>> stim.target_x(8).is_combiner False >>> stim.target_y(2).is_combiner False >>> stim.target_z(3).is_combiner False >>> stim.target_sweep_bit(9).is_combiner False >>> stim.target_rec(-5).is_combiner False >>> stim.target_combiner().is_combiner True )DOC") .data()); c.def_property_readonly( "is_sweep_bit_target", &GateTarget::is_sweep_bit_target, clean_doc_string(R"DOC( Returns whether or not this is a sweep bit target like `sweep[4]`. Examples: >>> import stim >>> stim.GateTarget(6).is_sweep_bit_target False >>> stim.target_inv(7).is_sweep_bit_target False >>> stim.target_x(8).is_sweep_bit_target False >>> stim.target_y(2).is_sweep_bit_target False >>> stim.target_z(3).is_sweep_bit_target False >>> stim.target_sweep_bit(9).is_sweep_bit_target True >>> stim.target_rec(-5).is_sweep_bit_target False )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two `stim.GateTarget`s are identical."); c.def(pybind11::self != pybind11::self, "Determines if two `stim.GateTarget`s are different."); c.def("__hash__", [](const GateTarget &self) { return pybind11::hash(pybind11::make_tuple("GateTarget", self.data)); }); c.def( "__repr__", &GateTarget::repr, "Returns text that is a valid python expression evaluating to an equivalent `stim.GateTarget`."); } ================================================ FILE: src/stim/circuit/gate_target.pybind.h ================================================ // 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. #ifndef _STIM_CIRCUIT_CIRCUIT_GATE_TARGET_PYBIND_H #define _STIM_CIRCUIT_CIRCUIT_GATE_TARGET_PYBIND_H #include #include #include #include "stim/circuit/gate_target.h" namespace stim_pybind { pybind11::class_ pybind_circuit_gate_target(pybind11::module &m); void pybind_circuit_gate_target_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind stim::GateTarget obj_to_gate_target(const pybind11::object &obj); stim::GateTarget handle_to_gate_target(const pybind11::handle &obj); #endif ================================================ FILE: src/stim/circuit/gate_target.test.cc ================================================ // 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. #include "stim/circuit/gate_target.h" #include "gtest/gtest.h" using namespace stim; TEST(gate_target, xyz) { ASSERT_THROW({ GateTarget::x(UINT32_MAX); }, std::invalid_argument); ASSERT_THROW({ GateTarget::y(UINT32_MAX, true); }, std::invalid_argument); ASSERT_THROW({ GateTarget::z(UINT32_MAX, false); }, std::invalid_argument); auto t = GateTarget::x(5, false); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), false); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), true); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "stim.target_x(5)"); ASSERT_EQ(t.value(), 5); ASSERT_TRUE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); t = GateTarget::x(7, true); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), true); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), true); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "stim.target_x(7, invert=True)"); ASSERT_EQ(t.value(), 7); ASSERT_TRUE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); t = GateTarget::y(11, false); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), false); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), true); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "stim.target_y(11)"); ASSERT_EQ(t.value(), 11); ASSERT_TRUE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); t = GateTarget::y(13, true); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), true); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), true); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "stim.target_y(13, invert=True)"); ASSERT_EQ(t.value(), 13); ASSERT_FALSE(t.is_sweep_bit_target()); t = GateTarget::z(17, false); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), false); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), true); ASSERT_EQ(t.str(), "stim.target_z(17)"); ASSERT_EQ(t.value(), 17); ASSERT_TRUE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); t = GateTarget::z(19, true); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), true); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), true); ASSERT_EQ(t.str(), "stim.target_z(19, invert=True)"); ASSERT_EQ(t.value(), 19); ASSERT_EQ(t.qubit_value(), 19); ASSERT_TRUE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); } TEST(gate_target, qubit) { ASSERT_THROW({ GateTarget::qubit(UINT32_MAX); }, std::invalid_argument); auto t = GateTarget::qubit(5, false); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), false); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), true); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "5"); ASSERT_EQ(t.value(), 5); ASSERT_EQ(t.qubit_value(), 5); ASSERT_TRUE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); t = GateTarget::qubit(7, true); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), true); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), true); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "stim.target_inv(7)"); ASSERT_EQ(t.value(), 7); ASSERT_TRUE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); } TEST(gate_target, record) { ASSERT_THROW({ GateTarget::rec(1); }, std::out_of_range); ASSERT_THROW({ GateTarget::rec(0); }, std::out_of_range); ASSERT_THROW({ GateTarget::rec(-(int32_t{1} << 30)); }, std::out_of_range); auto t = GateTarget::rec(-5); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), false); ASSERT_EQ(t.is_measurement_record_target(), true); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "stim.target_rec(-5)"); ASSERT_EQ(t.value(), -5); ASSERT_EQ(t.qubit_value(), 5); ASSERT_FALSE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); } TEST(gate_target, sweep) { auto t = GateTarget::sweep_bit(5); ASSERT_EQ(t.is_combiner(), false); ASSERT_EQ(t.is_inverted_result_target(), false); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "stim.target_sweep_bit(5)"); ASSERT_EQ(t.value(), 5); ASSERT_FALSE(t.has_qubit_value()); ASSERT_TRUE(t.is_sweep_bit_target()); } TEST(gate_target, combiner) { auto t = GateTarget::combiner(); ASSERT_EQ(t.is_combiner(), true); ASSERT_EQ(t.is_inverted_result_target(), false); ASSERT_EQ(t.is_measurement_record_target(), false); ASSERT_EQ(t.is_qubit_target(), false); ASSERT_EQ(t.is_x_target(), false); ASSERT_EQ(t.is_y_target(), false); ASSERT_EQ(t.is_z_target(), false); ASSERT_EQ(t.str(), "stim.GateTarget.combiner()"); ASSERT_EQ(t.qubit_value(), 0); ASSERT_FALSE(t.has_qubit_value()); ASSERT_FALSE(t.is_sweep_bit_target()); } TEST(gate_target, equality) { ASSERT_TRUE(GateTarget{0} == GateTarget{0}); ASSERT_FALSE(GateTarget{0} == GateTarget{1}); ASSERT_TRUE(GateTarget{0} != GateTarget{1}); ASSERT_FALSE(GateTarget{0} != GateTarget{0}); ASSERT_TRUE(GateTarget{0} < GateTarget{1}); ASSERT_FALSE(GateTarget{1} < GateTarget{0}); ASSERT_FALSE(GateTarget{0} < GateTarget{0}); } TEST(gate_target, inverse) { ASSERT_EQ(!GateTarget::qubit(5), GateTarget::qubit(5, true)); ASSERT_EQ(GateTarget::qubit(5), !GateTarget::qubit(5, true)); ASSERT_EQ(!GateTarget::x(5), GateTarget::x(5, true)); ASSERT_EQ(GateTarget::x(5), !GateTarget::x(5, true)); ASSERT_EQ(!GateTarget::y(5), GateTarget::y(5, true)); ASSERT_EQ(GateTarget::y(5), !GateTarget::y(5, true)); ASSERT_EQ(!GateTarget::z(9), GateTarget::z(9, true)); ASSERT_EQ(GateTarget::z(7), !GateTarget::z(7, true)); ASSERT_EQ(!!GateTarget::z(9), GateTarget::z(9)); ASSERT_THROW({ !GateTarget::combiner(); }, std::invalid_argument); ASSERT_THROW({ !GateTarget::rec(-3); }, std::invalid_argument); ASSERT_THROW({ !GateTarget::sweep_bit(6); }, std::invalid_argument); } TEST(gate_target, pauli) { ASSERT_EQ(GateTarget::combiner().pauli_type(), 'I'); ASSERT_EQ(GateTarget::sweep_bit(5).pauli_type(), 'I'); ASSERT_EQ(GateTarget::rec(-5).pauli_type(), 'I'); ASSERT_EQ(GateTarget::qubit(5).pauli_type(), 'I'); ASSERT_EQ(GateTarget::qubit(6, true).pauli_type(), 'I'); ASSERT_EQ(GateTarget::x(7).pauli_type(), 'X'); ASSERT_EQ(GateTarget::x(7, true).pauli_type(), 'X'); ASSERT_EQ(GateTarget::y(7).pauli_type(), 'Y'); ASSERT_EQ(GateTarget::y(7, true).pauli_type(), 'Y'); ASSERT_EQ(GateTarget::z(7).pauli_type(), 'Z'); ASSERT_EQ(GateTarget::z(7, true).pauli_type(), 'Z'); } TEST(gate_target, from_target_str) { ASSERT_EQ(GateTarget::from_target_str("5"), GateTarget::qubit(5)); ASSERT_EQ(GateTarget::from_target_str("rec[-3]"), GateTarget::rec(-3)); } TEST(gate_target, target_str_round_trip) { std::vector targets{ GateTarget::qubit(2), GateTarget::qubit(3, true), GateTarget::sweep_bit(5), GateTarget::rec(-7), GateTarget::x(11), GateTarget::x(13, true), GateTarget::y(17), GateTarget::y(19, true), GateTarget::z(23), GateTarget::z(29, true), GateTarget::combiner(), }; for (const auto &t : targets) { ASSERT_EQ(GateTarget::from_target_str(t.target_str().c_str()), t) << t; } } TEST(gate_target, is_pauli_target) { ASSERT_FALSE(GateTarget::qubit(2).is_pauli_target()); ASSERT_FALSE(GateTarget::qubit(3, true).is_pauli_target()); ASSERT_FALSE(GateTarget::sweep_bit(5).is_pauli_target()); ASSERT_FALSE(GateTarget::rec(-7).is_pauli_target()); ASSERT_TRUE(GateTarget::x(11).is_pauli_target()); ASSERT_TRUE(GateTarget::x(13, true).is_pauli_target()); ASSERT_TRUE(GateTarget::y(17).is_pauli_target()); ASSERT_TRUE(GateTarget::y(19, true).is_pauli_target()); ASSERT_TRUE(GateTarget::z(23).is_pauli_target()); ASSERT_TRUE(GateTarget::z(29, true).is_pauli_target()); ASSERT_FALSE(GateTarget::combiner().is_pauli_target()); } TEST(gate_target, is_classical_bit_target) { ASSERT_TRUE(GateTarget::sweep_bit(5).is_classical_bit_target()); ASSERT_TRUE(GateTarget::rec(-7).is_classical_bit_target()); ASSERT_FALSE(GateTarget::qubit(2).is_classical_bit_target()); ASSERT_FALSE(GateTarget::qubit(3, true).is_classical_bit_target()); ASSERT_FALSE(GateTarget::x(11).is_classical_bit_target()); ASSERT_FALSE(GateTarget::x(13, true).is_classical_bit_target()); ASSERT_FALSE(GateTarget::y(17).is_classical_bit_target()); ASSERT_FALSE(GateTarget::y(19, true).is_classical_bit_target()); ASSERT_FALSE(GateTarget::z(23).is_classical_bit_target()); ASSERT_FALSE(GateTarget::z(29, true).is_classical_bit_target()); ASSERT_FALSE(GateTarget::combiner().is_classical_bit_target()); } ================================================ FILE: src/stim/circuit/gate_target_pybind_test.py ================================================ # 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. import stim import pytest def test_init_and_equality(): assert stim.GateTarget(5) == stim.GateTarget(5) assert stim.GateTarget(5) == stim.GateTarget(value=5) assert not (stim.GateTarget(4) == stim.GateTarget(5)) assert stim.GateTarget(4) != stim.GateTarget(5) assert not (stim.GateTarget(5) != stim.GateTarget(5)) assert stim.GateTarget(stim.target_x(5)) != stim.GateTarget(5) assert stim.GateTarget(5) == stim.GateTarget(stim.GateTarget(5)) def test_properties(): g = stim.GateTarget(5) assert g.value == 5 assert not g.is_x_target assert not g.is_y_target assert not g.is_z_target assert not g.is_inverted_result_target assert not g.is_measurement_record_target assert not g.is_combiner assert g.is_qubit_target g = stim.GateTarget(stim.target_rec(-4)) assert g.value == -4 assert not g.is_x_target assert not g.is_y_target assert not g.is_z_target assert not g.is_inverted_result_target assert g.is_measurement_record_target assert not g.is_combiner assert not g.is_qubit_target g = stim.GateTarget(stim.target_x(3)) assert g.value == 3 assert g.is_x_target assert not g.is_y_target assert not g.is_z_target assert not g.is_inverted_result_target assert not g.is_measurement_record_target assert not g.is_combiner assert not g.is_qubit_target g = stim.GateTarget(stim.target_y(3)) assert g.value == 3 assert not g.is_x_target assert g.is_y_target assert not g.is_z_target assert not g.is_inverted_result_target assert not g.is_measurement_record_target assert not g.is_combiner assert not g.is_qubit_target g = stim.GateTarget(stim.target_z(3)) assert g.value == 3 assert not g.is_x_target assert not g.is_y_target assert g.is_z_target assert not g.is_inverted_result_target assert not g.is_measurement_record_target assert not g.is_combiner assert not g.is_qubit_target g = stim.GateTarget(stim.target_z(3, invert=True)) assert g.value == 3 assert not g.is_x_target assert not g.is_y_target assert g.is_z_target assert g.is_inverted_result_target assert not g.is_measurement_record_target assert not g.is_combiner assert not g.is_qubit_target g = stim.GateTarget(stim.target_inv(3)) assert g.value == 3 assert not g.is_x_target assert not g.is_y_target assert not g.is_z_target assert g.is_inverted_result_target assert not g.is_measurement_record_target assert not g.is_combiner assert g.is_qubit_target g = stim.target_combiner() assert not g.is_x_target assert not g.is_y_target assert not g.is_z_target assert not g.is_inverted_result_target assert not g.is_measurement_record_target assert not g.is_qubit_target assert g.is_combiner @pytest.mark.parametrize("value", [ stim.GateTarget(5), stim.GateTarget(stim.target_rec(-5)), stim.GateTarget(stim.target_x(5)), stim.GateTarget(stim.target_y(5)), stim.GateTarget(stim.target_z(5)), stim.GateTarget(stim.target_inv(5)), ]) def test_repr(value): assert eval(repr(value), {'stim': stim}) == value assert repr(eval(repr(value), {'stim': stim})) == repr(value) def test_hashable(): a = stim.GateTarget(5) b = stim.GateTarget(6) c = stim.GateTarget(5) assert hash(a) == hash(c) assert len({a, b, c}) == 2 def test_pauli_type(): assert stim.GateTarget(5).pauli_type == 'I' assert stim.target_inv(5).pauli_type == 'I' assert stim.target_rec(-5).pauli_type == 'I' assert stim.target_sweep_bit(6).pauli_type == 'I' assert stim.target_x(7).pauli_type == 'X' assert stim.target_y(8).pauli_type == 'Y' assert stim.target_y(8, invert=True).pauli_type == 'Y' assert stim.target_z(9).pauli_type == 'Z' ================================================ FILE: src/stim/cmd/command_analyze_errors.cc ================================================ // 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. #include "stim/cmd/command_analyze_errors.h" #include "stim/cmd/command_help.h" #include "stim/simulators/error_analyzer.h" #include "stim/util_bot/arg_parse.h" using namespace stim; int stim::command_analyze_errors(int argc, const char **argv) { check_for_unknown_arguments( { "--allow_gauge_detectors", "--approximate_disjoint_errors", "--block_decompose_from_introducing_remnant_edges", "--decompose_errors", "--fold_loops", "--ignore_decomposition_failures", "--in", "--out", }, {"--analyze_errors", "--detector_hypergraph"}, "analyze_errors", argc, argv); bool decompose_errors = find_bool_argument("--decompose_errors", argc, argv); bool fold_loops = find_bool_argument("--fold_loops", argc, argv); bool allow_gauge_detectors = find_bool_argument("--allow_gauge_detectors", argc, argv); bool ignore_decomposition_failures = find_bool_argument("--ignore_decomposition_failures", argc, argv); bool block_decompose_from_introducing_remnant_edges = find_bool_argument("--block_decompose_from_introducing_remnant_edges", argc, argv); const char *approximate_disjoint_errors_arg = find_argument("--approximate_disjoint_errors", argc, argv); float approximate_disjoint_errors_threshold; if (approximate_disjoint_errors_arg != nullptr && *approximate_disjoint_errors_arg == '\0') { approximate_disjoint_errors_threshold = 1; } else { approximate_disjoint_errors_threshold = find_float_argument("--approximate_disjoint_errors", 0, 0, 1, argc, argv); } FILE *in = find_open_file_argument("--in", stdin, "rb", argc, argv); auto out_stream = find_output_stream_argument("--out", true, argc, argv); std::ostream &out = out_stream.stream(); auto circuit = Circuit::from_file(in); if (in != stdin) { fclose(in); } out << ErrorAnalyzer::circuit_to_detector_error_model( circuit, decompose_errors, fold_loops, allow_gauge_detectors, approximate_disjoint_errors_threshold, ignore_decomposition_failures, block_decompose_from_introducing_remnant_edges) << "\n"; return EXIT_SUCCESS; } SubCommandHelp stim::command_analyze_errors_help() { SubCommandHelp result; result.subcommand_name = "analyze_errors"; result.description = "Converts a circuit into a detector error model."; result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example_circuit.stim R 0 1 X_ERROR(0.125) 0 1 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] >>> stim analyze_errors --in example_circuit.stim error(0.125) D0 error(0.125) D0 D1 )PARAGRAPH")); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> stim gen \ --code repetition_code \ --task memory \ --distance 3 \ --rounds 1000 \ --after_reset_flip_probability 0.125 \ > rep_code.stim >>> stim analyze_errors --fold_loops --in rep_code.stim error(0.125) D0 error(0.125) D0 D1 error(0.125) D0 D2 error(0.125) D1 D3 error(0.125) D1 L0 error(0.125) D2 D4 error(0.125) D3 D5 detector(1, 0) D0 detector(3, 0) D1 repeat 998 { error(0.125) D4 D6 error(0.125) D5 D7 shift_detectors(0, 1) 0 detector(1, 0) D2 detector(3, 0) D3 shift_detectors 2 } shift_detectors(0, 1) 0 detector(1, 0) D2 detector(3, 0) D3 detector(1, 1) D4 detector(3, 1) D5 )PARAGRAPH")); result.flags.push_back( SubCommandHelpFlag{ "--allow_gauge_detectors", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Allows non-deterministic detectors to appear in the circuit. Normally (without `--allow_gauge_detectors`), when a detector's detecting region anti-commutes with a reset or measurement, stim will raise an exception when analyzing the circuit. When `--allow_gauge_detectors` is set, stim will instead append an error mechanism into the detector error model that has a probability of 50% and flips all the detectors that anticommute with the operation. This is potentially useful in situations where the layout of detectors is supposed to stay fixed despite variations in the circuit structure. Decoders can interpret the existence of the 50% error as a weight 0 edge saying that the detectors should be fused together. For example, in the following stim circuit, the two detectors each anticommute with the reset operation: R 0 H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] Without `--allow_gauge_detectors`, stim will raise an exception when analyzing this circuit. With `--allow_gauge_detectors`, stim will add `error(0.5) D1 D2` to the output detector error model. BEWARE that gauge detectors are very tricky to work with, and not necessarily supported by all tools (even within stim itself). For example, when converting from measurements to detection events, there isn't a single choice for whether or not each individual gauge detector produced a detection event. This means that it is valid behavior for one conversion from measurements to detection events to give different results from another, as long as the gauge detectors that anticommute with the same operations flip together in a consistent fashion that respects the structure of the circuit. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--approximate_disjoint_errors", "probability", "0.0", {"[none]", "[switch]", "probability"}, clean_doc_string(R"PARAGRAPH( Allows disjoint errors to be approximated during the conversion. Detector error models require that all error mechanisms be specified as independent mechanisms. But some of the circuit error mechanisms that Stim allows can express errors that don't correspond to independent mechanisms. For example, the custom error channel `PAULI_CHANNEL_1(0.1, 0.2, 0.0)` can't be expressed exactly as a set of independent error mechanisms. But it can be approximated as an `X_ERROR(0.1)` followed by a `Y_ERROR(0.2)`. This flag can be set to any probability between 0 (the default when not specified) and 1 (the default when specified without a value). When set to a value strictly between 0 and 1, this determines the maximum disjoint probability that is allowed to be approximated as an independent probability. Without `--approximate_disjoint_errors`, attempting to convert a circuit containing `PAULI_CHANNEL_1(0.1, 0.2, 0.0)` will fail with an error stating an approximation is needed. With `--approximate_disjoint_errors`, the conversion will succeed by approximating the error into an `X_ERROR(0.1)` followed by an independent `Y_ERROR(0.2)`. Note that, although `DEPOLARIZE1` and `DEPOLARIZE2` are often defined in terms of disjoint errors, they can be exactly converted into a set of independent errors (unless the probability of the depolarizing error occurring exceeds maximum mixing, which is 75% for `DEPOLARIZE1` and 93.75% for `DEPOLARIZE2`). So the `--approximate_disjoint_errors` flag isn't needed for depolarizing errors that appear in practice. The error mechanisms that require approximations are: - PAULI_CHANNEL_1 - PAULI_CHANNEL_2 - ELSE_CORRELATED_ERROR In principle some custom Pauli channels can be converted exactly, but Stim does not currently contain logic that attempts to do this. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--block_decompose_from_introducing_remnant_edges", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Prevents A*B from being decomposed unless A,B BOTH appear elsewhere. Irrelevant unless `--decompose_errors` is specified. When `--decompose_errors` is specified, any circuit error that causes more than two detection events must be decomposed into a set of errors with at most two detection events. The main constraint on this process is that it must not use errors that couldn't otherwise occur, since introducing such errors could violate important properties that are used for decoding. For example, in the normal surface code, it is very important that the decoding graphs for X errors and Z errors are disjoint in the bulk, and decomposing an error into a set of errors that violated this property would be disastrous. However, a corner case in this logic occurs if an error E1 that produces detection events A*B needs to be decomposed when an error E2 that produces detection events A appears elsewhere but no error producing detection events B appears elsewhere. The detection events B can be produced by both E1 and E2 occurring, but this a combination of two errors and so treating it as one error can cause problems. For example, it can result in the code distance appearing to be smaller than it actually is. Introducing B is referred to as introducing a "remnant edge" because B *only* appears in the detector error model as a remnant of removing A from A*B. By default, Stim does allow remnant edges to be introduced. Stim will only do this if it is absolutely necessary, but it *will* do it. And there are in fact QEC circuits where the decomposition requires these edges to succeed. But sometimes the presence of a remnant edge is a hint that the DETECTOR declarations in the circuit are subtly wrong. To cause the decomposition process to fail in this case, the `--block_decompose_from_introducing_remnant_edges` can be specified. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--decompose_errors", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Decomposes errors with many detection events into "graphlike" parts. When `--decompose_errors` is specified, Stim will suggest how errors that cause more than 2 detection events (non-graphlike errors) can be decomposed into errors with at most 2 detection events (graphlike errors). For example, an error like `error(0.1) D0 D1 D2 D3` may be instead output as `error(0.1) D0 D1 ^ D2 D3` or as `error(0.1) D0 D3 ^ D1 ^ D2`. The purpose of this feature is to make matching apply to more cases. A common decoding strategy is "matching", where detection events are paired up in order to determine which errors occurred. Matching only works when the Tanner graph of the problem is a graph, not a hypergraph. In other words, it requires all errors to produce at most two detection events. This is a problem, because in practice there are essentially always circuit error mechanisms that produce more than two detection events. For example, in a CSS surface code, Y type errors on the data qubits will produce four detection events. For matching to work in these cases, non-graphlike errors (errors with more than two detection events) need to be approximated as a combination of graphlike errors. When Stim is decomposing errors, the main guarantee that it provides is that it will not introduce error mechanisms with symptoms that are otherwise impossible or that would require a combination of non-local errors to actually achieve. Informally, Stim guarantees it will preserve the "structure" of the detector error model when suggesting decompositions. It's also worth noting that the suggested decompositions are information preserving: the undecomposed model can always be recovered by simply filtering out all `^` characters splitting the errors into suggested components. Stim uses two strategies for decomposing errors: intra-channel and inter-channel. The *intra-channel* strategy is always applied first, and works by looking at the various detector/observable sets produced by each case of a single noise channel. If some cases are products of other cases, that product is *always* decomposed. For example, suppose that a single qubit depolarizing channel has a `Y5` case that produces four detection events `D0 D1 D2 D3`, an `X5` case that produces two detection events `D0 D1`, and a `Z5` case that produces two detection events `D2 D3`. Because `D0 D1 D2 D3` is the combination of `D0 D1` and `D2 D3`, the `Y5` case will be decomposed into `D0 D1 ^ D2 D3`. An important corner case here is the corner of the CSS surface code, where a Y error has two symptoms which is graphlike but because the intra-channel strategy is aggressive the Y error will still be decomposed into X and Z pieces. This can keep the X and Z decoding graphs disjoint. The *inter-channel* strategy is used when an error component is still not graphlike after the intra-channel strategy was applied. This strategy searches over all other error mechanisms looking for a combination that explains the error. If `--block_decompose_from_introducing_remnant_edges` is specified then this must be an exact match, otherwise the match can omit up to two of the symptoms in the error (resulting in the producing of a "remnant edge"). Note that the code implementing these strategies does not special case any Pauli basis. For example, it does not prefer to decompose Y into X*Z as opposed to X into Y*Z. It also does not prefer to decompose YY into IY*YI as opposed to IY into YY*YI. The code operates purely in terms of the sets of symptoms produced by the various cases, with little regard for how those sets were produced. If these strategies fail to decompose error into graphlike pieces, Stim will throw an error saying it failed to find a satisfying decomposition. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--fold_loops", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Allows the output to contain `repeat` blocks. This flag substantially improves performance on circuits with `REPEAT` blocks with large repetition counts. The analysis will take less time and the output will be more compact. This option is only OFF by default to maintain strict backwards compatibility of the output. When a circuit contains a `REPEAT` block, the structure of the detectors often settles into a form that is identical from iteration to iteration. Specifying the `--fold_loops` option tells Stim to watch for periodicity in the structure of detectors by using a "tortoise and hare" algorithm (see https://en.wikipedia.org/wiki/Cycle_detection ). This improves the asymptotic complexity of analyzing the loop from O(total_repetitions) to O(cycle_period). Note that, although logical observables can "cross" from the end of the loop to the start of the loop without preventing loop folding, detectors CANNOT. If there is any detector introduced after the loop, whose sensitivity region extends to before the loop, loop folding will fail and the code will fall back to flattening the loop. This is disastrous for loops with huge repetition counts (e.g. in the billions) because in that case loop folding is the difference between the error analysis finishing in seconds instead of in days. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--ignore_decomposition_failures", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Allows non-graphlike errors into the output when decomposing errors. Irrelevant unless `--decompose_errors` is specified. Without `--ignore_decomposition_failures`, circuit errors that fail to decompose into graphlike detector error model errors will cause an error and abort the conversion process. When `--ignore_decomposition_failures` is specified, circuit errors that fail to decompose into graphlike detector error model errors produce non-graphlike detector error models. Whatever processes the detector error model is then responsible for dealing with the undecomposed errors (e.g. a tool may choose to simply ignore them). )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in", "filepath", "{stdin}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses the stim circuit file to read the circuit to convert from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the output detector error model. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is a stim detector error model. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_analyze_errors.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_ANALYZE_ERRORS_H #define _STIM_CMD_COMMAND_ANALYZE_ERRORS_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_analyze_errors(int argc, const char **argv); SubCommandHelp command_analyze_errors_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_analyze_errors.test.cc ================================================ // 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. #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" using namespace stim; TEST(command_analyze_errors, detector_hypergraph_deprecated) { ASSERT_EQ( trim(run_captured_stim_main({"--detector_hypergraph"}, R"input( )input")), trim(R"output( [stderr=[DEPRECATION] Use `stim analyze_errors` instead of `--detector_hypergraph` ] )output")); } TEST(command_analyze_errors, analyze_errors) { ASSERT_EQ(run_captured_stim_main({"--analyze_errors"}, ""), "\n"); ASSERT_EQ( trim(run_captured_stim_main({"--analyze_errors"}, R"input( X_ERROR(0.25) 0 M 0 DETECTOR rec[-1] )input")), trim(R"output( error(0.25) D0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"analyze_errors"}, R"input( X_ERROR(0.25) 0 M 0 DETECTOR rec[-1] )input")), trim(R"output( error(0.25) D0 )output")); } TEST(command_analyze_errors, analyze_errors_fold_loops) { ASSERT_EQ( trim(run_captured_stim_main({"--analyze_errors", "--fold_loops"}, R"input( REPEAT 1000 { R 0 X_ERROR(0.25) 0 M 0 DETECTOR rec[-1] } )input")), trim(R"output( repeat 1000 { error(0.25) D0 shift_detectors 1 } )output")); } TEST(command_analyze_errors, analyze_errors_allow_gauge_detectors) { ASSERT_EQ( trim(run_captured_stim_main({"--analyze_errors", "--allow_gauge_detectors"}, R"input( R 0 H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )input")), trim(R"output( error(0.5) D0 D1 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--analyze_errors"}, R"input( R 0 H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )input")), trim( R"OUTPUT( [stderr=)OUTPUT" "\x1B" R"OUTPUT([31mThe circuit contains non-deterministic detectors. To make an SVG picture of the problem, you can use the python API like this: your_circuit.diagram('detslice-with-ops-svg', tick=range(0, 5), filter_coords=['D0', 'D1', ]) or the command line API like this: stim diagram --in your_circuit_file.stim --type detslice-with-ops-svg --tick 0:5 --filter_coords D0:D1 > output_image.svg This was discovered while analyzing a Z-basis reset (R) on: qubit 0 The collapse anti-commuted with these detectors/observables: D0 D1 The backward-propagating error sensitivity for D0 was: X0 Z1 The backward-propagating error sensitivity for D1 was: X0 Circuit stack trace: at instruction #1 [which is R 0] )OUTPUT" "\x1B" R"OUTPUT([0m] )OUTPUT")); } TEST(command_analyze_errors, analyze_errors_all_approximate_disjoint_errors) { ASSERT_EQ( trim(run_captured_stim_main({"--analyze_errors", "--approximate_disjoint_errors"}, R"input( R 0 PAULI_CHANNEL_1(0.125, 0.25, 0.375) 0 M 0 DETECTOR rec[-1] )input")), trim(R"output( error(0.375) D0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--analyze_errors"}, R"input( R 0 PAULI_CHANNEL_1(0.125, 0.25, 0.375) 0 M 0 DETECTOR rec[-1] )input")), trim( R"OUTPUT( [stderr=)OUTPUT" "\x1B" R"OUTPUT([31mEncountered the operation PAULI_CHANNEL_1 during error analysis, but this operation requires the `approximate_disjoint_errors` option to be enabled. If you're calling from python, using stim.Circuit.detector_error_model, you need to add the argument approximate_disjoint_errors=True. If you're calling from the command line, you need to specify --approximate_disjoint_errors. Circuit stack trace: at instruction #2 [which is PAULI_CHANNEL_1(0.125, 0.25, 0.375) 0] )OUTPUT" "\x1B" R"OUTPUT([0m] )OUTPUT")); ASSERT_EQ( trim(run_captured_stim_main({"--analyze_errors", "--approximate_disjoint_errors", "0.3"}, R"input( R 0 PAULI_CHANNEL_1(0.0, 0.25, 0.375) 0 M 0 DETECTOR rec[-1] )input")), trim( R"OUTPUT( [stderr=)OUTPUT" "\x1B" R"OUTPUT([31mPAULI_CHANNEL_1 has a probability argument (0.375) larger than the `approximate_disjoint_errors` threshold (0.3). Circuit stack trace: at instruction #2 [which is PAULI_CHANNEL_1(0, 0.25, 0.375) 0] )OUTPUT" "\x1B" R"OUTPUT([0m] )OUTPUT")); } ================================================ FILE: src/stim/cmd/command_convert.cc ================================================ // Copyright 2023 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. #include "stim/cmd/command_convert.h" #include #include "command_help.h" #include "stim/dem/detector_error_model.h" #include "stim/io/measure_record_batch_writer.h" #include "stim/io/measure_record_reader.h" #include "stim/io/stim_data_formats.h" #include "stim/mem/simd_bits.h" #include "stim/util_bot/arg_parse.h" using namespace stim; struct DataDetails { int num_measurements; int num_detectors; int num_observables; int bits_per_shot; bool include_measurements; bool include_detectors; bool include_observables; }; void process_num_flags(int argc, const char **argv, DataDetails *details_out) { details_out->num_measurements = find_int64_argument("--num_measurements", 0, 0, INT64_MAX, argc, argv); details_out->num_detectors = find_int64_argument("--num_detectors", 0, 0, INT64_MAX, argc, argv); details_out->num_observables = find_int64_argument("--num_observables", 0, 0, INT64_MAX, argc, argv); details_out->include_measurements = details_out->num_measurements > 0; details_out->include_detectors = details_out->num_detectors > 0; details_out->include_observables = details_out->num_observables > 0; } static void process_dem(const char *dem_path_c_str, DataDetails *details_out) { if (dem_path_c_str == nullptr) { return; } FILE *dem_file = fopen(dem_path_c_str, "rb"); if (dem_file == nullptr) { std::stringstream msg; msg << "Failed to open '" << dem_path_c_str << "'"; throw std::invalid_argument(msg.str()); } auto dem = DetectorErrorModel::from_file(dem_file); fclose(dem_file); details_out->num_detectors = dem.count_detectors(); details_out->num_observables = dem.count_observables(); details_out->include_detectors = details_out->num_detectors > 0; details_out->include_observables = details_out->num_observables > 0; } static void process_circuit(const char *circuit_path_c_str, const char *types, DataDetails *details_out) { if (circuit_path_c_str == nullptr) { return; } if (types == nullptr) { throw std::invalid_argument("--types required when passing circuit"); } FILE *circuit_file = fopen(circuit_path_c_str, "rb"); if (circuit_file == nullptr) { std::stringstream msg; msg << "Failed to open '" << circuit_path_c_str << "'"; throw std::invalid_argument(msg.str()); } auto circuit = Circuit::from_file(circuit_file); fclose(circuit_file); CircuitStats circuit_stats = circuit.compute_stats(); details_out->num_measurements = circuit_stats.num_measurements; details_out->num_detectors = circuit_stats.num_detectors; details_out->num_observables = circuit_stats.num_observables; while (types != nullptr && *types) { char c = *types; bool found_duplicate = false; if (c == 'M') { found_duplicate = details_out->include_measurements; details_out->include_measurements = true; } else if (c == 'D') { found_duplicate = details_out->include_detectors; details_out->include_detectors = true; } else if (c == 'L') { found_duplicate = details_out->include_observables; details_out->include_observables = true; } else { throw std::invalid_argument("Unknown type passed to --types"); } if (found_duplicate) { throw std::invalid_argument("Each type in types should only be specified once"); } ++types; } } int stim::command_convert(int argc, const char **argv) { check_for_unknown_arguments( { "--in_format", "--out_format", "--obs_out_format", "--in", "--out", "--obs_out", "--circuit", "--dem", "--types", "--num_measurements", "--num_detectors", "--num_observables", "--bits_per_shot", }, {}, "convert", argc, argv); DataDetails details; const auto &in_format = find_enum_argument("--in_format", nullptr, format_name_to_enum_map(), argc, argv); const auto &out_format = find_enum_argument("--out_format", "01", format_name_to_enum_map(), argc, argv); const auto &obs_out_format = find_enum_argument("--obs_out_format", "01", format_name_to_enum_map(), argc, argv); FILE *in = find_open_file_argument("--in", stdin, "rb", argc, argv); FILE *out = find_open_file_argument("--out", stdout, "wb", argc, argv); FILE *obs_out = find_open_file_argument("--obs_out", stdout, "wb", argc, argv); // Determine the necessary data needed to parse the input and // write to the new output. // First see if everything was just given directly. process_num_flags(argc, argv, &details); // Next see if we can infer from a given DEM file. const char *dem_path = find_argument("--dem", argc, argv); process_dem(dem_path, &details); // Finally see if we can infer from a given circuit file and // list of value types. const char *circuit_path_c_str = find_argument("--circuit", argc, argv); const char *types = find_argument("--types", argc, argv); try { process_circuit(circuit_path_c_str, types, &details); } catch (std::exception &e) { std::cerr << "\033[31m" << e.what() << std::endl; return EXIT_FAILURE; } // Not enough information to infer types, at this point we can only // convert arbitrary bits. if (!details.include_measurements && !details.include_detectors && !details.include_observables) { // dets outputs explicit value types, which we don't know if we get here. if (out_format.id == SampleFormat::SAMPLE_FORMAT_DETS) { std::cerr << "\033[31mNot enough information given to parse input file to write to dets. Please given a circuit " "with --types, a DEM file, or explicit number of each desired type\n"; return EXIT_FAILURE; } details.bits_per_shot = find_int64_argument("--bits_per_shot", 0, 0, INT64_MAX, argc, argv); if (details.bits_per_shot == 0) { std::cerr << "\033[31mNot enough information given to parse input file.\n"; return EXIT_FAILURE; } details.include_measurements = true; details.num_measurements = details.bits_per_shot; } auto reader = MeasureRecordReader::make( in, in_format.id, details.include_measurements ? details.num_measurements : 0, details.include_detectors ? details.num_detectors : 0, details.include_observables ? details.num_observables : 0); auto writer = MeasureRecordWriter::make(out, out_format.id); std::unique_ptr obs_writer; if (obs_out != stdout) { obs_writer = MeasureRecordWriter::make(obs_out, obs_out_format.id); } else { obs_out = nullptr; } simd_bits buf(reader->bits_per_record()); while (reader->start_and_read_entire_record(buf)) { int64_t offset = 0; if (details.include_measurements) { writer->begin_result_type('M'); for (int64_t i = 0; i < details.num_measurements; ++i) { writer->write_bit(buf[i]); } } offset += reader->num_measurements; if (details.include_detectors) { writer->begin_result_type('D'); for (int64_t i = 0; i < details.num_detectors; ++i) { writer->write_bit(buf[i + offset]); } } offset += reader->num_detectors; if (details.include_observables) { if (obs_writer) { obs_writer->begin_result_type('L'); } else { writer->begin_result_type('L'); } for (int64_t i = 0; i < details.num_observables; ++i) { if (obs_writer) { obs_writer->write_bit(buf[i + offset]); } else { writer->write_bit(buf[i + offset]); } } } if (obs_writer) { obs_writer->write_end(); } writer->write_end(); } if (in != stdin) { fclose(in); } if (out != stdout) { fclose(out); } if (obs_out != nullptr) { fclose(obs_out); } return EXIT_SUCCESS; } SubCommandHelp stim::command_convert_help() { SubCommandHelp result; result.subcommand_name = "convert"; result.description = clean_doc_string(R"PARAGRAPH( Convert data between result formats. See the various formats here: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md To read and write data, the size of the records must be known. If writing to a dets file, then the number of measurements, detectors and observables per record must also be known. Both of these pieces of information can either be given directly, or inferred from various data sources, such as circuit or dem files. )PARAGRAPH"); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example.01 10000 11001 00000 01001 >>> stim convert \ --in example.01 \ --in_format 01 \ --out_format dets --num_measurements 5 shot M0 shot M0 M1 M4 shot shot M1 M4 )PARAGRAPH")); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example.dem detector D0 detector D1 logical_observable L2 >>> cat example.dets shot D0 shot D0 D1 L2 shot shot D1 L2 >>> stim convert \ --in example.dets \ --in_format dets \ --out_format 01 --dem example.dem 10000 11001 00000 01001 )PARAGRAPH")); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example_circuit.stim X 0 M 0 1 DETECTOR rec[-2] DETECTOR rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] >>> cat example_measure_data.01 00 01 10 11 >>> stim convert \ --in example_measure_data.01 \ --in_format 01 \ --out_format dets --circuit example_circuit.stim \ --types M shot shot M1 shot M0 shot M0 M1 )PARAGRAPH")); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example.01 0010 0111 1000 1110 >>> stim convert \ --in example.01 \ --in_format 01 \ --out_format hits --bits_per_shot 4 2 1,2,3 0 0,1,2 )PARAGRAPH")); result.flags.push_back( SubCommandHelpFlag{ "--in_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when reading data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing output data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--obs_out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing observable flip data. Irrelevant unless `--obs_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in", "filepath", "{stdin}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses the file to read data from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input's format is specified by `--in_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output's format is specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--obs_out", "filepath", "", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Specifies the file to write observable flip data to. When producing detection event data, the goal is typically to predict whether or not the logical observables were flipped by using the detection events. This argument specifies where to write that observable flip data. If this argument isn't specified, the observable flip data isn't written to a file. The output is in a format specified by `--obs_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--circuit", "filepath", "", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Specifies where the circuit that generated the data is. This argument is optional, but can be used to infer the number of measurements, detectors and observables to use per record. The circuit file should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--types", "M|D|L", "", {"[none]" "types"}, clean_doc_string(R"PARAGRAPH( Specifies the types of events in the files. This argument is required if a circuit is given as the circuit can give the number of each type of event, but not which events are contained within an input file. Note that in most cases, a file will have either measurements only, detections only, or detections and observables. The type values (M, D, L) correspond to the value prefix letters in dets files. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md#dets )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--num_measurements", "int", "0", {"[none], int"}, clean_doc_string(R"PARAGRAPH( Specifies the number of measurements in the input/output files. This argument is required if writing to a dets file and the circuit is not given. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--num_detectors", "int", "0", {"[none], int"}, clean_doc_string(R"PARAGRAPH( Specifies the number of detectors in the input/output files. This argument is required if writing to a dets file and the circuit or dem is not given. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--num_observables", "int", "0", {"[none], int"}, clean_doc_string(R"PARAGRAPH( Specifies the number of observables in the input/output files. This argument is required if writing to a dets file and the circuit or dem is not given. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--bits_per_shot", "int", "0", {"[none], int"}, clean_doc_string(R"PARAGRAPH( Specifies the number of bits per shot in the input/output files. This argument is required if the circuit, dem or num_* flags are not given, and not supported when writing to a dets file. In this case we just treat the bits aas arbitrary data. It is up to the user to interpert it correctly. )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_convert.h ================================================ /* * Copyright 2023 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. */ #ifndef _STIM_CMD_COMMAND_CONVERT_H #define _STIM_CMD_COMMAND_CONVERT_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_convert(int argc, const char **argv); SubCommandHelp command_convert_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_convert.test.cc ================================================ // Copyright 2023 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. #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(command_convert, convert_measurements_with_circuit_to_dets) { RaiiTempNamedFile tmp(R"CIRCUIT( X 0 M 0 1 DETECTOR rec[-2] DETECTOR rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] )CIRCUIT"); std::vector> measurement_data{ std::make_tuple("01", "00\n01\n10\n11\n"), std::make_tuple("b8", std::string({0x00, 0x02, 0x01, 0x03})), std::make_tuple("hits", "\n1\n0\n0,1\n"), std::make_tuple("r8", std::string({0x02, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}))}; for (const auto &[in_format, in_data] : measurement_data) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format", in_format.c_str(), "--out_format", "dets", "--circuit", tmp.path.c_str(), "--types=M"}, in_data), "shot\nshot M1\nshot M0\nshot M0 M1\n"); } } TEST(command_convert, convert_detections_observables_with_circuit_to_dets) { RaiiTempNamedFile tmp(R"CIRCUIT( CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] TICK CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] rec[-2] TICK CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] rec[-2] TICK M 0 1 DETECTOR rec[-1] rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); std::vector> detection_data{ std::make_tuple("01", "00000\n11000\n01100\n00110\n00010\n00011\n"), std::make_tuple("b8", std::string({0x00, 0x03, 0x06, 0x0c, 0x08, 0x18})), std::make_tuple("hits", "\n0,1\n1,2\n2,3\n3\n3,4\n"), std::make_tuple( "r8", std::string({0x05, 0x00, 0x00, 0x03, 0x01, 0x00, 0x02, 0x02, 0x00, 0x01, 0x03, 0x01, 0x03, 0x00, 0x00}))}; for (const auto &[in_format, in_data] : detection_data) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format", in_format.c_str(), "--out_format", "dets", "--circuit", tmp.path.c_str(), "--types=DL"}, in_data), "shot\nshot D0 D1\nshot D1 D2\nshot D2 D3\nshot D3\nshot D3 L0\n"); } } TEST(command_convert, convert_detections_observables_with_circuit_to_dets_with_obs_out) { RaiiTempNamedFile tmp(R"CIRCUIT( CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] TICK CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] rec[-2] TICK CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] rec[-2] TICK M 0 1 DETECTOR rec[-1] rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); RaiiTempNamedFile tmp_obs; std::vector> detection_data{ std::make_tuple("01", "00000\n11000\n01100\n00110\n00010\n00011\n"), std::make_tuple("b8", std::string({0x00, 0x03, 0x06, 0x0c, 0x08, 0x18})), std::make_tuple("hits", "\n0,1\n1,2\n2,3\n3\n3,4\n"), std::make_tuple( "r8", std::string({0x05, 0x00, 0x00, 0x03, 0x01, 0x00, 0x02, 0x02, 0x00, 0x01, 0x03, 0x01, 0x03, 0x00, 0x00}))}; for (const auto &[in_format, in_data] : detection_data) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format", in_format.c_str(), "--out_format", "dets", "--obs_out_format", "dets", "--obs_out", tmp_obs.path.c_str(), "--circuit", tmp.path.c_str(), "--types=DL"}, in_data), "shot\nshot D0 D1\nshot D1 D2\nshot D2 D3\nshot D3\nshot D3\n"); ASSERT_EQ(tmp_obs.read_contents(), "shot\nshot\nshot\nshot\nshot\nshot L0\n"); } } TEST(command_convert, convert_detections_observables_with_circuit_no_dets) { RaiiTempNamedFile tmp(R"CIRCUIT( R 0 1 2 3 4 TICK CX 0 1 2 3 DEPOLARIZE2(0.3) 0 1 2 3 TICK CX 2 1 4 3 DEPOLARIZE2(0.3) 2 1 4 3 TICK MR 1 3 DETECTOR(1, 0) rec[-2] DETECTOR(3, 0) rec[-1] M 0 2 4 DETECTOR(1, 1) rec[-2] rec[-3] rec[-5] DETECTOR(3, 1) rec[-1] rec[-2] rec[-4] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); std::vector> detection_data{ std::make_tuple("01", "10100\n00011\n00000\n00100\n00000\n10000\n"), std::make_tuple("b8", std::string({0x05, 0x18, 0x00, 0x04, 0x00, 0x01})), std::make_tuple("hits", "0,2\n3,4\n\n2\n\n0\n"), std::make_tuple("r8", std::string({0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x05, 0x02, 0x02, 0x05, 0x00, 0x04}))}; for (const auto &[in_format, in_data] : detection_data) { for (const auto &[out_format, out_data] : detection_data) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format", in_format.c_str(), "--out_format", out_format.c_str(), "--circuit", tmp.path.c_str(), "--types=DL"}, in_data), out_data); } } } TEST(command_convert, convert_detections_observables_with_dem) { RaiiTempNamedFile tmp(R"DEM( detector D0 detector D1 logical_observable L2 )DEM"); std::vector> detection_data{ std::make_tuple("01", "10000\n11001\n00000\n01001\n"), std::make_tuple("b8", std::string({0x01, 0x13, 0x00, 0x12})), std::make_tuple("dets", "shot D0\nshot D0 D1 L2\nshot\nshot D1 L2\n"), std::make_tuple("hits", "0\n0,1,4\n\n1,4\n"), std::make_tuple("r8", std::string({0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x05, 0x01, 0x02, 0x00}))}; for (const auto &[in_format, in_data] : detection_data) { for (const auto &[out_format, out_data] : detection_data) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format", in_format.c_str(), "--out_format", out_format.c_str(), "--dem", tmp.path.c_str()}, in_data), out_data); } } } TEST(command_convert, convert_measurements_no_circuit_or_dem) { std::vector> measurement_data{ std::make_tuple("01", "100\n010\n110\n001\n010\n111\n"), std::make_tuple("b8", std::string({0x01, 0x02, 0x03, 0x04, 0x02, 0x07})), std::make_tuple("hits", "0\n1\n0,1\n2\n1\n0,1,2\n"), std::make_tuple("dets", "shot M0\nshot M1\nshot M0 M1\nshot M2\nshot M1\nshot M0 M1 M2\n"), std::make_tuple( "r8", std::string({0x00, 0x02, 0x01, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00}))}; for (const auto &[in_format, in_data] : measurement_data) { for (const auto &[out_format, out_data] : measurement_data) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format", in_format.c_str(), "--out_format", out_format.c_str(), "--num_measurements", "3"}, in_data), out_data); } } } TEST(command_convert, convert_detections_observables_no_circuit_or_dem) { std::vector> detection_data{ std::make_tuple("01", "10000\n11001\n00000\n01001\n"), std::make_tuple("b8", std::string({0x01, 0x13, 0x00, 0x12})), std::make_tuple("dets", "shot D0\nshot D0 D1 L2\nshot\nshot D1 L2\n"), std::make_tuple("hits", "0\n0,1,4\n\n1,4\n"), std::make_tuple("r8", std::string({0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x05, 0x01, 0x02, 0x00}))}; for (const auto &[in_format, in_data] : detection_data) { for (const auto &[out_format, out_data] : detection_data) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format", in_format.c_str(), "--out_format", out_format.c_str(), "--num_detectors", "2", "--num_observables", "3"}, in_data), out_data); } } } TEST(command_convert, convert_bits_per_shot_no_dets) { std::vector> measurement_data{ std::make_tuple("01", "00\n01\n10\n11\n"), std::make_tuple("b8", std::string({0x00, 0x02, 0x01, 0x03})), std::make_tuple("hits", "\n1\n0\n0,1\n"), std::make_tuple("r8", std::string({0x02, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}))}; for (const auto &[in_format, in_data] : measurement_data) { for (const auto &[out_format, out_data] : measurement_data) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format", in_format.c_str(), "--out_format", out_format.c_str(), "--bits_per_shot=2"}, in_data), out_data); } } } TEST(command_convert, convert_multiple_bitword_sized_records) { ASSERT_EQ( run_captured_stim_main( {"convert", "--in_format=b8", "--out_format=b8", "--bits_per_shot=2048"}, std::string(256, 0x6b)), std::string(256, 0x6b)); } TEST(command_convert, convert_circuit_fail_without_types) { RaiiTempNamedFile tmp(R"CIRCUIT( X 0 M 0 1 )CIRCUIT"); ASSERT_TRUE(matches( run_captured_stim_main( {"convert", "--in_format=01", "--out_format", "dets", "--circuit", tmp.path.c_str(), "--num_measurements=2"}, ""), ".*--types required when passing circuit.*")); } TEST(command_convert, convert_fail_without_any_information) { ASSERT_TRUE(matches( run_captured_stim_main({"convert", "--in_format=r8", "--out_format=b8"}, ""), ".*Not enough information given to parse input file.*")); } TEST(command_convert, convert_fail_with_bits_per_shot_to_dets) { ASSERT_TRUE(matches( run_captured_stim_main({"convert", "--in_format=01", "--out_format", "dets", "--bits_per_shot=2"}, ""), ".*Not enough information given to parse input file to write to dets.*")); } TEST(command_convert, convert_invalid_types) { RaiiTempNamedFile tmp(""); ASSERT_TRUE(matches( run_captured_stim_main( {"convert", "--in_format=dets", "--out_format=dets", "--circuit", tmp.path.c_str(), "--types=N"}, ""), ".*Unknown type passed to --types.*")); ASSERT_TRUE(matches( run_captured_stim_main( {"convert", "--in_format=dets", "--out_format=dets", "--circuit", tmp.path.c_str(), "--types=MM"}, ""), ".*Each type in types should only be specified once.*")); } ================================================ FILE: src/stim/cmd/command_detect.cc ================================================ // 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. #include "stim/cmd/command_detect.h" #include "command_help.h" #include "stim/io/raii_file.h" #include "stim/io/stim_data_formats.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_bot/probability_util.h" using namespace stim; int stim::command_detect(int argc, const char **argv) { check_for_unknown_arguments( {"--seed", "--shots", "--append_observables", "--out_format", "--out", "--in", "--obs_out", "--obs_out_format"}, {"--detect", "--prepend_observables"}, "detect", argc, argv); const auto &out_format = find_enum_argument("--out_format", "01", format_name_to_enum_map(), argc, argv); const auto &obs_out_format = find_enum_argument("--obs_out_format", "01", format_name_to_enum_map(), argc, argv); bool prepend_observables = find_bool_argument("--prepend_observables", argc, argv); if (prepend_observables) { std::cerr << "[DEPRECATION] Avoid using `--prepend_observables`. Data readers assume observables are appended, " "not prepended.\n"; } bool append_observables = find_bool_argument("--append_observables", argc, argv); uint64_t num_shots = find_argument("--shots", argc, argv) ? (uint64_t)find_int64_argument("--shots", 1, 0, INT64_MAX, argc, argv) : find_argument("--detect", argc, argv) ? (uint64_t)find_int64_argument("--detect", 1, 0, INT64_MAX, argc, argv) : 1; if (out_format.id == SampleFormat::SAMPLE_FORMAT_DETS && !append_observables) { prepend_observables = true; } RaiiFile in(find_open_file_argument("--in", stdin, "rb", argc, argv)); RaiiFile out(find_open_file_argument("--out", stdout, "wb", argc, argv)); RaiiFile obs_out(find_open_file_argument("--obs_out", stdout, "wb", argc, argv)); if (obs_out.f == stdout) { obs_out.f = nullptr; } if (out.f == stdout) { out.responsible_for_closing = false; } if (in.f == stdin) { out.responsible_for_closing = false; } if (num_shots == 0) { return EXIT_SUCCESS; } auto circuit = Circuit::from_file(in.f); in.done(); auto rng = optionally_seeded_rng(argc, argv); sample_batch_detection_events_writing_results_to_disk( circuit, num_shots, prepend_observables, append_observables, out.f, out_format.id, rng, obs_out.f, obs_out_format.id); return EXIT_SUCCESS; } SubCommandHelp stim::command_detect_help() { SubCommandHelp result; result.subcommand_name = "detect"; result.description = "Sample detection events and observable flips from a circuit."; result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example.stim H 0 CNOT 0 1 X_ERROR(0.1) 0 1 M 0 1 DETECTOR rec[-1] rec[-2] >>> stim detect --shots 5 --in example.stim 0 1 0 0 0 )PARAGRAPH")); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example.stim # Single-shot X-basis rep code circuit. RX 0 1 2 3 4 5 6 MPP X0*X1 X1*X2 X2*X3 X3*X4 X4*X5 X5*X6 Z_ERROR(0.1) 0 1 2 3 4 5 6 MPP X0 X1 X2 X3 X4 X5 X6 DETECTOR rec[-1] rec[-2] rec[-8] # X6 X5 now = X5*X6 before DETECTOR rec[-2] rec[-3] rec[-9] # X5 X4 now = X4*X5 before DETECTOR rec[-3] rec[-4] rec[-10] # X4 X3 now = X3*X4 before DETECTOR rec[-4] rec[-5] rec[-11] # X3 X2 now = X2*X3 before DETECTOR rec[-5] rec[-6] rec[-12] # X2 X1 now = X1*X2 before DETECTOR rec[-6] rec[-7] rec[-13] # X1 X0 now = X0*X1 before OBSERVABLE_INCLUDE(0) rec[-1] >>> stim detect \ --in example.stim \ --out_format dets \ --shots 10 shot shot shot L0 D0 D5 shot D1 D2 shot shot L0 D0 shot D5 shot shot D3 D4 shot D0 D1 )PARAGRAPH")); result.flags.push_back( SubCommandHelpFlag{ "--out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing output data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--seed", "int", "system_entropy", {"[none]", "int"}, clean_doc_string(R"PARAGRAPH( Makes simulation results PARTIALLY deterministic. The seed integer must be a non-negative 64 bit signed integer. When `--seed` isn't specified, the random number generator is seeded using fresh entropy requested from the operating system. When `--seed #` is set, the exact same simulation results will be produced every time ASSUMING: - the exact same other flags are specified - the exact same version of Stim is being used - the exact same machine architecture is being used (for example, you're not switching from a machine that has AVX2 instructions to one that doesn't). CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary other flags and modes. For example, `--skip_reference_sample` may result in fewer calls the to the random number generator before reported sampling begins. More generally, using the same seed for `stim sample` and `stim detect` will not result in detection events corresponding to the measurement results. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--shots", "int", "1", {"[none]", "int"}, clean_doc_string(R"PARAGRAPH( Specifies the number of samples to take from the circuit. Defaults to 1. Must be an integer between 0 and a quintillion (10^18). )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--append_observables", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Appends observable flips to the end of samples as extra detectors. PREFER --obs_out OVER THIS FLAG. Mixing the observable flip data into detection event data tends to require simply separating them again immediately, creating unnecessary work. For example, when testing a decoder, you do not want to give the observable flips to the decoder because that is the information the decoder is supposed to be predicting from the detection events. This flag causes observable flip data to be appended to each sample, as if the observables were extra detectors at the end of the circuit. For example, if there are 100 detectors and 10 observables in the circuit, then the output will contain 110 detectors and the last 10 are the observables. Note that, when using `--out_format dets`, this option is implicitly activated but observables are not appended as if they were detectors (because `dets` has type hinting information). For example, in the example from the last paragraph, the observables would be named `L0` through `L9` instead of `D100` through `D109`. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--obs_out", "filepath", "", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Specifies the file to write observable flip data to. When sampling detection event data, the goal is typically to predict whether or not the logical observables were flipped by using the detection events. This argument specifies where to write that observable flip data. If this argument isn't specified, the observable flip data isn't written to a file. The output is in a format specified by `--obs_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--obs_out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing observable flip data. Irrelevant unless `--obs_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in", "filepath", "{stdin}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses the stim circuit file to read the circuit to sample from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the sampled data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is in a format specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_detect.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_DETECT_H #define _STIM_CMD_COMMAND_DETECT_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_detect(int argc, const char **argv); SubCommandHelp command_detect_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_detect.test.cc ================================================ // 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. #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" using namespace stim; TEST(command_detect, detect_basic) { ASSERT_EQ( trim(run_captured_stim_main({"--detect"}, R"input( M 0 )input")), trim(R"output( )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect"}, R"input( M 0 DETECTOR rec[-1] )input")), trim(R"output( 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect"}, R"input( X_ERROR(1) 0 M 0 DETECTOR rec[-1] )input")), trim(R"output( 1 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect", "2"}, R"input( X_ERROR(1) 0 M 0 DETECTOR rec[-1] )input")), trim(R"output( 1 1 )output")); ASSERT_EQ( trim(run_captured_stim_main({"detect", "--shots", "2"}, R"input( X_ERROR(1) 0 M 0 DETECTOR rec[-1] )input")), trim(R"output( 1 1 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect"}, R"input( DETECTOR )input")), trim(R"output( 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect"}, R"input( X_ERROR(1) 0 MR 0 DETECTOR rec[-1] )input")), trim(R"output( 1 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect"}, R"input( X_ERROR(1) 0 M 0 M 0 DETECTOR rec[-1] rec[-2] )input")), trim(R"output( 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect"}, R"input( X_ERROR(1) 0 MR 0 MR 0 DETECTOR rec[-1] rec[-2] )input")), trim(R"output( 1 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect"}, R"input( M 2 M 2 REPEAT 3 { R 2 CNOT 0 2 1 2 DETECTOR rec[-1] rec[-2] M 2 } M 0 1 OBSERVABLE_INCLUDE(0) rec[-2] rec[-1] )input")), trim(R"output( 000 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect", "--prepend_observables"}, R"input( M 2 M 2 REPEAT 3 { R 2 CNOT 0 2 1 2 DETECTOR rec[-1] rec[-2] M 2 } M 0 1 OBSERVABLE_INCLUDE(0) rec[-2] rec[-1] )input")), trim(R"output( 0000 [stderr=[DEPRECATION] Avoid using `--prepend_observables`. Data readers assume observables are appended, not prepended. ] )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect", "--prepend_observables"}, R"input( M 2 M 2 X_ERROR(1) 0 1 REPEAT 3 { R 2 CNOT 0 2 1 2 DETECTOR rec[-1] rec[-2] M 2 } M 0 1 OBSERVABLE_INCLUDE(0) rec[-2] )input")), trim(R"output( 1000 [stderr=[DEPRECATION] Avoid using `--prepend_observables`. Data readers assume observables are appended, not prepended. ] )output")); ASSERT_EQ( trim(run_captured_stim_main({"--detect", "--append_observables"}, R"input( M 2 M 2 X_ERROR(1) 0 1 REPEAT 3 { R 2 CNOT 0 2 1 2 DETECTOR rec[-1] rec[-2] M 2 } M 0 1 OBSERVABLE_INCLUDE(0) rec[-2] )input")), trim(R"output( 0001 )output")); } TEST(command_detect, detection_event_simulator_counts_measurements_correctly) { auto s = run_captured_stim_main({"--detect=1000"}, "MPP Z8*X9\nDETECTOR rec[-1]"); size_t zeroes = 0; size_t ones = 0; for (size_t k = 0; k < s.size(); k += 2) { zeroes += s[k] == '0'; ones += s[k] == '1'; ASSERT_EQ(s[k + 1], '\n'); } ASSERT_EQ(zeroes + ones, 1000); ASSERT_TRUE(400 < zeroes && zeroes < 600); } TEST(command_detect, seeded_detecting) { ASSERT_EQ( run_captured_stim_main({"detect", "--shots=256", "--seed 5"}, R"input( X_ERROR(0.5) 0 M 0 DETECTOR rec[-1] )input"), run_captured_stim_main({"detect", "--shots=256", "--seed 5"}, R"input( X_ERROR(0.5) 0 M 0 DETECTOR rec[-1] )input")); ASSERT_NE( run_captured_stim_main({"detect", "--shots=256"}, R"input( X_ERROR(0.5) 0 M 0 DETECTOR rec[-1] )input"), run_captured_stim_main({"detect", "--shots=256"}, R"input( X_ERROR(0.5) 0 M 0 DETECTOR rec[-1] )input")); ASSERT_NE( run_captured_stim_main({"detect", "--shots=256", "--seed 5"}, R"input( X_ERROR(0.5) 0 M 0 DETECTOR rec[-1] )input"), run_captured_stim_main({"detect", "--shots=256", "--seed 6"}, R"input( X_ERROR(0.5) 0 M 0 DETECTOR rec[-1] )input")); } ================================================ FILE: src/stim/cmd/command_diagram.cc ================================================ // 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. #include "stim/cmd/command_diagram.h" #include #include "command_help.h" #include "stim/diagram/crumble.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/graph/match_graph_3d_drawer.h" #include "stim/diagram/graph/match_graph_svg_drawer.h" #include "stim/diagram/timeline/timeline_3d_drawer.h" #include "stim/diagram/timeline/timeline_ascii_drawer.h" #include "stim/diagram/timeline/timeline_svg_drawer.h" #include "stim/io/raii_file.h" #include "stim/simulators/error_analyzer.h" #include "stim/util_bot/arg_parse.h" using namespace stim; using namespace stim_draw_internal; enum class DiagramTypes { NOT_A_DIAGRAM, INTERACTIVE_HTML, TIMELINE_TEXT, TIMELINE_SVG, TIMELINE_3D, TIMELINE_3D_HTML, TIME_SLICE_SVG, TIME_SLICE_PLUS_DETECTOR_SLICE_SVG, MATCH_GRAPH_SVG, MATCH_GRAPH_3D, MATCH_GRAPH_3D_HTML, DETECTOR_SLICE_TEXT, DETECTOR_SLICE_SVG, }; stim::Circuit _read_circuit(RaiiFile &in, int argc, const char **argv) { auto circuit = Circuit::from_file(in.f); in.done(); if (find_bool_argument("--remove_noise", argc, argv)) { circuit = circuit.without_noise(); } return circuit; } stim::DetectorErrorModel _read_dem(RaiiFile &in, int argc, const char **argv) { if (find_bool_argument("--remove_noise", argc, argv)) { throw std::invalid_argument( "--remove_noise is incompatible with match graph diagrams, because the noise is needed to produce the " "match graph."); } std::string content; while (true) { int c = getc(in.f); if (c == EOF) { break; } content.push_back(c); } in.done(); try { return DetectorErrorModel(content); } catch (const std::exception &_) { } Circuit circuit(content); auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 1, true, false); if (dem.count_errors() == 0) { std::cerr << "Warning: the detector error model derived from the circuit had no errors.\n" "Did you input a noiseless circuit instead of a noisy one?\n"; } return dem; } std::vector _read_coord_filter(int argc, const char **argv) { const char *arg = find_argument("--filter_coords", argc, argv); if (arg == nullptr) { return std::vector{CoordFilter{}}; } std::vector result; for (std::string_view term : split_view(':', arg)) { result.push_back(CoordFilter::parse_from(term)); } return result; } DiagramTypes _read_diagram_type(int argc, const char **argv) { std::map diagram_types{ {"timeline-text", DiagramTypes::TIMELINE_TEXT}, {"timeline-svg", DiagramTypes::TIMELINE_SVG}, {"timeline-3d", DiagramTypes::TIMELINE_3D}, {"timeline-3d-html", DiagramTypes::TIMELINE_3D_HTML}, {"timeslice-svg", DiagramTypes::TIME_SLICE_SVG}, {"detslice-with-ops-svg", DiagramTypes::TIME_SLICE_PLUS_DETECTOR_SLICE_SVG}, {"matchgraph-svg", DiagramTypes::MATCH_GRAPH_SVG}, {"matchgraph-3d", DiagramTypes::MATCH_GRAPH_3D}, {"matchgraph-3d-html", DiagramTypes::MATCH_GRAPH_3D_HTML}, {"interactive-html", DiagramTypes::INTERACTIVE_HTML}, {"detslice-text", DiagramTypes::DETECTOR_SLICE_TEXT}, {"detslice-svg", DiagramTypes::DETECTOR_SLICE_SVG}, }; std::map quietly_allowed_diagram_types{ {"time-slice-svg", DiagramTypes::TIME_SLICE_SVG}, {"time+detector-slice-svg", DiagramTypes::TIME_SLICE_PLUS_DETECTOR_SLICE_SVG}, {"interactive", DiagramTypes::INTERACTIVE_HTML}, {"detector-slice-text", DiagramTypes::DETECTOR_SLICE_TEXT}, {"detector-slice-svg", DiagramTypes::DETECTOR_SLICE_SVG}, {"match-graph-svg", DiagramTypes::MATCH_GRAPH_SVG}, {"match-graph-3d", DiagramTypes::MATCH_GRAPH_3D}, {"match-graph-3d-html", DiagramTypes::MATCH_GRAPH_3D_HTML}, }; DiagramTypes type = DiagramTypes::NOT_A_DIAGRAM; try { type = find_enum_argument("--type", nullptr, quietly_allowed_diagram_types, argc, argv); } catch (const std::invalid_argument &_) { } if (type == DiagramTypes::NOT_A_DIAGRAM) { type = find_enum_argument("--type", nullptr, diagram_types, argc, argv); assert(type != DiagramTypes::NOT_A_DIAGRAM); } return type; } bool _read_tick(int argc, const char **argv, uint64_t *tick, uint64_t *tick_start, uint64_t *tick_num) { *tick = 0; *tick_start = 0; *tick_num = UINT64_MAX; if (find_argument("--tick", argc, argv) == nullptr) { return false; } std::string tick_str = find_argument("--tick", argc, argv); auto t = tick_str.find(':'); if (t != 0 && t != std::string::npos) { *tick_start = parse_exact_uint64_t_from_string(tick_str.substr(0, t)); uint64_t tick_end = parse_exact_uint64_t_from_string(tick_str.substr(t + 1)); if (tick_end <= *tick_start) { throw std::invalid_argument("tick_end <= tick_start"); } *tick_num = tick_end - *tick_start; *tick = *tick_start; } else { *tick = find_int64_argument("--tick", 0, 0, INT64_MAX, argc, argv); *tick_num = 1; *tick_start = *tick; } return true; } int stim::command_diagram(int argc, const char **argv) { check_for_unknown_arguments( { "--remove_noise", "--type", "--tick", "--filter_coords", "--in", "--out", }, {}, "diagram", argc, argv); RaiiFile in(find_open_file_argument("--in", stdin, "rb", argc, argv)); auto out_stream = find_output_stream_argument("--out", true, argc, argv); auto &out = out_stream.stream(); DiagramTypes type = _read_diagram_type(argc, argv); uint64_t tick = 0; uint64_t tick_start = 0; uint64_t tick_num = UINT64_MAX; bool has_tick_arg = _read_tick(argc, argv, &tick, &tick_start, &tick_num); if (type == DiagramTypes::TIMELINE_TEXT) { auto circuit = _read_circuit(in, argc, argv); out << DiagramTimelineAsciiDrawer::make_diagram(circuit); } else if (type == DiagramTypes::TIMELINE_SVG) { auto circuit = _read_circuit(in, argc, argv); auto coord_filter = _read_coord_filter(argc, argv); DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, out, tick_start, tick_num, DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE, coord_filter); } else if (type == DiagramTypes::TIME_SLICE_SVG) { auto circuit = _read_circuit(in, argc, argv); auto coord_filter = _read_coord_filter(argc, argv); DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, out, tick_start, tick_num, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_SLICE, coord_filter); } else if (type == DiagramTypes::TIME_SLICE_PLUS_DETECTOR_SLICE_SVG) { auto circuit = _read_circuit(in, argc, argv); auto coord_filter = _read_coord_filter(argc, argv); DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, out, tick_start, tick_num, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, coord_filter); } else if (type == DiagramTypes::TIMELINE_3D) { auto circuit = _read_circuit(in, argc, argv); DiagramTimeline3DDrawer::circuit_to_basic_3d_diagram(circuit).to_gltf_scene().to_json().write(out); } else if (type == DiagramTypes::TIMELINE_3D_HTML) { auto circuit = _read_circuit(in, argc, argv); std::stringstream tmp_out; DiagramTimeline3DDrawer::circuit_to_basic_3d_diagram(circuit).to_gltf_scene().to_json().write(tmp_out); write_html_viewer_for_gltf_data(tmp_out.str(), out); } else if (type == DiagramTypes::INTERACTIVE_HTML) { auto circuit = _read_circuit(in, argc, argv); write_crumble_html_with_preloaded_circuit(circuit, out); } else if (type == DiagramTypes::MATCH_GRAPH_3D) { auto dem = _read_dem(in, argc, argv); dem_match_graph_to_basic_3d_diagram(dem).to_gltf_scene().to_json().write(out); } else if (type == DiagramTypes::MATCH_GRAPH_3D_HTML) { auto dem = _read_dem(in, argc, argv); std::stringstream tmp_out; dem_match_graph_to_basic_3d_diagram(dem).to_gltf_scene().to_json().write(tmp_out); write_html_viewer_for_gltf_data(tmp_out.str(), out); } else if (type == DiagramTypes::MATCH_GRAPH_SVG) { auto dem = _read_dem(in, argc, argv); dem_match_graph_to_svg_diagram_write_to(dem, out); } else if (type == DiagramTypes::DETECTOR_SLICE_TEXT) { if (!has_tick_arg) { throw std::invalid_argument("Must specify --tick=# with --type=detector-slice-text"); } auto coord_filter = _read_coord_filter(argc, argv); auto circuit = _read_circuit(in, argc, argv); out << DetectorSliceSet::from_circuit_ticks(circuit, (uint64_t)tick, 1, coord_filter); } else if (type == DiagramTypes::DETECTOR_SLICE_SVG) { auto coord_filter = _read_coord_filter(argc, argv); auto circuit = _read_circuit(in, argc, argv); DetectorSliceSet::from_circuit_ticks(circuit, tick_start, tick_num, coord_filter).write_svg_diagram_to(out); } else { throw std::invalid_argument("Unknown type"); } out << '\n'; return EXIT_SUCCESS; } SubCommandHelp stim::command_diagram_help() { SubCommandHelp result; result.subcommand_name = "diagram"; result.description = clean_doc_string(R"PARAGRAPH( Produces various kinds of diagrams. )PARAGRAPH"); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example_circuit.stim H 0 CNOT 0 1 >>> stim diagram \ --in example_circuit.stim \ --type timeline-text q0: -H-@- | q1: ---X- )PARAGRAPH")); result.examples.push_back(clean_doc_string( R"PARAGRAPH( >>> # Making a video of detector slices moving around >>> # First, make a circuit to animate. >>> stim gen \ --code surface_code \ --task rotated_memory_x \ --distance 5 \ --rounds 100 \ > surface_code.stim >>> # Second, use gnu-parallel and stim diagram to make video frames. >>> parallel stim diagram \ --filter_coords 2,2:4,2 \ --type detector-slice-svg \ --tick {} \ --in surface_code.stim \ --out video_frame_{}.svg \ ::: {0050..0150} >>> # Third, use ffmpeg to turn the frames into a GIF. >>> # (note: the complex filter argument is optional; it turns the background white) >>> ffmpeg output_animation.gif \ -framerate 5 \ -pattern_type glob -i 'video_frame_*.svg' \ -pix_fmt rgb8 \ -filter_complex "[0]split=2[bg][fg];[bg]drawbox=c=white@1:t=fill[bg];[bg][fg]overlay=format=auto" >>> # Alternatively, make an MP4 video instead of a GIF. >>> ffmpeg output_video.mp4 \ -framerate 5 \ -pattern_type glob -i 'video_frame_*.svg' \ -vf scale=1024:-1 \ -c:v libx264 \ -vf format=yuv420p \ -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" )PARAGRAPH", true)); result.flags.push_back( SubCommandHelpFlag{ "--remove_noise", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Removes noise from the input before turning it into a diagram. For example, if the input is a noisy circuit and you aren't interested in the details of the noise but rather in the structure of the circuit, you can specify this flag in order to filter out the noise. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--tick", "int | int:int", "none", {"[none]", "int", "int-int"}, clean_doc_string(R"PARAGRAPH( Specifies that the diagram should apply to a specific TICK or range of TICKS from the input circuit. To specify a single tick, pass an integer like `--tick=5`. To specify a range, pass two integers separated by a colon like `--tick=start:end`. Note that the range is half open. In detector and time slice diagrams, `--tick` identifies which ticks to include in the diagram. Note that `--tick=0` is the very beginning of the circuit and `--tick=1` is the instant of the first TICK instruction. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--filter_coords", "(float.seperatedby(',') | L# | D#).seperatedby(':')", "", {"[none]", "(float.seperatedby(',') | L# | D#).seperatedby(':')"}, clean_doc_string(R"PARAGRAPH( Specifies coordinate filters that determine what appears in the diagram. A coordinate is a double precision floating point number. A point is a tuple of coordinates. The coordinates of a point are separate by commas (','). A filter is a set of points. Points are separated by colons (':'). Filters can also be set to specific detector or observable indices, like D0 or L0. Example: --filter-coords 2,3:4,5,6 In a detector slice diagram this means that only detectors whose first two coordinates are (2,3), or whose first three coordinate are (4,5,6), should be included in the diagram. --filter-coords L0 In a detector slice diagram this means that logical observable 0 should be included. Logical observables are only included if explicitly filtered in. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--type", "name", "", {"name"}, clean_doc_string(R"PARAGRAPH( The type of diagram to make. The available diagram types are: "timeline-text": Produces an ASCII text diagram of the operations performed by a circuit over time. The qubits are laid out into a line top to bottom, and time advances left to right. The input object should be a stim circuit. INPUT MUST BE A CIRCUIT. "timeline-svg": Produces an SVG image diagram of the operations performed by a circuit over time. The qubits are laid out into a line top to bottom, and time advances left to right. The input object should be a stim circuit. INPUT MUST BE A CIRCUIT. "timeline-3d": Produces a 3d model, in GLTF format, of the operations applied by a stim circuit over time. GLTF files can be opened with a variety of programs, or opened online in viewers such as https://gltf-viewer.donmccurdy.com/ . INPUT MUST BE A CIRCUIT. "timeline-3d-html": A web page containing a 3d model viewer of the operations applied by a stim circuit over time. INPUT MUST BE A CIRCUIT. "matchgraph-svg": An image of the decoding graph of a detector error model. Red lines are errors crossing a logical observable. INPUT MUST BE A DETECTOR ERROR MODEL OR A CIRCUIT. "matchgraph-3d": A 3d model, in GLTF format, of the decoding graph of a detector error model. Red lines are errors crossing a logical observable. GLTF files can be opened with a variety of programs, or opened online in viewers such as https://gltf-viewer.donmccurdy.com/ . INPUT MUST BE A DETECTOR ERROR MODEL OR A CIRCUIT. "matchgraph-3d-html": A web page containing a 3d model viewer of the decoding graph of a detector error model or circuit. INPUT MUST BE A DETECTOR ERROR MODEL OR A CIRCUIT. "detslice-text": An ASCII diagram of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. INPUT MUST BE A CIRCUIT. "detslice-svg": An SVG image of the stabilizers that detectors declared by the circuit correspond to during the TICK instruction identified by the `tick` argument. For example, a detector slice diagram of a CSS surface code circuit during the TICK between a measurement layer and a reset layer will produce the usual diagram of a surface code. Uses the Pauli color convention XYZ=RGB. INPUT MUST BE A CIRCUIT. "timeslice-svg": An SVG image of the operations that a circuit applies during the specified tick or range of ticks. INPUT MUST BE A CIRCUIT. "detslice-with-ops-svg": An SVG image of the operations that a circuit applies during the specified tick or range of ticks, combined with the detector slices after those operations are applied. INPUT MUST BE A CIRCUIT. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in", "filepath", "{stdin}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Where to read the object to diagram from. By default, the object is read from stdin. When `--in $FILEPATH` is specified, the object is instead read from the file at $FILEPATH. The expected type of object depends on the type of diagram. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the diagram to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The type of output produced depends on the type of diagram. )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_diagram.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_DIAGRAM_H #define _STIM_CMD_COMMAND_DIAGRAM_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_diagram(int argc, const char **argv); SubCommandHelp command_diagram_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_diagram.pybind.cc ================================================ // 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. #include "stim/cmd/command_diagram.pybind.h" #include "stim/cmd/command_help.h" #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/diagram/base64.h" #include "stim/diagram/crumble.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/graph/match_graph_3d_drawer.h" #include "stim/diagram/graph/match_graph_svg_drawer.h" #include "stim/diagram/timeline/timeline_3d_drawer.h" #include "stim/diagram/timeline/timeline_ascii_drawer.h" #include "stim/diagram/timeline/timeline_svg_drawer.h" #include "stim/simulators/error_analyzer.h" #include "stim/util_bot/arg_parse.h" using namespace stim; using namespace stim_pybind; using namespace stim_draw_internal; pybind11::class_ stim_pybind::pybind_diagram(pybind11::module &m) { auto c = pybind11::class_( m, "_DiagramHelper", clean_doc_string(R"DOC( A helper class for displaying diagrams in IPython notebooks. To write the diagram's contents to a file (for example, to write an SVG image to an SVG file), use `print(diagram, file=file)`. )DOC") .data()); return c; } std::string escape_html_for_srcdoc(std::string_view src) { // From https://stackoverflow.com/a/9907752 std::stringstream dst; for (char ch : src) { switch (ch) { case '&': dst << "&"; break; case '\'': dst << "'"; break; case '"': dst << """; break; case '<': dst << "<"; break; case '>': dst << ">"; break; default: dst << ch; break; } } return dst.str(); } pybind11::object diagram_as_html(const DiagramHelper &self) { std::string output = "None"; if (self.type == DiagramType::DIAGRAM_TYPE_TEXT) { return pybind11::cast("
" + self.content + "
"); } if (self.type == DiagramType::DIAGRAM_TYPE_SVG_HTML) { // Wrap the SVG image into an img tag. std::stringstream out; out << R"HTML()HTML"; output = out.str(); } else if (self.type == DiagramType::DIAGRAM_TYPE_SVG) { // Github's Jupyter notebook preview will fail to show SVG images if they are wrapped in HTML. // So, for SVG diagrams, we refuse to return an html repr in this case. return pybind11::none(); } if (self.type == DiagramType::DIAGRAM_TYPE_GLTF) { std::stringstream out; write_html_viewer_for_gltf_data(self.content, out); output = out.str(); } if (self.type == DiagramType::DIAGRAM_TYPE_HTML) { output = self.content; } if (output == "None") { return pybind11::none(); } // Wrap the output into an iframe. // In a Jupyter notebook this is very important, because it prevents output // cells from seeing each others' elements when finding elements by id. // Because, for some insane reason, Jupyter notebooks don't isolate the cells // from each other by default! Colab does the right thing at least... std::string framed = R"HTML()HTML"; return pybind11::cast(framed); } void stim_pybind::pybind_diagram_methods(pybind11::module &m, pybind11::class_ &c) { c.def("_repr_html_", &diagram_as_html); c.def("_repr_svg_", [](const DiagramHelper &self) -> pybind11::object { if (self.type != DiagramType::DIAGRAM_TYPE_SVG) { return pybind11::none(); } return pybind11::cast(self.content); }); c.def("_repr_pretty_", [](const DiagramHelper &self, pybind11::object p, pybind11::object cycle) -> void { pybind11::getattr(p, "text")(self.content); }); c.def("__repr__", [](const DiagramHelper &self) -> std::string { std::stringstream ss; ss << ""; return ss.str(); }); c.def("__str__", [](const DiagramHelper &self) -> pybind11::object { if (self.type == DiagramType::DIAGRAM_TYPE_SVG_HTML) { return diagram_as_html(self); } return pybind11::cast(self.content); }); } DiagramHelper stim_pybind::dem_diagram(const DetectorErrorModel &dem, std::string_view type) { if (type == "matchgraph-svg" || type == "match-graph-svg" || type == "match-graph-svg-html" || type == "matchgraph-svg-html") { std::stringstream out; dem_match_graph_to_svg_diagram_write_to(dem, out); DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; } else if (type == "matchgraph-3d" || type == "match-graph-3d") { std::stringstream out; dem_match_graph_to_basic_3d_diagram(dem).to_gltf_scene().to_json().write(out); return DiagramHelper{DiagramType::DIAGRAM_TYPE_GLTF, out.str()}; } else if (type == "matchgraph-3d-html" || type == "match-graph-3d-html") { std::stringstream out; dem_match_graph_to_basic_3d_diagram(dem).to_gltf_scene().to_json().write(out); std::stringstream out_html; write_html_viewer_for_gltf_data(out.str(), out_html); return DiagramHelper{DiagramType::DIAGRAM_TYPE_GLTF, out_html.str()}; } else { std::stringstream ss; ss << "Unrecognized diagram type: " << type; throw std::invalid_argument(ss.str()); } } CoordFilter item_to_filter_single(const pybind11::handle &obj) { if (pybind11::isinstance(obj)) { CoordFilter filter; filter.exact_target = pybind11::cast(obj).internal(); filter.use_target = true; return filter; } try { std::string_view text = pybind11::cast(obj); if (text.size() > 1 && text[0] == 'D') { CoordFilter filter; filter.exact_target = DemTarget::relative_detector_id(parse_exact_uint64_t_from_string(text.substr(1))); filter.use_target = true; return filter; } if (text.size() > 1 && text[0] == 'L') { CoordFilter filter; filter.exact_target = DemTarget::observable_id(parse_exact_uint64_t_from_string(text.substr(1))); filter.use_target = true; return filter; } } catch (const pybind11::cast_error &) { } catch (const std::invalid_argument &) { } CoordFilter filter; for (const auto &c : obj) { filter.coordinates.push_back(pybind11::cast(c)); } return filter; } std::vector item_to_filter_multi(const pybind11::object &obj) { if (obj.is_none()) { return {CoordFilter{}}; } try { return {item_to_filter_single(obj)}; } catch (const pybind11::cast_error &) { } catch (const std::invalid_argument &) { } std::vector filters; for (const auto &filter_case : obj) { filters.push_back(item_to_filter_single(filter_case)); } return filters; } DiagramHelper stim_pybind::circuit_diagram( const Circuit &circuit, std::string_view type, const pybind11::object &tick, const pybind11::object &rows, const pybind11::object &filter_coords_obj) { std::vector filter_coords; try { filter_coords = item_to_filter_multi(filter_coords_obj); } catch (const std::exception &_) { throw std::invalid_argument("filter_coords wasn't an Iterable[stim.DemTarget | Iterable[float]]."); } size_t num_rows = 0; if (!rows.is_none()) { num_rows = pybind11::cast(rows); } uint64_t tick_min; uint64_t num_ticks; if (tick.is_none()) { tick_min = 0; num_ticks = UINT64_MAX; } else if (pybind11::isinstance(tick, pybind11::module::import("builtins").attr("range"))) { tick_min = pybind11::cast(tick.attr("start")); auto tick_stop = pybind11::cast(tick.attr("stop")); auto tick_step = pybind11::cast(tick.attr("step")); if (tick_step != 1) { throw std::invalid_argument("tick.step != 1"); } if (tick_stop <= tick_min) { throw std::invalid_argument("tick.stop <= tick.start"); } num_ticks = tick_stop - tick_min; } else { tick_min = pybind11::cast(tick); num_ticks = 1; } if (type == "timeline-text") { if (!tick.is_none()) { throw std::invalid_argument("`tick` isn't used with type='timeline-text'"); } std::stringstream out; out << DiagramTimelineAsciiDrawer::make_diagram(circuit); return DiagramHelper{DiagramType::DIAGRAM_TYPE_TEXT, out.str()}; } else if (type == "timeline-svg" || type == "timeline" || type == "timeline-svg-html" || type == "timeline-html") { std::stringstream out; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, out, tick_min, num_ticks, DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE, filter_coords); DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; } else if ( type == "time-slice-svg" || type == "timeslice-svg" || type == "timeslice-html" || type == "timeslice-svg-html" || type == "time-slice-html" || type == "time-slice-svg-html" || type == "timeslice" || type == "time-slice") { std::stringstream out; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, out, tick_min, num_ticks, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_SLICE, filter_coords, num_rows); DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; } else if ( type == "detslice-svg" || type == "detslice" || type == "detslice-html" || type == "detslice-svg-html" || type == "detector-slice-svg" || type == "detector-slice") { std::stringstream out; DetectorSliceSet::from_circuit_ticks(circuit, tick_min, num_ticks, filter_coords) .write_svg_diagram_to(out, num_rows); DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; } else if ( type == "detslice-with-ops" || type == "detslice-with-ops-svg" || type == "detslice-with-ops-html" || type == "detslice-with-ops-svg-html" || type == "time+detector-slice-svg") { std::stringstream out; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, out, tick_min, num_ticks, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, filter_coords, num_rows); DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; } else if (type == "timeline-3d") { std::stringstream out; DiagramTimeline3DDrawer::circuit_to_basic_3d_diagram(circuit).to_gltf_scene().to_json().write(out); return DiagramHelper{DiagramType::DIAGRAM_TYPE_GLTF, out.str()}; } else if (type == "timeline-3d-html") { std::stringstream out; DiagramTimeline3DDrawer::circuit_to_basic_3d_diagram(circuit).to_gltf_scene().to_json().write(out); std::stringstream out_html; write_html_viewer_for_gltf_data(out.str(), out_html); return DiagramHelper{DiagramType::DIAGRAM_TYPE_GLTF, out_html.str()}; } else if (type == "detslice-text" || type == "detector-slice-text") { std::stringstream out; DetectorSliceSet::from_circuit_ticks(circuit, tick_min, num_ticks, filter_coords).write_text_diagram_to(out); return DiagramHelper{DiagramType::DIAGRAM_TYPE_TEXT, out.str()}; } else if (type == "interactive" || type == "interactive-html") { std::stringstream out; write_crumble_html_with_preloaded_circuit(circuit, out); return DiagramHelper{DiagramType::DIAGRAM_TYPE_HTML, out.str()}; } else if ( type == "match-graph-svg" || type == "matchgraph-svg" || type == "matchgraph-svg-html" || type == "matchgraph-html" || type == "match-graph-svg-html" || type == "match-graph-html" || type == "match-graph-3d" || type == "matchgraph-3d" || type == "match-graph-3d-html" || type == "matchgraph-3d-html") { DetectorErrorModel dem; try { // By default, try to decompose the errors. dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 1, false, false); } catch (const std::invalid_argument &) { // If any decomposition fails, don't decompose at all. dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, false, true, false, 1, false, false); } return dem_diagram(dem, type); } else { std::stringstream ss; ss << "Unrecognized diagram type: '"; ss << type << "'"; throw std::invalid_argument(ss.str()); } } ================================================ FILE: src/stim/cmd/command_diagram.pybind.h ================================================ // 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. #ifndef _STIM_CMD_COMMAND_DIAGRAM_PYBIND_H #define _STIM_CMD_COMMAND_DIAGRAM_PYBIND_H #include #include "stim/circuit/circuit.h" #include "stim/dem/detector_error_model.h" namespace stim_pybind { enum class DiagramType { DIAGRAM_TYPE_GLTF, DIAGRAM_TYPE_SVG, DIAGRAM_TYPE_TEXT, DIAGRAM_TYPE_HTML, DIAGRAM_TYPE_SVG_HTML, }; struct DiagramHelper { DiagramType type; std::string content; }; pybind11::class_ pybind_diagram(pybind11::module &m); void pybind_diagram_methods(pybind11::module &m, pybind11::class_ &c); DiagramHelper dem_diagram(const stim::DetectorErrorModel &dem, std::string_view type); DiagramHelper circuit_diagram( const stim::Circuit &circuit, std::string_view type, const pybind11::object &tick, const pybind11::object &rows, const pybind11::object &filter_coords_obj); } // namespace stim_pybind #endif ================================================ FILE: src/stim/cmd/command_diagram.test.cc ================================================ // 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. #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(command_diagram, run_captured_stim_main) { ASSERT_EQ( trim(run_captured_stim_main( { "diagram", "--type", "timeline-text", }, R"input( H 0 CNOT 0 1 )input")), trim(R"output( q0: -H-@- | q1: ---X- )output")); } TEST(command_diagram, run_captured_stim_main_detector_slice) { ASSERT_EQ( trim(run_captured_stim_main( {"diagram", "--type", "detector-slice-text", "--tick", "1"}, R"input( H 0 CNOT 0 1 0 2 TICK M 0 1 2 DETECTOR(4,5) rec[-1] rec[-2] DETECTOR(6) rec[-2] rec[-3] )input")), trim(R"output( q0: -------Z:D1- | q1: -Z:D0--Z:D1- | q2: -Z:D0------- )output")); ASSERT_EQ( trim(run_captured_stim_main( {"diagram", "--type", "detector-slice-text", "--tick", "1", "--filter_coords", "4"}, R"input( H 0 CNOT 0 1 0 2 TICK M 0 1 2 DETECTOR(4,5) rec[-1] rec[-2] DETECTOR(6) rec[-2] rec[-3] )input")), trim(R"output( q0: ------ q1: -Z:D0- | q2: -Z:D0- )output")); ASSERT_EQ( trim(run_captured_stim_main( {"diagram", "--type", "detector-slice-text", "--tick", "1", "--filter_coords", "5,6,7:6:7,8"}, R"input( H 0 CNOT 0 1 0 2 TICK M 0 1 2 DETECTOR(4,5) rec[-1] rec[-2] DETECTOR(6) rec[-2] rec[-3] )input")), trim(R"output( q0: -Z:D1- | q1: -Z:D1- q2: ------ )output")); } TEST(command_diagram, run_captured_stim_main_timeline_ticking) { auto circuit = R"input( R 0 1 TICK H 0 CNOT 0 1 TICK S 0 TICK H 0 M 0 1 )input"; auto result_txt = run_captured_stim_main({"diagram", "--type", "timeline-text"}, circuit); ASSERT_EQ("\n" + result_txt, R"DIAGRAM( /-\ /--------\ q0: -R-H-@-S-H-M:rec[0]- | q1: -R---X-----M:rec[1]- \-/ \--------/ )DIAGRAM"); auto result = run_captured_stim_main({"diagram", "--type", "timeline-svg"}, circuit); expect_string_is_identical_to_saved_file(result, "command_diagram_timeline.svg"); result = run_captured_stim_main({"diagram", "--type", "timeline-svg", "--tick", "0"}, circuit); expect_string_is_identical_to_saved_file(result, "command_diagram_timeline_tick0.svg"); result = run_captured_stim_main({"diagram", "--type", "timeline-svg", "--tick", "1"}, circuit); expect_string_is_identical_to_saved_file(result, "command_diagram_timeline_tick1.svg"); result = run_captured_stim_main({"diagram", "--type", "timeline-svg", "--tick", "2"}, circuit); expect_string_is_identical_to_saved_file(result, "command_diagram_timeline_tick2.svg"); result = run_captured_stim_main({"diagram", "--type", "timeline-svg", "--tick", "1:3"}, circuit); expect_string_is_identical_to_saved_file(result, "command_diagram_timeline_tick1_3.svg"); } TEST(command_diagram, run_captured_stim_main_works_various_arguments) { std::vector diagram_types{ "timeline-text", "timeline-svg", "timeline-3d", "timeline-3d-html", "match-graph-svg", "match-graph-3d", "match-graph-3d-html", "detector-slice-text", "detector-slice-svg", "time-slice-svg", "time+detector-slice-svg", }; for (const auto &type : diagram_types) { auto actual = run_captured_stim_main( { "diagram", "--type", type.c_str(), "--tick", "1:2", }, R"input( H 0 CNOT 0 1 X_ERROR(0.125) 0 TICK M 0 1 DETECTOR(1, 2, 3) rec[-1] rec[-2] )input"); if (actual.find("[stderr") != std::string::npos) { EXPECT_TRUE(false) << actual; } EXPECT_NE(actual, "") << type; } } TEST(command_diagram, warn_about_noiseless_match_graphs) { auto result = run_captured_stim_main({"diagram", "--type", "matchgraph-3d", "--remove_noise"}, "H 0"); ASSERT_NE(result.find("--remove_noise is incompatible"), std::string::npos); result = run_captured_stim_main({"diagram", "--type", "matchgraph-3d"}, "H 0"); ASSERT_NE( result.find("[stderr=Warning: the detector error model derived from the circuit had no errors"), std::string::npos); } ================================================ FILE: src/stim/cmd/command_explain_errors.cc ================================================ // 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. #include "stim/cmd/command_explain_errors.h" #include "command_help.h" #include "stim/simulators/error_matcher.h" #include "stim/util_bot/arg_parse.h" using namespace stim; int stim::command_explain_errors(int argc, const char **argv) { check_for_unknown_arguments({"--dem_filter", "--single", "--out", "--in"}, {}, "explain_errors", argc, argv); FILE *in = find_open_file_argument("--in", stdin, "rb", argc, argv); auto out_stream = find_output_stream_argument("--out", true, argc, argv); std::unique_ptr dem_filter; bool single = find_bool_argument("--single", argc, argv); bool has_filter = find_argument("--dem_filter", argc, argv) != nullptr; if (has_filter) { FILE *filter_file = find_open_file_argument("--dem_filter", stdin, "rb", argc, argv); dem_filter = std::unique_ptr(new DetectorErrorModel(DetectorErrorModel::from_file(filter_file))); fclose(filter_file); } auto circuit = Circuit::from_file(in); if (in != stdin) { fclose(in); } for (const auto &e : ErrorMatcher::explain_errors_from_circuit(circuit, dem_filter.get(), single)) { out_stream.stream() << e << "\n"; } return EXIT_SUCCESS; } SubCommandHelp stim::command_explain_errors_help() { SubCommandHelp result; result.subcommand_name = "explain_errors"; result.description = clean_doc_string(R"PARAGRAPH( Find circuit errors that produce certain detection events. Note that this command does not attempt to explain detection events by using multiple errors. This command can only tell you how to produce a set of detection events if they correspond to a specific single physical error annotated into the circuit. If you need to explain a detection event set using multiple errors, use a decoder such as pymatching to find the set of single detector error model errors that are needed and then use this command to convert those specific errors into circuit errors. )PARAGRAPH"); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> stim gen \ --code surface_code \ --task rotated_memory_z \ --distance 5 \ --rounds 10 \ --after_clifford_depolarization 0.001 \ > example.stim >>> echo "error(1) D97 D102" > example.dem >>> stim explain_errors \ --single \ --in example.stim \ --dem_filter example.dem ExplainedError { dem_error_terms: D97[coords 4,6,4] D102[coords 2,8,4] CircuitErrorLocation { flipped_pauli_product: Z36[coords 3,7] Circuit location stack trace: (after 25 TICKs) at instruction #83 (a REPEAT 9 block) in the circuit after 2 completed iterations at instruction #12 (DEPOLARIZE2) in the REPEAT block at targets #3 to #4 of the instruction resolving to DEPOLARIZE2(0.001) 46[coords 2,8] 36[coords 3,7] } } )PARAGRAPH")); result.flags.push_back( SubCommandHelpFlag{ "--dem_filter", "filepath", "01", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Specifies a detector error model to use as a filter. If `--dem_filter` isn't specified, an explanation of every single set of symptoms that can be produced by the circuit. If `--dem_filter` is specified, only explanations of the error mechanisms present in the filter will be output. This is useful when you are interested in a specific set of detection events. The filter is specified as a detector error model file. See https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--single", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Explain using a single simple error instead of all possible errors. When `--single` isn't specified, every single circuit error that produces a specific detector error model is output as a possible explanation of that error. When `--single` is specified, only the simplest circuit error is output. The "simplest" error is chosen by using heuristics such as "has fewer Pauli terms" and "happens earlier". )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in", "filepath", "{stdin}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses the stim circuit file to read the explanatory circuit from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the explanations to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is in an arbitrary semi-human-readable format. )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_explain_errors.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_EXPLAIN_ERRORS_H #define _STIM_CMD_COMMAND_EXPLAIN_ERRORS_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_explain_errors(int argc, const char **argv); SubCommandHelp command_explain_errors_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_explain_errors.test.cc ================================================ // 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. #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(command_explain_errors, explain_errors) { ASSERT_EQ(run_captured_stim_main({"explain_errors"}, ""), ""); RaiiTempNamedFile tmp("error(1) D0\n"); ASSERT_EQ( trim(run_captured_stim_main({"explain_errors", "--dem_filter", tmp.path.c_str()}, R"input( X_ERROR(0.25) 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )input")), trim(R"output( ExplainedError { dem_error_terms: D0 CircuitErrorLocation { flipped_pauli_product: X1 Circuit location stack trace: (after 0 TICKs) at instruction #1 (X_ERROR) in the circuit at target #2 of the instruction resolving to X_ERROR(0.25) 1 } } )output")); } ================================================ FILE: src/stim/cmd/command_gen.cc ================================================ #include "stim/cmd/command_gen.h" #include "command_help.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_color_code.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/util_bot/arg_parse.h" using namespace stim; int stim::command_gen(int argc, const char **argv) { std::map code_name_to_func_map{ {"color_code", &generate_color_code_circuit}, {"repetition_code", &generate_rep_code_circuit}, {"surface_code", &generate_surface_code_circuit}}; std::vector known_flags{ "--after_clifford_depolarization", "--after_reset_flip_probability", "--code", "--task", "--before_measure_flip_probability", "--before_round_data_depolarization", "--distance", "--out", "--in", "--rounds", }; std::vector known_flags_deprecated{ "--gen", }; check_for_unknown_arguments(known_flags, known_flags_deprecated, "gen", argc, argv); const char *code_flag_name = find_argument("--gen", argc, argv) ? "--gen" : "--code"; auto func = find_enum_argument(code_flag_name, nullptr, code_name_to_func_map, argc, argv); CircuitGenParameters params( (uint64_t)find_int64_argument("--rounds", -1, 1, INT64_MAX, argc, argv), (uint32_t)find_int64_argument("--distance", -1, 2, 2047, argc, argv), require_find_argument("--task", argc, argv)); params.before_round_data_depolarization = find_float_argument("--before_round_data_depolarization", 0, 0, 1, argc, argv); params.before_measure_flip_probability = find_float_argument("--before_measure_flip_probability", 0, 0, 1, argc, argv); params.after_reset_flip_probability = find_float_argument("--after_reset_flip_probability", 0, 0, 1, argc, argv); params.after_clifford_depolarization = find_float_argument("--after_clifford_depolarization", 0, 0, 1, argc, argv); auto out_stream = find_output_stream_argument("--out", true, argc, argv); std::ostream &out = out_stream.stream(); out << "# Generated " << find_argument(code_flag_name, argc, argv) << " circuit.\n"; out << "# task: " << params.task << "\n"; out << "# rounds: " << params.rounds << "\n"; out << "# distance: " << params.distance << "\n"; out << "# before_round_data_depolarization: " << params.before_round_data_depolarization << "\n"; out << "# before_measure_flip_probability: " << params.before_measure_flip_probability << "\n"; out << "# after_reset_flip_probability: " << params.after_reset_flip_probability << "\n"; out << "# after_clifford_depolarization: " << params.after_clifford_depolarization << "\n"; out << "# layout:\n"; auto generated = func(params); out << generated.layout_str(); out << generated.hint_str; out << generated.circuit; out << "\n"; return 0; } SubCommandHelp stim::command_gen_help() { SubCommandHelp result; result.subcommand_name = "gen"; result.description = clean_doc_string(R"PARAGRAPH( Generates example circuits. The generated circuits include annotations for noise, detectors, logical observables, the spatial locations of qubits, the spacetime locations of detectors, and the inexorable passage of TICKs. Note that the generated circuits are not intended to be sufficient for research. They are really just examples to make it easier to get started using Stim, so you can try things without having to first go through the entire effort of making a correctly annotated quantum error correction circuit. )PARAGRAPH"); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> stim gen \ --code repetition_code \ --task memory \ --distance 3 \ --rounds 100 \ --after_clifford_depolarization 0.001 # Generated repetition_code circuit. # task: memory # rounds: 100 # distance: 3 # before_round_data_depolarization: 0 # before_measure_flip_probability: 0 # after_reset_flip_probability: 0 # after_clifford_depolarization: 0.001 # layout: # L0 Z1 d2 Z3 d4 # Legend: # d# = data qubit # L# = data qubit with logical observable crossing # Z# = measurement qubit R 0 1 2 3 4 TICK CX 0 1 2 3 DEPOLARIZE2(0.001) 0 1 2 3 TICK CX 2 1 4 3 DEPOLARIZE2(0.001) 2 1 4 3 TICK MR 1 3 DETECTOR(1, 0) rec[-2] DETECTOR(3, 0) rec[-1] REPEAT 99 { TICK CX 0 1 2 3 DEPOLARIZE2(0.001) 0 1 2 3 TICK CX 2 1 4 3 DEPOLARIZE2(0.001) 2 1 4 3 TICK MR 1 3 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-2] rec[-4] DETECTOR(3, 0) rec[-1] rec[-3] } M 0 2 4 DETECTOR(1, 1) rec[-2] rec[-3] rec[-5] DETECTOR(3, 1) rec[-1] rec[-2] rec[-4] OBSERVABLE_INCLUDE(0) rec[-1] )PARAGRAPH")); result.flags.push_back( SubCommandHelpFlag{ "--code", "surface_code|repetition_code|color_code", "", {"surface_code|repetition_code|color_code"}, clean_doc_string(R"PARAGRAPH( The error correcting code to use. The available error correcting codes are: `surface_code` The surface code. A quantum code with a checkerboard pattern of alternating X and Z stabilizers. `repetition_code` The repetition code. The simplest classical code. `color_code` The color code. A quantum code with a hexagonal pattern of overlapping X and Z stabilizers. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--task", "name", "", {"name"}, clean_doc_string(R"PARAGRAPH( What the generated circuit should do; the experiment it should run. Different error correcting codes support different tasks. The available tasks are: `memory` (repetition_code): Initialize a logical `|0>`, preserve it against noise for the given number of rounds, then measure. `rotated_memory_x` (surface_code): Initialize a logical `|+>` in a rotated surface code, preserve it against noise for the given number of rounds, then measure in the X basis. `rotated_memory_z` (surface_code): Initialize a logical `|0>` in a rotated surface code, preserve it against noise for the given number of rounds, then measure in the X basis. `unrotated_memory_x` (surface_code): Initialize a logical `|+>` in an unrotated surface code, preserve it against noise for the given number of rounds, then measure in the Z basis. `unrotated_memory_z` (surface_code): Initialize a logical `|0>` in an unrotated surface code, preserve it against noise for the given number of rounds, then measure in the Z basis. `memory_xyz` (color_code): Initialize a logical `|0>`, preserve it against noise for the given number of rounds, then measure. Use a color code that alternates between measuring X, then Y, then Z stabilizers. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--distance", "int", "", {"int"}, clean_doc_string(R"PARAGRAPH( The minimum number of physical errors that cause a logical error. The code distance determines how spatially large the generated circuit has to be. Conventionally, the code distance specifically refers to single-qubit errors between rounds instead of circuit errors during rounds. The distance must always be a positive integer. Different codes/tasks may place additional constraints on the distance (e.g. must be larger than 2 or must be odd or etc). )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--rounds", "int", "", {"int"}, clean_doc_string(R"PARAGRAPH( The number of times the circuit's measurement qubits are measured. The number of rounds must be an integer between 1 and a quintillion (10^18). Different codes/tasks may place additional constraints on the number of rounds (e.g. enough rounds to have measured all the stabilizers at least once). )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--after_clifford_depolarization", "probability", "0", {"[none]", "probability"}, clean_doc_string(R"PARAGRAPH( Specifies a depolarizing noise level for unitary gates. Defaults to 0 when not specified. Must be a probability (a number between 0 and 1). Adds a `DEPOLARIZE1(p)` operation after every single-qubit Clifford operation, and a `DEPOLARIZE2(p)` noise operation after every two-qubit Clifford operation. When set to 0 or not set, these noise operations are not inserted at all. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--after_reset_flip_probability", "probability", "0", {"[none]", "probability"}, clean_doc_string(R"PARAGRAPH( Specifies a reset noise level. Defaults to 0 when not specified. Must be a probability (a number between 0 and 1). Adds an `X_ERROR(p)` after `R` and `RY` operations, and a `Z_ERROR(p)` after `RX` operations. When set to 0 or not set, these noise operations are not inserted at all. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--before_measure_flip_probability", "probability", "0", {"[none]", "probability"}, clean_doc_string(R"PARAGRAPH( Specifies a measurement noise level. Defaults to 0 when not specified. Must be a probability (a number between 0 and 1). Adds an `X_ERROR(p)` before `M` and `MY` operations, and a `Z_ERROR(p)` before `MX` operations. When set to 0 or not set, these noise operations are not inserted at all. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--before_round_data_depolarization", "probability", "0", {"[none]", "probability"}, clean_doc_string(R"PARAGRAPH( Specifies a quantum phenomenological noise level. Defaults to 0 when not specified. Must be a probability (a number between 0 and 1). Adds a `DEPOLARIZE1(p)` operation to each data qubit at the start of each round of stabilizer measurements. When set to 0 or not set, these noise operations are not inserted at all. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the generated circuit to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_gen.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_GEN_H #define _STIM_CMD_COMMAND_GEN_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_gen(int argc, const char **argv); SubCommandHelp command_gen_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_gen.test.cc ================================================ // 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. #include #include "gtest/gtest.h" #include "stim/gen/gen_color_code.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/main_namespaced.test.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(command_gen, no_noise_no_detections, { std::vector distances{2, 3, 4, 5, 6, 7, 15}; std::vector rounds{1, 2, 3, 4, 5, 6, 20}; std::map> funcs{ {"color", {"memory_xyz", &generate_color_code_circuit}}, {"surface", {"unrotated_memory_x", &generate_surface_code_circuit}}, {"surface", {"unrotated_memory_z", &generate_surface_code_circuit}}, {"surface", {"rotated_memory_x", &generate_surface_code_circuit}}, {"surface", {"rotated_memory_z", &generate_surface_code_circuit}}, {"rep", {"memory", &generate_rep_code_circuit}}, }; for (const auto &func : funcs) { for (auto d : distances) { for (auto r : rounds) { if (func.first == "color" && (r < 2 || d % 2 == 0 || d < 3)) { continue; } CircuitGenParameters params(r, d, func.second.first); auto circuit = func.second.second(params).circuit; auto rng = INDEPENDENT_TEST_RNG(); auto [det_samples, obs_samples] = sample_batch_detection_events(circuit, 256, rng); EXPECT_FALSE(det_samples.data.not_zero() || obs_samples.data.not_zero()) << "d=" << d << ", r=" << r << ", task=" << func.second.first << ", func=" << func.first; } } } }) TEST(command_gen, execute) { ASSERT_TRUE(matches( run_captured_stim_main({"--gen=repetition_code", "--rounds=3", "--distance=4", "--task=memory"}, ""), ".+Generated repetition_code.+")); ASSERT_TRUE(matches( run_captured_stim_main({"--gen=surface_code", "--rounds=3", "--distance=2", "--task=unrotated_memory_z"}, ""), ".+Generated surface_code.+")); ASSERT_TRUE(matches( run_captured_stim_main( {"gen", "--code=surface_code", "--rounds=3", "--distance=2", "--task=unrotated_memory_z"}, ""), ".+Generated surface_code.+")); ASSERT_TRUE(matches( run_captured_stim_main({"--gen=surface_code", "--rounds=3", "--distance=2", "--task=rotated_memory_x"}, ""), ".+Generated surface_code.+")); ASSERT_TRUE(matches( run_captured_stim_main({"--gen=color_code", "--rounds=3", "--distance=3", "--task=memory_xyz"}, ""), ".+Generated color_code.+")); } ================================================ FILE: src/stim/cmd/command_help.cc ================================================ // 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. #include "stim/cmd/command_help.h" #include #include #include #include #include #include "stim/circuit/circuit.h" #include "stim/cmd/command_analyze_errors.h" #include "stim/cmd/command_convert.h" #include "stim/cmd/command_detect.h" #include "stim/cmd/command_diagram.h" #include "stim/cmd/command_explain_errors.h" #include "stim/cmd/command_gen.h" #include "stim/cmd/command_m2d.h" #include "stim/cmd/command_repl.h" #include "stim/cmd/command_sample.h" #include "stim/cmd/command_sample_dem.h" #include "stim/gates/gates.h" #include "stim/io/stim_data_formats.h" #include "stim/stabilizers/flow.h" #include "stim/stabilizers/tableau.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_top/mbqc_decomposition.h" using namespace stim; std::string stim::clean_doc_string(const char *c, bool allow_too_long) { // Skip leading empty lines. while (*c == '\n') { c++; } // Determine indentation using first non-empty line. size_t indent = 0; while (*c == ' ') { indent++; c++; } std::string result; while (*c != '\0') { // Skip indentation. for (size_t j = 0; j < indent && *c == ' '; j++) { c++; } // Copy rest of line. size_t line_length = 0; while (*c != '\0') { result.push_back(*c); c++; if (result.back() == '\n') { break; } line_length++; } const char *start_of_line = result.c_str() + result.size() - line_length - 1; if (strstr(start_of_line, "\"\"\"") != nullptr) { std::stringstream ss; ss << "Docstring line contains \"\"\" (please use ''' instead):\n" << start_of_line << "\n"; throw std::invalid_argument(ss.str()); } if (!allow_too_long && line_length > 80) { if (memcmp(start_of_line, "@signature", strlen("@signature")) != 0 && memcmp(start_of_line, "@overload", strlen("@overload")) != 0 && strstr(start_of_line, "https://") == nullptr) { std::stringstream ss; ss << "Docstring line has length " << line_length << " > 80:\n" << start_of_line << std::string(80, '^') << "\n"; throw std::invalid_argument(ss.str()); } } } return result; } std::vector make_sub_command_help() { SubCommandHelp help_help; help_help.subcommand_name = "help"; help_help.description = "Prints helpful information about using stim."; auto result = std::vector{ command_analyze_errors_help(), command_convert_help(), command_detect_help(), command_diagram_help(), command_explain_errors_help(), command_gen_help(), command_m2d_help(), command_repl_help(), command_sample_help(), command_sample_dem_help(), help_help, }; std::sort(result.begin(), result.end(), [](const SubCommandHelp &a, const SubCommandHelp &b) { return a.subcommand_name < b.subcommand_name; }); return result; } std::string to_upper_case(std::string_view val) { std::string result; result.reserve(val.size()); for (char c : val) { result.push_back(toupper(c)); } return result; } struct Acc { std::string settled; std::stringstream working; int indent{}; void flush() { auto s = working.str(); for (char c : s) { settled.push_back(c); if (c == '\n') { for (int k = 0; k < indent; k++) { settled.push_back(' '); } } } working.str(""); } void change_indent(int t) { flush(); if (indent + t < 0) { throw std::out_of_range("negative indent"); } indent += t; working << '\n'; } template Acc &operator<<(const T &other) { working << other; return *this; } }; void print_fixed_width_float(Acc &out, float f, char u) { if (f == 0) { out << " "; } else if (fabs(f - 1) < 0.0001) { out << "+" << u; } else if (fabs(f + 1) < 0.0001) { out << "-" << u; } else { if (f > 0) { out << "+"; } out << f; } } void print_example(Acc &out, std::string_view name, const Gate &gate) { out << "\nExample:\n"; out.change_indent(+4); for (size_t k = 0; k < 3; k++) { out << name; if (gate.flags & GATE_IS_NOISY) { if (k == 2 || !(gate.flags & GATE_PRODUCES_RESULTS)) { out << "(" << 0.001 << ")"; } } if (k != 1) { out << " " << 5; if (gate.flags & GATE_TARGETS_PAIRS) { out << " " << 6; } } if (k != 0) { out << " "; if (gate.flags & GATE_PRODUCES_RESULTS) { out << "!"; } out << 42; if (gate.flags & GATE_TARGETS_PAIRS) { out << " " << 43; } } out << "\n"; } if (gate.flags & GATE_CAN_TARGET_BITS) { if (gate.name[0] == 'C' || gate.name[0] == 'Z') { out << gate.name << " rec[-1] 111\n"; } if (gate.name.back() == 'Z') { out << gate.name << " 111 rec[-1]\n"; } } out.change_indent(-4); } std::vector stim::gate_decomposition_help_targets_for_gate_type(GateType g) { if (g == GateType::MPP) { return { GateTarget::x(0), GateTarget::combiner(), GateTarget::y(1), GateTarget::combiner(), GateTarget::z(2), GateTarget::x(3), GateTarget::combiner(), GateTarget::x(4), }; } else if (g == GateType::SPP || g == GateType::SPP_DAG) { return { GateTarget::x(0), GateTarget::combiner(), GateTarget::y(1), GateTarget::combiner(), GateTarget::z(2), }; } else if (g == GateType::DETECTOR || g == GateType::OBSERVABLE_INCLUDE) { return {GateTarget::rec(-1)}; } else if (g == GateType::TICK || g == GateType::SHIFT_COORDS) { return {}; } else if (g == GateType::E || g == GateType::ELSE_CORRELATED_ERROR) { return {GateTarget::x(0)}; } else if (GATE_DATA[g].flags & GATE_TARGETS_PAIRS) { return {GateTarget::qubit(0), GateTarget::qubit(1)}; } else { return {GateTarget::qubit(0)}; } } void print_decomposition(Acc &out, const Gate &gate) { const char *decomposition = gate.h_s_cx_m_r_decomposition; if (decomposition != nullptr) { std::stringstream undecomposed; auto decomp_targets = gate_decomposition_help_targets_for_gate_type(gate.id); undecomposed << CircuitInstruction{gate.id, {}, decomp_targets, ""}; out << "Decomposition (into H, S, CX, M, R):\n"; out.change_indent(+4); out << "# The following circuit is equivalent (up to global phase) to `"; out << undecomposed.str() << "`"; out << decomposition; Circuit c(decomposition); if (c == Circuit(undecomposed.str())) { out << "\n# (The decomposition is trivial because this gate is in the target gate set.)\n"; } else if (c.operations.empty()) { out << "\n# (The decomposition is empty because this gate has no effect.)\n"; } out.change_indent(-4); } } void print_mbqc_decomposition(Acc &out, const Gate &gate) { const char *decomposition = mbqc_decomposition(gate.id); if (decomposition != nullptr) { std::stringstream undecomposed; auto decomp_targets = gate_decomposition_help_targets_for_gate_type(gate.id); undecomposed << CircuitInstruction{gate.id, {}, decomp_targets, ""}; out << "MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback):\n"; out.change_indent(+4); out << "# The following circuit performs `"; out << undecomposed.str() << "` (but affects the measurement record and an ancilla qubit)"; out << decomposition; Circuit c(decomposition); if (c == Circuit(undecomposed.str())) { out << "\n# (The decomposition is trivial because this gate is in the target gate set.)\n"; } else if (c.operations.empty()) { out << "\n# (The decomposition is empty because this gate has no effect.)\n"; } out.change_indent(-4); } } void print_stabilizer_generators(Acc &out, const Gate &gate) { auto flows = gate.flows(); if (flows.empty()) { return; } auto decomp_targets = gate_decomposition_help_targets_for_gate_type(gate.id); if (decomp_targets.size() > 2) { out << "Stabilizer Generators (for `"; out << CircuitInstruction{gate.id, {}, decomp_targets, ""}; out << "`):\n"; } else { out << "Stabilizer Generators:\n"; } out.change_indent(+4); for (const auto &flow : gate.flows()) { auto s = flow.str(); std::string no_plus; for (char c : s) { if (c != '+') { no_plus.push_back(c); } } out << no_plus << "\n"; } out.change_indent(-4); } void print_bloch_vector(Acc &out, const Gate &gate) { if (!(gate.flags & GATE_IS_UNITARY) || !(gate.flags & GATE_IS_SINGLE_QUBIT_GATE)) { return; } out << "Bloch Rotation (axis angle):\n"; out.change_indent(+4); auto axis_angle = gate.to_axis_angle(); auto rx = axis_angle[0]; auto ry = axis_angle[1]; auto rz = axis_angle[2]; auto angle = (int)round(axis_angle[3] * 180 / 3.14159265359); if (angle > 180) { angle -= 360; } out << "Axis: "; if (rx != 0) { out << "+-"[rx < 0] << 'X'; } if (ry != 0) { out << "+-"[ry < 0] << 'Y'; } if (rz != 0) { out << "+-"[rz < 0] << 'Z'; } out << "\n"; out << "Angle: " << angle << "°\n"; out.change_indent(-4); out << "Bloch Rotation (Euler angles):\n"; out.change_indent(+4); auto euler_angles = gate.to_euler_angles(); auto theta_deg = (int)round(euler_angles[0] * 180 / 3.14159265359) % 360; auto phi_deg = (int)round(euler_angles[1] * 180 / 3.14159265359) % 360; auto lambda_deg = (int)round(euler_angles[2] * 180 / 3.14159265359) % 360; out << " theta = " << theta_deg << "°\n"; out << " phi = " << phi_deg << "°\n"; out << " lambda = " << lambda_deg << "°\n"; out << "unitary = RotZ(phi) * RotY(theta) * RotZ(lambda)\n"; out << "unitary = RotZ(" << phi_deg << "°) * RotY(" << theta_deg << "°) * RotZ(" << lambda_deg << "°)\n"; out << "unitary = "; std::array y_rots{"I", "SQRT_Y", "Y", "SQRT_Y_DAG"}; std::array z_rots{"I", "S", "Z", "S_DAG"}; out << z_rots[(phi_deg / 90) & 3]; out << " * "; out << y_rots[(theta_deg / 90) & 3]; out << " * "; out << z_rots[(lambda_deg / 90) & 3]; out.change_indent(-4); out << "\n"; } void print_unitary_matrix(Acc &out, const Gate &gate) { if (!gate.has_known_unitary_matrix()) { return; } auto matrix = gate.unitary(); out << "Unitary Matrix"; if (gate.flags & GATE_TARGETS_PAIRS) { out << " (little endian)"; } out << ":\n"; out.change_indent(+4); bool all_halves = true; bool all_sqrt_halves = true; double s = sqrt(0.5); for (const auto &row : matrix) { for (const auto &cell : row) { all_halves &= cell.real() == 0.5 || cell.real() == 0 || cell.real() == -0.5; all_halves &= cell.imag() == 0.5 || cell.imag() == 0 || cell.imag() == -0.5; all_sqrt_halves &= fabs(fabs(cell.real()) - s) < 0.001 || cell.real() == 0; all_sqrt_halves &= fabs(fabs(cell.imag()) - s) < 0.001 || cell.imag() == 0; } } double factor = all_halves ? 2 : all_sqrt_halves ? 1 / s : 1; bool first_row = true; for (const auto &row : matrix) { if (first_row) { first_row = false; } else { out << "\n"; } out << "["; bool first = true; for (const auto &cell : row) { if (first) { first = false; } else { out << ", "; } print_fixed_width_float(out, cell.real() * factor, '1'); print_fixed_width_float(out, cell.imag() * factor, 'i'); } out << "]"; } if (all_halves) { out << " / 2"; } if (all_sqrt_halves) { out << " / sqrt(2)"; } out << "\n"; out.change_indent(-4); } std::string generate_per_gate_help_markdown(const Gate &alt_gate, int indent, bool anchor) { Acc out; out.indent = indent; const Gate &gate = GATE_DATA.at(alt_gate.name); if (anchor) { out << "\n"; } if (gate.flags & GATE_IS_UNITARY) { out << "### The '" << alt_gate.name << "' Gate\n"; } else { out << "### The '" << alt_gate.name << "' Instruction\n"; } for (const auto &entry : GATE_DATA.hashed_name_to_gate_type_table) { if (entry.expected_name.size() > 0 && entry.id == alt_gate.id && entry.expected_name != alt_gate.name) { out << "\nAlternate name: "; if (anchor) { out << ""; } out << "`" << entry.expected_name << "`\n"; } } out << gate.help; if (std::string(gate.help).find("xample:\n") == std::string::npos && std::string(gate.help).find("xamples:\n") == std::string::npos) { print_example(out, alt_gate.name, gate); } print_stabilizer_generators(out, gate); print_bloch_vector(out, gate); print_unitary_matrix(out, gate); print_decomposition(out, gate); print_mbqc_decomposition(out, gate); out.flush(); return out.settled; } std::string generate_subcommand_markdown(const SubCommandHelp &data, int indent, bool anchor) { Acc out; out.indent = indent; if (anchor) { out << "\n"; } out << "### stim " << data.subcommand_name << "\n\n"; out << "```\n"; out << data.str_help(); out << "```\n"; out.flush(); return out.settled; } std::string generate_per_format_markdown(const FileFormatData &format_data, int indent, bool anchor) { Acc out; out.indent = indent; if (anchor) { out << ""; } out << "The `" << format_data.name << "` Format\n"; out << format_data.help; out << "\n"; out << "*Example " << format_data.name << " parsing code (python)*:\n"; out << "```python"; out << format_data.help_python_parse; out << "```\n"; out << "*Example " << format_data.name << " saving code (python):*\n"; out << "```python"; out << format_data.help_python_save; out << "```\n"; out.flush(); return out.settled; } std::map generate_format_help_markdown() { std::map result; std::stringstream all; all << "Result data formats supported by Stim\n"; all << "\n# Index\n"; for (const auto &kv : format_name_to_enum_map()) { all << kv.first << "\n"; } result[std::string("FORMATS")] = all.str(); for (const auto &kv : format_name_to_enum_map()) { result[to_upper_case(kv.first)] = generate_per_format_markdown(kv.second, 0, false); } all.str(""); all << R"MARKDOWN(# Introduction A *result format* is a way of representing bits from shots sampled from a circuit. It is some way of converting between a list-of-list-of-bits (a list-of-shots) and a flat string of bytes or characters. Generally, the result data formats supported by Stim are extremely minimalist. They do not contain metadata about which circuit was run, how many shots were taken, how many bits are in each shot, or even self-identifying information like a header with magic bytes. They produce *raw* data. Even details about which bits are measurements, which are detection events, and which are observable frame changes must be determined from context. The major driver for having multiple formats is context-dependent preferences for binary-vs-human-readable and dense-vs-sparse. For example, '`01`' is a dense text format and '`r8`' is a sparse binary format. Sometimes you want to be able to eyeball your data, so you want a text format. Other times you want maximum efficiency, so you want a binary format. Sometimes your data is high entropy, with as many 1s as 0s, so you use a dense format. Other times the data is highly biased, with 1s being much rarer and more interesting than 0s, so you use a sparse format. # Index )MARKDOWN"; for (const auto &kv : format_name_to_enum_map()) { all << "- [The **" << kv.first << "** Format](#" << kv.first << ")\n"; } all << "\n\n"; for (const auto &kv : format_name_to_enum_map()) { all << "# " << generate_per_format_markdown(kv.second, 0, true) << "\n"; } result[std::string("FORMATS_MARKDOWN")] = all.str(); return result; } std::map generate_command_help_topics() { std::map result; auto sub_command_data = make_sub_command_help(); for (const auto &subcommand : sub_command_data) { result[to_upper_case(subcommand.subcommand_name)] = subcommand.str_help(); } { std::stringstream markdown; markdown << "# Stim command line reference\n\n"; markdown << "## Index\n\n"; for (const auto &subcommand : sub_command_data) { markdown << "- [stim " << subcommand.subcommand_name << "](#" << subcommand.subcommand_name << ")\n"; } markdown << "## Commands\n\n"; for (const auto &subcommand : sub_command_data) { markdown << generate_subcommand_markdown(subcommand, 0, true) << "\n"; } result["COMMANDS_MARKDOWN"] = markdown.str(); } { std::stringstream commands_help; commands_help << "Available stim commands:\n\n"; for (const auto &subcommand : sub_command_data) { commands_help << " stim " << subcommand.subcommand_name << std::string(20 - subcommand.subcommand_name.size(), ' '); auto summary = subcommand.description; auto n = summary.find('\n'); if (n != std::string::npos) { summary = summary.substr(0, n); } commands_help << "# " << summary << "\n"; } result["COMMANDS"] = commands_help.str(); } result[""] = result["COMMANDS"] + R"PARAGRAPH( Use `stim help [topic]` for help on specific topics. Available topics include: stim help commands # List all tasks performed by stim. stim help gates # List all circuit instructions supported by stim. stim help formats # List all result formats supported by stim. stim help [command] # Print information about a command, e.g. "sample". stim help [gate] # Print information about a gate, e.g. "CNOT". stim help [format] # Print information about a result format, e.g. "01". )PARAGRAPH"; return result; } std::map generate_gate_help_markdown() { std::map result; for (const auto &e : GATE_DATA.hashed_name_to_gate_type_table) { if (!e.expected_name.empty()) { result[std::string(e.expected_name)] = generate_per_gate_help_markdown(GATE_DATA[e.id], 0, false); } } std::map> categories; for (const auto &e : GATE_DATA.hashed_name_to_gate_type_table) { if (!e.expected_name.empty()) { const auto &rep = GATE_DATA.at(e.expected_name); categories[std::string(rep.category)].insert(std::string(e.expected_name)); } } std::stringstream all; all << "Gates supported by Stim\n"; all << "=======================\n"; for (auto &category : categories) { all << category.first.substr(2) << ":\n"; for (const auto &name : category.second) { all << " " << name << "\n"; } } result["GATES"] = all.str(); all.str(""); all << "# Gates supported by Stim\n\n"; for (auto &category : categories) { all << "- " << category.first.substr(2) << "\n"; for (const auto &name : category.second) { all << " - [" << name << "](#" << name << ")\n"; } } all << "\n"; for (auto &category : categories) { all << "## " << category.first.substr(2) << "\n\n"; for (const auto &name : category.second) { if (name == GATE_DATA.at(name).name) { all << generate_per_gate_help_markdown(GATE_DATA.at(name), 0, true) << "\n"; } } } result[std::string("GATES_MARKDOWN")] = all.str(); return result; } std::string stim::help_for(std::string help_key) { auto m1 = generate_gate_help_markdown(); auto m2 = generate_format_help_markdown(); auto m3 = generate_command_help_topics(); auto key = to_upper_case(help_key); auto p = m1.find(key); if (p == m1.end()) { p = m2.find(key); if (p == m2.end()) { p = m3.find(key); if (p == m3.end()) { return ""; } } } return p->second; } int stim::command_help(int argc, const char **argv) { const char *help = find_argument("--help", argc, argv); if (help == nullptr) { help = "\0"; } if (help[0] == '\0' && argc == 3) { help = argv[2]; // Handle usage like "stim sample --help". if (strcmp(help, "help") == 0 || strcmp(help, "--help") == 0) { help = argv[1]; } } auto msg = help_for(help); if (msg == "") { std::cerr << "Unrecognized help topic '" << help << "'.\n"; return EXIT_FAILURE; } std::cout << msg; return EXIT_SUCCESS; } ================================================ FILE: src/stim/cmd/command_help.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_HELP_H #define _STIM_CMD_COMMAND_HELP_H #include #include #include #include "stim/circuit/gate_target.h" #include "stim/gates/gates.h" namespace stim { int command_help(int argc, const char **argv); std::string help_for(std::string help_key); std::string clean_doc_string(const char *c, bool allow_too_long = false); std::vector gate_decomposition_help_targets_for_gate_type(stim::GateType g); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_m2d.cc ================================================ #include "stim/cmd/command_m2d.h" #include "command_help.h" #include "stim/io/stim_data_formats.h" #include "stim/simulators/measurements_to_detection_events.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_top/transform_without_feedback.h" using namespace stim; int stim::command_m2d(int argc, const char **argv) { check_for_unknown_arguments( { "--circuit", "--in_format", "--append_observables", "--out_format", "--out", "--in", "--skip_reference_sample", "--sweep", "--sweep_format", "--obs_out", "--obs_out_format", "--ran_without_feedback", }, { "--m2d", }, "m2d", argc, argv); const auto &in_format = find_enum_argument("--in_format", nullptr, format_name_to_enum_map(), argc, argv); const auto &out_format = find_enum_argument("--out_format", "01", format_name_to_enum_map(), argc, argv); const auto &sweep_format = find_enum_argument("--sweep_format", "01", format_name_to_enum_map(), argc, argv); const auto &obs_out_format = find_enum_argument("--obs_out_format", "01", format_name_to_enum_map(), argc, argv); bool append_observables = find_bool_argument("--append_observables", argc, argv); bool skip_reference_sample = find_bool_argument("--skip_reference_sample", argc, argv); bool ran_without_feedback = find_bool_argument("--ran_without_feedback", argc, argv); FILE *circuit_file = find_open_file_argument("--circuit", nullptr, "rb", argc, argv); auto circuit = Circuit::from_file(circuit_file); fclose(circuit_file); if (ran_without_feedback) { circuit = circuit_with_inlined_feedback(circuit); } FILE *in = find_open_file_argument("--in", stdin, "rb", argc, argv); FILE *out = find_open_file_argument("--out", stdout, "wb", argc, argv); FILE *sweep_in = find_open_file_argument("--sweep", stdin, "rb", argc, argv); FILE *obs_out = find_open_file_argument("--obs_out", stdout, "wb", argc, argv); if (sweep_in == stdin) { sweep_in = nullptr; } if (obs_out == stdout) { obs_out = nullptr; } stream_measurements_to_detection_events( in, in_format.id, sweep_in, sweep_format.id, out, out_format.id, circuit, append_observables, skip_reference_sample, obs_out, obs_out_format.id); if (in != stdin) { fclose(in); } if (sweep_in != nullptr) { fclose(sweep_in); } if (obs_out != nullptr) { fclose(obs_out); } if (out != stdout) { fclose(out); } return EXIT_SUCCESS; } SubCommandHelp stim::command_m2d_help() { SubCommandHelp result; result.subcommand_name = "m2d"; result.description = clean_doc_string(R"PARAGRAPH( Convert measurement data into detection event data. When sampling data from hardware, instead of from simulators, it's necessary to convert the measurement data into detection event data that can be fed into a decoder. This is necessary both because of complexities in the exact sets of measurements being compared by a circuit to produce detection events and also because the expected parity of a detector's measurement set can vary due to (for example) spin echo operations in the circuit. Stim performs this conversion by simulating taking a reference sample from the circuit, in order to determine the expected parity of the measurements sets defining detectors and observables, and then comparing the sampled measurement data to these expectations. )PARAGRAPH"); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example_circuit.stim X 0 M 0 1 DETECTOR rec[-2] DETECTOR rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] >>> cat example_measure_data.01 00 01 10 11 >>> stim m2d \ --append_observables \ --circuit example_circuit.stim \ --in example_measure_data.01 \ --in_format 01 \ --out_format dets shot D0 shot D0 D1 L2 shot shot D1 L2 )PARAGRAPH")); result.flags.push_back( SubCommandHelpFlag{ "--out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing output detection data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--obs_out", "filepath", "", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Specifies the file to write observable flip data to. When producing detection event data, the goal is typically to predict whether or not the logical observables were flipped by using the detection events. This argument specifies where to write that observable flip data. If this argument isn't specified, the observable flip data isn't written to a file. The output is in a format specified by `--obs_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--obs_out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing observable flip data. Irrelevant unless `--obs_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when reading measurement data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--sweep", "filepath", "", {"filepath"}, clean_doc_string(R"PARAGRAPH( Specifies a file to read sweep configuration data from. Sweep bits are used to vary whether certain Pauli gates are included in a circuit, or not, from shot to shot. For example, if a circuit contains the instruction "CX sweep[5] 0" then there is an X pauli that is included only in shots where the corresponding sweep data has the bit at index 5 set to True. If `--sweep` is not specified, all sweep bits default to OFF. If `--sweep` is specified, each shot's sweep configuratoin data is read from the specified file. The sweep data's format is specified by `--sweep_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--sweep_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when reading sweep config data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in", "filepath", "{stdin}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses the file to read measurement data from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input's format is specified by `--in_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the sampled data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output's format is specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--circuit", "filepath", "", {"filepath"}, clean_doc_string(R"PARAGRAPH( Specifies where the circuit that generated the measurements is. This argument is required, because the circuit is what specifies how to derive detection event data from measurement data. The circuit file should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--append_observables", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Appends observable flips to the end of samples as extra detectors. PREFER --obs_out OVER THIS FLAG. Mixing the observable flip data into detection event data tends to require simply separating them again immediately, creating unnecessary work. For example, when testing a decoder, you do not want to give the observable flips to the decoder because that is the information the decoder is supposed to be predicting from the detection events. This flag causes observable flip data to be appended to each sample, as if the observables were extra detectors at the end of the circuit. For example, if there are 100 detectors and 10 observables in the circuit, then the output will contain 110 detectors and the last 10 are the observables. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--ran_without_feedback", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Converts the results assuming all feedback operations were skipped. Pauli feedback operations don't need to be performed on the quantum computer. They can be performed within the classical control system. As such, it often makes sense to skip them when sampling from the circuit on hardware, and then account for this later during data analysis. Turning on this flag means that the quantum computer didn't apply the feedback operations, and it's the job of the m2d conversion to read the measurement data, rewrite it to account for feedback effects, then convert to detection events. In the python API, the same effect can be achieved by using stim.Circuit.with_inlined_feedback().compile_m2d_converter(). )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--skip_reference_sample", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Asserts the circuit can produce a noiseless sample that is just 0s. When this argument is specified, the reference sample (that the measurement data will be compared to) is generated by simply setting all measurements to 0 instead of by simulating the circuit without noise. Skipping the reference sample can significantly improve performance, because acquiring the reference sample requires using the tableau simulator. If the vacuous reference sample is actually a result that can be produced by the circuit, under noiseless execution, then specifying this flag has no observable outcome other than improving performance. CAUTION. When the all-zero sample isn't a result that can be produced by the circuit under noiseless execution, specifying this flag will cause incorrect output to be produced. )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_m2d.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_M2D_H #define _STIM_CMD_COMMAND_M2D_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_m2d(int argc, const char **argv); SubCommandHelp command_m2d_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_m2d.test.cc ================================================ // 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. #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(command_m2d, m2d) { RaiiTempNamedFile tmp(R"CIRCUIT( X 0 M 0 1 DETECTOR rec[-2] DETECTOR rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] )CIRCUIT"); ASSERT_EQ( trim(run_captured_stim_main( {"m2d", "--in_format=01", "--out_format=dets", "--circuit", tmp.path.c_str(), "--append_observables"}, "00\n01\n10\n11\n")), trim(R"output( shot D0 shot D0 D1 L2 shot shot D1 L2 )output")); ASSERT_EQ( trim(run_captured_stim_main( {"m2d", "--in_format=01", "--out_format=dets", "--circuit", tmp.path.c_str()}, "00\n01\n10\n11\n")), trim(R"output( shot D0 shot D0 D1 shot shot D1 )output")); ASSERT_EQ( trim(run_captured_stim_main( {"m2d", "--in_format=01", "--out_format=dets", "--circuit", tmp.path.c_str(), "--skip_reference_sample"}, "00\n01\n10\n11\n")), trim(R"output( shot shot D1 shot D0 shot D0 D1 )output")); } TEST(command_m2d, m2d_without_feedback) { RaiiTempNamedFile tmp(R"CIRCUIT( CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] TICK CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] rec[-2] TICK CX 0 2 1 2 M 2 CX rec[-1] 2 DETECTOR rec[-1] rec[-2] TICK M 0 1 DETECTOR rec[-1] rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); ASSERT_EQ( trim(run_captured_stim_main( {"m2d", "--in_format=01", "--append_observables", "--out_format=dets", "--circuit", tmp.path.c_str()}, "00000\n10000\n01000\n00100\n00010\n00001\n")), trim(R"output( shot shot D0 D1 shot D1 D2 shot D2 D3 shot D3 shot D3 L0 )output")); ASSERT_EQ( trim(run_captured_stim_main( {"m2d", "--in_format=01", "--append_observables", "--out_format=dets", "--circuit", tmp.path.c_str(), "--ran_without_feedback"}, "00000\n11100\n01100\n00100\n00010\n00001\n")), trim(R"output( shot shot D0 D1 shot D1 D2 shot D2 D3 shot D3 shot D3 L0 )output")); } TEST(command_m2d, m2d_obs_size_misalign_1_obs) { RaiiTempNamedFile tmp_circuit(R"CIRCUIT( M 0 REPEAT 1024 { DETECTOR rec[-1] } OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); RaiiTempNamedFile tmp_obs; ASSERT_EQ( trim(run_captured_stim_main( {"m2d", "--in_format=01", "--obs_out", tmp_obs.path.c_str(), "--circuit", tmp_circuit.path.c_str()}, "0\n")), trim(std::string(1024, '0') + "\n")); ASSERT_EQ(tmp_obs.read_contents(), "0\n"); } TEST(command_m2d, m2d_obs_size_misalign_11_obs) { RaiiTempNamedFile tmp_circuit(R"CIRCUIT( M 0 REPEAT 1024 { DETECTOR rec[-1] } OBSERVABLE_INCLUDE(10) rec[-1] )CIRCUIT"); RaiiTempNamedFile tmp_obs; ASSERT_EQ( trim(run_captured_stim_main( {"m2d", "--in_format=01", "--obs_out", tmp_obs.path.c_str(), "--circuit", tmp_circuit.path.c_str()}, "0\n")), trim(std::string(1024, '0') + "\n")); ASSERT_EQ(tmp_obs.read_contents(), "00000000000\n"); } TEST(command_m2d, unphysical_observable_annotations) { RaiiTempNamedFile tmp_circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(0, 1) 2 QUBIT_COORDS(1, 1) 3 OBSERVABLE_INCLUDE(0) X0 X1 OBSERVABLE_INCLUDE(1) Z0 Z2 MPP X0*X1*X2*X3 Z0*Z1 Z2*Z3 DEPOLARIZE1(0.001) 0 1 2 3 MPP X0*X1*X2*X3 Z0*Z1 Z2*Z3 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] rec[-6] OBSERVABLE_INCLUDE(0) X0 X1 OBSERVABLE_INCLUDE(1) Z0 Z2 )CIRCUIT"); RaiiTempNamedFile tmp_obs; ASSERT_EQ( trim(run_captured_stim_main( {"m2d", "--in_format=01", "--obs_out", tmp_obs.path.c_str(), "--circuit", tmp_circuit.path.c_str()}, "000000\n100100\n000110\n")), trim("000\n000\n011\n")); ASSERT_EQ(tmp_obs.read_contents(), "00\n00\n00\n"); } ================================================ FILE: src/stim/cmd/command_repl.cc ================================================ // 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. #include "stim/cmd/command_repl.h" #include "command_help.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_bot/probability_util.h" using namespace stim; int stim::command_repl(int argc, const char **argv) { check_for_unknown_arguments({}, {"--repl"}, "repl", argc, argv); auto rng = externally_seeded_rng(); TableauSimulator::sample_stream(stdin, stdout, SampleFormat::SAMPLE_FORMAT_01, true, rng); return EXIT_SUCCESS; } SubCommandHelp stim::command_repl_help() { SubCommandHelp result; result.subcommand_name = "repl"; result.description = clean_doc_string(R"PARAGRAPH( Runs stim in interactive read-evaluate-print (REPL) mode. Reads operations from stdin while immediately writing measurement results to stdout. )PARAGRAPH"); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> stim repl ... M 0 0 ... X 0 ... M 0 1 ... X 2 3 9 ... M 0 1 2 3 4 5 6 7 8 9 1 0 1 1 0 0 0 0 0 1 ... REPEAT 5 { ... R 0 1 ... H 0 ... CNOT 0 1 ... M 0 1 ... } 00 11 11 00 11 )PARAGRAPH")); return result; } ================================================ FILE: src/stim/cmd/command_repl.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_REPL_H #define _STIM_CMD_COMMAND_REPL_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_repl(int argc, const char **argv); SubCommandHelp command_repl_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_sample.cc ================================================ // 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. #include "stim/cmd/command_sample.h" #include "command_help.h" #include "stim/io/stim_data_formats.h" #include "stim/simulators/frame_simulator.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_bot/probability_util.h" #include "stim/util_top/reference_sample_tree.h" using namespace stim; int stim::command_sample(int argc, const char **argv) { check_for_unknown_arguments( {"--seed", "--skip_reference_sample", "--skip_loop_folding", "--out_format", "--out", "--in", "--shots"}, {"--sample", "--frame0"}, "sample", argc, argv); const auto &out_format = find_enum_argument("--out_format", "01", format_name_to_enum_map(), argc, argv); bool skip_reference_sample = find_bool_argument("--skip_reference_sample", argc, argv); bool skip_loop_folding = find_bool_argument("--skip_loop_folding", argc, argv); uint64_t num_shots = find_argument("--shots", argc, argv) ? (uint64_t)find_int64_argument("--shots", 1, 0, INT64_MAX, argc, argv) : find_argument("--sample", argc, argv) ? (uint64_t)find_int64_argument("--sample", 1, 0, INT64_MAX, argc, argv) : 1; if (num_shots == 0) { return EXIT_SUCCESS; } FILE *in = find_open_file_argument("--in", stdin, "rb", argc, argv); FILE *out = find_open_file_argument("--out", stdout, "wb", argc, argv); auto rng = optionally_seeded_rng(argc, argv); bool deprecated_frame0 = find_bool_argument("--frame0", argc, argv); if (deprecated_frame0) { std::cerr << "[DEPRECATION] Use `--skip_reference_sample` instead of `--frame0`\n"; skip_reference_sample = true; } if (num_shots == 1 && !skip_reference_sample) { TableauSimulator::sample_stream(in, out, out_format.id, false, rng); } else { assert(num_shots > 0); auto circuit = Circuit::from_file(in); simd_bits ref(0); if (!skip_reference_sample) { if (skip_loop_folding) { ref = TableauSimulator::reference_sample_circuit(circuit); } else { ReferenceSampleTree reference_sample_measurement_bits = ReferenceSampleTree::from_circuit_reference_sample(circuit.aliased_noiseless_circuit()); reference_sample_measurement_bits.decompress_into(ref); } } sample_batch_measurements_writing_results_to_disk(circuit, ref, num_shots, out, out_format.id, rng); } if (in != stdin) { fclose(in); } if (out != stdout) { fclose(out); } return EXIT_SUCCESS; } SubCommandHelp stim::command_sample_help() { SubCommandHelp result; result.subcommand_name = "sample"; result.description = "Samples measurements from a circuit."; result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example_circuit.stim H 0 CNOT 0 1 M 0 1 >>> stim sample --shots 5 < example_circuit.stim 00 11 11 00 11 )PARAGRAPH")); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example_circuit.stim X 2 3 5 M 0 1 2 3 4 5 6 7 8 9 >>> stim sample --in example_circuit.stim --out_format dets shot M2 M3 M5 )PARAGRAPH")); result.flags.push_back( SubCommandHelpFlag{ "--skip_reference_sample", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Asserts the circuit can produce a noiseless sample that is just 0s. When this argument is specified, the reference sample (that is used to convert measurement flip data from frame simulations into actual measurement data) is generated by simply setting all measurements to 0 instead of by performing a stabilizer tableau simulation of the circuit without noise. Skipping the reference sample can significantly improve performance, because acquiring the reference sample requires using the tableau simulator. If the vacuous reference sample is actually a result that can be produced by the circuit, under noiseless execution, then specifying this flag has no observable outcome other than improving performance. CAUTION. When the all-zero sample isn't a result that can be produced by the circuit under noiseless execution, specifying this flag will cause incorrect output to be produced. Specifically, the output measurement bits will be whether each measurement was *FLIPPED* instead of the actual absolute value of the measurement. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--skip_loop_folding", "bool", "false", {"[none]", "[switch]"}, clean_doc_string(R"PARAGRAPH( Skips loop folding logic on the reference sample calculation. When this argument is specified, the reference sample (that is used to convert measurement flip data from frame simulations into actual measurement data) is generated by iterating through the entire flattened circuit with no loop detection. Loop folding can enormously improve performance for circuits containing REPEAT blocks with large repeat counts, by detecting periodicity in loops and fast-forwarding across them when computing the reference sample for the circuit. However, in some cases the analysis is not able to detect the periodicity that is present. For example, this has been observed in honeycomb code circuits. When this happens, the folding-capable analysis is slower than simply analyzing the flattened circuit without any specialized loop logic. The `--skip_loop_folding` flag can be used to just analyze the flattened circuit, bypassing this slowdown for circuits such as honeycomb code circuits. By default, loop detection is enabled. Pass this flag to disable it (when appropriate by use case). )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing output data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--seed", "int", "system_entropy", {"[none]", "int"}, clean_doc_string(R"PARAGRAPH( Makes simulation results PARTIALLY deterministic. The seed integer must be a non-negative 64 bit signed integer. When `--seed` isn't specified, the random number generator is seeded using fresh entropy requested from the operating system. When `--seed #` is set, the exact same simulation results will be produced every time ASSUMING: - the exact same other flags are specified - the exact same version of Stim is being used - the exact same machine architecture is being used (for example, you're not switching from a machine that has AVX2 instructions to one that doesn't). CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary other flags and modes. For example, `--skip_reference_sample` may result in fewer calls the to the random number generator before reported sampling begins. More generally, using the same seed for `stim sample` and `stim detect` will not result in detection events corresponding to the measurement results. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--shots", "int", "1", {"[none]", "int"}, clean_doc_string(R"PARAGRAPH( Specifies the number of samples to take from the circuit. Defaults to 1. Must be an integer between 0 and a quintillion (10^18). )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in", "filepath", "{stdin}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses the stim circuit file to read the circuit to sample from. By default, the circuit is read from stdin. When `--in $FILEPATH` is specified, the circuit is instead read from the file at $FILEPATH. The input should be a stim circuit. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the sampled data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is in a format specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_sample.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_SAMPLE_H #define _STIM_CMD_COMMAND_SAMPLE_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_sample(int argc, const char **argv); SubCommandHelp command_sample_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_sample.test.cc ================================================ // 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. #include #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; std::unordered_map line_freq_with_lifetime_matching_arg(std::string_view data) { data = trim(data); std::unordered_map result{}; size_t start = 0; for (size_t k = 0; k <= data.size(); k++) { if (data[k] == '\n' || data[k] == '\0') { result[data.substr(start, k - start)]++; start = k + 1; } } return result; } std::string deviation(std::string_view sample_content, const std::unordered_map &expected) { auto actual = line_freq_with_lifetime_matching_arg(sample_content); size_t actual_total = 0; for (const auto &kv : actual) { if (expected.find(kv.first) == expected.end()) { return "Sampled " + std::string(kv.first) + " which was not expected."; } actual_total += kv.second; } if (actual_total == 0) { return "No samples."; } double expected_unity = 0; for (const auto &kv : expected) { expected_unity += kv.second; } if (fabs(expected_unity - 1) > 1e-5) { return "Expected distribution doesn't add up to 1."; } for (const auto &kv : expected) { float expected_rate = kv.second; float allowed_variation = 5 * sqrtf(expected_rate * (1 - expected_rate) / actual_total); if (expected_rate - allowed_variation < 0 || expected_rate + allowed_variation > 1) { return "Not enough samples to bound results away from extremes."; } float actual_rate = actual[kv.first] / (float)actual_total; if (fabs(expected_rate - actual_rate) > allowed_variation) { return "Actual rate " + std::to_string(actual_rate) + " of sample '" + std::string(kv.first) + "' is more than 5 standard deviations from expected rate " + std::to_string(expected_rate); } } return ""; } TEST(command_sample, sample_flag) { ASSERT_EQ( trim(run_captured_stim_main({"--sample"}, R"input( M 0 )input")), trim(R"output( 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--sample=1"}, R"input( M 0 )input")), trim(R"output( 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"sample", "--shots", "1"}, R"input( M 0 )input")), trim(R"output( 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"sample", "--shots", "2"}, R"input( M 0 )input")), trim(R"output( 0 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--sample=0"}, R"input( M 0 )input")), trim(R"output( )output")); ASSERT_EQ( trim(run_captured_stim_main({"--sample=2"}, R"input( M 0 )input")), trim(R"output( 0 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--sample"}, R"input( X 0 M 0 )input")), trim(R"output( 1 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--sample"}, R"input( M !0 )input")), trim(R"output( 1 )output")); } TEST(command_sample, intentional_failures) { ASSERT_EQ( "Sampled 10 which was not expected.", deviation( run_captured_stim_main({"--sample=1000"}, R"input( X 0 M 0 1 )input"), {{"00", 0.5}, {"11", 0.5}})); ASSERT_NE( "Sampled 10 which was not expected.", deviation( run_captured_stim_main({"--sample=1000"}, R"input( H 0 M 0 )input"), {{"0", 0.1}, {"1", 0.9}})); } TEST(command_sample, basic_distributions) { ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=10000"}, R"input( H 0 CNOT 0 1 M 0 1 )input"), {{"00", 0.5}, {"11", 0.5}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=10000"}, R"input( H 0 CNOT 0 1 SQRT_X 0 1 M 0 1 )input"), {{"10", 0.5}, {"01", 0.5}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=10000"}, R"input( H 0 CNOT 0 1 SQRT_Y 0 1 M 0 1 )input"), {{"00", 0.5}, {"11", 0.5}})); } TEST(command_sample, sample_x_error) { ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=100000"}, R"input( X_ERROR(0.1) 0 1 M 0 1 )input"), {{"00", 0.9 * 0.9}, {"01", 0.9 * 0.1}, {"10", 0.9 * 0.1}, {"11", 0.1 * 0.1}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=10"}, R"input( H 0 1 X_ERROR(0.1) 0 1 H 0 1 M 0 1 )input"), {{"00", 1}})); } TEST(command_sample, sample_z_error) { ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=100000"}, R"input( H 0 1 Z_ERROR(0.1) 0 1 H 0 1 M 0 1 )input"), {{"00", 0.9 * 0.9}, {"01", 0.9 * 0.1}, {"10", 0.9 * 0.1}, {"11", 0.1 * 0.1}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=10"}, R"input( Z_ERROR(0.1) 0 1 M 0 1 )input"), {{"00", 1}})); } TEST(command_sample, sample_y_error) { ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=100000"}, R"input( Y_ERROR(0.1) 0 1 M 0 1 )input"), {{"00", 0.9 * 0.9}, {"01", 0.9 * 0.1}, {"10", 0.9 * 0.1}, {"11", 0.1 * 0.1}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=10"}, R"input( H_YZ 0 1 Y_ERROR(0.1) 0 1 H_YZ 0 1 M 0 1 )input"), {{"00", 1}})); } TEST(command_sample, sample_depolarize1_error) { ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=100000"}, R"input( DEPOLARIZE1(0.3) 0 1 M 0 1 )input"), {{"00", 0.8 * 0.8}, {"01", 0.8 * 0.2}, {"10", 0.8 * 0.2}, {"11", 0.2 * 0.2}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=100000"}, R"input( H 0 1 DEPOLARIZE1(0.3) 0 1 H 0 1 M 0 1 )input"), {{"00", 0.8 * 0.8}, {"01", 0.8 * 0.2}, {"10", 0.8 * 0.2}, {"11", 0.2 * 0.2}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=100000"}, R"input( H_YZ 0 1 DEPOLARIZE1(0.3) 0 1 H_YZ 0 1 M 0 1 )input"), {{"00", 0.8 * 0.8}, {"01", 0.8 * 0.2}, {"10", 0.8 * 0.2}, {"11", 0.2 * 0.2}})); } TEST(command_sample, sample_depolarize2_error) { ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=100000"}, R"input( DEPOLARIZE2(0.1) 0 1 M 0 1 )input"), {{"00", 0.1 * 3 / 15 + 0.9}, {"01", 0.1 * 4 / 15}, {"10", 0.1 * 4 / 15}, {"11", 0.1 * 4 / 15}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=100000"}, R"input( H 0 H_YZ 1 DEPOLARIZE2(0.3) 0 1 H 0 H_YZ 1 M 0 1 )input"), {{"00", 0.3 * 3 / 15 + 0.7}, {"01", 0.3 * 4 / 15}, {"10", 0.3 * 4 / 15}, {"11", 0.3 * 4 / 15}})); } TEST(command_sample, sample_measure_reset) { ASSERT_EQ( trim(run_captured_stim_main({"--sample"}, R"input( X 0 R 0 M 0 )input")), trim(R"output( 0 )output")); ASSERT_EQ( trim(run_captured_stim_main({"--sample"}, R"input( X 0 MR 0 MR 0 )input")), trim(R"output( 10 )output")); } TEST(command_sample, skip_reference_sample_flag) { ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample", "--skip_reference_sample"}, R"input( H 0 S 0 S 0 H 0 M 0 )input"), {{"0", 1}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=10", "--skip_reference_sample"}, R"input( H 0 S 0 S 0 H 0 M 0 )input"), {{"0", 1}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample"}, R"input( H 0 S 0 S 0 H 0 M 0 )input"), {{"1", 1}})); ASSERT_EQ( "", deviation( run_captured_stim_main({"--sample=10"}, R"input( H 0 S 0 S 0 H 0 M 0 )input"), {{"1", 1}})); } TEST(command_sample, seeded_sampling) { ASSERT_EQ( run_captured_stim_main({"sample", "--shots=256", "--seed 5"}, R"input( H 0 M 0 )input"), run_captured_stim_main({"sample", "--shots=256", "--seed 5"}, R"input( H 0 M 0 )input")); ASSERT_NE( run_captured_stim_main({"sample", "--shots=256"}, R"input( H 0 M 0 )input"), run_captured_stim_main({"sample", "--shots=256"}, R"input( H 0 M 0 )input")); ASSERT_NE( run_captured_stim_main({"sample", "--shots=256", "--seed 5"}, R"input( H 0 M 0 )input"), run_captured_stim_main({"sample", "--shots=256", "--seed 6"}, R"input( H 0 M 0 )input")); } ================================================ FILE: src/stim/cmd/command_sample_dem.cc ================================================ // 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. #include "stim/cmd/command_sample_dem.h" #include "command_help.h" #include "stim/io/raii_file.h" #include "stim/simulators/dem_sampler.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_bot/probability_util.h" using namespace stim; int stim::command_sample_dem(int argc, const char **argv) { check_for_unknown_arguments( { "--seed", "--shots", "--out_format", "--out", "--in", "--obs_out", "--obs_out_format", "--err_out", "--err_out_format", "--replay_err_in", "--replay_err_in_format", }, {}, "sample_dem", argc, argv); const auto &out_format = find_enum_argument("--out_format", "01", format_name_to_enum_map(), argc, argv); const auto &obs_out_format = find_enum_argument("--obs_out_format", "01", format_name_to_enum_map(), argc, argv); const auto &err_out_format = find_enum_argument("--err_out_format", "01", format_name_to_enum_map(), argc, argv); const auto &err_in_format = find_enum_argument("--replay_err_in_format", "01", format_name_to_enum_map(), argc, argv); uint64_t num_shots = find_int64_argument("--shots", 1, 0, INT64_MAX, argc, argv); RaiiFile in(find_open_file_argument("--in", stdin, "rb", argc, argv)); RaiiFile out(find_open_file_argument("--out", stdout, "wb", argc, argv)); RaiiFile obs_out(find_open_file_argument("--obs_out", stdout, "wb", argc, argv)); RaiiFile err_out(find_open_file_argument("--err_out", stdout, "wb", argc, argv)); RaiiFile err_in(find_open_file_argument("--replay_err_in", stdin, "rb", argc, argv)); if (obs_out.f == stdout) { obs_out.f = nullptr; } if (err_out.f == stdout) { err_out.f = nullptr; } if (err_in.f == stdin) { err_in.f = nullptr; } if (out.f == stdout) { out.responsible_for_closing = false; } if (in.f == stdin) { out.responsible_for_closing = false; } if (num_shots == 0) { return EXIT_SUCCESS; } auto dem = DetectorErrorModel::from_file(in.f); in.done(); DemSampler sampler(std::move(dem), optionally_seeded_rng(argc, argv), 1024); sampler.sample_write( num_shots, out.f, out_format.id, obs_out.f, obs_out_format.id, err_out.f, err_out_format.id, err_in.f, err_in_format.id); return EXIT_SUCCESS; } SubCommandHelp stim::command_sample_dem_help() { SubCommandHelp result; result.subcommand_name = "sample_dem"; result.description = clean_doc_string(R"PARAGRAPH( Samples detection events from a detector error model. Supports recording and replaying the errors that occurred. )PARAGRAPH"); result.examples.push_back(clean_doc_string(R"PARAGRAPH( >>> cat example.dem error(0) D0 error(0.5) D1 L0 error(1) D2 D3 >>> stim sample_dem \ --shots 5 \ --in example.dem \ --out dets.01 \ --out_format 01 \ --obs_out obs_flips.01 \ --obs_out_format 01 >>> cat dets.01 0111 0011 0011 0111 0111 >>> cat obs_flips.01 1 0 0 1 1 )PARAGRAPH")); result.flags.push_back( SubCommandHelpFlag{ "--replay_err_in", "filepath", "", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Specifies a file to read error data to replay from. When replaying error information, errors are no longer sampled randomly but are instead driven by the file data. For example, this file data could come from a previous run that wrote error data using `--err_out`. The input is in a format specified by `--err_in_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--replay_err_in_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when reading error data to replay. Irrelevant unless `--replay_err_in` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--err_out", "filepath", "", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Specifies a file to write a record of which errors occurred. For example, the errors that occurred can be analyzed, modified, and then given to `--replay_err_in` to see the effects of changes. The output is in a format specified by `--err_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--err_out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing recorded error data. Irrelevant unless `--err_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--obs_out", "filepath", "", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Specifies the file to write observable flip data to. When sampling detection event data, the goal is typically to predict whether or not the logical observables were flipped by using the detection events. This argument specifies where to write that observable flip data. If this argument isn't specified, the observable flip data isn't written to a file. The output is in a format specified by `--obs_out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--obs_out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing observable flip data. Irrelevant unless `--obs_out` is specified. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out_format", "01|b8|r8|ptb64|hits|dets", "01", {"[none]", "format"}, clean_doc_string(R"PARAGRAPH( Specifies the data format to use when writing output data. The available formats are: 01 (default): dense human readable b8: bit packed binary r8: run length binary ptb64: partially transposed bit packed binary for SIMD hits: sparse human readable dets: sparse human readable with type hints For a detailed description of each result format, see the result format reference: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--seed", "int", "system_entropy", {"[none]", "int"}, clean_doc_string(R"PARAGRAPH( Makes simulation results PARTIALLY deterministic. The seed integer must be a non-negative 64 bit signed integer. When `--seed` isn't specified, the random number generator is seeded using fresh entropy requested from the operating system. When `--seed #` is set, the exact same simulation results will be produced every time ASSUMING: - the exact same other flags are specified - the exact same version of Stim is being used - the exact same machine architecture is being used (for example, you're not switching from a machine that has AVX2 instructions to one that doesn't). CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary other flags and modes. For example, `--skip_reference_sample` may result in fewer calls the to the random number generator before reported sampling begins. More generally, using the same seed for `stim sample` and `stim detect` will not result in detection events corresponding to the measurement results. )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--shots", "int", "1", {"[none]", "int"}, clean_doc_string(R"PARAGRAPH( Specifies the number of samples to take from the detector error model. Defaults to 1. Must be an integer between 0 and a quintillion (10^18). )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--in", "filepath", "{stdin}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses the file to read the detector error model to sample from. By default, the detector error model is read from stdin. When `--in $FILEPATH` is specified, the detector error model is instead read from the file at $FILEPATH. The input should be a stim detector error model. See: https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md )PARAGRAPH"), }); result.flags.push_back( SubCommandHelpFlag{ "--out", "filepath", "{stdout}", {"[none]", "filepath"}, clean_doc_string(R"PARAGRAPH( Chooses where to write the sampled data to. By default, the output is written to stdout. When `--out $FILEPATH` is specified, the output is instead written to the file at $FILEPATH. The output is in a format specified by `--out_format`. See: https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md )PARAGRAPH"), }); return result; } ================================================ FILE: src/stim/cmd/command_sample_dem.h ================================================ /* * 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. */ #ifndef _STIM_CMD_COMMAND_SAMPLE_DEM_H #define _STIM_CMD_COMMAND_SAMPLE_DEM_H #include "stim/util_bot/arg_parse.h" namespace stim { int command_sample_dem(int argc, const char **argv); SubCommandHelp command_sample_dem_help(); } // namespace stim #endif ================================================ FILE: src/stim/cmd/command_sample_dem.test.cc ================================================ // 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. #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(main, sample_dem) { ASSERT_EQ(run_captured_stim_main({"sample_dem"}, ""), "\n"); RaiiTempNamedFile obs_out; ASSERT_EQ( trim(run_captured_stim_main( { "sample_dem", "--obs_out", obs_out.path.c_str(), "--out_format", "01", "--obs_out_format", "01", "--shots", "5", "--seed", "0", }, R"input( error(0) D0 error(1) D1 L2 )input")), trim(R"output( 01 01 01 01 01 )output")); ASSERT_EQ(obs_out.read_contents(), "001\n001\n001\n001\n001\n"); } ================================================ FILE: src/stim/dem/dem_instruction.cc ================================================ #include "stim/dem/dem_instruction.h" #include #include "stim/dem/detector_error_model.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_bot/str_util.h" using namespace stim; constexpr uint64_t OBSERVABLE_BIT = uint64_t{1} << 63; constexpr uint64_t SEPARATOR_SYGIL = UINT64_MAX; DemTarget DemTarget::observable_id(uint64_t id) { if (id > MAX_OBS) { throw std::invalid_argument("id > 0xFFFFFFFF"); } return {OBSERVABLE_BIT | id}; } DemTarget DemTarget::relative_detector_id(uint64_t id) { if (id > MAX_DET) { throw std::invalid_argument("Relative detector id too large."); } return {id}; } bool DemTarget::is_observable_id() const { return data != SEPARATOR_SYGIL && (data & OBSERVABLE_BIT); } bool DemTarget::is_separator() const { return data == SEPARATOR_SYGIL; } bool DemTarget::is_relative_detector_id() const { return data != SEPARATOR_SYGIL && !(data & OBSERVABLE_BIT); } uint64_t DemTarget::raw_id() const { return data & ~OBSERVABLE_BIT; } uint64_t DemTarget::val() const { if (data == SEPARATOR_SYGIL) { throw std::invalid_argument("Separator doesn't have an integer value."); } return raw_id(); } bool DemTarget::operator==(const DemTarget &other) const { return data == other.data; } bool DemTarget::operator!=(const DemTarget &other) const { return !(*this == other); } bool DemTarget::operator<(const DemTarget &other) const { return data < other.data; } std::ostream &stim::operator<<(std::ostream &out, const DemTarget &v) { if (v.is_separator()) { out << "^"; return out; } else if (v.is_relative_detector_id()) { out << "D" << v.raw_id(); } else { out << "L" << v.raw_id(); } return out; } std::string DemTarget::str() const { std::stringstream s; s << *this; return s.str(); } void DemTarget::shift_if_detector_id(int64_t offset) { if (is_relative_detector_id()) { data = (uint64_t)((int64_t)data + offset); } } DemTarget DemTarget::from_text(std::string_view text) { if (text == "^") { return DemTarget::separator(); } if (!text.empty()) { bool is_det = text[0] == 'D'; bool is_obs = text[0] == 'L'; if (is_det || is_obs) { int64_t parsed = 0; if (parse_int64(text.substr(1), &parsed)) { if (parsed >= 0) { if (is_det && (uint64_t)parsed <= MAX_DET) { return DemTarget::relative_detector_id(parsed); } else if (is_obs && (uint64_t)parsed <= MAX_OBS) { return DemTarget::observable_id(parsed); } } } } } throw std::invalid_argument("Failed to parse as a stim.DemTarget: '" + std::string(text) + "'"); } bool DemInstruction::operator<(const DemInstruction &other) const { if (type != other.type) { return type < other.type; } if (target_data != other.target_data) { return target_data < other.target_data; } if (tag != other.tag) { return tag < other.tag; } return arg_data < other.arg_data; } bool DemInstruction::operator==(const DemInstruction &other) const { return approx_equals(other, 0); } bool DemInstruction::operator!=(const DemInstruction &other) const { return !(*this == other); } bool DemInstruction::approx_equals(const DemInstruction &other, double atol) const { if (target_data != other.target_data) { return false; } if (type != other.type) { return false; } if (tag != other.tag) { return false; } if (arg_data.size() != other.arg_data.size()) { return false; } for (size_t k = 0; k < arg_data.size(); k++) { if (fabs(arg_data[k] - other.arg_data[k]) > atol) { return false; } } return true; } std::string DemInstruction::str() const { std::stringstream s; s << *this; return s.str(); } std::ostream &stim::operator<<(std::ostream &out, const DemInstructionType &type) { switch (type) { case DemInstructionType::DEM_ERROR: out << "error"; break; case DemInstructionType::DEM_DETECTOR: out << "detector"; break; case DemInstructionType::DEM_LOGICAL_OBSERVABLE: out << "logical_observable"; break; case DemInstructionType::DEM_SHIFT_DETECTORS: out << "shift_detectors"; break; case DemInstructionType::DEM_REPEAT_BLOCK: out << "repeat"; break; default: out << "???unknown_instruction_type???"; break; } return out; } std::ostream &stim::operator<<(std::ostream &out, const DemInstruction &op) { out << op.type; if (!op.tag.empty()) { out << '['; write_tag_escaped_string_to(op.tag, out); out << ']'; } if (!op.arg_data.empty()) { out << "(" << comma_sep(op.arg_data) << ")"; } if (op.type == DemInstructionType::DEM_SHIFT_DETECTORS || op.type == DemInstructionType::DEM_REPEAT_BLOCK) { for (const auto &e : op.target_data) { out << " " << e.data; } } else { for (const auto &e : op.target_data) { out << " " << e; } } return out; } void DemInstruction::validate() const { switch (type) { case DemInstructionType::DEM_ERROR: if (arg_data.size() != 1) { throw std::invalid_argument( "'error' instruction takes 1 argument (a probability), but got " + std::to_string(arg_data.size()) + " arguments."); } if (arg_data[0] < 0 || arg_data[0] > 1) { throw std::invalid_argument( "'error' instruction argument must be a probability (0 to 1) but got " + std::to_string(arg_data[0])); } if (!target_data.empty()) { if (target_data.front() == DemTarget::separator() || target_data.back() == DemTarget::separator()) { throw std::invalid_argument( "First/last targets of 'error' instruction shouldn't be separators (^)."); } } for (size_t k = 1; k < target_data.size(); k++) { if (target_data[k - 1] == DemTarget::separator() && target_data[k] == DemTarget::separator()) { throw std::invalid_argument("'error' instruction has adjacent separators (^ ^)."); } } break; case DemInstructionType::DEM_SHIFT_DETECTORS: if (target_data.size() != 1) { throw std::invalid_argument( "'shift_detectors' instruction takes 1 target, but got " + std::to_string(target_data.size()) + " targets."); } break; case DemInstructionType::DEM_DETECTOR: if (target_data.size() != 1) { throw std::invalid_argument( "'detector' instruction takes 1 target but got " + std::to_string(target_data.size()) + " arguments."); } if (!target_data[0].is_relative_detector_id()) { throw std::invalid_argument( "'detector' instruction takes a relative detector target (D#) but got " + target_data[0].str() + " arguments."); } break; case DemInstructionType::DEM_LOGICAL_OBSERVABLE: if (arg_data.size() != 0) { throw std::invalid_argument( "'logical_observable' instruction takes 0 arguments but got " + std::to_string(arg_data.size()) + " arguments."); } if (target_data.size() != 1) { throw std::invalid_argument( "'logical_observable' instruction takes 1 target but got " + std::to_string(target_data.size()) + " arguments."); } if (!target_data[0].is_observable_id()) { throw std::invalid_argument( "'logical_observable' instruction takes a logical observable target (L#) but got " + target_data[0].str() + " arguments."); } break; case DemInstructionType::DEM_REPEAT_BLOCK: // Handled elsewhere. break; default: throw std::invalid_argument("Unknown instruction type."); } } uint64_t DemInstruction::repeat_block_rep_count() const { assert(target_data.size() > 0); return target_data[0].data; } const DetectorErrorModel &DemInstruction::repeat_block_body(const DetectorErrorModel &host) const { assert(target_data.size() == 2); auto b = target_data[1].data; assert(b < host.blocks.size()); return host.blocks[b]; } DetectorErrorModel &DemInstruction::repeat_block_body(DetectorErrorModel &host) const { assert(target_data.size() == 2); auto b = target_data[1].data; assert(b < host.blocks.size()); return host.blocks[b]; } ================================================ FILE: src/stim/dem/dem_instruction.h ================================================ #ifndef _STIM_DEM_DEM_INSTRUCTION_H #define _STIM_DEM_DEM_INSTRUCTION_H #include #include #include #include #include "stim/mem/span_ref.h" namespace stim { constexpr uint64_t MAX_OBS = 0xFFFFFFFF; constexpr uint64_t MAX_DET = (uint64_t{1} << 62) - 1; enum class DemInstructionType : uint8_t { DEM_ERROR, DEM_SHIFT_DETECTORS, DEM_DETECTOR, DEM_LOGICAL_OBSERVABLE, DEM_REPEAT_BLOCK, }; struct DemTarget { uint64_t data; static DemTarget observable_id(uint64_t id); static DemTarget relative_detector_id(uint64_t id); static constexpr DemTarget separator() { return {UINT64_MAX}; } uint64_t raw_id() const; uint64_t val() const; bool is_observable_id() const; bool is_separator() const; bool is_relative_detector_id() const; void shift_if_detector_id(int64_t offset); bool operator==(const DemTarget &other) const; bool operator!=(const DemTarget &other) const; bool operator<(const DemTarget &other) const; std::string str() const; static DemTarget from_text(std::string_view text); }; struct DetectorErrorModel; struct DemInstruction { SpanRef arg_data; SpanRef target_data; std::string_view tag; DemInstructionType type; bool operator<(const DemInstruction &other) const; bool operator==(const DemInstruction &other) const; bool operator!=(const DemInstruction &other) const; bool approx_equals(const DemInstruction &other, double atol) const; std::string str() const; void validate() const; uint64_t repeat_block_rep_count() const; const DetectorErrorModel &repeat_block_body(const DetectorErrorModel &host) const; DetectorErrorModel &repeat_block_body(DetectorErrorModel &host) const; template inline void for_separated_targets(CALLBACK callback) const { size_t start = 0; do { size_t end = start + 1; while (end < target_data.size() && !target_data[end].is_separator()) { end++; } std::span group = target_data.sub(start, std::min(end, target_data.size())); callback(group); start = end + 1; } while (start < target_data.size()); } }; std::ostream &operator<<(std::ostream &out, const DemInstructionType &type); std::ostream &operator<<(std::ostream &out, const DemTarget &v); std::ostream &operator<<(std::ostream &out, const DemInstruction &v); } // namespace stim #endif ================================================ FILE: src/stim/dem/dem_instruction.pybind.cc ================================================ #include "stim/dem/dem_instruction.pybind.h" #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/py/base.pybind.h" #include "stim/util_bot/str_util.h" using namespace stim; using namespace stim_pybind; std::vector> ExposedDemInstruction::target_groups() const { std::vector> result; as_dem_instruction().for_separated_targets([&](std::span group) { std::vector copy; for (auto e : group) { copy.push_back(e); } result.push_back(copy); }); return result; } DemInstruction ExposedDemInstruction::as_dem_instruction() const { return DemInstruction{arguments, targets, tag, type}; } ExposedDemInstruction ExposedDemInstruction::from_dem_instruction(stim::DemInstruction instruction) { std::vector arguments; std::vector targets; arguments.insert(arguments.begin(), instruction.arg_data.begin(), instruction.arg_data.end()); targets.insert(targets.begin(), instruction.target_data.begin(), instruction.target_data.end()); return ExposedDemInstruction{arguments, targets, std::string(instruction.tag), instruction.type}; } ExposedDemInstruction ExposedDemInstruction::from_str(std::string_view text) { DetectorErrorModel host; host.append_from_text(text); if (host.instructions.size() != 1 || host.instructions[0].type == DemInstructionType::DEM_REPEAT_BLOCK) { throw std::invalid_argument("Given text didn't parse to a single DemInstruction."); } return from_dem_instruction(host.instructions[0]); } std::string ExposedDemInstruction::type_name() const { std::stringstream out; out << type; return out.str(); } std::string ExposedDemInstruction::str() const { return as_dem_instruction().str(); } std::string ExposedDemInstruction::repr() const { std::stringstream out; out << "stim.DemInstruction('" << type << "', ["; out << comma_sep(arguments); out << "], ["; bool first = true; for (const auto &e : targets) { if (first) { first = false; } else { out << ", "; } if (type == DemInstructionType::DEM_SHIFT_DETECTORS) { out << e.data; } else if (e.is_relative_detector_id()) { out << "stim.target_relative_detector_id(" << e.raw_id() << ")"; } else if (e.is_separator()) { out << "stim.target_separator()"; } else { out << "stim.target_logical_observable_id(" << e.raw_id() << ")"; } } out << "]"; if (!tag.empty()) { out << ", tag=" << pybind11::cast(pybind11::repr(pybind11::cast(tag))); } out << ")"; return out.str(); } bool ExposedDemInstruction::operator==(const ExposedDemInstruction &other) const { return type == other.type && arguments == other.arguments && targets == other.targets && tag == other.tag; } bool ExposedDemInstruction::operator!=(const ExposedDemInstruction &other) const { return !(*this == other); } std::vector ExposedDemInstruction::targets_copy() const { std::vector result; if (type == DemInstructionType::DEM_SHIFT_DETECTORS) { for (const auto &e : targets) { result.push_back(pybind11::cast(e.data)); } } else { for (const auto &e : targets) { result.push_back(pybind11::cast(ExposedDemTarget{e})); } } return result; } std::vector ExposedDemInstruction::args_copy() const { return arguments; } pybind11::class_ stim_pybind::pybind_detector_error_model_instruction(pybind11::module &m) { return pybind11::class_( m, "DemInstruction", clean_doc_string(R"DOC( An instruction from a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 L0 ... error(0.125) D1 D2 ... error(0.125) D2 D3 ... error(0.125) D3 ... ''') >>> instruction = model[0] >>> instruction stim.DemInstruction('error', [0.125], [stim.target_relative_detector_id(0)]) )DOC") .data()); } void stim_pybind::pybind_detector_error_model_instruction_methods( pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init( [](std::string_view type, pybind11::object &arguments, pybind11::object &targets, std::string_view tag) -> ExposedDemInstruction { if (arguments.is_none() && targets.is_none()) { return ExposedDemInstruction::from_str(type); } std::string lower; for (char c : type) { lower.push_back(tolower(c)); } DemInstructionType conv_type; std::vector conv_targets; if (lower == "error") { conv_type = DemInstructionType::DEM_ERROR; } else if (lower == "shift_detectors") { conv_type = DemInstructionType::DEM_SHIFT_DETECTORS; } else if (lower == "detector") { conv_type = DemInstructionType::DEM_DETECTOR; } else if (lower == "logical_observable") { conv_type = DemInstructionType::DEM_LOGICAL_OBSERVABLE; } else { throw std::invalid_argument("Unrecognized instruction name '" + lower + "'."); } if (!targets.is_none()) { if (conv_type == DemInstructionType::DEM_SHIFT_DETECTORS) { for (const auto &e : targets) { try { conv_targets.push_back(DemTarget{pybind11::cast(e)}); } catch (pybind11::cast_error &) { throw std::invalid_argument( "Instruction '" + lower + "' only takes unsigned integer targets."); } } } else { for (const auto &e : targets) { try { conv_targets.push_back(pybind11::cast(e).internal()); } catch (pybind11::cast_error &) { throw std::invalid_argument( "Instruction '" + lower + "' only takes stim.target_relative_detector_id(k), " "stim.target_logical_observable_id(k), " "stim.target_separator() targets."); } } } } std::vector conv_args; if (!arguments.is_none()) { conv_args = pybind11::cast>(arguments); } ExposedDemInstruction result{ std::move(conv_args), std::move(conv_targets), std::string(tag), conv_type}; result.as_dem_instruction().validate(); return result; }), pybind11::arg("type"), pybind11::arg("args") = pybind11::none(), pybind11::arg("targets") = pybind11::none(), pybind11::kw_only(), pybind11::arg("tag") = "", clean_doc_string(R"DOC( @signature def __init__(self, type: str, args: Optional[Iterable[float]] = None, targets: Optional[Iterable[stim.DemTarget]] = None, *, tag: str = "") -> None: Creates or parses a stim.DemInstruction. Args: type: The name of the instruction type (e.g. "error" or "shift_detectors"). If `args` and `targets` aren't specified, this can also be set to a full line of text from a dem file, like "error(0.25) D0". args: Numeric values parameterizing the instruction (e.g. the 0.1 in "error(0.1)"). targets: The objects the instruction involves (e.g. the "D0" and "L1" in "error(0.1) D0 L1"). tag: An arbitrary piece of text attached to the instruction. Examples: >>> import stim >>> instruction = stim.DemInstruction( ... 'error', ... [0.125], ... [stim.target_relative_detector_id(5)], ... tag='test-tag', ... ) >>> print(instruction) error[test-tag](0.125) D5 >>> print(stim.DemInstruction('error(0.125) D5 L6 ^ D4 # comment')) error(0.125) D5 L6 ^ D4 )DOC") .data()); c.def( "args_copy", &ExposedDemInstruction::args_copy, clean_doc_string(R"DOC( @signature def args_copy(self) -> List[float]: Returns a copy of the list of numbers parameterizing the instruction. For example, this would be coordinates of a detector instruction or the probability of an error instruction. The result is a copy, meaning that editing it won't change the instruction's targets or future copies. Examples: >>> import stim >>> instruction = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''')[0] >>> instruction.args_copy() [0.125] >>> instruction.args_copy() == instruction.args_copy() True >>> instruction.args_copy() is instruction.args_copy() False )DOC") .data()); c.def_readonly( "tag", &ExposedDemInstruction::tag, clean_doc_string(R"DOC( Returns the arbitrary text tag attached to the instruction. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error[test-tag](0.125) D0 ... error(0.125) D0 ... ''') >>> dem[0].tag 'test-tag' >>> dem[1].tag '' )DOC") .data()); c.def( "target_groups", &ExposedDemInstruction::target_groups, clean_doc_string(R"DOC( @signature def target_groups(self) -> List[List[stim.DemTarget]]: Returns a copy of the instruction's targets, split by target separators. When a detector error model instruction contains a suggested decomposition, its targets contain separators (`stim.DemTarget("^")`). This method splits the targets into groups based the separators, similar to how `str.split` works. Returns: A list of groups of targets. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.01) D0 D1 ^ D2 ... error(0.01) D0 L0 ... error(0.01) ... ''') >>> dem[0].target_groups() [[stim.DemTarget('D0'), stim.DemTarget('D1')], [stim.DemTarget('D2')]] >>> dem[1].target_groups() [[stim.DemTarget('D0'), stim.DemTarget('L0')]] >>> dem[2].target_groups() [[]] )DOC") .data()); c.def( "targets_copy", &ExposedDemInstruction::targets_copy, clean_doc_string(R"DOC( @signature def targets_copy(self) -> List[Union[int, stim.DemTarget]]: Returns a copy of the instruction's targets. The result is a copy, meaning that editing it won't change the instruction's targets or future copies. Examples: >>> import stim >>> instruction = stim.DetectorErrorModel(''' ... error(0.125) D0 L2 ... ''')[0] >>> instruction.targets_copy() [stim.DemTarget('D0'), stim.DemTarget('L2')] >>> instruction.targets_copy() == instruction.targets_copy() True >>> instruction.targets_copy() is instruction.targets_copy() False )DOC") .data()); c.def_property_readonly( "type", &ExposedDemInstruction::type_name, clean_doc_string(R"DOC( The name of the instruction type (e.g. "error" or "shift_detectors"). )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two instructions have identical contents."); c.def(pybind11::self != pybind11::self, "Determines if two instructions have non-identical contents."); c.def( "__str__", &ExposedDemInstruction::str, "Returns detector error model (.dem) instructions (that can be parsed by stim) for the model."); c.def( "__repr__", &ExposedDemInstruction::repr, "Returns text that is a valid python expression evaluating to an equivalent `stim.DetectorErrorModel`."); c.def("__hash__", [](const ExposedDemInstruction &self) { return pybind11::hash(pybind11::str(self.str())); }); } ================================================ FILE: src/stim/dem/dem_instruction.pybind.h ================================================ #ifndef _STIM_DEM_DETECTOR_ERROR_MODEL_INSTRUCTION_PYBIND_H #define _STIM_DEM_DETECTOR_ERROR_MODEL_INSTRUCTION_PYBIND_H #include #include "stim/dem/detector_error_model_target.pybind.h" namespace stim_pybind { struct ExposedDemInstruction { std::vector arguments; std::vector targets; std::string tag; stim::DemInstructionType type; static ExposedDemInstruction from_str(std::string_view text); static ExposedDemInstruction from_dem_instruction(stim::DemInstruction instruction); std::vector> target_groups() const; std::vector args_copy() const; std::vector targets_copy() const; stim::DemInstruction as_dem_instruction() const; std::string type_name() const; std::string str() const; std::string repr() const; bool operator==(const ExposedDemInstruction &other) const; bool operator!=(const ExposedDemInstruction &other) const; }; pybind11::class_ pybind_detector_error_model_instruction(pybind11::module &m); void pybind_detector_error_model_instruction_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/dem/dem_instruction.test.cc ================================================ #include "stim/dem/dem_instruction.h" #include "gtest/gtest.h" #include "stim/dem/detector_error_model.h" using namespace stim; TEST(dem_instruction, from_str) { ASSERT_EQ(DemTarget::from_text("D5"), DemTarget::relative_detector_id(5)); ASSERT_EQ(DemTarget::from_text("D0"), DemTarget::relative_detector_id(0)); ASSERT_EQ(DemTarget::from_text("D4611686018427387903"), DemTarget::relative_detector_id(4611686018427387903)); ASSERT_EQ(DemTarget::from_text("L5"), DemTarget::observable_id(5)); ASSERT_EQ(DemTarget::from_text("L0"), DemTarget::observable_id(0)); ASSERT_EQ(DemTarget::from_text("L4294967295"), DemTarget::observable_id(4294967295)); ASSERT_THROW({ DemTarget::from_text("D4611686018427387904"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("L4294967296"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("L-1"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("L-1"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("D-1"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("Da"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("Da "); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text(" Da"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("X"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text(""); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("1"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("-1"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("0"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text("'"); }, std::invalid_argument); ASSERT_THROW({ DemTarget::from_text(" "); }, std::invalid_argument); } TEST(dem_instruction, for_separated_targets) { DetectorErrorModel dem("error(0.1) D0 ^ D2 L0 ^ D1 D2 D3"); std::vector> results; dem.instructions[0].for_separated_targets([&](std::span group) { std::vector items; for (auto g : group) { items.push_back(g); } results.push_back(items); }); ASSERT_EQ( results, (std::vector>{ {DemTarget::relative_detector_id(0)}, {DemTarget::relative_detector_id(2), DemTarget::observable_id(0)}, {DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), DemTarget::relative_detector_id(3)}, })); dem = DetectorErrorModel("error(0.1) D0"); results.clear(); dem.instructions[0].for_separated_targets([&](std::span group) { std::vector items; for (auto g : group) { items.push_back(g); } results.push_back(items); }); ASSERT_EQ( results, (std::vector>{ {DemTarget::relative_detector_id(0)}, })); dem = DetectorErrorModel("error(0.1)"); results.clear(); dem.instructions[0].for_separated_targets([&](std::span group) { std::vector items; for (auto g : group) { items.push_back(g); } results.push_back(items); }); ASSERT_EQ( results, (std::vector>{ {}, })); } ================================================ FILE: src/stim/dem/dem_instruction_pybind_test.py ================================================ import stim import pytest def test_args_copy(): assert stim.DemInstruction("error", [0.25], [stim.target_relative_detector_id(3)]).args_copy() == [0.25] assert stim.DemInstruction("error", [0.125], [stim.target_relative_detector_id(3)]).args_copy() == [0.125] assert stim.DemInstruction("shift_detectors", [], [1]).args_copy() == [] assert stim.DemInstruction("shift_detectors", [0.125, 0.25], [1]).args_copy() == [0.125, 0.25] def test_targets_copy(): t1 = [stim.target_relative_detector_id(3), stim.target_separator(), stim.target_logical_observable_id(2)] assert stim.DemInstruction("error", [0.25], t1).targets_copy() == t1 assert stim.DemInstruction("shift_detectors", [], [1]).targets_copy() == [1] t2 = [stim.target_logical_observable_id(3)] assert stim.DemInstruction("logical_observable", [], t2).targets_copy() == t2 def test_type(): assert stim.DemInstruction("error", [0.25], [stim.target_relative_detector_id(3)]).type == "error" assert stim.DemInstruction("ERROR", [0.25], [stim.target_relative_detector_id(3)]).type == "error" assert stim.DemInstruction("shift_detectors", [], [1]).type == "shift_detectors" assert stim.DemInstruction("detector", [], [stim.target_relative_detector_id(3)]).type == "detector" assert stim.DemInstruction("logical_observable", [], [stim.target_logical_observable_id(3)]).type == "logical_observable" def test_equality(): e1 = stim.DemInstruction("error", [0.25], [stim.target_relative_detector_id(3)]) assert e1 == stim.DemInstruction("error", [0.25], [stim.target_relative_detector_id(3)]) assert not (e1 != stim.DemInstruction("error", [0.25], [stim.target_relative_detector_id(3)])) assert e1 != stim.DemInstruction("error", [0.35], [stim.target_relative_detector_id(3)]) assert not (e1 == stim.DemInstruction("error", [0.35], [stim.target_relative_detector_id(3)])) assert e1 != stim.DemInstruction("error", [0.35], [stim.target_relative_detector_id(4)]) assert e1 != stim.DemInstruction("shift_detectors", [0.35], [3]) def test_validation(): with pytest.raises(ValueError, match="takes 1 arg"): stim.DemInstruction("error", [], [stim.target_relative_detector_id(3)]) with pytest.raises(ValueError, match="takes 1 arg"): stim.DemInstruction("error", [0.5, 0.5], [stim.target_relative_detector_id(3)]) with pytest.raises(ValueError, match="last target.+separator"): stim.DemInstruction("error", [0.25], [stim.target_separator()]) with pytest.raises(ValueError, match="0 to 1"): stim.DemInstruction("error", [-0.1], [stim.target_relative_detector_id(3)]) with pytest.raises(ValueError, match="0 to 1"): stim.DemInstruction("error", [1.1], [stim.target_relative_detector_id(3)]) with pytest.raises(ValueError, match="detector.+targets"): stim.DemInstruction("error", [0.25], [3]) with pytest.raises(ValueError, match="integer targets"): stim.DemInstruction("shift_detectors", [1.1], [stim.target_relative_detector_id(3)]) def test_str(): v = stim.DemInstruction("ERROR", [0.125], [stim.target_relative_detector_id(3), stim.target_logical_observable_id(6)]) assert str(v) == "error(0.125) D3 L6" v = stim.DemInstruction("Shift_detectors", [1.5, 2.5, 5.5], [6]) assert str(v) == "shift_detectors(1.5, 2.5, 5.5) 6" def test_repr(): v = stim.DemInstruction("error", [0.25], [stim.target_relative_detector_id(3), stim.target_logical_observable_id(6)]) assert eval(repr(v), {"stim": stim}) == v v = stim.DemInstruction("shift_detectors", [1.5, 2.5, 5.5], [6]) assert eval(repr(v), {"stim": stim}) == v def test_hashable(): a = stim.DemInstruction("error", [0.25], [stim.target_relative_detector_id(3)]) b = stim.DemInstruction("error", [0.125], [stim.target_relative_detector_id(3)]) c = stim.DemInstruction("error", [0.25], [stim.target_relative_detector_id(3)]) assert hash(a) == hash(c) assert len({a, b, c}) == 2 def test_target_groups(): dem = stim.DetectorErrorModel("detector D0") assert dem[0].target_groups() == [[stim.DemTarget("D0")]] def test_init_from_str(): assert stim.DemInstruction("detector D0") == stim.DemInstruction("detector", [], [stim.target_relative_detector_id(0)]) with pytest.raises(ValueError, match="single DemInstruction"): stim.DemInstruction("") with pytest.raises(ValueError, match="single DemInstruction"): stim.DemInstruction(""" repeat 5 { error(0.25) D0 shift_detectors 1 } """) with pytest.raises(ValueError, match="single DemInstruction"): stim.DemInstruction(""" detector D0 detector D1 """) def test_tag(): assert stim.DemInstruction("error[test](0.25) D1").tag == 'test' assert stim.DemInstruction("error", [0.25], [stim.DemTarget("D1")], tag="test").tag == 'test' dem = stim.DetectorErrorModel(''' error[test-tag](0.125) D0 error(0.125) D0 ''') assert dem[0].tag == 'test-tag' assert dem[1].tag == '' ================================================ FILE: src/stim/dem/detector_error_model.cc ================================================ /* * 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. */ #include "stim/dem/detector_error_model.h" #include #include #include #include "stim/util_bot/str_util.h" using namespace stim; void DetectorErrorModel::append_error_instruction( double probability, SpanRef targets, std::string_view tag) { append_dem_instruction(DemInstruction{&probability, targets, tag, DemInstructionType::DEM_ERROR}); } void DetectorErrorModel::append_shift_detectors_instruction( SpanRef coord_shift, uint64_t detector_shift, std::string_view tag) { DemTarget shift{detector_shift}; append_dem_instruction(DemInstruction{coord_shift, &shift, tag, DemInstructionType::DEM_SHIFT_DETECTORS}); } void DetectorErrorModel::append_detector_instruction( SpanRef coords, DemTarget target, std::string_view tag) { append_dem_instruction(DemInstruction{coords, &target, tag, DemInstructionType::DEM_DETECTOR}); } void DetectorErrorModel::append_logical_observable_instruction(DemTarget target, std::string_view tag) { append_dem_instruction(DemInstruction{{}, &target, tag, DemInstructionType::DEM_LOGICAL_OBSERVABLE}); } void DetectorErrorModel::append_dem_instruction(const DemInstruction &instruction) { assert(instruction.type != DemInstructionType::DEM_REPEAT_BLOCK); instruction.validate(); auto stored_targets = target_buf.take_copy(instruction.target_data); auto stored_args = arg_buf.take_copy(instruction.arg_data); auto tag = tag_buf.take_copy(instruction.tag); instructions.push_back(DemInstruction{stored_args, stored_targets, tag, instruction.type}); } void DetectorErrorModel::append_repeat_block(uint64_t repeat_count, DetectorErrorModel &&body, std::string_view tag) { std::array data; data[0].data = repeat_count; data[1].data = blocks.size(); auto stored_targets = target_buf.take_copy(data); blocks.push_back(std::move(body)); tag = tag_buf.take_copy(tag); instructions.push_back({{}, stored_targets, tag, DemInstructionType::DEM_REPEAT_BLOCK}); } void DetectorErrorModel::append_repeat_block( uint64_t repeat_count, const DetectorErrorModel &body, std::string_view tag) { DemTarget data[2]; data[0].data = repeat_count; data[1].data = blocks.size(); auto stored_targets = target_buf.take_copy({&data[0], &data[2]}); blocks.push_back(body); tag = tag_buf.take_copy(tag); instructions.push_back({{}, stored_targets, tag, DemInstructionType::DEM_REPEAT_BLOCK}); } bool DetectorErrorModel::operator==(const DetectorErrorModel &other) const { return instructions == other.instructions && blocks == other.blocks; } bool DetectorErrorModel::operator!=(const DetectorErrorModel &other) const { return !(*this == other); } bool DetectorErrorModel::approx_equals(const DetectorErrorModel &other, double atol) const { if (instructions.size() != other.instructions.size() || blocks.size() != other.blocks.size()) { return false; } for (size_t k = 0; k < instructions.size(); k++) { if (!instructions[k].approx_equals(other.instructions[k], atol)) { return false; } } for (size_t k = 0; k < blocks.size(); k++) { if (!blocks[k].approx_equals(other.blocks[k], atol)) { return false; } } return true; } std::string DetectorErrorModel::str() const { std::stringstream s; s << *this; return s.str(); } void stim::print_detector_error_model(std::ostream &out, const DetectorErrorModel &v, size_t indent) { bool first = true; for (const auto &e : v.instructions) { if (first) { first = false; } else { out << "\n"; } for (size_t k = 0; k < indent; k++) { out << " "; } if (e.type == DemInstructionType::DEM_REPEAT_BLOCK) { out << "repeat"; if (!e.tag.empty()) { out << '['; write_tag_escaped_string_to(e.tag, out); out << ']'; } out << " " << e.repeat_block_rep_count() << " {\n"; print_detector_error_model(out, e.repeat_block_body(v), indent + 4); out << "\n"; for (size_t k = 0; k < indent; k++) { out << " "; } out << "}"; } else { out << e; } } } std::ostream &stim::operator<<(std::ostream &out, const DetectorErrorModel &v) { out << std::setprecision(std::numeric_limits::digits10 + 1); print_detector_error_model(out, v, 0); return out; } DetectorErrorModel::DetectorErrorModel() { } DetectorErrorModel::DetectorErrorModel(const DetectorErrorModel &other) : arg_buf(other.arg_buf.total_allocated()), target_buf(other.target_buf.total_allocated()), tag_buf(other.tag_buf.total_allocated()), instructions(other.instructions), blocks(other.blocks) { // Keep local copy of buffer data. for (auto &e : instructions) { e.arg_data = arg_buf.take_copy(e.arg_data); e.target_data = target_buf.take_copy(e.target_data); e.tag = tag_buf.take_copy(e.tag); } } DetectorErrorModel::DetectorErrorModel(DetectorErrorModel &&other) noexcept : arg_buf(std::move(other.arg_buf)), target_buf(std::move(other.target_buf)), tag_buf(std::move(other.tag_buf)), instructions(std::move(other.instructions)), blocks(std::move(other.blocks)) { } DetectorErrorModel &DetectorErrorModel::operator=(const DetectorErrorModel &other) { if (&other != this) { instructions = other.instructions; blocks = other.blocks; // Keep local copy of operation data. arg_buf = MonotonicBuffer(other.arg_buf.total_allocated()); target_buf = MonotonicBuffer(other.target_buf.total_allocated()); tag_buf = MonotonicBuffer(other.tag_buf.total_allocated()); for (auto &e : instructions) { e.arg_data = arg_buf.take_copy(e.arg_data); e.target_data = target_buf.take_copy(e.target_data); e.tag = tag_buf.take_copy(e.tag); } } return *this; } DetectorErrorModel &DetectorErrorModel::operator=(DetectorErrorModel &&other) noexcept { if (&other != this) { instructions = std::move(other.instructions); blocks = std::move(other.blocks); arg_buf = std::move(other.arg_buf); target_buf = std::move(other.target_buf); tag_buf = std::move(other.tag_buf); } return *this; } enum class DEM_READ_CONDITION { DEM_READ_AS_LITTLE_AS_POSSIBLE, DEM_READ_UNTIL_END_OF_BLOCK, DEM_READ_UNTIL_END_OF_FILE, }; inline bool is_name_char(int c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; } template inline DemInstructionType read_instruction_name(int &c, SOURCE read_char) { char name_buf[32]; size_t n = 0; while (is_name_char(c) && n < sizeof(name_buf) - 1) { name_buf[n] = tolower((char)c); c = read_char(); n++; } name_buf[n] = 0; if (!strcmp(name_buf, "error")) { return DemInstructionType::DEM_ERROR; } if (!strcmp(name_buf, "shift_detectors")) { return DemInstructionType::DEM_SHIFT_DETECTORS; } if (!strcmp(name_buf, "detector")) { return DemInstructionType::DEM_DETECTOR; } if (!strcmp(name_buf, "logical_observable")) { return DemInstructionType::DEM_LOGICAL_OBSERVABLE; } if (!strcmp(name_buf, "repeat")) { return DemInstructionType::DEM_REPEAT_BLOCK; } throw std::out_of_range("Unrecognized instruction name: " + std::string(name_buf)); } template uint64_t read_uint60_t(int &c, SOURCE read_char) { if (!(c >= '0' && c <= '9')) { throw std::invalid_argument("Expected a digit but got '" + std::string(1, c) + "'"); } uint64_t result = 0; do { result *= 10; result += c - '0'; if (result >= uint64_t{1} << 60) { throw std::out_of_range("Number too large."); } c = read_char(); } while (c >= '0' && c <= '9'); return result; } template inline void read_arbitrary_dem_targets_into(int &c, SOURCE read_char, DetectorErrorModel &model) { while (read_until_next_line_arg(c, read_char)) { switch (c) { case 'd': case 'D': c = read_char(); model.target_buf.append_tail(DemTarget::relative_detector_id(read_uint60_t(c, read_char))); break; case 'l': case 'L': c = read_char(); model.target_buf.append_tail(DemTarget::observable_id(read_uint60_t(c, read_char))); break; case '^': c = read_char(); model.target_buf.append_tail(DemTarget::separator()); break; default: throw std::invalid_argument("Unrecognized target prefix '" + std::string(1, c) + "'."); } } } template void dem_read_instruction(DetectorErrorModel &model, char lead_char, SOURCE read_char) { int c = lead_char; DemInstructionType type = read_instruction_name(c, read_char); std::string_view tail_tag; try { read_tag(c, "", read_char, model.tag_buf); if (!model.tag_buf.tail.empty()) { tail_tag = std::string_view(model.tag_buf.tail.ptr_start, model.tag_buf.tail.size()); } if (type == DemInstructionType::DEM_REPEAT_BLOCK) { if (!read_until_next_line_arg(c, read_char)) { throw std::invalid_argument("Missing repeat count of repeat block."); } model.target_buf.append_tail(DemTarget{read_uint60_t(c, read_char)}); if (read_until_next_line_arg(c, read_char)) { throw std::invalid_argument("Too many numeric values given to repeat block."); } if (c != '{') { throw std::invalid_argument("Missing '{' at start of repeat block."); } } else { read_parens_arguments(c, "detector error model instruction", read_char, model.arg_buf); if (type == DemInstructionType::DEM_SHIFT_DETECTORS) { if (read_until_next_line_arg(c, read_char)) { model.target_buf.append_tail(DemTarget{read_uint60_t(c, read_char)}); } } read_arbitrary_dem_targets_into(c, read_char, model); if (c == '{') { throw std::invalid_argument("Unexpected '{'."); } DemInstruction{model.arg_buf.tail, model.target_buf.tail, tail_tag, type}.validate(); } } catch (const std::invalid_argument &) { model.tag_buf.discard_tail(); model.target_buf.discard_tail(); model.arg_buf.discard_tail(); throw; } model.tag_buf.commit_tail(); model.instructions.push_back( DemInstruction{ .arg_data = model.arg_buf.commit_tail(), .target_data = model.target_buf.commit_tail(), .tag = tail_tag, .type = type, }); } template void model_read_operations(DetectorErrorModel &model, SOURCE read_char, DEM_READ_CONDITION read_condition) { auto &ops = model.instructions; do { int c = read_char(); read_past_dead_space_between_commands(c, read_char); if (c == EOF) { if (read_condition == DEM_READ_CONDITION::DEM_READ_UNTIL_END_OF_BLOCK) { throw std::out_of_range("Unterminated block. Got a '{' without an eventual '}'."); } return; } if (c == '}') { if (read_condition != DEM_READ_CONDITION::DEM_READ_UNTIL_END_OF_BLOCK) { throw std::out_of_range("Uninitiated block. Got a '}' without a '{'."); } return; } dem_read_instruction(model, c, read_char); if (ops.back().type == DemInstructionType::DEM_REPEAT_BLOCK) { // Temporarily remove instruction until block is parsed. auto repeat_count = ops.back().repeat_block_rep_count(); auto tag = ops.back().tag; ops.pop_back(); // Recursively read the block contents. DetectorErrorModel block; model_read_operations(block, read_char, DEM_READ_CONDITION::DEM_READ_UNTIL_END_OF_BLOCK); // Restore repeat block instruction, including block reference. model.append_repeat_block(repeat_count, std::move(block), tag); } } while (read_condition != DEM_READ_CONDITION::DEM_READ_AS_LITTLE_AS_POSSIBLE); } void DetectorErrorModel::append_from_file(FILE *file, bool stop_asap) { model_read_operations( *this, [&]() { return getc(file); }, stop_asap ? DEM_READ_CONDITION::DEM_READ_AS_LITTLE_AS_POSSIBLE : DEM_READ_CONDITION::DEM_READ_UNTIL_END_OF_FILE); } void DetectorErrorModel::append_from_text(std::string_view text) { size_t k = 0; model_read_operations( *this, [&]() { return k < text.size() ? text[k++] : EOF; }, DEM_READ_CONDITION::DEM_READ_UNTIL_END_OF_FILE); } DetectorErrorModel DetectorErrorModel::from_file(FILE *file) { DetectorErrorModel result; result.append_from_file(file, false); return result; } DetectorErrorModel::DetectorErrorModel(std::string_view text) { append_from_text(text); } void DetectorErrorModel::clear() { target_buf.clear(); arg_buf.clear(); instructions.clear(); blocks.clear(); } DetectorErrorModel DetectorErrorModel::rounded(uint8_t digits) const { double scale = 1; for (size_t k = 0; k < digits; k++) { scale *= 10; } DetectorErrorModel result; for (const auto &e : instructions) { if (e.type == DemInstructionType::DEM_REPEAT_BLOCK) { auto reps = e.repeat_block_rep_count(); auto &block = e.repeat_block_body(*this); result.append_repeat_block(reps, block.rounded(digits), e.tag); } else if (e.type == DemInstructionType::DEM_ERROR) { std::vector rounded_args; for (auto a : e.arg_data) { rounded_args.push_back(round(a * scale) / scale); } result.append_dem_instruction({rounded_args, e.target_data, e.tag, DemInstructionType::DEM_ERROR}); } else { result.append_dem_instruction(e); } } return result; } uint64_t DetectorErrorModel::total_detector_shift() const { uint64_t result = 0; for (const auto &e : instructions) { if (e.type == DemInstructionType::DEM_SHIFT_DETECTORS) { result += e.target_data[0].data; } else if (e.type == DemInstructionType::DEM_REPEAT_BLOCK) { result += e.repeat_block_rep_count() * e.repeat_block_body(*this).total_detector_shift(); } } return result; } void flattened_helper( const DetectorErrorModel &body, std::vector &cur_coordinate_shift, uint64_t &cur_detector_shift, DetectorErrorModel &out) { for (const auto &op : body.instructions) { if (op.type == DemInstructionType::DEM_SHIFT_DETECTORS) { while (cur_coordinate_shift.size() < op.arg_data.size()) { cur_coordinate_shift.push_back(0); } for (size_t k = 0; k < op.arg_data.size(); k++) { cur_coordinate_shift[k] += op.arg_data[k]; } if (!op.target_data.empty()) { cur_detector_shift += op.target_data[0].data; } } else if (op.type == DemInstructionType::DEM_REPEAT_BLOCK) { const auto &loop_body = op.repeat_block_body(body); auto reps = op.repeat_block_rep_count(); for (uint64_t k = 0; k < reps; k++) { flattened_helper(loop_body, cur_coordinate_shift, cur_detector_shift, out); } } else if (op.type == DemInstructionType::DEM_LOGICAL_OBSERVABLE) { out.append_dem_instruction( DemInstruction{{}, op.target_data, op.tag, DemInstructionType::DEM_LOGICAL_OBSERVABLE}); } else if (op.type == DemInstructionType::DEM_DETECTOR) { while (cur_coordinate_shift.size() < op.arg_data.size()) { cur_coordinate_shift.push_back(0); } std::vector shifted_coords; for (size_t k = 0; k < op.arg_data.size(); k++) { shifted_coords.push_back(op.arg_data[k] + cur_coordinate_shift[k]); } std::vector shifted_detectors; for (DemTarget t : op.target_data) { t.shift_if_detector_id(cur_detector_shift); shifted_detectors.push_back(t); } out.append_dem_instruction( DemInstruction{shifted_coords, shifted_detectors, op.tag, DemInstructionType::DEM_DETECTOR}); } else if (op.type == DemInstructionType::DEM_ERROR) { std::vector shifted_detectors; for (DemTarget t : op.target_data) { t.shift_if_detector_id(cur_detector_shift); shifted_detectors.push_back(t); } out.append_dem_instruction( DemInstruction{op.arg_data, shifted_detectors, op.tag, DemInstructionType::DEM_ERROR}); } else { throw std::invalid_argument("Unrecognized instruction type: " + op.str()); } } } DetectorErrorModel DetectorErrorModel::flattened() const { DetectorErrorModel result; std::vector shift; uint64_t det_shift = 0; flattened_helper(*this, shift, det_shift, result); return result; } uint64_t DetectorErrorModel::count_detectors() const { uint64_t offset = 1; uint64_t max_num = 0; for (const auto &e : instructions) { switch (e.type) { case DemInstructionType::DEM_LOGICAL_OBSERVABLE: break; case DemInstructionType::DEM_SHIFT_DETECTORS: offset += e.target_data[0].data; break; case DemInstructionType::DEM_REPEAT_BLOCK: { auto &block = e.repeat_block_body(*this); auto n = block.count_detectors(); auto reps = e.repeat_block_rep_count(); auto block_shift = block.total_detector_shift(); // Note: quadratic overhead in nesting level. offset += block_shift * reps; if (reps > 0 && n > 0) { max_num = std::max(max_num, offset + n - 1 - block_shift); } } break; case DemInstructionType::DEM_DETECTOR: case DemInstructionType::DEM_ERROR: for (const auto &t : e.target_data) { if (t.is_relative_detector_id()) { max_num = std::max(max_num, offset + t.raw_id()); } } break; default: throw std::invalid_argument("Instruction type not implemented in count_detectors: " + e.str()); } } return max_num; } uint64_t DetectorErrorModel::count_errors() const { uint64_t total = 0; for (const auto &e : instructions) { switch (e.type) { case DemInstructionType::DEM_LOGICAL_OBSERVABLE: break; case DemInstructionType::DEM_SHIFT_DETECTORS: break; case DemInstructionType::DEM_REPEAT_BLOCK: { auto &block = e.repeat_block_body(*this); auto n = block.count_errors(); auto reps = e.repeat_block_rep_count(); total += n * reps; break; } case DemInstructionType::DEM_DETECTOR: break; case DemInstructionType::DEM_ERROR: total++; break; default: throw std::invalid_argument("Instruction type not implemented in count_errors: " + e.str()); } } return total; } uint64_t DetectorErrorModel::count_observables() const { uint64_t max_num = 0; for (const auto &e : instructions) { switch (e.type) { case DemInstructionType::DEM_SHIFT_DETECTORS: case DemInstructionType::DEM_DETECTOR: break; case DemInstructionType::DEM_REPEAT_BLOCK: { auto &block = e.repeat_block_body(*this); max_num = std::max(max_num, block.count_observables()); } break; case DemInstructionType::DEM_LOGICAL_OBSERVABLE: case DemInstructionType::DEM_ERROR: for (const auto &t : e.target_data) { if (t.is_observable_id()) { max_num = std::max(max_num, t.raw_id() + 1); } } break; default: throw std::invalid_argument("Instruction type not implemented in count_observables: " + e.str()); } } return max_num; } DetectorErrorModel DetectorErrorModel::py_get_slice(int64_t start, int64_t step, int64_t slice_length) const { assert(slice_length >= 0); assert(slice_length == 0 || start >= 0); DetectorErrorModel result; for (size_t k = 0; k < (size_t)slice_length; k++) { const auto &op = instructions[start + step * k]; if (op.type == DemInstructionType::DEM_REPEAT_BLOCK) { result.append_repeat_block(op.repeat_block_rep_count(), op.repeat_block_body(*this), op.tag); } else { auto args = result.arg_buf.take_copy(op.arg_data); auto targets = result.target_buf.take_copy(op.target_data); result.instructions.push_back(DemInstruction{args, targets, op.tag, op.type}); } } return result; } DetectorErrorModel DetectorErrorModel::operator*(size_t repetitions) const { DetectorErrorModel copy = *this; copy *= repetitions; return copy; } DetectorErrorModel &DetectorErrorModel::operator*=(size_t repetitions) { if (repetitions == 0) { clear(); } if (repetitions <= 1) { return *this; } DetectorErrorModel other = std::move(*this); clear(); append_repeat_block(repetitions, std::move(other), ""); return *this; } DetectorErrorModel DetectorErrorModel::operator+(const DetectorErrorModel &other) const { DetectorErrorModel result = *this; result += other; return result; } DetectorErrorModel &DetectorErrorModel::operator+=(const DetectorErrorModel &other) { if (&other == this) { instructions.insert(instructions.end(), instructions.begin(), instructions.end()); return *this; } for (auto &e : other.instructions) { if (e.type == DemInstructionType::DEM_REPEAT_BLOCK) { uint64_t repeat_count = e.repeat_block_rep_count(); const DetectorErrorModel &block = e.repeat_block_body(other); append_repeat_block(repeat_count, block, e.tag); } else { append_dem_instruction(e); } } return *this; } std::pair> DetectorErrorModel::final_detector_and_coord_shift() const { uint64_t detector_offset = 0; std::vector coord_shift; for (const auto &op : instructions) { if (op.type == DemInstructionType::DEM_SHIFT_DETECTORS) { vec_pad_add_mul(coord_shift, op.arg_data); detector_offset += op.target_data[0].data; } else if (op.type == DemInstructionType::DEM_REPEAT_BLOCK) { const auto &block = op.repeat_block_body(*this); uint64_t reps = op.repeat_block_rep_count(); auto rec = block.final_detector_and_coord_shift(); vec_pad_add_mul(coord_shift, rec.second, reps); detector_offset += reps * rec.first; } } return {detector_offset, coord_shift}; } bool get_detector_coordinates_helper( const DetectorErrorModel &dem, const std::set &included_detector_indices, std::set::const_iterator &iter_desired_detector_index, std::vector &coord_shift, uint64_t &detector_offset, std::map> &out, bool top) { if (iter_desired_detector_index == included_detector_indices.end()) { return true; } // Fills in data for a detector that was found while iterating. // Returns true if all data has been filled in. auto fill_in_data = [&](uint64_t fill_index, SpanRef fill_data) { if (!included_detector_indices.contains(fill_index)) { // Not interested in the index for this data. return false; } if (out.contains(fill_index)) { // Already have this data. Detector may have been declared twice? return false; } // Write data to result dictionary. std::vector det_coords; det_coords.reserve(fill_data.size()); for (size_t k = 0; k < fill_data.size(); k++) { det_coords.push_back(fill_data[k]); if (k < coord_shift.size()) { det_coords[k] += coord_shift[k]; } } out[fill_index] = std::move(det_coords); // Advance the iterator past values that have been written in. // If the end has been reached, we're done. while (out.contains(*iter_desired_detector_index)) { ++iter_desired_detector_index; if (iter_desired_detector_index == included_detector_indices.end()) { return true; } } return false; }; for (const auto &op : dem.instructions) { if (op.type == DemInstructionType::DEM_SHIFT_DETECTORS) { vec_pad_add_mul(coord_shift, op.arg_data); detector_offset += op.target_data[0].data; while (*iter_desired_detector_index < detector_offset) { // Shifting past an index proves that it will never be given data. // So set the coordinate data to the empty list. if (fill_in_data(*iter_desired_detector_index, {})) { return true; } } } else if (op.type == DemInstructionType::DEM_DETECTOR) { for (const auto &t : op.target_data) { if (fill_in_data(t.data + detector_offset, op.arg_data)) { return true; } } } else if (op.type == DemInstructionType::DEM_REPEAT_BLOCK) { const auto &block = op.repeat_block_body(dem); uint64_t reps = op.repeat_block_rep_count(); // TODO: Finish in time proportional to len(instructions) + len(desired) instead of len(execution). for (uint64_t k = 0; k < reps; k++) { if (get_detector_coordinates_helper( block, included_detector_indices, iter_desired_detector_index, coord_shift, detector_offset, out, false)) { return true; } } } } // If we've reached the end of the detector error model, then all remaining // values should be given empty data. if (top && out.size() < included_detector_indices.size()) { uint64_t n = dem.count_detectors(); while (*iter_desired_detector_index < n) { if (fill_in_data(*iter_desired_detector_index, {})) { return true; } } } return false; } std::map> DetectorErrorModel::get_detector_coordinates( const std::set &included_detector_indices) const { std::map> out; uint64_t detector_offset = 0; std::set::const_iterator iter = included_detector_indices.begin(); std::vector coord_shift; get_detector_coordinates_helper(*this, included_detector_indices, iter, coord_shift, detector_offset, out, true); if (iter != included_detector_indices.end()) { std::stringstream msg; msg << "Detector index " << *iter << " is too big. The detector error model has "; msg << count_detectors() << " detectors)"; throw std::invalid_argument(msg.str()); } return out; } DetectorErrorModel DetectorErrorModel::without_tags() const { DetectorErrorModel result; for (DemInstruction inst : instructions) { if (inst.type == DemInstructionType::DEM_REPEAT_BLOCK) { result.append_repeat_block(inst.repeat_block_rep_count(), inst.repeat_block_body(*this).without_tags(), ""); } else { inst.tag = ""; result.append_dem_instruction(inst); } } return result; } ================================================ FILE: src/stim/dem/detector_error_model.h ================================================ #ifndef _STIM_DEM_DETECTOR_ERROR_MODEL_H #define _STIM_DEM_DETECTOR_ERROR_MODEL_H #include #include #include #include #include "stim/circuit/circuit.h" #include "stim/dem/dem_instruction.h" #include "stim/mem/monotonic_buffer.h" namespace stim { struct DetectorErrorModel { MonotonicBuffer arg_buf; MonotonicBuffer target_buf; MonotonicBuffer tag_buf; std::vector instructions; std::vector blocks; /// Constructs an empty detector error model. DetectorErrorModel(); /// Parses a detector error model from the given text. explicit DetectorErrorModel(std::string_view text); /// Copy constructor. DetectorErrorModel(const DetectorErrorModel &other); /// Move constructor. DetectorErrorModel(DetectorErrorModel &&other) noexcept; /// Copy assignment. DetectorErrorModel &operator=(const DetectorErrorModel &other); /// Move assignment. DetectorErrorModel &operator=(DetectorErrorModel &&other) noexcept; DetectorErrorModel &operator*=(size_t repetitions); DetectorErrorModel operator*(size_t repetitions) const; DetectorErrorModel operator+(const DetectorErrorModel &other) const; DetectorErrorModel &operator+=(const DetectorErrorModel &other); void append_dem_instruction(const DemInstruction &instruction); void append_error_instruction(double probability, SpanRef targets, std::string_view tag); void append_shift_detectors_instruction( SpanRef coord_shift, uint64_t detector_shift, std::string_view tag); void append_detector_instruction(SpanRef coords, DemTarget target, std::string_view tag); void append_logical_observable_instruction(DemTarget target, std::string_view tag); void append_repeat_block(uint64_t repeat_count, DetectorErrorModel &&body, std::string_view tag); void append_repeat_block(uint64_t repeat_count, const DetectorErrorModel &body, std::string_view tag); /// Grows the detector error model using operations from a string. void append_from_text(std::string_view text); /// Grows the detector error model using operations from a file. /// /// Args: /// file: The opened file to read from. /// stop_asap: When set to true, the reading process stops after the next instruction or block is read. This is /// potentially useful for interactive/streaming usage, where errors are being processed on the fly. void append_from_file(FILE *file, bool stop_asap = false); /// Parses a detector error model from a file. static DetectorErrorModel from_file(FILE *file); bool operator==(const DetectorErrorModel &other) const; bool operator!=(const DetectorErrorModel &other) const; bool approx_equals(const DetectorErrorModel &other, double atol) const; std::string str() const; DetectorErrorModel without_tags() const; uint64_t total_detector_shift() const; uint64_t count_detectors() const; uint64_t count_observables() const; uint64_t count_errors() const; std::pair> final_detector_and_coord_shift() const; std::map> get_detector_coordinates( const std::set &included_detector_indices) const; void clear(); private: template void iter_flatten_error_instructions_helper(const CALLBACK &callback, uint64_t &detector_shift) const { std::vector translate_buf; for (const auto &op : instructions) { switch (op.type) { case DemInstructionType::DEM_ERROR: translate_buf.clear(); translate_buf.insert(translate_buf.end(), op.target_data.begin(), op.target_data.end()); for (auto &t : translate_buf) { t.shift_if_detector_id((int64_t)detector_shift); } callback(DemInstruction{op.arg_data, translate_buf, op.tag, op.type}); break; case DemInstructionType::DEM_REPEAT_BLOCK: { const auto &block = op.repeat_block_body(*this); auto reps = op.repeat_block_rep_count(); for (uint64_t k = 0; k < reps; k++) { block.iter_flatten_error_instructions_helper(callback, detector_shift); } break; } case DemInstructionType::DEM_SHIFT_DETECTORS: detector_shift += op.target_data[0].data; break; case DemInstructionType::DEM_DETECTOR: case DemInstructionType::DEM_LOGICAL_OBSERVABLE: break; default: throw std::invalid_argument("Unrecognized DEM instruction type: " + op.str()); } } } public: /// Iterates through the error model, invoking the given callback on each found error mechanism. /// /// Automatically flattens `repeat` blocks into repeated instructions. /// Automatically folds `shift_detectors` instructions into adjusted indices of later error instructions. /// /// Args: /// callback: A function that takes a DemInstruction and returns no value. This function will be invoked once /// for each error instruction. The DemInstruction's arg_data will contain a single value (the error's /// probability) and the absolute targets of the error. template void iter_flatten_error_instructions(const CALLBACK &callback) const { uint64_t offset = 0; iter_flatten_error_instructions_helper(callback, offset); } /// Gets a python-style slice of the error model's instructions. DetectorErrorModel py_get_slice(int64_t start, int64_t step, int64_t slice_length) const; /// Rounds error probabilities to a given number of digits. DetectorErrorModel rounded(uint8_t digits) const; /// Returns an equivalent detector error model with no repeat blocks or detector_shift instructions. DetectorErrorModel flattened() const; }; void print_detector_error_model(std::ostream &out, const DetectorErrorModel &v, size_t indent); std::ostream &operator<<(std::ostream &out, const DemInstructionType &type); std::ostream &operator<<(std::ostream &out, const DetectorErrorModel &v); std::ostream &operator<<(std::ostream &out, const DemTarget &v); std::ostream &operator<<(std::ostream &out, const DemInstruction &v); } // namespace stim #endif ================================================ FILE: src/stim/dem/detector_error_model.pybind.cc ================================================ // 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. #include "stim/dem/detector_error_model.pybind.h" #include #include "stim/circuit/circuit.pybind.h" #include "stim/cmd/command_diagram.pybind.h" #include "stim/dem/dem_instruction.pybind.h" #include "stim/dem/detector_error_model_repeat_block.pybind.h" #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/io/raii_file.h" #include "stim/py/base.pybind.h" #include "stim/search/search.h" #include "stim/simulators/dem_sampler.h" using namespace stim; std::string stim_pybind::detector_error_model_repr(const DetectorErrorModel &self) { if (self.instructions.empty()) { return "stim.DetectorErrorModel()"; } std::stringstream ss; ss << "stim.DetectorErrorModel('''\n"; print_detector_error_model(ss, self, 4); ss << "\n''')"; return ss.str(); } std::vector python_arg_to_instruction_arguments(const pybind11::object &arg) { if (arg.is_none()) { return {}; } try { return {pybind11::cast(arg)}; } catch (const pybind11::cast_error &) { } try { return pybind11::cast>(arg); } catch (const pybind11::cast_error &) { } throw std::invalid_argument("parens_arguments must be None, a double, or a list of doubles."); } static DemInstructionType non_block_instruction_name_to_enum(std::string_view name) { std::string low; for (char c : name) { low.push_back(tolower(c)); } if (low == "error") { return DemInstructionType::DEM_ERROR; } else if (low == "shift_detectors") { return DemInstructionType::DEM_SHIFT_DETECTORS; } else if (low == "detector") { return DemInstructionType::DEM_DETECTOR; } else if (low == "logical_observable") { return DemInstructionType::DEM_LOGICAL_OBSERVABLE; } throw std::invalid_argument("Not a non-block detector error model instruction name: " + std::string(name)); } pybind11::class_ stim_pybind::pybind_detector_error_model(pybind11::module &m) { auto c = pybind11::class_( m, "DetectorErrorModel", clean_doc_string(R"DOC( An error model built out of independent error mechanics. This class is one of the most important classes in Stim, because it is the mechanism used to explain circuits to decoders. A typical workflow would look something like: 1. Create a quantum error correction circuit annotated with detectors and observables. 2. Fail at configuring your favorite decoder using the circuit, because it's a pain to convert circuit error mechanisms into a format understood by the decoder. 2a. Call circuit.detector_error_model(), with decompose_errors=True if working with a matching-based code. This converts the circuit errors into a straightforward list of independent "with probability p these detectors and observables get flipped" terms. 3. Write tedious but straightforward glue code to create whatever graph-like object the decoder needs from the detector error model. 3a. Actually, ideally, someone has already done that for you. For example, pymatching can take detector error models directly and sinter knows how to explain a detector error model to fusion_blossom. 4. Get samples using circuit.compile_detector_sampler(), feed them to the decoder, and compare its observable flip predictions to the actual flips recorded in the samples. 4a. Actually, sinter will basically handle steps 2 through 4 for you. So you should probably have just generated your circuits, called `sinter collect` on them, then `sinter plot` on the results. 5. Write the paper. Error mechanisms are described in terms of the visible detection events and the hidden observable frame changes that they causes. Error mechanisms can also suggest decompositions of their effects into components, which can be helpful for decoders that want to work with a simpler decomposed error model instead of the full error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 L0 ... error(0.125) D1 D2 ... error(0.125) D2 D3 ... error(0.125) D3 ... ''') >>> len(model) 5 >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model() stim.DetectorErrorModel(''' error(0.125) D0 error(0.375) D0 D1 error(0.25) D1 ''') )DOC") .data()); return c; } void stim_pybind::pybind_detector_error_model_methods( pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init([](const char *detector_error_model_text) { DetectorErrorModel self; self.append_from_text(detector_error_model_text); return self; }), pybind11::arg("detector_error_model_text") = "", clean_doc_string(R"DOC( Creates a stim.DetectorErrorModel. Args: detector_error_model_text: Defaults to empty. Describes instructions to append into the circuit in the detector error model (.dem) format. Examples: >>> import stim >>> empty = stim.DetectorErrorModel() >>> not_empty = stim.DetectorErrorModel(''' ... error(0.125) D0 L0 ... ''') )DOC") .data()); c.def_property_readonly( "num_detectors", &DetectorErrorModel::count_detectors, clean_doc_string(R"DOC( Counts the number of detectors (e.g. `D2`) in the error model. Detector indices are assumed to be contiguous from 0 up to whatever the maximum detector id is. If the largest detector's absolute id is n-1, then the number of detectors is n. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... X_ERROR(0.25) 1 ... CORRELATED_ERROR(0.375) X0 X1 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''').detector_error_model().num_detectors 2 >>> stim.DetectorErrorModel(''' ... error(0.1) D0 D199 ... ''').num_detectors 200 >>> stim.DetectorErrorModel(''' ... shift_detectors 1000 ... error(0.1) D0 D199 ... ''').num_detectors 1200 )DOC") .data()); c.def_property_readonly( "num_errors", &DetectorErrorModel::count_errors, clean_doc_string(R"DOC( Counts the number of errors (e.g. `error(0.1) D0`) in the error model. Error instructions inside repeat blocks count once per repetition. Redundant errors with the same targets count as separate errors. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... repeat 100 { ... repeat 5 { ... error(0.25) D1 ... } ... } ... ''').num_errors 501 )DOC") .data()); c.def_property_readonly( "num_observables", &DetectorErrorModel::count_observables, clean_doc_string(R"DOC( Counts the number of frame changes (e.g. `L2`) in the error model. Observable indices are assumed to be contiguous from 0 up to whatever the maximum observable id is. If the largest observable's id is n-1, then the number of observables is n. Examples: >>> import stim >>> stim.Circuit(''' ... X_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(99) rec[-1] ... ''').detector_error_model().num_observables 100 >>> stim.DetectorErrorModel(''' ... error(0.1) L399 ... ''').num_observables 400 )DOC") .data()); c.def( "clear", &DetectorErrorModel::clear, clean_doc_string(R"DOC( Clears the contents of the detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.1) D0 D1 ... ''') >>> model.clear() >>> model stim.DetectorErrorModel() )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two detector error models have identical contents."); c.def(pybind11::self != pybind11::self, "Determines if two detector error models have non-identical contents."); c.def( "__str__", &DetectorErrorModel::str, clean_doc_string(R"DOC( Returns the contents of a detector error model file (.dem) encoding the model. )DOC") .data()); c.def( "__repr__", &detector_error_model_repr, clean_doc_string(R"DOC( Returns valid python code evaluating to an equivalent `stim.DetectorErrorModel`. )DOC") .data()); c.def( "copy", [](DetectorErrorModel &self) -> DetectorErrorModel { return self; }, clean_doc_string(R"DOC( Returns a copy of the detector error model. The copy is an independent detector error model with the same contents. Examples: >>> import stim >>> c1 = stim.DetectorErrorModel("error(0.1) D0 D1") >>> c2 = c1.copy() >>> c2 is c1 False >>> c2 == c1 True )DOC") .data()); c.def( "__len__", [](const DetectorErrorModel &self) -> size_t { return self.instructions.size(); }, clean_doc_string(R"DOC( Returns the number of top-level instructions/blocks in the detector error model. Instructions inside of blocks are not included in this count. Examples: >>> import stim >>> len(stim.DetectorErrorModel()) 0 >>> len(stim.DetectorErrorModel(''' ... error(0.1) D0 D1 ... shift_detectors 100 ... logical_observable L5 ... ''')) 3 >>> len(stim.DetectorErrorModel(''' ... repeat 100 { ... error(0.1) D0 D1 ... error(0.1) D1 D2 ... } ... ''')) 1 )DOC") .data()); c.def( "__getitem__", [](const DetectorErrorModel &self, const pybind11::object &index_or_slice) -> pybind11::object { pybind11::ssize_t index, step, slice_length; if (normalize_index_or_slice(index_or_slice, self.instructions.size(), &index, &step, &slice_length)) { return pybind11::cast(self.py_get_slice(index, step, slice_length)); } auto &op = self.instructions[index]; if (op.type == DemInstructionType::DEM_REPEAT_BLOCK) { return pybind11::cast(ExposedDemRepeatBlock{op.repeat_block_rep_count(), op.repeat_block_body(self)}); } ExposedDemInstruction result; result.targets.insert(result.targets.begin(), op.target_data.begin(), op.target_data.end()); result.arguments.insert(result.arguments.begin(), op.arg_data.begin(), op.arg_data.end()); result.type = op.type; result.tag = op.tag; return pybind11::cast(result); }, pybind11::arg("index_or_slice"), clean_doc_string(R"DOC( Returns copies of instructions from the detector error model. @overload def __getitem__(self, index_or_slice: int) -> Union[stim.DemInstruction, stim.DemRepeatBlock]: @overload def __getitem__(self, index_or_slice: slice) -> stim.DetectorErrorModel: Args: index_or_slice: An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D1 L1 ... repeat 100 { ... error(0.125) D1 D2 ... shift_detectors 1 ... } ... error(0.125) D2 ... logical_observable L0 ... detector D5 ... ''') >>> model[0] stim.DemInstruction('error', [0.125], [stim.target_relative_detector_id(0)]) >>> model[2] stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' error(0.125) D1 D2 shift_detectors 1 ''')) >>> model[1::2] stim.DetectorErrorModel(''' error(0.125) D1 L1 error(0.125) D2 detector D5 ''') )DOC") .data()); c.def( "approx_equals", [](const DetectorErrorModel &self, const pybind11::object &obj, double atol) -> bool { try { return self.approx_equals(pybind11::cast(obj), atol); } catch (const pybind11::cast_error &) { return false; } }, pybind11::arg("other"), pybind11::kw_only(), pybind11::arg("atol"), clean_doc_string(R"DOC( Checks if detector error models are approximately equal. Two detector error model are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example `error(0.100) D0` is approximately equal to `error(0.099) D0` within an absolute tolerance of 0.002. All other details of the models (such as the ordering of errors and their targets) must be exactly the same. Args: other: The detector error model, or other object, to compare to this one. atol: The absolute error tolerance. The maximum amount each probability may have been perturbed by. Returns: True if the given object is a detector error model approximately equal up to the receiving circuit up to the given tolerance, otherwise False. Examples: >>> import stim >>> base = stim.DetectorErrorModel(''' ... error(0.099) D0 D1 ... ''') >>> base.approx_equals(base, atol=0) True >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0) False >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0.0001) False >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.101) D0 D1 ... '''), atol=0.01) True >>> base.approx_equals(stim.DetectorErrorModel(''' ... error(0.099) D0 D1 L0 L1 L2 L3 L4 ... '''), atol=9999) False )DOC") .data()); c.def( "append", [](DetectorErrorModel &self, const pybind11::object &instruction, const pybind11::object &parens_arguments, const std::vector &targets, std::string_view tag) { bool is_name = pybind11::isinstance(instruction); if (!is_name && (!targets.empty() || !parens_arguments.is_none())) { throw std::invalid_argument( "Can't specify `parens_arguments` or `targets` when instruction is a " "stim.DemInstruction (instead of an instruction name)."); } if (is_name && (targets.empty() || parens_arguments.is_none())) { throw std::invalid_argument( "Must specify `parens_arguments` and `targets` when instruction is an instruction name."); } if (is_name) { std::string_view name = pybind11::cast(instruction); DemInstructionType type = non_block_instruction_name_to_enum(name); std::vector conv_args = python_arg_to_instruction_arguments(parens_arguments); std::vector conv_targets; for (const auto &e : targets) { try { if (type == DemInstructionType::DEM_SHIFT_DETECTORS) { conv_targets.push_back(DemTarget{pybind11::cast(e)}); } else { conv_targets.push_back(DemTarget{pybind11::cast(e).data}); } } catch (pybind11::cast_error &) { std::stringstream ss; ss << "Bad target '"; ss << pybind11::repr(e); ss << "' for instruction '"; ss << name; ss << "'."; throw std::invalid_argument(ss.str()); } } self.append_dem_instruction( DemInstruction{ conv_args, conv_targets, tag, type, }); } else if (pybind11::isinstance(instruction)) { const ExposedDemInstruction &exp = pybind11::cast(instruction); self.append_dem_instruction(DemInstruction{exp.arguments, exp.targets, exp.tag, exp.type}); } else if (pybind11::isinstance(instruction)) { const ExposedDemRepeatBlock &block = pybind11::cast(instruction); self.append_repeat_block(block.repeat_count, block.body, block.tag); } else if (pybind11::isinstance(instruction)) { self += pybind11::cast(instruction); } else { throw std::invalid_argument( "First argument to stim.DetectorErrorModel.append must be a str (an instruction name), " "a stim.DemInstruction, " "or a stim.DemRepeatBlock"); } }, pybind11::arg("instruction"), pybind11::arg("parens_arguments") = pybind11::none(), pybind11::arg("targets") = pybind11::make_tuple(), pybind11::kw_only(), pybind11::arg("tag") = "", clean_doc_string(R"DOC( Appends an instruction to the detector error model. Args: instruction: Either the name of an instruction, a stim.DemInstruction, a stim.DemRepeatBlock. or a stim.DetectorErrorModel. The `parens_arguments`, `targets`, and 'tag' arguments should be given iff the instruction is a name. parens_arguments: Numeric values parameterizing the instruction. The numbers inside parentheses in a detector error model file (eg. the `0.25` in `error(0.25) D0`). This argument can be given either a list of doubles, or a single double (which will be implicitly wrapped into a list). targets: The instruction targets, such as the `D0` in `error(0.25) D0`. tag: An arbitrary piece of text attached to the repeat instruction. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.125, [ ... stim.DemTarget.relative_detector_id(1), ... ]) >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(1), ... stim.DemTarget.separator(), ... stim.DemTarget.relative_detector_id(2), ... stim.DemTarget.logical_observable_id(3), ... ], tag='test-tag') >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 ''') >>> m.append("shift_detectors", (1, 2, 3), [5]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 ''') >>> m += m * 3 >>> m.append(m[0]) >>> m.append(m[-2]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 repeat 3 { error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } error(0.125) D1 repeat 3 { error(0.125) D1 error[test-tag](0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } ''') )DOC") .data()); c.def( "__imul__", &DetectorErrorModel::operator*=, pybind11::arg("repetitions"), clean_doc_string(R"DOC( Mutates the detector error model by putting its contents into a repeat block. Special case: if the repetition count is 0, the model is cleared. Special case: if the repetition count is 1, nothing happens. Args: repetitions: The number of times the repeat block should repeat. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> m *= 3 >>> print(m) repeat 3 { error(0.25) D0 shift_detectors 1 } )DOC") .data()); c.def( "get_detector_coordinates", [](const DetectorErrorModel &self, const pybind11::object &obj) { return self.get_detector_coordinates(obj_to_abs_detector_id_set(obj, [&]() { return self.count_detectors(); })); }, pybind11::arg("only") = pybind11::none(), clean_doc_string(R"DOC( Returns the coordinate metadata of detectors in the detector error model. Args: only: Defaults to None (meaning include all detectors). A list of detector indices to include in the result. Detector indices beyond the end of the detector error model cause an error. Returns: A dictionary mapping integers (detector indices) to lists of floats (coordinates). Detectors with no specified coordinate data are mapped to an empty tuple. If `only` is specified, then `set(result.keys()) == set(only)`. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.25) D0 D1 ... detector(1, 2, 3) D1 ... shift_detectors(5) 1 ... detector(1, 2) D2 ... ''') >>> dem.get_detector_coordinates() {0: [], 1: [1.0, 2.0, 3.0], 2: [], 3: [6.0, 2.0]} >>> dem.get_detector_coordinates(only=[1]) {1: [1.0, 2.0, 3.0]} )DOC") .data()); c.def( "__add__", &DetectorErrorModel::operator+, pybind11::arg("second"), clean_doc_string(R"DOC( Creates a detector error model by appending two models. Examples: >>> import stim >>> m1 = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''') >>> m2 = stim.DetectorErrorModel(''' ... error(0.25) D1 ... ''') >>> m1 + m2 stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D1 ''') )DOC") .data()); c.def( "__iadd__", &DetectorErrorModel::operator+=, pybind11::arg("second"), clean_doc_string(R"DOC( Appends a detector error model into the receiving model (mutating it). Examples: >>> import stim >>> m1 = stim.DetectorErrorModel(''' ... error(0.125) D0 ... ''') >>> m2 = stim.DetectorErrorModel(''' ... error(0.25) D1 ... ''') >>> m1 += m2 >>> print(repr(m1)) stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D1 ''') )DOC") .data()); c.def( "__mul__", &DetectorErrorModel::operator*, pybind11::arg("repetitions"), clean_doc_string(R"DOC( Repeats the detector error model using a repeat block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the repeat block should repeat. Returns: repetitions=0: An empty detector error model. repetitions=1: A copy of this detector error model. repetitions>=2: A detector error model with a single repeat block, where the contents of that repeat block are this detector error model. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> m * 3 stim.DetectorErrorModel(''' repeat 3 { error(0.25) D0 shift_detectors 1 } ''') )DOC") .data()); c.def( "__rmul__", &DetectorErrorModel::operator*, pybind11::arg("repetitions"), clean_doc_string(R"DOC( Repeats the detector error model using a repeat block. Has special cases for 0 repetitions and 1 repetitions. Args: repetitions: The number of times the repeat block should repeat. Returns: repetitions=0: An empty detector error model. repetitions=1: A copy of this detector error model. repetitions>=2: A detector error model with a single repeat block, where the contents of that repeat block are this detector error model. Examples: >>> import stim >>> m = stim.DetectorErrorModel(''' ... error(0.25) D0 ... shift_detectors 1 ... ''') >>> 3 * m stim.DetectorErrorModel(''' repeat 3 { error(0.25) D0 shift_detectors 1 } ''') )DOC") .data()); c.def( pybind11::pickle( [](const DetectorErrorModel &self) -> pybind11::str { return self.str(); }, [](const pybind11::str &text) -> DetectorErrorModel { return DetectorErrorModel(pybind11::cast(text)); })); c.def( "shortest_graphlike_error", &shortest_graphlike_undetectable_logical_error, pybind11::arg("ignore_ungraphlike_errors") = true, clean_doc_string(R"DOC( Finds a minimum set of graphlike errors to produce an undetected logical error. Note that this method does not pay attention to error probabilities (other than ignoring errors with probability 0). It searches for a logical error with the minimum *number* of physical errors, not the maximum probability of those physical errors all occurring. This method works by looking for errors that have frame changes (eg. "error(0.1) D0 D1 L5" flips the frame of observable 5). These errors are converted into one or two symptoms and a net frame change. The symptoms can then be moved around by following errors touching that symptom. Each symptom is moved until it disappears into a boundary or cancels against another remaining symptom, while leaving the other symptoms alone (ensuring only one symptom is allowed to move significantly reduces waste in the search space). Eventually a path or cycle of errors is found that cancels out the symptoms, and if there is still a frame change at that point then that path or cycle is a logical error (otherwise all that was found was a stabilizer of the system; a dead end). The search process advances like a breadth first search, seeded from all the frame-change errors and branching them outward in tandem, until one of them wins the race to find a solution. Args: ignore_ungraphlike_errors: Defaults to True. When False, an exception is raised if there are any errors in the model that are not graphlike. When True, those errors are skipped as if they weren't present. A graphlike error is an error with less than two symptoms. For the purposes of this method, errors are also considered graphlike if they are decomposed into graphlike components: graphlike: error(0.1) D0 error(0.1) D0 D1 error(0.1) D0 D1 L0 not graphlike but decomposed into graphlike components: error(0.1) D0 D1 ^ D2 not graphlike, not decomposed into graphlike components: error(0.1) D0 D1 D2 error(0.1) D0 D1 D2 ^ D3 Returns: A detector error model containing just the error instructions corresponding to an undetectable logical error. There will be no other kinds of instructions (no `repeat`s, no `shift_detectors`, etc). The error probabilities will all be set to 1. The `len` of the returned model is the graphlike code distance of the circuit. But beware that in general the true code distance may be smaller. For example, in the XZ surface code with twists, the true minimum sized logical error is likely to use Y errors. But each Y error decomposes into two graphlike components (the X part and the Z part). As a result, the graphlike code distance in that context is likely to be nearly twice as large as the true code distance. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.125) D0 D1 ... error(0.125) D1 L55 ... error(0.125) D1 ... ''').shortest_graphlike_error() stim.DetectorErrorModel(''' error(1) D1 error(1) D1 L55 ''') >>> stim.DetectorErrorModel(''' ... error(0.125) D0 D1 D2 ... error(0.125) L0 ... ''').shortest_graphlike_error(ignore_ungraphlike_errors=True) stim.DetectorErrorModel(''' error(1) L0 ''') >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... before_round_data_depolarization=0.01) >>> model = circuit.detector_error_model(decompose_errors=True) >>> len(model.shortest_graphlike_error()) 7 )DOC") .data()); c.def_static( "from_file", [](pybind11::object &obj) { if (pybind11::isinstance(obj)) { std::string_view path = pybind11::cast(obj); RaiiFile f(path, "rb"); return DetectorErrorModel::from_file(f.f); } auto py_path = pybind11::module::import("pathlib").attr("Path"); if (pybind11::isinstance(obj, py_path)) { auto obj_str = pybind11::str(obj); std::string_view path = pybind11::cast(obj_str); RaiiFile f(path, "rb"); return DetectorErrorModel::from_file(f.f); } auto py_text_io_base = pybind11::module::import("io").attr("TextIOBase"); if (pybind11::isinstance(obj, py_text_io_base)) { auto contents = obj.attr("read")(); return DetectorErrorModel(pybind11::cast(contents)); } std::stringstream ss; ss << "Don't know how to read from "; ss << pybind11::repr(obj); throw std::invalid_argument(ss.str()); }, pybind11::arg("file"), clean_doc_string(R"DOC( @signature def from_file(file: Union[io.TextIOBase, str, pathlib.Path]) -> stim.DetectorErrorModel: Reads a detector error model from a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md Args: file: A file path or open file object to read from. Returns: The circuit parsed from the file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('error(0.25) D2 D3', file=f) ... circuit = stim.DetectorErrorModel.from_file(path) >>> circuit stim.DetectorErrorModel(''' error(0.25) D2 D3 ''') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... print('error(0.25) D2 D3', file=f) ... with open(path) as f: ... circuit = stim.DetectorErrorModel.from_file(f) >>> circuit stim.DetectorErrorModel(''' error(0.25) D2 D3 ''') )DOC") .data()); c.def( "to_file", [](const DetectorErrorModel &self, pybind11::object &obj) { if (pybind11::isinstance(obj)) { std::string path = pybind11::cast(obj); std::ofstream out(path, std::ofstream::out); if (!out.is_open()) { throw std::invalid_argument("Failed to open " + path); } out << self << '\n'; return; } auto py_path = pybind11::module::import("pathlib").attr("Path"); if (pybind11::isinstance(obj, py_path)) { std::string path = pybind11::cast(pybind11::str(obj)); std::ofstream out(path, std::ofstream::out); if (!out.is_open()) { throw std::invalid_argument("Failed to open " + path); } out << self << '\n'; return; } auto py_text_io_base = pybind11::module::import("io").attr("TextIOBase"); if (pybind11::isinstance(obj, py_text_io_base)) { obj.attr("write")(pybind11::str(self.str())); obj.attr("write")(pybind11::str("\n")); return; } std::stringstream ss; ss << "Don't know how to write to "; ss << pybind11::repr(obj); throw std::invalid_argument(ss.str()); }, pybind11::arg("file"), clean_doc_string(R"DOC( @signature def to_file(self, file: Union[io.TextIOBase, str, pathlib.Path]) -> None: Writes the detector error model to a file. The file format is defined at https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md Args: file: A file path or an open file to write to. Examples: >>> import stim >>> import tempfile >>> c = stim.DetectorErrorModel('error(0.25) D2 D3') >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... with open(path, 'w') as f: ... c.to_file(f) ... with open(path) as f: ... contents = f.read() >>> contents 'error(0.25) D2 D3\n' >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = tmpdir + '/tmp.stim' ... c.to_file(path) ... with open(path) as f: ... contents = f.read() >>> contents 'error(0.25) D2 D3\n' )DOC") .data()); c.def( "compile_sampler", [](const DetectorErrorModel &self, const pybind11::object &seed) -> DemSampler { return DemSampler(self, make_py_seeded_rng(seed), 1024); }, pybind11::kw_only(), pybind11::arg("seed") = pybind11::none(), clean_doc_string(R"DOC( Returns a CompiledDemSampler that can batch sample from detector error models. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Returns: A seeded stim.CompiledDemSampler for the given detector error model. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) )DOC") .data()); c.def( "flattened", &DetectorErrorModel::flattened, clean_doc_string(R"DOC( Returns the detector error model without repeat or detector_shift instructions. Returns: A `stim.DetectorErrorModel` with the same errors in the same order, but with repeat loops flattened into actually repeated instructions and with all coordinate/index shifts inlined. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error(0.125) D0 ... REPEAT 5 { ... error(0.25) D0 D1 ... shift_detectors 1 ... } ... error(0.125) D0 L0 ... ''').flattened() stim.DetectorErrorModel(''' error(0.125) D0 error(0.25) D0 D1 error(0.25) D1 D2 error(0.25) D2 D3 error(0.25) D3 D4 error(0.25) D4 D5 error(0.125) D5 L0 ''') )DOC") .data()); c.def( "rounded", &DetectorErrorModel::rounded, clean_doc_string(R"DOC( Creates an equivalent detector error model but with rounded error probabilities. Args: digits: The number of digits to round to. Returns: A `stim.DetectorErrorModel` with the same instructions in the same order, but with the parens arguments of error instructions rounded to the given precision. Instructions whose error probability was rounded to zero are still included in the output. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.019499) D0 ... error(0.000001) D0 D1 ... ''') >>> dem.rounded(2) stim.DetectorErrorModel(''' error(0.02) D0 error(0) D0 D1 ''') >>> dem.rounded(3) stim.DetectorErrorModel(''' error(0.019) D0 error(0) D0 D1 ''') )DOC") .data()); c.def( "diagram", &dem_diagram, pybind11::arg("type"), clean_doc_string(R"DOC( @signature def diagram(self, type: Literal["matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html"] = 'matchgraph-svg') -> Any: Returns a diagram of the circuit, from a variety of options. Args: type: The type of diagram. Available types are: "matchgraph-svg": An image of the decoding graph of the detector error model. Red lines are errors crossing a logical observable. Blue lines are undecomposed hyper errors. "matchgraph-svg-html": Same as matchgraph-svg but with the SVG wrapped in a resizable HTML iframe. "matchgraph-3d": A 3d model of the decoding graph of the detector error model. Red lines are errors crossing a logical observable. Blue lines are undecomposed hyper errors. GLTF files can be opened with a variety of programs, or opened online in viewers such as https://gltf-viewer.donmccurdy.com/ . Red lines are errors crossing a logical observable. "matchgraph-3d-html": Same 3d model as 'match-graph-3d' but embedded into an HTML web page containing an interactive THREE.js viewer for the 3d model. Returns: An object whose `__str__` method returns the diagram, so that writing the diagram to a file works correctly. The returned object also defines a `_repr_html_` method, so that ipython notebooks recognize it can be shown using a specialized viewer instead of as raw text. Examples: >>> import stim >>> import tempfile >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... rounds=10, ... distance=7, ... after_clifford_depolarization=0.01) >>> dem = circuit.detector_error_model(decompose_errors=True) >>> with tempfile.TemporaryDirectory() as d: ... diagram = circuit.diagram("match-graph-svg") ... with open(f"{d}/dem_image.svg", "w") as f: ... print(diagram, file=f) >>> with tempfile.TemporaryDirectory() as d: ... diagram = circuit.diagram("match-graph-3d") ... with open(f"{d}/dem_3d_model.gltf", "w") as f: ... print(diagram, file=f) )DOC") .data()); c.def( "without_tags", &DetectorErrorModel::without_tags, clean_doc_string(R"DOC( Returns a copy of the detector error model with all tags removed. Returns: A `stim.DetectorErrorModel` with the same instructions except all tags have been removed. Examples: >>> import stim >>> stim.DetectorErrorModel(''' ... error[test-tag](0.25) D0 ... ''').without_tags() stim.DetectorErrorModel(''' error(0.25) D0 ''') )DOC") .data()); } ================================================ FILE: src/stim/dem/detector_error_model.pybind.h ================================================ // 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. #ifndef _STIM_DEM_DETECTOR_ERROR_MODEL_PYBIND_H #define _STIM_DEM_DETECTOR_ERROR_MODEL_PYBIND_H #include #include "stim/dem/detector_error_model.h" namespace stim_pybind { std::string detector_error_model_repr(const stim::DetectorErrorModel &self); pybind11::class_ pybind_detector_error_model(pybind11::module &m); void pybind_detector_error_model_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/dem/detector_error_model.test.cc ================================================ // 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. #include "stim/dem/detector_error_model.h" #include "gtest/gtest.h" #include "stim/gen/gen_surface_code.h" #include "stim/simulators/error_analyzer.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(detector_error_model, init_equality) { DetectorErrorModel model1; DetectorErrorModel model2; ASSERT_TRUE(model1 == model2); ASSERT_TRUE(!(model1 != model2)); model1.append_shift_detectors_instruction({}, 5, ""); ASSERT_TRUE(model1 != model2); ASSERT_TRUE(!(model1 == model2)); model2.append_shift_detectors_instruction({}, 4, ""); ASSERT_NE(model1, model2); model1.clear(); model2.clear(); ASSERT_EQ(model1, model2); model1.append_repeat_block(5, {}, ""); model2.append_repeat_block(4, {}, ""); ASSERT_NE(model1, model2); model1.append_error_instruction(0.2, {}, ""); model2.append_repeat_block(4, {}, ""); ASSERT_NE(model1, model2); } TEST(detector_error_model, append_shift_detectors_instruction) { DetectorErrorModel model; ASSERT_EQ(model.instructions.size(), 0); ASSERT_EQ(model.blocks.size(), 0); std::vector arg_data{1.5, 2.5}; SpanRef arg_data_ref = arg_data; model.append_shift_detectors_instruction(arg_data_ref, 5, ""); ASSERT_EQ(model.instructions.size(), 1); ASSERT_EQ(model.instructions[0].type, DemInstructionType::DEM_SHIFT_DETECTORS); ASSERT_EQ(model.instructions[0].target_data.size(), 1); ASSERT_EQ(model.instructions[0].target_data[0].data, 5); ASSERT_EQ(model.instructions[0].arg_data, arg_data_ref); ASSERT_EQ(model.blocks.size(), 0); } TEST(detector_error_model, append_detector_instruction) { DetectorErrorModel model; ASSERT_EQ(model.instructions.size(), 0); ASSERT_EQ(model.blocks.size(), 0); std::vector arg_data{1.5, 2.5}; SpanRef arg_data_ref = arg_data; model.append_detector_instruction(arg_data_ref, DemTarget::relative_detector_id(5), ""); ASSERT_EQ(model.instructions.size(), 1); ASSERT_EQ(model.instructions[0].type, DemInstructionType::DEM_DETECTOR); ASSERT_EQ(model.instructions[0].target_data.size(), 1); ASSERT_EQ(model.instructions[0].target_data[0], DemTarget::relative_detector_id(5)); ASSERT_EQ(model.instructions[0].arg_data, arg_data_ref); ASSERT_EQ(model.blocks.size(), 0); ASSERT_THROW({ model.append_detector_instruction({}, DemTarget::separator(), ""); }, std::invalid_argument); ASSERT_THROW({ model.append_detector_instruction({}, DemTarget::observable_id(4), ""); }, std::invalid_argument); model.append_detector_instruction({}, DemTarget::relative_detector_id(4), ""); } TEST(detector_error_model, append_logical_observable_instruction) { DetectorErrorModel model; ASSERT_EQ(model.instructions.size(), 0); ASSERT_EQ(model.blocks.size(), 0); model.append_logical_observable_instruction(DemTarget::observable_id(5), ""); ASSERT_EQ(model.instructions.size(), 1); ASSERT_EQ(model.instructions[0].type, DemInstructionType::DEM_LOGICAL_OBSERVABLE); ASSERT_EQ(model.instructions[0].target_data.size(), 1); ASSERT_EQ(model.instructions[0].target_data[0], DemTarget::observable_id(5)); ASSERT_EQ(model.instructions[0].arg_data.size(), 0); ASSERT_EQ(model.blocks.size(), 0); ASSERT_THROW({ model.append_logical_observable_instruction(DemTarget::separator(), ""); }, std::invalid_argument); ASSERT_THROW( { model.append_logical_observable_instruction(DemTarget::relative_detector_id(4), ""); }, std::invalid_argument); model.append_logical_observable_instruction(DemTarget::observable_id(4), ""); } TEST(detector_error_model, append_error_instruction) { DetectorErrorModel model; std::vector symptoms; symptoms.push_back(DemTarget::observable_id(3)); symptoms.push_back(DemTarget::relative_detector_id(4)); model.append_error_instruction(0.25, symptoms, ""); ASSERT_EQ(model.instructions.size(), 1); ASSERT_EQ(model.blocks.size(), 0); ASSERT_EQ(model.instructions[0].type, DemInstructionType::DEM_ERROR); ASSERT_EQ(model.instructions[0].target_data, (SpanRef)symptoms); ASSERT_EQ(model.instructions[0].arg_data.size(), 1); ASSERT_EQ(model.instructions[0].arg_data[0], 0.25); model.clear(); ASSERT_EQ(model.instructions.size(), 0); symptoms.push_back(DemTarget::separator()); symptoms.push_back(DemTarget::observable_id(4)); model.append_error_instruction(0.125, symptoms, ""); ASSERT_EQ(model.instructions.size(), 1); ASSERT_EQ(model.blocks.size(), 0); ASSERT_EQ(model.instructions[0].type, DemInstructionType::DEM_ERROR); ASSERT_EQ(model.instructions[0].target_data, (SpanRef)symptoms); ASSERT_EQ(model.instructions[0].arg_data.size(), 1); ASSERT_EQ(model.instructions[0].arg_data[0], 0.125); ASSERT_THROW({ model.append_error_instruction(1.5, symptoms, ""); }, std::invalid_argument); ASSERT_THROW({ model.append_error_instruction(-0.5, symptoms, ""); }, std::invalid_argument); symptoms = {DemTarget::separator()}; ASSERT_THROW({ model.append_error_instruction(0.25, symptoms, ""); }, std::invalid_argument); symptoms = {DemTarget::separator(), DemTarget::observable_id(0)}; ASSERT_THROW({ model.append_error_instruction(0.25, symptoms, ""); }, std::invalid_argument); symptoms = {DemTarget::observable_id(0), DemTarget::separator()}; ASSERT_THROW({ model.append_error_instruction(0.25, symptoms, ""); }, std::invalid_argument); symptoms = { DemTarget::observable_id(0), DemTarget::separator(), DemTarget::separator(), DemTarget::relative_detector_id(4)}; ASSERT_THROW({ model.append_error_instruction(0.25, symptoms, ""); }, std::invalid_argument); symptoms = {DemTarget::observable_id(0), DemTarget::separator(), DemTarget::relative_detector_id(4)}; model.append_error_instruction(0.25, symptoms, ""); } TEST(detector_error_model, append_block) { DetectorErrorModel model; DetectorErrorModel block; block.append_shift_detectors_instruction({}, 3, ""); DetectorErrorModel block2 = block; model.append_repeat_block(5, block, ""); block.append_shift_detectors_instruction({}, 4, ""); model.append_repeat_block(6, std::move(block), ""); model.append_repeat_block(20, block2, ""); ASSERT_EQ(model.instructions.size(), 3); ASSERT_EQ(model.blocks.size(), 3); ASSERT_EQ(model.instructions[0].type, DemInstructionType::DEM_REPEAT_BLOCK); ASSERT_EQ(model.instructions[0].target_data[0].data, 5); ASSERT_EQ(model.instructions[0].target_data[1].data, 0); ASSERT_EQ(model.instructions[1].type, DemInstructionType::DEM_REPEAT_BLOCK); ASSERT_EQ(model.instructions[1].target_data[0].data, 6); ASSERT_EQ(model.instructions[1].target_data[1].data, 1); ASSERT_EQ(model.instructions[2].type, DemInstructionType::DEM_REPEAT_BLOCK); ASSERT_EQ(model.instructions[2].target_data[0].data, 20); ASSERT_EQ(model.instructions[2].target_data[1].data, 2); ASSERT_EQ(model.blocks[0], block2); ASSERT_EQ(model.blocks[2], block2); block2.append_shift_detectors_instruction({}, 4, ""); ASSERT_EQ(model.blocks[1], block2); } TEST(detector_error_model, round_trip_str) { const char *t = R"MODEL(error(0.125) D0 repeat 100 { repeat 200 { error[test-tag](0.25) D0 D1 L0 ^ D2 shift_detectors(1.5, 3) 10 detector(0.5) D0 detector D1 } error(0.375) D0 D1 shift_detectors 20 logical_observable L0 })MODEL"; ASSERT_EQ(DetectorErrorModel(t).str(), std::string(t)); } TEST(detector_error_model, parse) { DetectorErrorModel expected; ASSERT_EQ(DetectorErrorModel(""), expected); expected.append_error_instruction(0.125, (std::vector{DemTarget::relative_detector_id(0)}), ""); ASSERT_EQ( DetectorErrorModel(R"MODEL( error(0.125) D0 )MODEL"), expected); expected.append_error_instruction(0.125, (std::vector{DemTarget::relative_detector_id(5)}), ""); ASSERT_EQ( DetectorErrorModel(R"MODEL( error(0.125) D0 error(0.125) D5 )MODEL"), expected); expected.append_error_instruction( 0.25, (std::vector{ DemTarget::relative_detector_id(5), DemTarget::separator(), DemTarget::observable_id(4)}), ""); ASSERT_EQ( DetectorErrorModel(R"MODEL( error(0.125) D0 error(0.125) D5 error(0.25) D5 ^ L4 )MODEL"), expected); expected.append_shift_detectors_instruction(std::vector{1.5, 2}, 60, ""); ASSERT_EQ( DetectorErrorModel(R"MODEL( error(0.125) D0 error(0.125) D5 error(0.25) D5 ^ L4 shift_detectors(1.5, 2) 60 )MODEL"), expected); expected.append_repeat_block(100, expected, ""); ASSERT_EQ( DetectorErrorModel(R"MODEL( error(0.125) D0 error(0.125) D5 error(0.25) D5 ^ L4 shift_detectors(1.5, 2) 60 repeat 100 { error(0.125) D0 error(0.125) D5 error(0.25) D5 ^ L4 shift_detectors(1.5, 2) 60 } )MODEL"), expected); } TEST(detector_error_model, parse_tag) { std::string_view t = R"MODEL(error[test1](0.125) D0 repeat[test2] 10 { shift_detectors[test3](1.5, 3) 10 detector[test4](0.5) D0 } logical_observable[test5] L0)MODEL"; DetectorErrorModel dem(t); ASSERT_EQ(dem.str(), t); } TEST(detector_error_model, movement) { const char *t = R"MODEL( error(0.2) D0 REPEAT 100 { REPEAT 200 { error(0.1) D0 D1 L0 ^ D2 shift_detectors 10 } error(0.1) D0 D2 shift_detectors 20 } )MODEL"; DetectorErrorModel d1(t); DetectorErrorModel d2(d1); ASSERT_EQ(d1, d2); ASSERT_EQ(d1, DetectorErrorModel(t)); DetectorErrorModel d3(std::move(d1)); ASSERT_EQ(d1, DetectorErrorModel()); ASSERT_EQ(d2, d3); ASSERT_EQ(d2, DetectorErrorModel(t)); d1 = d3; ASSERT_EQ(d1, d2); ASSERT_EQ(d2, d3); ASSERT_EQ(d2, DetectorErrorModel(t)); d1 = std::move(d2); ASSERT_EQ(d1, d3); ASSERT_EQ(d2, DetectorErrorModel()); ASSERT_EQ(d1, DetectorErrorModel(t)); } TEST(dem_target, general) { DemTarget d = DemTarget::relative_detector_id(3); ASSERT_TRUE(d == DemTarget::relative_detector_id(3)); ASSERT_TRUE(!(d != DemTarget::relative_detector_id(3))); ASSERT_TRUE(!(d == DemTarget::relative_detector_id(4))); ASSERT_TRUE(d != DemTarget::relative_detector_id(4)); ASSERT_EQ(d, DemTarget::relative_detector_id(3)); ASSERT_NE(d, DemTarget::observable_id(5)); ASSERT_NE(d, DemTarget::separator()); DemTarget d3 = DemTarget::relative_detector_id(72); DemTarget s = DemTarget::separator(); DemTarget o = DemTarget::observable_id(3); ASSERT_EQ(d.str(), "D3"); ASSERT_EQ(d3.str(), "D72"); ASSERT_EQ(o.str(), "L3"); ASSERT_EQ(s.str(), "^"); ASSERT_TRUE(!o.is_separator()); ASSERT_TRUE(!d3.is_separator()); ASSERT_TRUE(s.is_separator()); ASSERT_TRUE(o.is_observable_id()); ASSERT_TRUE(!d3.is_observable_id()); ASSERT_TRUE(!s.is_observable_id()); ASSERT_TRUE(!o.is_relative_detector_id()); ASSERT_TRUE(d3.is_relative_detector_id()); ASSERT_TRUE(!s.is_relative_detector_id()); } TEST(dem_instruction, general) { std::vector d1; d1.push_back(DemTarget::observable_id(4)); d1.push_back(DemTarget::relative_detector_id(3)); std::vector d2; d2.push_back(DemTarget::observable_id(4)); std::vector p125{0.125}; std::vector p25{0.25}; std::vector p126{0.126}; DemInstruction i1{p125, d1, "", DemInstructionType::DEM_ERROR}; DemInstruction i1a{p125, d1, "", DemInstructionType::DEM_ERROR}; DemInstruction i2{p125, d2, "", DemInstructionType::DEM_ERROR}; ASSERT_TRUE(i1 == i1a); ASSERT_TRUE(!(i1 != i1a)); ASSERT_TRUE(!(i2 == i1a)); ASSERT_TRUE(i2 != i1a); ASSERT_EQ(i1, (DemInstruction{p125, d1, "", DemInstructionType::DEM_ERROR})); ASSERT_NE(i1, (DemInstruction{p125, d2, "", DemInstructionType::DEM_ERROR})); ASSERT_NE(i1, (DemInstruction{p25, d1, "", DemInstructionType::DEM_ERROR})); ASSERT_NE( ((DemInstruction{{}, {}, "", DemInstructionType::DEM_DETECTOR})), (DemInstruction{{}, {}, "", DemInstructionType::DEM_LOGICAL_OBSERVABLE})); ASSERT_TRUE(i1.approx_equals(DemInstruction{p125, d1, "", DemInstructionType::DEM_ERROR}, 0)); ASSERT_TRUE(!i1.approx_equals(DemInstruction{p126, d1, "", DemInstructionType::DEM_ERROR}, 0)); ASSERT_TRUE(i1.approx_equals(DemInstruction{p126, d1, "", DemInstructionType::DEM_ERROR}, 0.01)); ASSERT_TRUE(!i1.approx_equals(DemInstruction{p125, d2, "", DemInstructionType::DEM_ERROR}, 9999)); ASSERT_EQ(i1.str(), "error(0.125) L4 D3"); ASSERT_EQ(i2.str(), "error(0.125) L4"); d1.push_back(DemTarget::separator()); d1.push_back(DemTarget::observable_id(11)); ASSERT_EQ((DemInstruction{p25, d1, "", DemInstructionType::DEM_ERROR}).str(), "error(0.25) L4 D3 ^ L11"); } TEST(detector_error_model, total_detector_shift) { ASSERT_EQ(DetectorErrorModel("").total_detector_shift(), 0); ASSERT_EQ(DetectorErrorModel("error(0.3) D2").total_detector_shift(), 0); ASSERT_EQ(DetectorErrorModel("shift_detectors 5").total_detector_shift(), 5); ASSERT_EQ(DetectorErrorModel("shift_detectors 5\nshift_detectors 4").total_detector_shift(), 9); ASSERT_EQ( DetectorErrorModel(R"MODEL( shift_detectors 5 repeat 1000 { shift_detectors 4 } )MODEL") .total_detector_shift(), 4005); } TEST(detector_error_model, count_detectors) { ASSERT_EQ(DetectorErrorModel("").count_detectors(), 0); ASSERT_EQ(DetectorErrorModel("error(0.3) D2 L1000").count_detectors(), 3); ASSERT_EQ(DetectorErrorModel("shift_detectors 5").count_detectors(), 0); ASSERT_EQ(DetectorErrorModel("shift_detectors 5\ndetector D3").count_detectors(), 9); ASSERT_EQ( DetectorErrorModel(R"MODEL( shift_detectors 50 repeat 1000 { detector D0 error(0.1) D0 D1 shift_detectors 4 } )MODEL") .count_detectors(), 4048); } TEST(detector_error_model, count_observables) { ASSERT_EQ(DetectorErrorModel("").count_observables(), 0); ASSERT_EQ(DetectorErrorModel("error(0.3) L2 D9999").count_observables(), 3); ASSERT_EQ(DetectorErrorModel("shift_detectors 5\nlogical_observable L3").count_observables(), 4); ASSERT_EQ( DetectorErrorModel(R"MODEL( shift_detectors 50 repeat 1000 { logical_observable L5 error(0.1) D0 D1 L6 shift_detectors 4 } )MODEL") .count_observables(), 7); } TEST(detector_error_model, from_file) { FILE *f = tmpfile(); const char *program = R"MODEL( error(0.125) D1 REPEAT 99 { error(0.25) D3 D4 shift_detectors 1 } )MODEL"; fprintf(f, "%s", program); rewind(f); auto d = DetectorErrorModel::from_file(f); ASSERT_EQ(d, DetectorErrorModel(program)); d.clear(); rewind(f); d.append_from_file(f); ASSERT_EQ(d, DetectorErrorModel(program)); d.clear(); rewind(f); d.append_from_file(f, false); ASSERT_EQ(d, DetectorErrorModel(program)); d.clear(); rewind(f); d.append_from_file(f, true); ASSERT_EQ(d, DetectorErrorModel("error(0.125) D1")); d.append_from_file(f, true); ASSERT_EQ(d, DetectorErrorModel(program)); } TEST(detector_error_model, py_get_slice) { DetectorErrorModel d(R"MODEL( detector D2 logical_observable L1 error(0.125) D0 L1 REPEAT 100 { shift_detectors(0.25) 5 REPEAT 20 { } } error(0.125) D1 D2 REPEAT 999 { } )MODEL"); ASSERT_EQ(d.py_get_slice(0, 1, 6), d); ASSERT_EQ(d.py_get_slice(0, 1, 4), DetectorErrorModel(R"MODEL( detector D2 logical_observable L1 error(0.125) D0 L1 REPEAT 100 { shift_detectors(0.25) 5 REPEAT 20 { } } )MODEL")); ASSERT_EQ(d.py_get_slice(2, 1, 3), DetectorErrorModel(R"MODEL( error(0.125) D0 L1 REPEAT 100 { shift_detectors(0.25) 5 REPEAT 20 { } } error(0.125) D1 D2 )MODEL")); ASSERT_EQ(d.py_get_slice(4, -1, 3), DetectorErrorModel(R"MODEL( error(0.125) D1 D2 REPEAT 100 { shift_detectors(0.25) 5 REPEAT 20 { } } error(0.125) D0 L1 )MODEL")); ASSERT_EQ(d.py_get_slice(5, -2, 3), DetectorErrorModel(R"MODEL( REPEAT 999 { } REPEAT 100 { shift_detectors(0.25) 5 REPEAT 20 { } } logical_observable L1 )MODEL")); DetectorErrorModel d2 = d; DetectorErrorModel d3 = d2.py_get_slice(0, 1, 6); d2.clear(); ASSERT_EQ(d, d3); } TEST(detector_error_model, mul) { DetectorErrorModel original(R"MODEL( error(0.25) D0 REPEAT 999 { error(0.25) D1 } )MODEL"); DetectorErrorModel d = original; ASSERT_EQ(d * 3, DetectorErrorModel(R"MODEL( REPEAT 3 { error(0.25) D0 REPEAT 999 { error(0.25) D1 } } )MODEL")); ASSERT_EQ(d * 1, d); ASSERT_EQ(d * 0, DetectorErrorModel()); ASSERT_EQ(d, original); } TEST(detector_error_model, imul) { DetectorErrorModel original(R"MODEL( error(0.25) D0 REPEAT 999 { error(0.25) D1 } )MODEL"); DetectorErrorModel d = original; d *= 3; ASSERT_EQ(d, DetectorErrorModel(R"MODEL( REPEAT 3 { error(0.25) D0 REPEAT 999 { error(0.25) D1 } } )MODEL")); d = original; d *= 1; ASSERT_EQ(d, original); d = original; d *= 0; ASSERT_EQ(d, DetectorErrorModel()); } TEST(detector_error_model, add) { DetectorErrorModel a(R"MODEL( error(0.25) D0 REPEAT 999 { error(0.25) D1 } )MODEL"); DetectorErrorModel b(R"MODEL( error(0.125) D1 REPEAT 2 { REPEAT 3 { error(0.125) D1 } } )MODEL"); ASSERT_EQ(a + b, DetectorErrorModel(R"MODEL( error(0.25) D0 REPEAT 999 { error(0.25) D1 } error(0.125) D1 REPEAT 2 { REPEAT 3 { error(0.125) D1 } } )MODEL")); ASSERT_EQ(a + DetectorErrorModel(), a); ASSERT_EQ(DetectorErrorModel() + a, a); ASSERT_EQ(b + DetectorErrorModel(), b); ASSERT_EQ(DetectorErrorModel() + b, b); ASSERT_EQ(DetectorErrorModel() + DetectorErrorModel(), DetectorErrorModel()); } TEST(detector_error_model, iadd) { DetectorErrorModel a(R"MODEL( error(0.25) D0 REPEAT 999 { error(0.25) D1 } )MODEL"); DetectorErrorModel b(R"MODEL( error(0.125) D1 REPEAT 2 { REPEAT 3 { error(0.125) D1 } } )MODEL"); a += b; ASSERT_EQ(a, DetectorErrorModel(R"MODEL( error(0.25) D0 REPEAT 999 { error(0.25) D1 } error(0.125) D1 REPEAT 2 { REPEAT 3 { error(0.125) D1 } } )MODEL")); DetectorErrorModel original = b; b += DetectorErrorModel(); ASSERT_EQ(b, original); b += a; ASSERT_NE(b, original); // Aliased. a = original; a += a; a = DetectorErrorModel(a.str()); // Remove memory deduplication, because it affects equality. ASSERT_EQ(a, original + original); } TEST(detector_error_model, iter_flatten_error_instructions) { DetectorErrorModel d(R"MODEL( error(0.25) D0 shift_detectors 1 error(0.375) D0 D1 repeat 5 { error(0.125) D0 D1 D2 L0 shift_detectors 2 } detector D5000 logical_observable L5000 )MODEL"); DetectorErrorModel dem; d.iter_flatten_error_instructions([&](const DemInstruction &e) { EXPECT_EQ(e.type, DemInstructionType::DEM_ERROR); dem.append_error_instruction(e.arg_data[0], e.target_data, ""); }); ASSERT_EQ(dem, DetectorErrorModel(R"MODEL( error(0.25) D0 error(0.375) D1 D2 error(0.125) D1 D2 D3 L0 error(0.125) D3 D4 D5 L0 error(0.125) D5 D6 D7 L0 error(0.125) D7 D8 D9 L0 error(0.125) D9 D10 D11 L0 )MODEL")); } TEST(detector_error_model, get_detector_coordinates_nested_loops) { DetectorErrorModel dem(R"MODEL( repeat 200 { repeat 100 { detector(0, 0, 0, 4) D1 shift_detectors(1, 0, 0) 10 } detector(0, 0, 0, 3) D2 shift_detectors(0, 1, 0) 0 } detector(0, 0, 0, 2) D3 )MODEL"); ASSERT_THROW({ dem.get_detector_coordinates({4000000000}); }, std::invalid_argument); ASSERT_THROW({ dem.get_detector_coordinates({dem.count_detectors()}); }, std::invalid_argument); auto result = dem.get_detector_coordinates({ 0, 1, 11, 991, 1001, 1002, 1011, 1021, }); ASSERT_EQ( result, (std::map>{ {0, {}}, {1, {0, 0, 0, 4}}, {11, {1, 0, 0, 4}}, {991, {99, 0, 0, 4}}, {1001, {100, 1, 0, 4}}, {1002, {100, 0, 0, 3}}, {1011, {101, 1, 0, 4}}, {1021, {102, 1, 0, 4}}, })); } TEST(detector_error_model, get_detector_coordinates_trivial) { DetectorErrorModel dem; dem = DetectorErrorModel(R"MODEL( detector(1, 2) D1 )MODEL"); ASSERT_EQ( dem.get_detector_coordinates({0, 1}), (std::map>{ {0, {}}, {1, {1, 2}}, })); ASSERT_THROW({ dem.get_detector_coordinates({2}); }, std::invalid_argument); dem = DetectorErrorModel(R"MODEL( error(0.25) D0 D1 )MODEL"); ASSERT_EQ( dem.get_detector_coordinates({0, 1}), (std::map>{ {0, {}}, {1, {}}, })); ASSERT_THROW({ dem.get_detector_coordinates({2}); }, std::invalid_argument); dem = DetectorErrorModel(R"MODEL( error(0.25) D0 D1 detector(1, 2, 3) D1 shift_detectors(5) 1 detector(1, 2) D2 )MODEL"); ASSERT_EQ( dem.get_detector_coordinates({0, 1, 2, 3}), (std::map>{ {0, {}}, {1, {1, 2, 3}}, {2, {}}, {3, {6, 2}}, })); ASSERT_THROW({ dem.get_detector_coordinates({4}); }, std::invalid_argument); } TEST(detector_error_model, final_detector_and_coord_shift) { DetectorErrorModel dem(R"MODEL( repeat 1000 { repeat 2000 { repeat 3000 { shift_detectors(0, 0, 1) 0 } shift_detectors(1) 2 } shift_detectors(0, 1) 0 } )MODEL"); ASSERT_EQ( dem.final_detector_and_coord_shift(), (std::pair>{4000000, {2000000, 1000, 6000000000}})); } TEST(detector_error_model, rounded) { DetectorErrorModel dem(R"DEM( error(0.01000002) D0 D1 repeat 2 { error(0.123456789) D1 D2 L3 } detector(0.0200000334,0.12345) D0 shift_detectors(5.0300004,0.12345) 3 )DEM"); ASSERT_EQ(dem.rounded(0), DetectorErrorModel(R"DEM( error(0) D0 D1 repeat 2 { error(0) D1 D2 L3 } detector(0.0200000334,0.12345) D0 shift_detectors(5.0300004,0.12345) 3 )DEM")); ASSERT_EQ(dem.rounded(1), DetectorErrorModel(R"DEM( error(0) D0 D1 repeat 2 { error(0.1) D1 D2 L3 } detector(0.0200000334,0.12345) D0 shift_detectors(5.0300004,0.12345) 3 )DEM")); ASSERT_EQ(dem.rounded(2), DetectorErrorModel(R"DEM( error(0.01) D0 D1 repeat 2 { error(0.12) D1 D2 L3 } detector(0.0200000334,0.12345) D0 shift_detectors(5.0300004,0.12345) 3 )DEM")); ASSERT_EQ(dem.rounded(3), DetectorErrorModel(R"DEM( error(0.010) D0 D1 repeat 2 { error(0.123) D1 D2 L3 } detector(0.0200000334,0.12345) D0 shift_detectors(5.0300004,0.12345) 3 )DEM")); } TEST(detector_error_model, surface_code_coords_dont_infinite_loop) { CircuitGenParameters params(7, 5, "rotated_memory_x"); params.after_clifford_depolarization = 0.01; params.before_measure_flip_probability = 0; params.after_reset_flip_probability = 0; params.before_round_data_depolarization = 0; auto circuit = generate_surface_code_circuit(params).circuit; auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); std::set filter; size_t n = dem.count_detectors(); for (size_t k = 0; k < n; k++) { filter.insert(k); } auto coords1 = dem.get_detector_coordinates(filter); auto coords2 = circuit.get_detector_coordinates(filter); ASSERT_EQ(coords1.size(), coords2.size()); ASSERT_EQ(coords1.size(), n); ASSERT_EQ(n, 168); } TEST(detector_error_model, flattened) { ASSERT_EQ(DetectorErrorModel().flattened(), DetectorErrorModel()); ASSERT_EQ( DetectorErrorModel(R"DEM( error(0.125) D0 D1 L0 )DEM") .flattened(), DetectorErrorModel(R"DEM( error(0.125) D0 D1 L0 )DEM")); ASSERT_EQ( DetectorErrorModel(R"DEM( error(0.125) D0 D1 L0 shift_detectors 5 )DEM") .flattened(), DetectorErrorModel(R"DEM( error(0.125) D0 D1 L0 )DEM")); ASSERT_EQ( DetectorErrorModel(R"DEM( shift_detectors 5 error(0.125) D0 D1 L0 )DEM") .flattened(), DetectorErrorModel(R"DEM( error(0.125) D5 D6 L0 )DEM")); ASSERT_EQ( DetectorErrorModel(R"DEM( detector(10, 20) D0 detector(10, 20, 30, 40) D1 logical_observable L0 shift_detectors(1, 2, 3) 5 detector(10, 20) D0 detector(10, 20, 30, 40) D1 logical_observable L1 )DEM") .flattened(), DetectorErrorModel(R"DEM( detector(10, 20) D0 detector(10, 20, 30, 40) D1 logical_observable L0 detector(11, 22) D5 detector(11, 22, 33, 40) D6 logical_observable L1 )DEM")); ASSERT_EQ( DetectorErrorModel(R"DEM( repeat 5 { error(0.125) D0 shift_detectors(3) 2 } detector(10, 20, 30, 40) D0 )DEM") .flattened(), DetectorErrorModel(R"DEM( error(0.125) D0 error(0.125) D2 error(0.125) D4 error(0.125) D6 error(0.125) D8 detector(25, 20, 30, 40) D10 )DEM")); } TEST(detector_error_model, parse_windows_newlines) { ASSERT_EQ( DetectorErrorModel("error(0.125) D0\r\ndetector(5) D10\r\n"), DetectorErrorModel("error(0.125) D0\r\ndetector(5) D10\r\n")); } ================================================ FILE: src/stim/dem/detector_error_model_pybind_test.py ================================================ # 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. import pathlib import tempfile import pytest import stim def test_init_get(): model = stim.DetectorErrorModel(""" error(0.125) D0 L0 ERROR(0.25) D0 ^ D1 repeat 100 { shift_detectors 1 error(0.125) D0 D1 } shift_detectors(1, 1.5, 2, 2.5) 1 shift_detectors 5 """) assert len(model) == 5 assert model[0] == stim.DemInstruction( "error", [0.125], [stim.target_relative_detector_id(0), stim.target_logical_observable_id(0)]) assert model[1] == stim.DemInstruction( "error", [0.25], [stim.target_relative_detector_id(0), stim.target_separator(), stim.target_relative_detector_id(1)]) assert model[2] == stim.DemRepeatBlock( 100, stim.DetectorErrorModel(""" shift_detectors 1 error(0.125) D0 D1 """)) assert model[3] == stim.DemInstruction( "shift_detectors", [1, 1.5, 2, 2.5], [1]) assert model[4] == stim.DemInstruction( "shift_detectors", [], [5]) def test_equality(): assert stim.DetectorErrorModel() == stim.DetectorErrorModel() assert not (stim.DetectorErrorModel() != stim.DetectorErrorModel()) assert not (stim.DetectorErrorModel() == stim.DetectorErrorModel("error(0.125) D0")) assert stim.DetectorErrorModel() != stim.DetectorErrorModel("error(0.125) D0") assert stim.DetectorErrorModel("error(0.125) D0") == stim.DetectorErrorModel("error(0.125) D0") assert stim.DetectorErrorModel("error(0.125) D0") != stim.DetectorErrorModel("error(0.126) D0") assert stim.DetectorErrorModel("error(0.125) D0") != stim.DetectorErrorModel("detector(0.125) D0") assert stim.DetectorErrorModel("error(0.125) D0") != stim.DetectorErrorModel("error(0.125) D1") assert stim.DetectorErrorModel("error(0.125) D0") != stim.DetectorErrorModel("error(0.125) L0") assert stim.DetectorErrorModel("error(0.125) D0") != stim.DetectorErrorModel("error(0.125) D0 D1") assert stim.DetectorErrorModel(""" REPEAT 3 { shift_detectors 4 } """) == stim.DetectorErrorModel(""" REPEAT 3 { shift_detectors 4 } """) assert stim.DetectorErrorModel(""" REPEAT 3 { shift_detectors 4 } """) != stim.DetectorErrorModel(""" REPEAT 4 { shift_detectors 4 } """) assert stim.DetectorErrorModel(""" REPEAT 3 { shift_detectors 4 } """) != stim.DetectorErrorModel(""" REPEAT 3 { shift_detectors 5 } """) def test_repr(): v = stim.DetectorErrorModel() assert eval(repr(v), {"stim": stim}) == v v = stim.DetectorErrorModel("error(0.125) D0 D1") assert eval(repr(v), {"stim": stim}) == v def test_approx_equals(): base = stim.DetectorErrorModel("error(0.099) D0") assert not base.approx_equals(stim.DetectorErrorModel("error(0.101) D0"), atol=0) assert not base.approx_equals(stim.DetectorErrorModel("error(0.101) D0"), atol=0.00001) assert base.approx_equals(stim.DetectorErrorModel("error(0.101) D0"), atol=0.01) assert base.approx_equals(stim.DetectorErrorModel("error(0.101) D0"), atol=999) assert not base.approx_equals(stim.DetectorErrorModel("error(0.101) D0 D1"), atol=999) assert not base.approx_equals(object(), atol=999) assert not base.approx_equals(stim.PauliString("XYZ"), atol=999) def test_append(): m = stim.DetectorErrorModel() m.append("error", 0.125, [ stim.DemTarget.relative_detector_id(1), ]) m.append("error", 0.25, [ stim.DemTarget.relative_detector_id(1), stim.DemTarget.separator(), stim.DemTarget.relative_detector_id(2), stim.DemTarget.logical_observable_id(3), ]) m.append("shift_detectors", (1, 2, 3), [5]) m += m * 3 m.append(m[0]) m.append(m[-2]) assert m == stim.DetectorErrorModel(""" error(0.125) D1 error(0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 repeat 3 { error(0.125) D1 error(0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } error(0.125) D1 repeat 3 { error(0.125) D1 error(0.25) D1 ^ D2 L3 shift_detectors(1, 2, 3) 5 } """) def test_append_bad(): m = stim.DetectorErrorModel() m.append("error", 0.125, [stim.target_relative_detector_id(0)]) m.append("error", [0.125], [stim.target_relative_detector_id(0)]) m.append("shift_detectors", [], [5]) m += m * 3 with pytest.raises(ValueError, match=r"Bad target 'stim.DemTarget\('D0'\)' for instruction 'shift_detectors'"): m.append("shift_detectors", [0.125, 0.25], [stim.target_relative_detector_id(0)]) with pytest.raises(ValueError, match="takes 1 argument"): m.append("error", [0.125, 0.25], [stim.target_relative_detector_id(0)]) with pytest.raises(ValueError, match="Bad target '0' for instruction 'error'"): m.append("error", [0.125], [0]) with pytest.raises(ValueError, match="First argument"): m.append(None) with pytest.raises(ValueError, match="First argument"): m.append(object()) with pytest.raises(ValueError, match="Must specify.*instruction name"): m.append("error") with pytest.raises(ValueError, match="Can't specify.*instruction is a"): m.append(m[0], 0.125, []) with pytest.raises(ValueError, match="Can't specify.*instruction is a"): m.append(m[-1], 0.125, []) def test_pickle(): import pickle t = stim.DetectorErrorModel(""" repeat 100 { error(0.25) D0 L1 shift_detectors(1, 2) 3 } """) a = pickle.dumps(t) assert pickle.loads(a) == t def test_count_errors(): assert stim.DetectorErrorModel().num_errors == 0 assert stim.DetectorErrorModel(""" logical_observable L100 detector D100 shift_detectors(100, 100, 100) 100 error(0.125) D100 """).num_errors == 1 assert stim.DetectorErrorModel(""" error(0.125) D0 REPEAT 100 { REPEAT 5 { error(0.25) D1 } } """).num_errors == 501 def test_shortest_graphlike_error_trivial(): with pytest.raises(ValueError, match="any graphlike logical errors"): _ = stim.DetectorErrorModel().shortest_graphlike_error() with pytest.raises(ValueError, match="any graphlike logical errors"): _ = stim.DetectorErrorModel(""" error(0.1) D0 """).shortest_graphlike_error() with pytest.raises(ValueError, match="any graphlike logical errors"): _ = stim.DetectorErrorModel(""" error(0.1) D0 L0 """).shortest_graphlike_error() assert stim.DetectorErrorModel(""" error(0.1) L0 """).shortest_graphlike_error() == stim.DetectorErrorModel(""" error(1) L0 """) assert stim.DetectorErrorModel(""" error(0.1) D0 D1 L0 error(0.1) D0 D1 """).shortest_graphlike_error() == stim.DetectorErrorModel(""" error(1) D0 D1 error(1) D0 D1 L0 """) def test_shortest_graphlike_error_line(): assert stim.DetectorErrorModel(""" error(0.125) D0 error(0.125) D0 D1 error(0.125) D1 L55 error(0.125) D1 """).shortest_graphlike_error() == stim.DetectorErrorModel(""" error(1) D1 error(1) D1 L55 """) assert len(stim.DetectorErrorModel(""" error(0.1) D0 D1 L5 REPEAT 1000 { error(0.1) D0 D2 error(0.1) D1 D3 shift_detectors 2 } error(0.1) D0 error(0.1) D1 """).shortest_graphlike_error()) == 2003 def test_shortest_graphlike_error_ignore(): assert stim.DetectorErrorModel(""" error(0.125) D0 D1 D2 error(0.125) L0 """).shortest_graphlike_error(ignore_ungraphlike_errors=True) == stim.DetectorErrorModel(""" error(1) L0 """) def test_shortest_graphlike_error_rep_code(): circuit = stim.Circuit.generated("repetition_code:memory", rounds=10, distance=7, before_round_data_depolarization=0.01) model = circuit.detector_error_model(decompose_errors=True) assert len(model.shortest_graphlike_error()) == 7 def test_shortest_graphlike_error_msgs(): with pytest.raises(ValueError, match=r"NO OBSERVABLES(.|\n)*NO DETECTORS(.|\n)*NO ERRORS"): stim.Circuit().detector_error_model(decompose_errors=True).shortest_graphlike_error() c = stim.Circuit(""" M 0 OBSERVABLE_INCLUDE(0) rec[-1] """) with pytest.raises(ValueError, match=r"NO DETECTORS(.|\n)*NO ERRORS"): c.detector_error_model(decompose_errors=True).shortest_graphlike_error() c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 """) with pytest.raises(ValueError, match=r"NO OBSERVABLES(.|\n)*NO DETECTORS(.|\n)*NO ERRORS"): c.detector_error_model(decompose_errors=True).shortest_graphlike_error() c = stim.Circuit(""" M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """) with pytest.raises(ValueError, match=r"NO ERRORS"): c.detector_error_model(decompose_errors=True).shortest_graphlike_error() c = stim.Circuit(""" X_ERROR(0.1) 0 M 0 DETECTOR rec[-1] """) with pytest.raises(ValueError, match=r"NO OBSERVABLES"): c.detector_error_model(decompose_errors=True).shortest_graphlike_error() def test_coords(): circuit = stim.Circuit(""" M 0 DETECTOR(1, 2, 3) rec[-1] REPEAT 3 { DETECTOR(2) rec[-1] SHIFT_COORDS(5) } """) dem = circuit.detector_error_model() assert dem.get_detector_coordinates() == { 0: [1, 2, 3], 1: [2], 2: [7], 3: [12], } assert circuit.get_detector_coordinates() == { 0: [1, 2, 3], 1: [2], 2: [7], 3: [12], } assert dem.get_detector_coordinates([1]) == { 1: [2], } assert circuit.get_detector_coordinates([1]) == { 1: [2], } assert dem.get_detector_coordinates(1) == { 1: [2], } assert circuit.get_detector_coordinates(1) == { 1: [2], } assert dem.get_detector_coordinates({1}) == { 1: [2], } assert circuit.get_detector_coordinates({1}) == { 1: [2], } assert dem.get_detector_coordinates(stim.DemTarget.relative_detector_id(1)) == { 1: [2], } assert circuit.get_detector_coordinates(stim.DemTarget.relative_detector_id(1)) == { 1: [2], } assert dem.get_detector_coordinates((stim.DemTarget.relative_detector_id(1),)) == { 1: [2], } assert circuit.get_detector_coordinates((stim.DemTarget.relative_detector_id(1),)) == { 1: [2], } assert dem.get_detector_coordinates(only=[2, 3]) == { 2: [7], 3: [12], } assert circuit.get_detector_coordinates(only=[2, 3]) == { 2: [7], 3: [12], } with pytest.raises(ValueError, match="Expected a detector id"): dem.get_detector_coordinates([-1]) with pytest.raises(ValueError, match="too big"): dem.get_detector_coordinates([500]) with pytest.raises(ValueError, match="Expected a detector id"): circuit.get_detector_coordinates([-1]) with pytest.raises(ValueError, match="too big"): circuit.get_detector_coordinates([500]) def test_dem_from_file(): with tempfile.TemporaryDirectory() as tmpdir: path = tmpdir + '/tmp.stim' with open(path, 'w') as f: print('error(0.125) D0 L5', file=f) assert stim.DetectorErrorModel.from_file(path) == stim.DetectorErrorModel('error(0.125) D0 L5') with tempfile.TemporaryDirectory() as tmpdir: path = pathlib.Path(tmpdir) / 'tmp.stim' with open(path, 'w') as f: print('error(0.125) D0 L5', file=f) assert stim.DetectorErrorModel.from_file(path) == stim.DetectorErrorModel('error(0.125) D0 L5') with tempfile.TemporaryDirectory() as tmpdir: path = tmpdir + '/tmp.stim' with open(path, 'w') as f: print('error(0.125) D0 L5', file=f) with open(path) as f: assert stim.DetectorErrorModel.from_file(f) == stim.DetectorErrorModel('error(0.125) D0 L5') with pytest.raises(ValueError, match="how to read"): stim.DetectorErrorModel.from_file(object()) with pytest.raises(ValueError, match="how to read"): stim.DetectorErrorModel.from_file(123) def test_dem_to_file(): c = stim.DetectorErrorModel('error(0.125) D0 L5\n') with tempfile.TemporaryDirectory() as tmpdir: path = tmpdir + '/tmp.stim' c.to_file(path) with open(path) as f: assert f.read() == 'error(0.125) D0 L5\n' with tempfile.TemporaryDirectory() as tmpdir: path = pathlib.Path(tmpdir) / 'tmp.stim' c.to_file(path) with open(path) as f: assert f.read() == 'error(0.125) D0 L5\n' with tempfile.TemporaryDirectory() as tmpdir: path = tmpdir + '/tmp.stim' with open(path, 'w') as f: c.to_file(f) with open(path) as f: assert f.read() == 'error(0.125) D0 L5\n' with pytest.raises(ValueError, match="how to write"): c.to_file(object()) with pytest.raises(ValueError, match="how to write"): c.to_file(123) def test_flattened(): dem = stim.DetectorErrorModel(""" shift_detectors 5 repeat 2 { error(0.125) D0 D1 } """) assert dem.flattened() == stim.DetectorErrorModel(""" error(0.125) D5 D6 error(0.125) D5 D6 """) def test_rounded(): dem = stim.DetectorErrorModel(""" error(0.1248) D0 D1 """) assert dem.rounded(1) == stim.DetectorErrorModel(""" error(0.1) D0 D1 """) assert dem.rounded(2) == stim.DetectorErrorModel(""" error(0.12) D0 D1 """) assert dem.rounded(3) == stim.DetectorErrorModel(""" error(0.125) D0 D1 """) assert dem.rounded(4) == stim.DetectorErrorModel(""" error(0.1248) D0 D1 """) assert dem.rounded(5) == stim.DetectorErrorModel(""" error(0.1248) D0 D1 """) dem = stim.DetectorErrorModel(""" error(0.01248) D0 D1 """) assert dem.rounded(1) == stim.DetectorErrorModel(""" error(0) D0 D1 """) assert dem.rounded(2) == stim.DetectorErrorModel(""" error(0.01) D0 D1 """) assert dem.rounded(3) == stim.DetectorErrorModel(""" error(0.012) D0 D1 """) assert dem.rounded(4) == stim.DetectorErrorModel(""" error(0.0125) D0 D1 """) def test_diagram(): circuit = stim.Circuit.generated("repetition_code:memory", rounds=10, distance=7, before_round_data_depolarization=0.01) dem = circuit.detector_error_model(decompose_errors=True) assert dem.diagram("matchgraph-svg") is not None assert dem.diagram("matchgraph-3d") is not None assert dem.diagram("matchgraph-3d-html") is not None assert dem.diagram("match-graph-svg") is not None assert dem.diagram(type="match-graph-svg") is not None assert dem.diagram(type="match-graph-3d") is not None assert dem.diagram(type="match-graph-3d-html") is not None assert "iframe" in str(dem.diagram(type="match-graph-svg-html")) def test_shortest_graphlike_error_remnant(): c = stim.Circuit(""" X_ERROR(0.125) 0 1 2 3 4 5 6 7 10 E(0.125) X2 X3 X10 M 0 1 2 3 4 5 6 7 10 OBSERVABLE_INCLUDE(0) rec[-2] DETECTOR rec[-1] DETECTOR rec[-2] rec[-3] DETECTOR rec[-3] rec[-4] DETECTOR rec[-4] rec[-5] DETECTOR rec[-5] rec[-6] DETECTOR rec[-6] rec[-7] DETECTOR rec[-7] rec[-8] DETECTOR rec[-8] rec[-9] """) d = stim.DetectorErrorModel(""" error(0.125) D0 error(0.125) D0 ^ D4 D6 error(0.125) D1 D2 error(0.125) D1 L0 error(0.125) D2 D3 error(0.125) D3 D4 error(0.125) D4 D5 error(0.125) D5 D6 error(0.125) D6 D7 error(0.125) D7 """) assert c.detector_error_model(decompose_errors=True) == d assert len(c.shortest_graphlike_error(ignore_ungraphlike_errors=False)) == 7 assert len(d.shortest_graphlike_error(ignore_ungraphlike_errors=False)) == 7 assert len(c.shortest_graphlike_error(ignore_ungraphlike_errors=True)) == 8 assert len(d.shortest_graphlike_error(ignore_ungraphlike_errors=True)) == 8 assert len(c.shortest_graphlike_error()) == 8 assert len(d.shortest_graphlike_error()) == 8 def test_init_parse(): assert stim.DemInstruction("error(0.125) D0 D1") == stim.DemInstruction("error", [0.125], [stim.DemTarget("D0"), stim.DemTarget("D1")]) def test_without_tags(): dem = stim.DetectorErrorModel(""" error[tag](0.25) D5 """) assert dem.without_tags() == stim.DetectorErrorModel(""" error(0.25) D5 """) def test_append_dem_to_dem(): dem = stim.DetectorErrorModel(""" error(0.25) D0 """) dem.append(stim.DetectorErrorModel(""" error(0.125) D1 error(0.25) D2 """)) assert dem == stim.DetectorErrorModel(""" error(0.25) D0 error(0.125) D1 error(0.25) D2 """) ================================================ FILE: src/stim/dem/detector_error_model_repeat_block.pybind.cc ================================================ // 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. #include "stim/dem/detector_error_model_repeat_block.pybind.h" #include "stim/dem/dem_instruction.pybind.h" #include "stim/dem/detector_error_model.pybind.h" #include "stim/py/base.pybind.h" using namespace stim; using namespace stim_pybind; pybind11::class_ stim_pybind::pybind_detector_error_model_repeat_block(pybind11::module &m) { return pybind11::class_( m, "DemRepeatBlock", clean_doc_string(R"DOC( A repeat block from a detector error model. Examples: >>> import stim >>> model = stim.DetectorErrorModel(''' ... repeat 100 { ... error(0.125) D0 D1 ... shift_detectors 1 ... } ... ''') >>> model[0] stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' error(0.125) D0 D1 shift_detectors 1 ''')) )DOC") .data()); } void stim_pybind::pybind_detector_error_model_repeat_block_methods( pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init(), pybind11::arg("repeat_count"), pybind11::arg("block"), clean_doc_string(R"DOC( Creates a stim.DemRepeatBlock. Args: repeat_count: The number of times the repeat block's body is supposed to execute. block: The body of the repeat block as a DetectorErrorModel containing the instructions to repeat. Examples: >>> import stim >>> repeat_block = stim.DemRepeatBlock(100, stim.DetectorErrorModel(''' ... error(0.125) D0 D1 ... shift_detectors 1 ... ''')) )DOC") .data()); c.def_readonly( "repeat_count", &ExposedDemRepeatBlock::repeat_count, "The number of times the repeat block's body is supposed to execute."); c.def( "body_copy", &ExposedDemRepeatBlock::body_copy, clean_doc_string(R"DOC( Returns a copy of the block's body, as a stim.DetectorErrorModel. Examples: >>> import stim >>> body = stim.DetectorErrorModel(''' ... error(0.125) D0 D1 ... shift_detectors 1 ... ''') >>> repeat_block = stim.DemRepeatBlock(100, body) >>> repeat_block.body_copy() == body True >>> repeat_block.body_copy() is repeat_block.body_copy() False )DOC") .data()); c.def_property_readonly( "type", [](const ExposedDemRepeatBlock &self) -> pybind11::object { return pybind11::cast("repeat"); }, clean_doc_string(R"DOC( Returns the type name "repeat". This is a duck-typing convenience method. It exists so that code that doesn't know whether it has a `stim.DemInstruction` or a `stim.DemRepeatBlock` can check the type field without having to do an `instanceof` check first. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0.1) D0 L0 ... repeat 5 { ... error(0.1) D0 D1 ... shift_detectors 1 ... } ... logical_observable L0 ... ''') >>> [instruction.type for instruction in dem] ['error', 'repeat', 'logical_observable'] )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two repeat blocks are identical."); c.def(pybind11::self != pybind11::self, "Determines if two repeat blocks are different."); c.def( "__repr__", &ExposedDemRepeatBlock::repr, "Returns text that is a valid python expression evaluating to an equivalent `stim.DemRepeatBlock`."); } stim::DetectorErrorModel ExposedDemRepeatBlock::body_copy() { return body; } std::string ExposedDemRepeatBlock::repr() const { std::stringstream out; out << "stim.DemRepeatBlock(" << repeat_count << ", " << detector_error_model_repr(body); if (!tag.empty()) { out << ", tag=" << pybind11::cast(pybind11::repr(pybind11::cast(tag))); } out << ")"; return out.str(); } bool ExposedDemRepeatBlock::operator==(const ExposedDemRepeatBlock &other) const { return repeat_count == other.repeat_count && body == other.body && tag == other.tag; } bool ExposedDemRepeatBlock::operator!=(const ExposedDemRepeatBlock &other) const { return !(*this == other); } ================================================ FILE: src/stim/dem/detector_error_model_repeat_block.pybind.h ================================================ // 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. #ifndef _STIM_DEM_DETECTOR_ERROR_MODEL_REPEAT_BLOCK_PYBIND_H #define _STIM_DEM_DETECTOR_ERROR_MODEL_REPEAT_BLOCK_PYBIND_H #include #include "stim/dem/detector_error_model.h" namespace stim_pybind { struct ExposedDemRepeatBlock { uint64_t repeat_count; stim::DetectorErrorModel body; std::string tag; stim::DetectorErrorModel body_copy(); std::string repr() const; bool operator==(const ExposedDemRepeatBlock &other) const; bool operator!=(const ExposedDemRepeatBlock &other) const; }; pybind11::class_ pybind_detector_error_model_repeat_block(pybind11::module &m); void pybind_detector_error_model_repeat_block_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/dem/detector_error_model_repeat_block_pybind_test.py ================================================ # 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. import stim def test_init_vs_properties(): v = stim.DemRepeatBlock(5, stim.DetectorErrorModel('error(0.125) D1 L2')) assert v.repeat_count == 5 assert v.body_copy() == stim.DetectorErrorModel('error(0.125) D1 L2') assert v.body_copy() is not v.body_copy() def test_equality(): m0 = stim.DetectorErrorModel() m1 = stim.DetectorErrorModel('error(0.125) D1 L2') assert stim.DemRepeatBlock(5, m0) == stim.DemRepeatBlock(5, m0) assert not (stim.DemRepeatBlock(5, m0) != stim.DemRepeatBlock(5, m0)) assert stim.DemRepeatBlock(5, m0) != stim.DemRepeatBlock(5, m1) assert not (stim.DemRepeatBlock(5, m0) == stim.DemRepeatBlock(5, m1)) assert stim.DemRepeatBlock(5, m0) != stim.DemRepeatBlock(6, m0) def test_repr(): v = stim.DemRepeatBlock(5, stim.DetectorErrorModel('error(0.125) D1 L2')) assert eval(repr(v), {"stim": stim}) == v def test_type(): assert [e.type for e in stim.DetectorErrorModel(''' detector D0 REPEAT 5 { error(0.1) D0 } logical_observable L0 ''')] == ['detector', 'repeat', 'logical_observable'] ================================================ FILE: src/stim/dem/detector_error_model_target.pybind.cc ================================================ // 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. #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/dem/detector_error_model.pybind.h" #include "stim/py/base.pybind.h" #include "stim/util_bot/arg_parse.h" using namespace stim; using namespace stim_pybind; pybind11::class_ stim_pybind::pybind_detector_error_model_target(pybind11::module &m) { return pybind11::class_( m, "DemTarget", "An instruction target from a detector error model (.dem) file."); } void stim_pybind::pybind_detector_error_model_target_methods( pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init([](const pybind11::object &arg) -> ExposedDemTarget { if (pybind11::isinstance(arg)) { return pybind11::cast(arg); } if (pybind11::isinstance(arg)) { std::string_view contents = pybind11::cast(arg); return DemTarget::from_text(contents); } std::stringstream ss; ss << "Don't know how to convert this into a stim.DemTarget: "; ss << pybind11::repr(arg); throw pybind11::type_error(ss.str()); }), pybind11::arg("arg"), pybind11::pos_only(), clean_doc_string(R"DOC( Creates a stim.DemTarget from the given object. Args: arg: A string to parse as a stim.DemTarget, or some other object to convert into a stim.DemTarget. Examples: >>> import stim >>> stim.DemTarget("D5") == stim.target_relative_detector_id(5) True >>> stim.DemTarget("L2") == stim.target_logical_observable_id(2) True >>> stim.DemTarget("^") == stim.target_separator() True )DOC") .data()); m.def( "target_relative_detector_id", &ExposedDemTarget::relative_detector_id, pybind11::arg("index"), clean_doc_string(R"DOC( Returns a relative detector id (e.g. "D5" in a .dem file). Args: index: The index of the detector, relative to the current detector offset. Returns: The relative detector target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_relative_detector_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D13 ''') )DOC") .data()); m.def( "target_logical_observable_id", &ExposedDemTarget::observable_id, pybind11::arg("index"), clean_doc_string(R"DOC( Returns a logical observable id identifying a frame change. Args: index: The index of the observable. Returns: The logical observable target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_logical_observable_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) L13 ''') )DOC") .data()); m.def( "target_separator", &ExposedDemTarget::separator, clean_doc_string(R"DOC( Returns a target separator (e.g. "^" in a .dem file). Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.target_relative_detector_id(1), ... stim.target_separator(), ... stim.target_relative_detector_id(2), ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D1 ^ D2 ''') )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two `stim.DemTarget`s are identical."); c.def(pybind11::self != pybind11::self, "Determines if two `stim.DemTarget`s are different."); c.def_static( "relative_detector_id", &ExposedDemTarget::relative_detector_id, pybind11::arg("index"), clean_doc_string(R"DOC( Returns a relative detector id (e.g. "D5" in a .dem file). Args: index: The index of the detector, relative to the current detector offset. Returns: The relative detector target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D13 ''') )DOC") .data()); c.def_static( "logical_observable_id", &ExposedDemTarget::observable_id, pybind11::arg("index"), clean_doc_string(R"DOC( Returns a logical observable id identifying a frame change. Args: index: The index of the observable. Returns: The logical observable target. Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.logical_observable_id(13) ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) L13 ''') )DOC") .data()); c.def_static( "separator", &ExposedDemTarget::separator, clean_doc_string(R"DOC( Returns a target separator (e.g. "^" in a .dem file). Examples: >>> import stim >>> m = stim.DetectorErrorModel() >>> m.append("error", 0.25, [ ... stim.DemTarget.relative_detector_id(1), ... stim.DemTarget.separator(), ... stim.DemTarget.relative_detector_id(2), ... ]) >>> print(repr(m)) stim.DetectorErrorModel(''' error(0.25) D1 ^ D2 ''') )DOC") .data()); c.def( "__repr__", &ExposedDemTarget::repr, "Returns valid python code evaluating to an equivalent `stim.DemTarget`."); c.def("__str__", &ExposedDemTarget::str, "Returns a text description of the detector error model target."); c.def( "is_relative_detector_id", &ExposedDemTarget::is_relative_detector_id, clean_doc_string(R"DOC( Determines if the detector error model target is a relative detector id target. In a detector error model file, detectors are prefixed by `D`. For example, in `error(0.25) D0 L1` the `D0` is a relative detector target. Examples: >>> import stim >>> stim.DemTarget("L2").is_relative_detector_id() False >>> stim.DemTarget("D3").is_relative_detector_id() True >>> stim.DemTarget("^").is_relative_detector_id() False )DOC") .data()); c.def( "is_logical_observable_id", &ExposedDemTarget::is_observable_id, clean_doc_string(R"DOC( Determines if the detector error model target is a logical observable id target. In a detector error model file, observable targets are prefixed by `L`. For example, in `error(0.25) D0 L1` the `L1` is an observable target. Examples: >>> import stim >>> stim.DemTarget("L2").is_logical_observable_id() True >>> stim.DemTarget("D3").is_logical_observable_id() False >>> stim.DemTarget("^").is_logical_observable_id() False )DOC") .data()); c.def_property_readonly( "val", &ExposedDemTarget::val, clean_doc_string(R"DOC( Returns the target's integer value. Example: >>> import stim >>> stim.DemTarget("D5").val 5 >>> stim.DemTarget("L6").val 6 )DOC") .data()); c.def( "is_separator", &ExposedDemTarget::is_separator, clean_doc_string(R"DOC( Determines if the detector error model target is a separator. Separates separate the components of a suggested decompositions within an error. For example, the `^` in `error(0.25) D1 D2 ^ D3 D4` is the separator. Examples: >>> import stim >>> stim.DemTarget("L2").is_separator() False >>> stim.DemTarget("D3").is_separator() False >>> stim.DemTarget("^").is_separator() True )DOC") .data()); c.def("__hash__", [](const ExposedDemTarget &self) { return pybind11::hash(pybind11::make_tuple("DemInstruction", self.data)); }); } std::string ExposedDemTarget::repr() const { std::stringstream out; if (is_relative_detector_id()) { out << "stim.DemTarget('D" << raw_id() << "')"; } else if (is_separator()) { out << "stim.target_separator()"; } else { out << "stim.DemTarget('L" << raw_id() << "')"; } return out.str(); } ExposedDemTarget::ExposedDemTarget(DemTarget target) : DemTarget(target) { } ExposedDemTarget ExposedDemTarget::observable_id(uint32_t id) { return {DemTarget::observable_id(id)}; } ExposedDemTarget ExposedDemTarget::relative_detector_id(uint64_t id) { return {DemTarget::relative_detector_id(id)}; } ExposedDemTarget ExposedDemTarget::separator() { return ExposedDemTarget(DemTarget::separator()); } stim::DemTarget ExposedDemTarget::internal() const { return {data}; } ================================================ FILE: src/stim/dem/detector_error_model_target.pybind.h ================================================ // 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. #ifndef _STIM_DEM_DETECTOR_ERROR_MODEL_TARGET_PYBIND_H #define _STIM_DEM_DETECTOR_ERROR_MODEL_TARGET_PYBIND_H #include #include "stim/dem/detector_error_model.h" namespace stim_pybind { struct ExposedDemTarget : stim::DemTarget { std::string repr() const; ExposedDemTarget(stim::DemTarget target); stim::DemTarget internal() const; static ExposedDemTarget observable_id(uint32_t id); static ExposedDemTarget relative_detector_id(uint64_t id); static ExposedDemTarget separator(); }; pybind11::class_ pybind_detector_error_model_target(pybind11::module &m); void pybind_detector_error_model_target_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/dem/detector_error_model_target_pybind_test.py ================================================ # 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. import pytest import stim def test_equality(): assert stim.target_relative_detector_id(5) == stim.target_relative_detector_id(5) assert not (stim.target_relative_detector_id(5) != stim.target_relative_detector_id(5)) assert stim.target_relative_detector_id(4) != stim.target_relative_detector_id(5) assert not (stim.target_relative_detector_id(4) == stim.target_relative_detector_id(5)) assert stim.target_relative_detector_id(5) != stim.target_logical_observable_id(5) assert stim.target_logical_observable_id(5) == stim.target_logical_observable_id(5) assert stim.target_relative_detector_id(5) != stim.target_separator() assert stim.target_separator() == stim.target_separator() def test_str(): assert str(stim.target_relative_detector_id(5)) == "D5" assert str(stim.target_logical_observable_id(6)) == "L6" assert str(stim.target_separator()) == "^" def test_properties(): assert stim.target_relative_detector_id(6).val == 6 assert stim.target_relative_detector_id(5).val == 5 assert stim.target_relative_detector_id(5).is_relative_detector_id() assert not stim.target_relative_detector_id(5).is_logical_observable_id() assert not stim.target_relative_detector_id(5).is_separator() assert stim.target_logical_observable_id(6).val == 6 assert stim.target_logical_observable_id(5).val == 5 assert not stim.target_logical_observable_id(5).is_relative_detector_id() assert stim.target_logical_observable_id(5).is_logical_observable_id() assert not stim.target_logical_observable_id(5).is_separator() assert not stim.target_separator().is_relative_detector_id() assert not stim.target_separator().is_logical_observable_id() assert stim.target_separator().is_separator() with pytest.raises(ValueError, match="Separator"): _ = stim.target_separator().val def test_repr(): v = stim.target_relative_detector_id(5) assert eval(repr(v), {"stim": stim}) == v v = stim.target_logical_observable_id(6) assert eval(repr(v), {"stim": stim}) == v v = stim.target_separator() assert eval(repr(v), {"stim": stim}) == v def test_static_constructors(): assert stim.DemTarget.relative_detector_id(5) == stim.target_relative_detector_id(5) assert stim.DemTarget.logical_observable_id(5) == stim.target_logical_observable_id(5) assert stim.DemTarget.separator() == stim.target_separator() def test_hashable(): a = stim.DemTarget.relative_detector_id(3) b = stim.DemTarget.logical_observable_id(5) c = stim.DemTarget.relative_detector_id(3) assert hash(a) == hash(c) assert len({a, b, c}) == 2 def test_init(): assert stim.DemTarget("D0") == stim.target_relative_detector_id(0) assert stim.DemTarget("D5") == stim.target_relative_detector_id(5) assert stim.DemTarget("L0") == stim.target_logical_observable_id(0) assert stim.DemTarget("L5") == stim.target_logical_observable_id(5) assert stim.DemTarget("^") == stim.target_separator() assert stim.DemTarget(f"D{2**62 - 1}") == stim.target_relative_detector_id(2**62 - 1) assert stim.DemTarget(f"L{0xFFFFFFFF}") == stim.target_logical_observable_id(0xFFFFFFFF) with pytest.raises(ValueError, match="Failed to parse"): _ = stim.DemTarget(f"D{2**62}") with pytest.raises(ValueError, match="Failed to parse"): _ = stim.DemTarget(f"L{0x100000000}") with pytest.raises(ValueError, match="Failed to parse"): _ = stim.DemTarget(f"L-1") with pytest.raises(ValueError, match="Failed to parse"): _ = stim.DemTarget(f"X5") with pytest.raises(ValueError, match="Failed to parse"): _ = stim.DemTarget(f"5") ================================================ FILE: src/stim/diagram/ascii_diagram.cc ================================================ #include "stim/diagram/ascii_diagram.h" #include #include "stim/mem/span_ref.h" using namespace stim; using namespace stim_draw_internal; /// Describes sizes and offsets within a diagram with variable-sized columns and rows. struct AsciiLayout { size_t num_x; size_t num_y; std::vector x_spans; std::vector y_spans; std::vector x_offsets; std::vector y_offsets; }; AsciiDiagramPos::AsciiDiagramPos(size_t x, size_t y, float align_x, float align_y) : x(x), y(y), align_x(align_x), align_y(align_y) { } bool AsciiDiagramPos::operator==(const AsciiDiagramPos &other) const { return x == other.x && y == other.y; } bool AsciiDiagramPos::operator<(const AsciiDiagramPos &other) const { if (x != other.x) { return x < other.x; } return y < other.y; } AsciiDiagramEntry::AsciiDiagramEntry(AsciiDiagramPos center, std::string label) : center(center), label(label) { } void AsciiDiagram::add_entry(AsciiDiagramEntry entry) { cells.insert({entry.center, entry}); } void AsciiDiagram::for_each_pos(const std::function &callback) const { for (const auto &item : cells) { callback(item.first); } for (const auto &item : lines) { callback(item.first); callback(item.second); } } AsciiLayout compute_sizing(const AsciiDiagram &diagram) { AsciiLayout layout{0, 0, {}, {}, {}, {}}; diagram.for_each_pos([&](AsciiDiagramPos pos) { layout.num_x = std::max(layout.num_x, pos.x + 1); layout.num_y = std::max(layout.num_y, pos.y + 1); }); layout.x_spans.resize(layout.num_x, 1); layout.y_spans.resize(layout.num_y, 1); for (const auto &item : diagram.cells) { const auto &box = item.second; auto &dx = layout.x_spans[box.center.x]; auto &dy = layout.y_spans[box.center.y]; dx = std::max(dx, box.label.size()); dy = std::max(dy, (size_t)1); } layout.x_offsets.push_back(0); layout.y_offsets.push_back(0); for (const auto &e : layout.x_spans) { layout.x_offsets.push_back(layout.x_offsets.back() + e); } for (const auto &e : layout.y_spans) { layout.y_offsets.push_back(layout.y_offsets.back() + e); } return layout; } AsciiDiagramPos AsciiDiagramPos::transposed() const { return {y, x, align_y, align_x}; } AsciiDiagramEntry AsciiDiagramEntry::transposed() const { return {center.transposed(), label}; } AsciiDiagram AsciiDiagram::transposed() const { AsciiDiagram result; for (const auto &e : cells) { result.cells.insert({e.first.transposed(), e.second.transposed()}); } result.lines.reserve(lines.size()); for (const auto &e : lines) { result.lines.push_back({e.first.transposed(), e.second.transposed()}); } return result; } void strip_padding_from_lines_and_write_to(SpanRef out_lines, std::ostream &out) { // Strip spacing at end of lines and end of diagram. for (auto &line : out_lines) { while (!line.empty() && line.back() == ' ') { line.pop_back(); } } // Strip empty lines at start and end. while (!out_lines.empty() && out_lines.back().empty()) { out_lines.ptr_end--; } while (!out_lines.empty() && out_lines.front().empty()) { out_lines.ptr_start++; } // Find indentation. size_t indentation = SIZE_MAX; for (const auto &line : out_lines) { size_t k = 0; while (k < line.length() && line[k] == ' ') { k++; } indentation = std::min(indentation, k); } // Output while stripping empty lines at start of diagram. for (size_t k = 0; k < out_lines.size(); k++) { if (k) { out.put('\n'); } out.write(out_lines[k].data() + indentation, out_lines[k].size() - indentation); } } void AsciiDiagram::render(std::ostream &out) const { AsciiLayout layout = compute_sizing(*this); std::vector out_lines; out_lines.resize(layout.y_offsets.back()); for (auto &line : out_lines) { line.resize(layout.x_offsets.back(), ' '); } auto p_align = [&](size_t c0, size_t cn, float align) { if (align == 0.5f) { cn--; } return c0 + (int)floor(align * cn); }; auto x_align = [&](AsciiDiagramPos pos) { return p_align(layout.x_offsets[pos.x], layout.x_spans[pos.x], pos.align_x); }; auto y_align = [&](AsciiDiagramPos pos) { return p_align(layout.y_offsets[pos.y], layout.y_spans[pos.y], pos.align_y); }; for (const auto &line : lines) { auto &p1 = line.first; auto &p2 = line.second; auto x1 = x_align(p1); auto x2 = x_align(p2); auto y1 = y_align(p1); auto y2 = y_align(p2); if (x1 > x2) { std::swap(x1, x2); } if (y1 > y2) { std::swap(y1, y2); } bool bx = x1 != x2; while (x1 < x2) { out_lines[y1][x1] = '-'; x1++; } char next_char = bx ? '.' : '|'; while (y1 < y2) { out_lines[y1][x1] = next_char; next_char = '|'; y1++; } } for (const auto &item : cells) { const auto &box = item.second; auto x = layout.x_offsets[box.center.x]; auto y = layout.y_offsets[box.center.y]; x += (int)floor(box.center.align_x * (layout.x_spans[box.center.x] - box.label.size())); y += (int)floor(box.center.align_y * (layout.y_spans[box.center.y] - 1)); for (size_t k = 0; k < box.label.size(); k++) { out_lines[y][x + k] = box.label[k]; } } strip_padding_from_lines_and_write_to(out_lines, out); } std::string AsciiDiagram::str() const { std::stringstream ss; render(ss); return ss.str(); } std::ostream &stim_draw_internal::operator<<(std::ostream &out, const AsciiDiagram &drawer) { drawer.render(out); return out; } ================================================ FILE: src/stim/diagram/ascii_diagram.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_TIMELINE_ASCII_DIAGRAM_ASCII_DIAGRAM_H #define _STIM_DIAGRAM_TIMELINE_ASCII_DIAGRAM_ASCII_DIAGRAM_H #include #include #include #include #include namespace stim_draw_internal { /// Identifies a location within a cell in a diagram with variable-sized columns and rows. struct AsciiDiagramPos { size_t x; /// The column that the cell is in. size_t y; /// The row that the cell is in. float align_x; /// Identifies a pixel column within the cell, proportionally from left to right. float align_y; /// Identifies a pixel row within the cell, proportionally from top to bottom. AsciiDiagramPos(size_t x, size_t y, float align_x, float align_y); bool operator==(const AsciiDiagramPos &other) const; bool operator<(const AsciiDiagramPos &other) const; AsciiDiagramPos transposed() const; }; /// Describes what to draw within a cell of a diagram with variable-sized columns and rows. struct AsciiDiagramEntry { /// The location of the cell, and the alignment to use for the text. AsciiDiagramPos center; /// The text to write. std::string label; AsciiDiagramEntry(AsciiDiagramPos center, std::string label); AsciiDiagramEntry transposed() const; }; struct AsciiDiagram { /// What to draw in various cells. std::map cells; /// Lines to draw in between cells. std::vector> lines; void add_entry(AsciiDiagramEntry cell); void for_each_pos(const std::function &callback) const; AsciiDiagram transposed() const; void render(std::ostream &out) const; std::string str() const; }; std::ostream &operator<<(std::ostream &out, const AsciiDiagram &drawer); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/ascii_diagram.test.cc ================================================ // 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. #include "stim/diagram/ascii_diagram.h" #include "gtest/gtest.h" using namespace stim_draw_internal; TEST(ascii_diagram, basic) { AsciiDiagram diagram; diagram.add_entry( AsciiDiagramEntry{ AsciiDiagramPos{0, 0, 0.5, 0.5}, {"ABC"}, }); ASSERT_EQ("\n" + diagram.str() + "\n", R"DIAGRAM( ABC )DIAGRAM"); diagram.add_entry( AsciiDiagramEntry{ AsciiDiagramPos{2, 0, 0.5, 0.5}, {"DE"}, }); ASSERT_EQ("\n" + diagram.str() + "\n", R"DIAGRAM( ABC DE )DIAGRAM"); diagram.lines.push_back({ AsciiDiagramPos{0, 1, 1, 0.5}, AsciiDiagramPos{2, 1, 0, 0.5}, }); diagram.lines.push_back({ AsciiDiagramPos{0, 2, 0, 0.5}, AsciiDiagramPos{2, 2, 1, 0.5}, }); diagram.lines.push_back({ AsciiDiagramPos{1, 3, 0, 0.5}, AsciiDiagramPos{1, 3, 1, 0.5}, }); diagram.lines.push_back({ AsciiDiagramPos{1, 4, 1, 0.5}, AsciiDiagramPos{1, 4, 0, 0.5}, }); ASSERT_EQ("\n" + diagram.str() + "\n", R"DIAGRAM( ABC DE - ------ - - )DIAGRAM"); } ================================================ FILE: src/stim/diagram/base64.cc ================================================ #include "stim/diagram/base64.h" using namespace stim_draw_internal; char u6_to_base64_char(uint8_t v) { if (v < 26) { return 'A' + v; } else if (v < 52) { return 'a' + v - 26; } else if (v < 62) { return '0' + v - 52; } else if (v == 62) { return '+'; } else { return '/'; } } void stim_draw_internal::write_data_as_base64_to(std::string_view data, std::ostream &out) { uint32_t buf = 0; size_t bits_in_buf = 0; for (char c : data) { buf <<= 8; buf |= (uint8_t)c; bits_in_buf += 8; if (bits_in_buf == 24) { out << u6_to_base64_char((buf >> 18) & 0x3F); out << u6_to_base64_char((buf >> 12) & 0x3F); out << u6_to_base64_char((buf >> 6) & 0x3F); out << u6_to_base64_char((buf >> 0) & 0x3F); bits_in_buf = 0; buf = 0; } } if (bits_in_buf) { buf <<= (24 - bits_in_buf); out << u6_to_base64_char((buf >> 18) & 0x3F); out << u6_to_base64_char((buf >> 12) & 0x3F); out << (bits_in_buf == 8 ? '=' : u6_to_base64_char((buf >> 6) & 0x3F)); out << '='; } } ================================================ FILE: src/stim/diagram/base64.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_BASE64_H #define _STIM_DIAGRAM_BASE64_H #include #include namespace stim_draw_internal { void write_data_as_base64_to(std::string_view data, std::ostream &out); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/base64.test.cc ================================================ #include "stim/diagram/base64.h" #include "gtest/gtest.h" using namespace stim_draw_internal; TEST(base64, write_base64) { auto f = [](std::string_view c) { std::stringstream ss; write_data_as_base64_to(c, ss); return ss.str(); }; EXPECT_EQ(f("light work."), "bGlnaHQgd29yay4="); EXPECT_EQ(f("light work"), "bGlnaHQgd29yaw=="); EXPECT_EQ(f("light wor"), "bGlnaHQgd29y"); EXPECT_EQ(f("light wo"), "bGlnaHQgd28="); EXPECT_EQ(f("light w"), "bGlnaHQgdw=="); EXPECT_EQ(f(""), ""); EXPECT_EQ(f("f"), "Zg=="); EXPECT_EQ(f("fo"), "Zm8="); EXPECT_EQ(f("foo"), "Zm9v"); EXPECT_EQ(f("foob"), "Zm9vYg=="); EXPECT_EQ(f("fooba"), "Zm9vYmE="); EXPECT_EQ(f("foobar"), "Zm9vYmFy"); } ================================================ FILE: src/stim/diagram/basic_3d_diagram.cc ================================================ #include "stim/diagram/basic_3d_diagram.h" #include "gate_data_3d.h" using namespace stim; using namespace stim_draw_internal; GltfScene Basic3dDiagram::to_gltf_scene() const { GltfScene scene{{"everything"}, {}}; auto black_material = std::shared_ptr(new GltfMaterial{ {"black"}, {0, 0, 0, 1}, 1, 1, true, nullptr, }); auto red_material = std::shared_ptr(new GltfMaterial{ {"red"}, {1, 0, 0, 1}, 1, 1, true, nullptr, }); auto blue_material = std::shared_ptr(new GltfMaterial{ {"blue"}, {0, 0, 1, 1}, 1, 1, true, nullptr, }); auto purple_material = std::shared_ptr(new GltfMaterial{ {"purple"}, {1, 0, 1, 1}, 1, 1, true, nullptr, }); auto buf_scattered_lines = std::shared_ptr>(new GltfBuffer<3>{ {"buf_scattered_lines"}, line_data, }); auto buf_red_scattered_lines = std::shared_ptr>(new GltfBuffer<3>{ {"buf_red_scattered_lines"}, red_line_data, }); auto buf_blue_scattered_lines = std::shared_ptr>(new GltfBuffer<3>{ {"buf_blue_scattered_lines"}, blue_line_data, }); auto buf_purple_scattered_lines = std::shared_ptr>(new GltfBuffer<3>{ {"buf_purple_scattered_lines"}, purple_line_data, }); auto gate_data = make_gate_primitives(); for (const auto &g : elements) { auto p = gate_data.find(g.gate_piece); if (p == gate_data.end()) { throw std::invalid_argument("Basic3dDiagram unknown gate piece: " + std::string(g.gate_piece)); } scene.nodes.push_back( std::shared_ptr(new GltfNode{ {""}, p->second, g.center, })); } if (!buf_scattered_lines->vertices.empty()) { scene.nodes.push_back( std::shared_ptr(new GltfNode{ {"node_scattered_lines"}, std::shared_ptr(new GltfMesh{ {"mesh_scattered_lines"}, { std::shared_ptr(new GltfPrimitive{ {"primitive_scattered_lines"}, GL_LINES, buf_scattered_lines, nullptr, black_material, }), }, }), {0, 0, 0}, })); } if (!buf_red_scattered_lines->vertices.empty()) { scene.nodes.push_back( std::shared_ptr(new GltfNode{ {"node_red_scattered_lines"}, std::shared_ptr(new GltfMesh{ {"mesh_scattered_lines"}, { std::shared_ptr(new GltfPrimitive{ {"primitive_red_scattered_lines"}, GL_LINES, buf_red_scattered_lines, nullptr, red_material, }), }, }), {0, 0, 0}, })); } if (!buf_blue_scattered_lines->vertices.empty()) { scene.nodes.push_back( std::shared_ptr(new GltfNode{ {"node_blue_scattered_lines"}, std::shared_ptr(new GltfMesh{ {"mesh_scattered_lines"}, { std::shared_ptr(new GltfPrimitive{ {"primitive_blue_scattered_lines"}, GL_LINES, buf_blue_scattered_lines, nullptr, blue_material, }), }, }), {0, 0, 0}, })); } if (!buf_purple_scattered_lines->vertices.empty()) { scene.nodes.push_back( std::shared_ptr(new GltfNode{ {"node_purple_scattered_lines"}, std::shared_ptr(new GltfMesh{ {"mesh_scattered_lines"}, { std::shared_ptr(new GltfPrimitive{ {"primitive_purple_scattered_lines"}, GL_LINES, buf_purple_scattered_lines, nullptr, purple_material, }), }, }), {0, 0, 0}, })); } if (scene.nodes.empty()) { auto buf_message_lines = std::shared_ptr>(new GltfBuffer<3>{ {"buf_blue_scattered_lines"}, std::vector>{ // E {0, 0, 0}, {0, 2, 0}, {0, 2, 0}, {1, 2, 0}, {0, 1, 0}, {1, 1, 0}, {0, 0, 0}, {1, 0, 0}, // m {2, 1, 0}, {3, 1, 0}, {2, 0, 0}, {2, 1, 0}, {2.5, 0, 0}, {2.5, 1, 0}, {3, 0, 0}, {3, 1, 0}, // p {4, 1, 0}, {4, -1, 0}, {4, 1, 0}, {5, 1, 0}, {5, 1, 0}, {5, 0, 0}, {4, 0, 0}, {5, 0, 0}, // t {6, 0, 0}, {6, 2, 0}, {5.5, 1.5, 0}, {6.5, 1.5, 0}, // y {7, -1, 0}, {8, 1, 0}, {7, 1, 0}, {7.5, 0, 0}, }}); scene.nodes.push_back( std::shared_ptr(new GltfNode{ {"empty_message"}, std::shared_ptr(new GltfMesh{ {"mesh_scattered_lines"}, { std::shared_ptr(new GltfPrimitive{ {"primitive_blue_scattered_lines"}, GL_LINES, buf_message_lines, nullptr, red_material, }), }, }), {0, 0, 0}, })); } return scene; } ================================================ FILE: src/stim/diagram/basic_3d_diagram.h ================================================ /* * 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. */ #ifndef _STIM_DRAW_3D_DIAGRAM_3D_H #define _STIM_DRAW_3D_DIAGRAM_3D_H #include #include "stim/diagram/gltf.h" namespace stim_draw_internal { struct Basic3DElement { std::string gate_piece; Coord<3> center; }; struct Basic3dDiagram { std::vector elements; std::vector> line_data; std::vector> red_line_data; std::vector> blue_line_data; std::vector> purple_line_data; GltfScene to_gltf_scene() const; }; } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/circuit_timeline_helper.cc ================================================ #include "stim/diagram/circuit_timeline_helper.h" using namespace stim; using namespace stim_draw_internal; void CircuitTimelineHelper::skip_loop_iterations(const CircuitTimelineLoopData &loop_data, uint64_t skipped_reps) { if (loop_data.num_repetitions > 0) { vec_pad_add_mul(cur_coord_shift, loop_data.shift_per_iteration, skipped_reps); measure_offset += loop_data.measurements_per_iteration * skipped_reps; detector_offset += loop_data.detectors_per_iteration * skipped_reps; num_ticks_seen += loop_data.ticks_per_iteration * skipped_reps; } } void CircuitTimelineHelper::do_repeat_block(const Circuit &circuit, const CircuitInstruction &op) { const auto &body = op.repeat_block_body(circuit); CircuitTimelineLoopData loop_data{ op.repeat_block_rep_count(), body.count_measurements(), body.count_detectors(), body.count_ticks(), body.final_coord_shift(), }; cur_loop_nesting.push_back(loop_data); if (unroll_loops) { for (size_t k = 0; k < loop_data.num_repetitions; k++) { do_circuit(body); } } else { start_repeat_callback(loop_data); do_circuit(body); end_repeat_callback(loop_data); skip_loop_iterations(loop_data, loop_data.num_repetitions - 1); } cur_loop_nesting.pop_back(); } void CircuitTimelineHelper::do_atomic_operation( GateType gate_type, SpanRef args, SpanRef targets) { resolved_op_callback({gate_type, args, targets}); } void CircuitTimelineHelper::do_operation_with_target_combiners(const CircuitInstruction &op) { bool paired = GATE_DATA[op.gate_type].flags & GATE_TARGETS_PAIRS; size_t start = 0; while (start < op.targets.size()) { size_t end = start + 1; while (end < op.targets.size() && op.targets[end].is_combiner()) { end += 2; } if (paired) { end++; while (end < op.targets.size() && op.targets[end].is_combiner()) { end += 2; } } if (GATE_DATA[op.gate_type].flags & GATE_PRODUCES_RESULTS) { do_record_measure_result(op.targets[start].qubit_value()); } do_atomic_operation(op.gate_type, op.args, {&op.targets[start], &op.targets[end]}); start = end; } } void CircuitTimelineHelper::do_multi_qubit_atomic_operation(const CircuitInstruction &op) { do_atomic_operation(op.gate_type, op.args, op.targets); } void CircuitTimelineHelper::do_two_qubit_gate(const CircuitInstruction &op) { for (size_t k = 0; k < op.targets.size(); k += 2) { const GateTarget *p = &op.targets[k]; if (GATE_DATA[op.gate_type].flags & GATE_PRODUCES_RESULTS) { do_record_measure_result(p[0].qubit_value()); } do_atomic_operation(op.gate_type, op.args, {p, p + 2}); } } void CircuitTimelineHelper::do_single_qubit_gate(const CircuitInstruction &op) { for (const auto &t : op.targets) { if (GATE_DATA[op.gate_type].flags & GATE_PRODUCES_RESULTS) { do_record_measure_result(t.qubit_value()); } do_atomic_operation(op.gate_type, op.args, {&t}); } } GateTarget CircuitTimelineHelper::rec_to_qubit(const GateTarget &target) { return GateTarget::qubit(measure_index_to_qubit.get(measure_offset + (decltype(measure_offset))target.value())); } GateTarget CircuitTimelineHelper::pick_pseudo_target_representing_measurements(const CircuitInstruction &op) { for (const auto &t : op.targets) { if (t.is_qubit_target() || t.is_pauli_target()) { return t; } } // First check if coordinates prefix-match a qubit's coordinates. if (!op.args.empty()) { auto coords = shifted_coordinates_in_workspace(op.args); for (size_t q = 0; q < latest_qubit_coords.size(); q++) { SpanRef v = latest_qubit_coords[q]; if (!v.empty() && v.size() <= coords.size()) { SpanRef prefix = {coords.ptr_start, coords.ptr_start + v.size()}; if (prefix == v) { return GateTarget::qubit((uint32_t)q); } } } } // Otherwise fall back to picking the qubit of one of the targeted measurements. if (op.targets.empty()) { return GateTarget::qubit(0); } GateTarget pseudo_target = rec_to_qubit(op.targets[0]); for (const auto &t : op.targets) { GateTarget q = rec_to_qubit(t); if (q.value() < pseudo_target.value()) { pseudo_target = q; } } return pseudo_target; } SpanRef CircuitTimelineHelper::shifted_coordinates_in_workspace(SpanRef coords) { while (coord_workspace.size() < coords.size()) { coord_workspace.push_back(0); } for (size_t k = 0; k < coords.size(); k++) { coord_workspace[k] = coords[k]; if (k < cur_coord_shift.size()) { coord_workspace[k] += cur_coord_shift[k]; } } return {coord_workspace.data(), coord_workspace.data() + coords.size()}; } void CircuitTimelineHelper::do_detector(const CircuitInstruction &op) { GateTarget pseudo_target = pick_pseudo_target_representing_measurements(op); targets_workspace.clear(); targets_workspace.push_back(pseudo_target); targets_workspace.insert(targets_workspace.end(), op.targets.begin(), op.targets.end()); do_atomic_operation(op.gate_type, shifted_coordinates_in_workspace(op.args), targets_workspace); detector_offset++; } void CircuitTimelineHelper::do_observable_include(const CircuitInstruction &op) { GateTarget pseudo_target = pick_pseudo_target_representing_measurements(op); targets_workspace.clear(); targets_workspace.push_back(pseudo_target); targets_workspace.insert(targets_workspace.end(), op.targets.begin(), op.targets.end()); do_atomic_operation(op.gate_type, op.args, targets_workspace); } void CircuitTimelineHelper::do_qubit_coords(const CircuitInstruction &op) { for (const auto &target : op.targets) { auto shifted = shifted_coordinates_in_workspace(op.args); while (target.qubit_value() >= latest_qubit_coords.size()) { latest_qubit_coords.push_back({}); } auto &store = latest_qubit_coords[target.qubit_value()]; store.clear(); store.insert(store.begin(), shifted.begin(), shifted.end()); do_atomic_operation(op.gate_type, shifted, {&target}); } } void CircuitTimelineHelper::do_shift_coords(const CircuitInstruction &op) { vec_pad_add_mul(cur_coord_shift, op.args); } void CircuitTimelineHelper::do_record_measure_result(uint32_t target_qubit) { u64_workspace.clear(); for (const auto &e : cur_loop_nesting) { u64_workspace.push_back(e.measurements_per_iteration); } for (const auto &e : cur_loop_nesting) { u64_workspace.push_back(e.num_repetitions); } const uint64_t *p = u64_workspace.data(); auto n = cur_loop_nesting.size(); measure_index_to_qubit.set(measure_offset, {p, p + n}, {p + n, p + 2 * n}, target_qubit); measure_offset++; } void CircuitTimelineHelper::do_next_operation(const Circuit &circuit, const CircuitInstruction &op) { if (op.gate_type == GateType::REPEAT) { do_repeat_block(circuit, op); } else if (op.gate_type == GateType::DETECTOR) { do_detector(op); } else if (op.gate_type == GateType::OBSERVABLE_INCLUDE) { do_observable_include(op); } else if (op.gate_type == GateType::SHIFT_COORDS) { do_shift_coords(op); } else if (op.gate_type == GateType::E || op.gate_type == GateType::ELSE_CORRELATED_ERROR) { do_multi_qubit_atomic_operation(op); } else if (op.gate_type == GateType::QUBIT_COORDS) { do_qubit_coords(op); } else if (op.gate_type == GateType::TICK) { do_atomic_operation(op.gate_type, {}, {}); num_ticks_seen += 1; } else if (GATE_DATA[op.gate_type].flags & GATE_TARGETS_COMBINERS) { do_operation_with_target_combiners(op); } else if (GATE_DATA[op.gate_type].flags & GATE_TARGETS_PAIRS) { do_two_qubit_gate(op); } else { do_single_qubit_gate(op); } } void CircuitTimelineHelper::do_circuit(const Circuit &circuit) { for (const auto &op : circuit.operations) { do_next_operation(circuit, op); } } ================================================ FILE: src/stim/diagram/circuit_timeline_helper.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_TIMELINE_ASCII_DIAGRAM_CIRCUIT_WITH_RESOLVED_TIMELINE_INFO_H #define _STIM_DIAGRAM_TIMELINE_ASCII_DIAGRAM_CIRCUIT_WITH_RESOLVED_TIMELINE_INFO_H #include #include #include "lattice_map.h" #include "stim/circuit/circuit.h" namespace stim_draw_internal { struct CircuitTimelineLoopData { uint64_t num_repetitions; uint64_t measurements_per_iteration; uint64_t detectors_per_iteration; uint64_t ticks_per_iteration; std::vector shift_per_iteration; }; struct CircuitTimelineHelper; /// Changes w.r.t. normal circuit operations: /// There is no broadcasting. Single-qubit gates have a single target, etc. /// DETECTOR and OBSERVABLE_INCLUDE instructions start with a qubit pseudo-target as a location hint. /// All coordinates have the effects SHIFT_COORDS folded in. /// QUBIT_COORDS is treated as a single-qubit gate. Will only have one target. struct ResolvedTimelineOperation { stim::GateType gate_type; stim::SpanRef args; stim::SpanRef targets; }; struct CircuitTimelineHelper { std::function resolved_op_callback; std::function start_repeat_callback; std::function end_repeat_callback; std::vector cur_coord_shift; uint64_t measure_offset = 0; uint64_t detector_offset = 0; uint64_t num_ticks_seen = 0; bool unroll_loops = false; std::vector coord_workspace; std::vector u64_workspace; std::vector targets_workspace; std::vector> latest_qubit_coords; std::vector cur_loop_nesting; LatticeMap measure_index_to_qubit; void do_atomic_operation( const stim::GateType gate_type, stim::SpanRef args, stim::SpanRef targets); stim::GateTarget rec_to_qubit(const stim::GateTarget &target); stim::GateTarget pick_pseudo_target_representing_measurements(const stim::CircuitInstruction &op); void skip_loop_iterations(const CircuitTimelineLoopData &loop_data, uint64_t skipped_reps); void do_record_measure_result(uint32_t target_qubit); void do_repeat_block(const stim::Circuit &circuit, const stim::CircuitInstruction &op); void do_next_operation(const stim::Circuit &circuit, const stim::CircuitInstruction &op); void do_circuit(const stim::Circuit &circuit); void do_operation_with_target_combiners(const stim::CircuitInstruction &op); void do_multi_qubit_atomic_operation(const stim::CircuitInstruction &op); void do_two_qubit_gate(const stim::CircuitInstruction &op); void do_single_qubit_gate(const stim::CircuitInstruction &op); void do_detector(const stim::CircuitInstruction &op); void do_observable_include(const stim::CircuitInstruction &op); void do_shift_coords(const stim::CircuitInstruction &op); void do_qubit_coords(const stim::CircuitInstruction &op); stim::SpanRef shifted_coordinates_in_workspace(stim::SpanRef coords); }; } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/coord.h ================================================ #ifndef _STIM_DIAGRAM_COORD_H #define _STIM_DIAGRAM_COORD_H #include #include #include #include "stim/mem/span_ref.h" namespace stim_draw_internal { /// Coordinate data. Used for individual vertex positions and also UV texture coordinates. template struct Coord { std::array xyz; Coord &operator+=(const Coord &other) { for (size_t k = 0; k < DIM; k++) { xyz[k] += other.xyz[k]; } return *this; } Coord &operator-=(const Coord &other) { for (size_t k = 0; k < DIM; k++) { xyz[k] -= other.xyz[k]; } return *this; } Coord &operator/=(float f) { for (size_t k = 0; k < DIM; k++) { xyz[k] /= f; } return *this; } Coord &operator*=(float f) { for (size_t k = 0; k < DIM; k++) { xyz[k] *= f; } return *this; } Coord operator+(const Coord &other) const { Coord result = *this; result += other; return result; } Coord operator-(const Coord &other) const { Coord result = *this; result -= other; return result; } Coord operator/(float other) const { Coord result = *this; result /= other; return result; } Coord operator*(float other) const { Coord result = *this; result *= other; return result; } float dot(const Coord &other) const { float t = 0; for (size_t k = 0; k < DIM; k++) { t += xyz[k] * other.xyz[k]; } return t; } float norm2() const { return dot(*this); } float norm() const { return sqrtf(norm2()); } bool operator<(Coord other) const { for (size_t k = 0; k < DIM; k++) { if (xyz[k] != other.xyz[k]) { return xyz[k] < other.xyz[k]; } } return false; } bool operator==(Coord other) const { return xyz == other.xyz; } static std::pair, Coord> min_max(stim::SpanRef> coords) { if (coords.empty()) { return {{}, {}}; } Coord v_min; Coord v_max; for (size_t k = 0; k < DIM; k++) { v_min.xyz[k] = INFINITY; v_max.xyz[k] = -INFINITY; } for (const auto &v : coords) { for (size_t k = 0; k < DIM; k++) { v_min.xyz[k] = std::min(v_min.xyz[k], v.xyz[k]); v_max.xyz[k] = std::max(v_max.xyz[k], v.xyz[k]); } } return {v_min, v_max}; } }; template std::ostream &operator<<(std::ostream &out, const Coord &coord) { for (size_t k = 0; k < DIM; k++) { if (k) { out << ','; } out << coord.xyz[k]; } return out; } } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/coord.test.cc ================================================ #include "stim/diagram/coord.h" #include "gtest/gtest.h" #include "stim/diagram/ascii_diagram.h" using namespace stim_draw_internal; TEST(coord, arithmetic) { ASSERT_EQ((Coord<2>{2, 3} + Coord<2>{5, 7}), (Coord<2>{7, 10})); ASSERT_EQ((Coord<2>{2, 3} - Coord<2>{5, 7}), (Coord<2>{-3, -4})); ASSERT_EQ((Coord<2>{2, 3} * 5), (Coord<2>{10, 15})); ASSERT_EQ((Coord<2>{2, 3} / 8), (Coord<2>{0.25, 0.375})); ASSERT_EQ((Coord<2>{2, 3}.dot(Coord<2>{5, 7})), 2 * 5 + 3 * 7); ASSERT_EQ((Coord<2>{2, 3}.norm2()), 13); ASSERT_EQ((Coord<2>{3, 4}.norm()), 5); ASSERT_TRUE((Coord<2>{3, 4} == Coord<2>{3, 4})); ASSERT_FALSE((Coord<2>{3, 4} == Coord<2>{2, 4})); ASSERT_FALSE((Coord<2>{3, 4} == Coord<2>{3, 2})); ASSERT_FALSE((Coord<2>{3, 4} < Coord<2>{3, 4})); ASSERT_FALSE((Coord<2>{3, 4} < Coord<2>{2, 4})); ASSERT_TRUE((Coord<2>{3, 4} < Coord<2>{4, 4})); ASSERT_TRUE((Coord<2>{3, 4} < Coord<2>{3, 6})); ASSERT_FALSE((Coord<2>{3, 4} < Coord<2>{3, 2})); ASSERT_TRUE((Coord<2>{3, 4} < Coord<2>{10, 0})); ASSERT_FALSE((Coord<2>{3, 4} < Coord<2>{0, 10})); } TEST(coord, min_max) { std::vector> a{ {1, 10}, {10, 30}, {50, 5}, }; ASSERT_EQ((Coord<2>::min_max(a)), (std::pair, Coord<2>>{{1, 5}, {50, 30}})); } ================================================ FILE: src/stim/diagram/crumble.cc ================================================ #include "stim/diagram/crumble.h" #include "stim/diagram/crumble_data.h" using namespace stim; using namespace stim_draw_internal; void stim_draw_internal::write_crumble_html_with_preloaded_circuit(const Circuit &circuit, std::ostream &out) { auto html = make_crumble_html(); const char *indicator = "[[[DEFAULT_CIRCUIT_CONTENT_LITERAL]]]"; size_t start = html.find(indicator); out << html.substr(0, start); out << circuit; out << html.substr(start + strlen(indicator)); } ================================================ FILE: src/stim/diagram/crumble.h ================================================ #ifndef _STIM_DIAGRAM_CRUMBLE_H #define _STIM_DIAGRAM_CRUMBLE_H #include #include "stim/circuit/circuit.h" namespace stim_draw_internal { void write_crumble_html_with_preloaded_circuit(const stim::Circuit &circuit, std::ostream &out); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/crumble_data.cc ================================================ #include "stim/diagram/crumble_data.h" std::string stim_draw_internal::make_crumble_html() { std::string result; result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( Crumble )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( Crumble is a prototype stabilizer circuit editor.
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( Read the manual )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); return result; } ================================================ FILE: src/stim/diagram/crumble_data.h ================================================ #ifndef _STIM_DIAGRAM_CRUMBLE_DATA_H #define _STIM_DIAGRAM_CRUMBLE_DATA_H #include namespace stim_draw_internal { std::string make_crumble_html(); } #endif ================================================ FILE: src/stim/diagram/detector_slice/detector_slice_set.cc ================================================ #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/dem/detector_error_model.h" #include "stim/diagram/coord.h" #include "stim/diagram/diagram_util.h" #include "stim/diagram/timeline/timeline_ascii_drawer.h" #include "stim/simulators/error_analyzer.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_bot/str_util.h" constexpr float SLICE_WINDOW_GAP = 1.1f; using namespace stim; using namespace stim_draw_internal; template inline void write_key_val(std::ostream &out, const char *key, const T &val) { out << ' ' << key << "=\"" << val << "\""; } struct DetectorSliceSetComputer { SparseUnsignedRevFrameTracker tracker; uint64_t tick_cur; uint64_t first_yield_tick; uint64_t num_yield_ticks; std::set used_qubits; std::function on_tick_callback; DetectorSliceSetComputer(const Circuit &circuit, uint64_t first_yield_tick, uint64_t num_yield_ticks); bool process_block_rev(const Circuit &block); bool process_op_rev(const Circuit &parent, const CircuitInstruction &op); bool process_tick(); void process_undo_start_of_circuit(); }; bool DetectorSliceSetComputer::process_block_rev(const Circuit &block) { for (size_t k = block.operations.size(); k--;) { if (process_op_rev(block, block.operations[k])) { return true; } } return false; } bool DetectorSliceSetComputer::process_tick() { if (tick_cur >= first_yield_tick && tick_cur < first_yield_tick + num_yield_ticks) { on_tick_callback(); } tick_cur--; // Offset by 1 to go one tick further, and catch any anticommutation issues. return tick_cur + 1 < first_yield_tick; } void DetectorSliceSetComputer::process_undo_start_of_circuit() { tracker.undo_implicit_RZs_at_start_of_circuit(); } bool DetectorSliceSetComputer::process_op_rev(const Circuit &parent, const CircuitInstruction &op) { if (op.gate_type == GateType::TICK) { return process_tick(); } else if (op.gate_type == GateType::REPEAT) { const auto &loop_body = op.repeat_block_body(parent); uint64_t stop_iter = first_yield_tick + num_yield_ticks; uint64_t max_skip = std::max(tick_cur, stop_iter) - stop_iter; uint64_t reps = op.repeat_block_rep_count(); uint64_t ticks_per_iteration = loop_body.count_ticks(); uint64_t skipped_iterations = max_skip == 0 ? 0 : ticks_per_iteration == 0 ? reps : std::min(reps, max_skip / ticks_per_iteration); if (skipped_iterations) { // We can allow the analyzer to fold parts of the loop we aren't yielding. tracker.undo_loop(loop_body, skipped_iterations); reps -= skipped_iterations; tick_cur -= ticks_per_iteration * skipped_iterations; } while (reps > 0) { if (process_block_rev(loop_body)) { return true; } reps--; } return false; } else { for (auto t : op.targets) { if (t.has_qubit_value()) { used_qubits.insert(t.qubit_value()); } } tracker.undo_gate(op); return false; } } DetectorSliceSetComputer::DetectorSliceSetComputer( const Circuit &circuit, uint64_t first_yield_tick, uint64_t num_yield_ticks) : tracker(circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors(), false), first_yield_tick(first_yield_tick), num_yield_ticks(num_yield_ticks) { tick_cur = circuit.count_ticks() + 1; // +1 because of artificial TICKs at start and end. } std::string DetectorSliceSet::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::ostream &stim_draw_internal::operator<<(std::ostream &out, const DetectorSliceSet &slice) { slice.write_text_diagram_to(out); return out; } std::ostream &stim_draw_internal::operator<<(std::ostream &out, const CoordFilter &filter) { if (filter.use_target) { out << filter.exact_target; } else { out << comma_sep(filter.coordinates); } return out; } void DetectorSliceSet::write_text_diagram_to(std::ostream &out) const { DiagramTimelineAsciiDrawer drawer(num_qubits, false); drawer.moment_spacing = 2; for (const auto &s : anticommutations) { drawer.reserve_drawing_room_for_targets(s.second); for (const auto &t : s.second) { std::stringstream ss; ss << "ANTICOMMUTED"; ss << ":"; ss << s.first.second; drawer.diagram.add_entry( AsciiDiagramEntry{ AsciiDiagramPos{ drawer.m2x(drawer.cur_moment + 1), drawer.q2y(t.qubit_value()), 0, 0.5, }, ss.str(), }); } } for (const auto &s : slices) { drawer.reserve_drawing_room_for_targets(s.second); for (const auto &t : s.second) { std::stringstream ss; if (t.is_x_target()) { ss << "X"; } else if (t.is_y_target()) { ss << "Y"; } else if (t.is_z_target()) { ss << "Z"; } else { ss << "?"; } ss << ":"; ss << s.first.second; drawer.diagram.add_entry( AsciiDiagramEntry{ AsciiDiagramPos{ drawer.m2x(drawer.cur_moment), drawer.q2y(t.qubit_value()), 0, 0.5, }, ss.str(), }); } } // Make sure qubit lines are drawn first, so they are in the background. drawer.diagram.lines.insert(drawer.diagram.lines.begin(), drawer.num_qubits, {{0, 0, 0.0, 0.5}, {0, 0, 1.0, 0.5}}); for (size_t q = 0; q < drawer.num_qubits; q++) { drawer.diagram.lines[q] = { {0, drawer.q2y(q), 1.0, 0.5}, {drawer.m2x(drawer.cur_moment) + 1, drawer.q2y(q), 1.0, 0.5}, }; std::stringstream ss; ss << "q"; ss << q; ss << ":"; auto p = coordinates.find(q); if (p != coordinates.end() && !p->second.empty()) { ss << "(" << comma_sep(p->second) << ")"; } ss << " "; drawer.diagram.add_entry( AsciiDiagramEntry{ {0, drawer.q2y(q), 1.0, 0.5}, ss.str(), }); } drawer.diagram.render(out); } std::set DetectorSliceSet::used_qubits() const { std::set result; for (const auto &e : coordinates) { result.insert(e.first); } for (const auto &e : slices) { for (const auto &t : e.second) { result.insert(t.qubit_value()); } } return result; } bool CoordFilter::matches(stim::SpanRef coords, stim::DemTarget target) const { if (use_target) { return target == exact_target; } if (!target.is_relative_detector_id()) { return false; } for (size_t k = 0; k < coordinates.size(); k++) { if (!std::isnan(coordinates[k])) { if (coords.size() <= k || coords[k] != coordinates[k]) { return false; } } } return true; } CoordFilter CoordFilter::parse_from(std::string_view data) { CoordFilter filter; if (data.empty()) { // no filter } else if (data[0] == 'D') { filter.use_target = true; filter.exact_target = DemTarget::relative_detector_id(parse_exact_uint64_t_from_string(data.substr(1))); } else if (data[0] == 'L') { filter.use_target = true; filter.exact_target = DemTarget::observable_id(parse_exact_uint64_t_from_string(data.substr(1))); } else { for (const auto &v : split_view(',', data)) { if (v == "*") { filter.coordinates.push_back(std::numeric_limits::quiet_NaN()); } else { filter.coordinates.push_back(parse_exact_double_from_string(v)); } } } return filter; } DetectorSliceSet DetectorSliceSet::from_circuit_ticks( const stim::Circuit &circuit, uint64_t start_tick, uint64_t num_ticks, SpanRef coord_filter) { num_ticks = std::max(uint64_t{1}, std::min(num_ticks, circuit.count_ticks() - start_tick + 1)); DetectorSliceSetComputer helper(circuit, start_tick, num_ticks); size_t num_qubits = helper.tracker.xs.size(); std::set xs; std::set ys; std::set zs; DetectorSliceSet result; result.num_qubits = num_qubits; result.min_tick = start_tick; result.num_ticks = num_ticks; auto process_anticommutations = [&](size_t out_tick) { for (const auto &[d, g] : helper.tracker.anticommutations) { result.anticommutations[{out_tick, d}].push_back(g); // Stop propagating it backwards if it broke. for (size_t q = 0; q < num_qubits; q++) { if (helper.tracker.xs[q].contains(d)) { helper.tracker.xs[q].xor_item(d); } if (helper.tracker.zs[q].contains(d)) { helper.tracker.zs[q].xor_item(d); } } } helper.tracker.anticommutations.clear(); }; helper.on_tick_callback = [&]() { process_anticommutations(helper.tick_cur + 1); // Record locations of detectors and observables. for (size_t q = 0; q < num_qubits; q++) { xs.clear(); ys.clear(); zs.clear(); for (auto t : helper.tracker.xs[q]) { xs.insert(t); } for (auto t : helper.tracker.zs[q]) { if (xs.find(t) == xs.end()) { zs.insert(t); } else { xs.erase(t); ys.insert(t); } } for (const auto &t : xs) { result.slices[{helper.tick_cur, t}].push_back(GateTarget::x(q)); } for (const auto &t : ys) { result.slices[{helper.tick_cur, t}].push_back(GateTarget::y(q)); } for (const auto &t : zs) { result.slices[{helper.tick_cur, t}].push_back(GateTarget::z(q)); } } }; if (!helper.process_tick()) { bool early_exit = helper.process_block_rev(circuit); if (!early_exit) { helper.process_undo_start_of_circuit(); process_anticommutations(1); } } std::set included_detectors; for (const auto &t : result.slices) { if (t.first.second.is_relative_detector_id()) { included_detectors.insert(t.first.second.data); } } result.detector_coordinates = circuit.get_detector_coordinates(included_detectors); result.coordinates = circuit.get_final_qubit_coords(); for (const auto &q : helper.used_qubits) { result.coordinates[q]; // Default construct if doesn't exist. } auto keep = [&](DemTarget t) { SpanRef coords{}; if (t.is_relative_detector_id()) { auto coords_ptr = result.detector_coordinates.find(t.data); if (coords_ptr != result.detector_coordinates.end()) { coords = coords_ptr->second; } } for (const auto &filter : coord_filter) { if (filter.matches(coords, t)) { return true; } } return false; }; std::vector> removed; for (const auto &t : result.slices) { if (!keep(t.first.second)) { removed.push_back(t.first); } } for (auto t : removed) { result.slices.erase(t); if (t.second.is_relative_detector_id()) { result.detector_coordinates.erase(t.second.raw_id()); } } return result; } Coord<2> flattened_2d(SpanRef c) { float x = 0; float y = 0; if (c.size() >= 1) { x = (float)c[0]; } if (c.size() >= 2) { y = (float)c[1]; } // Arbitrary orthographic projection. for (size_t k = 2; k < c.size(); k++) { x += (float)c[k] / k; y += (float)c[k] / (k * k); } return {x, y}; } float pick_characteristic_distance(const std::set &used, const std::vector> &coords_2d) { if (used.size() == 0) { return 1; } Coord<2> biggest{-INFINITY, -INFINITY}; for (auto q : used) { biggest = std::max(biggest, coords_2d[q]); } float closest_squared_distance = INFINITY; for (auto pt : coords_2d) { if (biggest == pt) { continue; } auto delta = biggest - pt; auto d = delta.xyz[0] * delta.xyz[0] + delta.xyz[1] * delta.xyz[1]; if (d < closest_squared_distance) { closest_squared_distance = d; } } float result = sqrtf(closest_squared_distance); if (result == INFINITY) { result = 1; } return result; } FlattenedCoords FlattenedCoords::from(const DetectorSliceSet &set, float desired_unit_distance) { auto used = set.used_qubits(); FlattenedCoords result; for (uint64_t q = 0; q < set.num_qubits; q++) { Coord<2> c{(float)q, 0}; auto p = set.coordinates.find(q); if (p != set.coordinates.end() && !p->second.empty()) { c = flattened_2d(p->second); } result.qubit_coords.push_back(c); } result.unscaled_qubit_coords = result.qubit_coords; for (const auto &e : set.detector_coordinates) { result.det_coords.insert({e.first, flattened_2d(e.second)}); } float characteristic_distance = pick_characteristic_distance(used, result.qubit_coords); result.unit_distance = desired_unit_distance; float scale = desired_unit_distance / characteristic_distance; for (auto &c : result.qubit_coords) { c *= scale; } for (auto &e : result.det_coords) { e.second *= scale; } if (!used.empty()) { std::vector> used_coords; for (const auto &u : used) { used_coords.push_back(result.qubit_coords[u]); } auto minmax = Coord<2>::min_max(used_coords); auto offset = minmax.first; offset *= -1; offset.xyz[0] += 16; offset.xyz[1] += 16; for (auto &c : result.qubit_coords) { c += offset; } for (auto &c : used_coords) { c += offset; } for (auto &e : result.det_coords) { e.second += offset; } result.size = minmax.second - minmax.first; result.size.xyz[0] += 32; result.size.xyz[1] += 32; } else { result.size.xyz[0] = 1; result.size.xyz[1] = 1; } return result; } const char *pick_color(SpanRef terms) { bool has_x = false; bool has_y = false; bool has_z = false; for (const auto &term : terms) { has_x |= term.is_x_target(); has_y |= term.is_y_target(); has_z |= term.is_z_target(); } if (has_x + has_y + has_z != 1) { return nullptr; } else if (has_x) { return X_RED; } else if (has_y) { return Y_GREEN; } else { assert(has_z); return Z_BLUE; } } float offset_angle_from_to(Coord<2> origin, Coord<2> dst) { auto d = dst - origin; if (d.xyz[0] * d.xyz[0] + d.xyz[1] * d.xyz[1] < 1e-6) { return 0.0f; } float offset_angle = atan2f(d.xyz[1], d.xyz[0]); offset_angle += 2.0f * 3.14159265359f; offset_angle = fmodf(offset_angle, 2.0f * 3.14159265359f); // The -0.01f is to move the wraparound float error away from the common angle PI. if (offset_angle > 3.14159265359f - 0.01f) { offset_angle -= 2.0f * 3.14159265359f; } return offset_angle; } float _mirror_score(SpanRef> coords, size_t i, size_t j) { auto para = coords[j] - coords[i]; float f = para.norm2(); if (f < 1e-4) { return INFINITY; } para /= sqrtf(f); Coord<2> perp = {-para.xyz[0], para.xyz[1]}; std::vector> left; std::vector> right; for (size_t k = 0; k < coords.size(); k++) { if (k == i || k == j) { continue; } auto d = coords[k] - coords[i]; float x = d.dot(para); float y = d.dot(perp); if (y < 0) { right.push_back({x, -y}); } else { left.push_back({x, y}); } } if (left.size() != right.size()) { return INFINITY; } std::stable_sort(left.begin(), left.end()); std::stable_sort(right.begin(), right.end()); for (size_t k = 0; k < left.size(); k++) { if ((left[k] - right[k]).norm2() > 1e-2) { return INFINITY; } } float max_distance = 0; for (size_t k = 0; k < left.size(); k++) { max_distance = std::max(max_distance, left[k].xyz[1]); } return max_distance; } bool _pick_center_using_mirror_symmetry(SpanRef> coords, Coord<2> &out) { float best_score = INFINITY; for (size_t i = 0; i < coords.size(); i++) { for (size_t j = i + 1; j < coords.size(); j++) { float f = _mirror_score(coords, i, j); if (f < best_score) { out = (coords[i] + coords[j]) / 2; best_score = f; } } } return best_score < INFINITY; } Coord<2> stim_draw_internal::pick_polygon_center(SpanRef> coords) { Coord<2> center{0, 0}; if (_pick_center_using_mirror_symmetry(coords, center)) { return center; } for (const auto &coord : coords) { center += coord; } center /= coords.size(); return center; } bool stim_draw_internal::is_colinear(Coord<2> a, Coord<2> b, Coord<2> c, float atol) { for (size_t k = 0; k < 3; k++) { auto d1 = a - b; auto d2 = b - c; if (d1.norm() < atol || d2.norm() < atol) { return true; } d1 /= d1.norm(); d2 /= d2.norm(); if (fabs(d1.dot({d2.xyz[1], -d2.xyz[0]})) < atol) { return true; } std::swap(a, b); std::swap(b, c); } return false; } double stim_draw_internal::inv_space_fill_transform(Coord<2> a) { double dx = ldexp((double)a.xyz[0], 4); double dy = ldexp((double)a.xyz[1], 4); uint64_t x = (uint64_t)std::min((double)(1ULL << 31), std::max(dx, 0.0)); uint64_t y = (uint64_t)std::min((double)(1ULL << 31), std::max(dy, 0.0)); for (size_t k = 64; k-- > 0;) { uint64_t b = 1ULL << k; uint64_t m = b - 1; if ((x ^ y) & b) { x ^= m; } if (!(y & b)) { x ^= y & m; y ^= x & m; x ^= y & m; } } y ^= x; uint64_t gray = 0; for (size_t k = 64; k--;) { uint64_t b = 1ULL << k; if (y & b) { gray ^= b - 1; } } x ^= gray; y ^= gray; uint64_t interleave = 0; for (size_t k = 32; k--;) { interleave |= ((x >> k) & 1ULL) << (2 * k + 1); interleave |= ((y >> k) & 1ULL) << (2 * k); } return interleave; } void _draw_observable( std::ostream &out, uint64_t index, const std::function(uint32_t qubit)> &unscaled_coords, const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, SpanRef terms, std::vector> &pts_workspace, bool next_tick_exists, size_t scale) { std::vector terms_copy; terms_copy.insert(terms_copy.end(), terms.begin(), terms.end()); std::stable_sort(terms_copy.begin(), terms_copy.end(), [&](GateTarget a, GateTarget b) { auto a2 = inv_space_fill_transform(unscaled_coords(a.qubit_value())); auto b2 = inv_space_fill_transform(unscaled_coords(b.qubit_value())); if (a2 != b2) { return a2 < b2; } a2 = inv_space_fill_transform(coords(tick, a.qubit_value())); b2 = inv_space_fill_transform(coords(tick, b.qubit_value())); return a2 < b2; }); pts_workspace.clear(); for (const auto &term : terms_copy) { pts_workspace.push_back(coords(tick, term.qubit_value())); } // TODO: CURRENTLY DISABLED BECAUSE IT'S UNCLEAR IF IT HELPS OR HURTS. // // Draw a semi-janky path connecting the observable together. // out << " 50 * scale) { // dif /= dif.norm() / 50 / scale; // } // Coord<2> perp{-dif.xyz[1], dif.xyz[0]}; // auto c1 = average + perp * 0.1 - dif * 0.1; // auto c2 = average + perp * 0.1 + dif * 0.1; // // out << "C"; // out << c1.xyz[0] << " " << c1.xyz[1] << ", "; // out << c2.xyz[0] << " " << c2.xyz[1] << ", "; // out << a.xyz[0] << " " << a.xyz[1]; // } // out << "\" id=\"obs-path:" << index << ":" << tick << "\""; // write_key_val(out, "stroke", BG_GREY); // write_key_val(out, "fill", "none"); // write_key_val(out, "stroke-width", scale); // out << "/>\n"; for (size_t k = 0; k < terms_copy.size(); k++) { const auto &t = terms_copy[k]; out << "\n"; } if (next_tick_exists) { for (size_t k = 0; k < terms_copy.size(); k++) { const auto &t = terms_copy[k]; out << "\n"; } } } void _start_many_body_svg_path( std::ostream &out, const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, SpanRef terms, std::vector> &pts_workspace) { pts_workspace.clear(); for (const auto &term : terms) { pts_workspace.push_back(coords(tick, term.qubit_value())); } auto center = pick_polygon_center(pts_workspace); std::stable_sort(pts_workspace.begin(), pts_workspace.end(), [&](Coord<2> a, Coord<2> b) { return offset_angle_from_to(center, a) < offset_angle_from_to(center, b); }); out << "(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, SpanRef terms) { auto a = coords(tick, terms[0].qubit_value()); auto b = coords(tick, terms[1].qubit_value()); auto dif = b - a; auto average = (a + b) * 0.5; if (dif.norm() > 64) { dif /= dif.norm() / 64; } Coord<2> perp{-dif.xyz[1], dif.xyz[0]}; auto ac1 = average + perp * 0.2f - dif * 0.2f; auto ac2 = average + perp * 0.2f + dif * 0.2f; auto bc1 = average + perp * -0.2f + dif * 0.2f; auto bc2 = average + perp * -0.2f - dif * 0.2f; out << "(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, SpanRef terms, size_t scale) { auto c = coords(tick, terms[0].qubit_value()); out << "(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, SpanRef terms, std::vector> &pts_workspace, size_t scale) { if (terms.size() > 2) { _start_many_body_svg_path(out, coords, tick, terms, pts_workspace); } else if (terms.size() == 2) { _start_two_body_svg_path(out, coords, tick, terms); } else if (terms.size() == 1) { _start_one_body_svg_path(out, coords, tick, terms, scale); } } void DetectorSliceSet::write_svg_diagram_to(std::ostream &out, size_t num_rows) const { size_t num_cols; if (num_rows == 0) { num_cols = (uint64_t)ceil(sqrt((double)num_ticks)); num_rows = num_ticks / num_cols; while (num_cols * num_rows < num_ticks) { num_rows++; } while (num_cols * num_rows >= num_ticks + num_rows) { num_cols--; } } else { num_cols = (num_ticks + num_rows - 1) / num_rows; } auto coordsys = FlattenedCoords::from(*this, 32); out << R"SVG()SVG"; out << "\n"; auto coords = [&](uint64_t tick, uint32_t qubit) { auto result = coordsys.qubit_coords[qubit]; uint64_t s = tick - min_tick; uint64_t sx = s % num_cols; uint64_t sy = s / num_cols; result.xyz[0] += coordsys.size.xyz[0] * sx * SLICE_WINDOW_GAP; result.xyz[1] += coordsys.size.xyz[1] * sy * SLICE_WINDOW_GAP; return result; }; auto unscaled_coords = [&](uint32_t qubit) { return coordsys.unscaled_qubit_coords[qubit]; }; write_svg_contents_to(out, unscaled_coords, coords, min_tick + num_ticks, 6); out << "\n"; for (size_t k = 0; k < num_ticks; k++) { for (auto q : used_qubits()) { auto t = min_tick + k; std::stringstream id_ss; id_ss << "qubit_dot"; id_ss << ":" << q; add_coord_summary_to_ss(id_ss, coordinates.at(q)); // the raw qubit coordinates, not projected to 2D id_ss << ":" << t; // the absolute tick auto sc = coords(t, q); // the svg coordinates, offset to the correct slice plot out << "\n"; } } out << "\n"; // Border around different slices. if (num_ticks > 1) { size_t k = 0; out << "\n"; for (uint64_t col = 0; col < num_cols; col++) { for (uint64_t row = 0; row < num_rows && row * num_cols + col < num_ticks; row++) { auto sw = coordsys.size.xyz[0]; auto sh = coordsys.size.xyz[1]; std::stringstream id_ss; id_ss << "tick_border:" << k; id_ss << ":" << row << "_" << col; id_ss << ":" << k + min_tick; out << "\n"; k++; } } out << "\n"; } out << R"SVG()SVG"; } void DetectorSliceSet::write_svg_contents_to( std::ostream &out, const std::function(uint32_t qubit)> &unscaled_coords, const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t end_tick, size_t scale) const { size_t clip_id = 0; std::vector> pts_workspace; bool haveDrawnCorners = false; using tup = std::tuple, bool>; std::vector sorted_terms; for (const auto &e : slices) { sorted_terms.push_back({e.first.first, e.first.second, e.second, false}); } for (const auto &e : anticommutations) { sorted_terms.push_back({e.first.first, e.first.second, e.second, true}); } std::stable_sort(sorted_terms.begin(), sorted_terms.end(), [](const tup &e1, const tup &e2) -> int { int a = (int)std::get<2>(e1).size(); int b = (int)std::get<2>(e2).size(); return a > b; }); // Draw detector slices. for (const auto &e : sorted_terms) { uint64_t tick = std::get<0>(e); DemTarget target = std::get<1>(e); SpanRef terms = std::get<2>(e); bool is_anticommutation = std::get<3>(e); if (is_anticommutation) { for (const auto &anti_target : terms) { auto c = coords(tick, anti_target.qubit_value()); out << R"SVG(\n"; } continue; } if (target.is_observable_id()) { _draw_observable( out, target.val(), unscaled_coords, coords, tick, terms, pts_workspace, tick < end_tick - 1, scale); continue; } const char *color = pick_color(terms); bool drawCorners = false; if (color == nullptr) { drawCorners = true; color = BG_GREY; } // Open the group element for this slice out << "\n"; _start_slice_shape_command(out, coords, tick, terms, pts_workspace, scale); write_key_val(out, "stroke", "none"); if (terms.size() > 2) { write_key_val(out, "fill-opacity", 0.75); } write_key_val(out, "fill", color); out << "/>\n"; if (drawCorners) { haveDrawnCorners = true; // controls later writing out the universal gradients we'll reference here out << R"SVG("; _start_slice_shape_command(out, coords, tick, terms, pts_workspace, scale); out << "/>\n"; size_t blur_radius = scale == 6 ? 20 : scale * 1.8f; for (const auto &t : terms) { auto c = coords(tick, t.qubit_value()); out << R"SVG(\n"; } clip_id++; } // Draw outline _start_slice_shape_command(out, coords, tick, terms, pts_workspace, scale); write_key_val(out, "stroke", "black"); write_key_val(out, "fill", "none"); out << "/>\n"; // Close the group element for this slice out << "\n"; } if (haveDrawnCorners) { // write out the universal radialGradients that all corners reference out << "\n"; static const char *const names[] = {"xgrad", "ygrad", "zgrad"}; static const char *const colors[] = {X_RED, Y_GREEN, Z_BLUE}; for (int i = 0; i < 3; ++i) { out << "\n"; } out << "\n"; } } ================================================ FILE: src/stim/diagram/detector_slice/detector_slice_set.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_DETECTOR_SLICE_DETECTOR_SLICE_SET_H #define _STIM_DIAGRAM_DETECTOR_SLICE_DETECTOR_SLICE_SET_H #include #include #include "stim/circuit/circuit.h" #include "stim/dem/detector_error_model.h" #include "stim/diagram/coord.h" namespace stim_draw_internal { struct CoordFilter { std::vector coordinates{}; bool use_target = false; stim::DemTarget exact_target{}; bool matches(stim::SpanRef coords, stim::DemTarget target) const; static CoordFilter parse_from(std::string_view data); }; struct DetectorSliceSet { uint64_t num_qubits; uint64_t min_tick; uint64_t num_ticks; /// Qubit ID -> qubit coordinates std::map> coordinates; /// DemTarget value -> detector coordinates std::map> detector_coordinates; /// (tick, DemTarget) -> terms in the slice std::map, std::vector> slices; /// (tick, DemTarget) -> anticommutations in the slice std::map, std::vector> anticommutations; /// Args: /// circuit: The circuit to make a detector slice diagram from. /// tick_index: The tick to target. tick_index=0 is the start of the /// circuit. tick_index=1 is the first TICK instruction, and so /// forth. /// coord_filter: Detectors that fail to match these coordinates /// are excluded. static DetectorSliceSet from_circuit_ticks( const stim::Circuit &circuit, uint64_t start_tick, uint64_t num_ticks, stim::SpanRef coord_filter); std::set used_qubits() const; std::string str() const; void write_text_diagram_to(std::ostream &out) const; void write_svg_diagram_to(std::ostream &out, size_t num_rows = 0) const; void write_svg_contents_to( std::ostream &out, const std::function(uint32_t qubit)> &unscaled_coords, const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t end_tick, size_t scale) const; }; double inv_space_fill_transform(Coord<2> a); struct FlattenedCoords { std::vector> unscaled_qubit_coords; std::vector> qubit_coords; std::map> det_coords; Coord<2> size; float unit_distance; static FlattenedCoords from(const DetectorSliceSet &set, float desired_unit_distance); }; Coord<2> pick_polygon_center(stim::SpanRef> coords); bool is_colinear(Coord<2> a, Coord<2> b, Coord<2> c, float atol); std::ostream &operator<<(std::ostream &out, const DetectorSliceSet &slice); std::ostream &operator<<(std::ostream &out, const CoordFilter &filter); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/detector_slice/detector_slice_set.test.cc ================================================ // 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. #include "stim/diagram/detector_slice/detector_slice_set.h" #include #include "gtest/gtest.h" #include "stim/diagram/timeline/timeline_svg_drawer.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/util_bot/test_util.test.h" using namespace stim; using namespace stim_draw_internal; TEST(detector_slice_set, from_circuit) { CoordFilter empty_filter; auto slice_set = DetectorSliceSet::from_circuit_ticks( stim::Circuit(R"CIRCUIT( QUBIT_COORDS(3, 5) 1 R 0 M 0 DETECTOR rec[-1] TICK CX 2 1 TICK # Here H 0 CX 1 0 M 0 DETECTOR rec[-1] M 1 DETECTOR rec[-1] REPEAT 100 { TICK R 0 M 0 DETECTOR rec[-1] } )CIRCUIT"), 2, 1, {&empty_filter}); ASSERT_EQ(slice_set.coordinates, (std::map>{{0, {}}, {1, {3, 5}}, {2, {}}})); ASSERT_EQ( slice_set.slices, (std::map, std::vector>{ {{2, DemTarget::relative_detector_id(1)}, {GateTarget::x(0), GateTarget::z(1)}}, {{2, DemTarget::relative_detector_id(2)}, {GateTarget::z(1)}}, })); } TEST(detector_slice_set, big_loop_seeking) { stim::Circuit circuit(R"CIRCUIT( REPEAT 100000 { REPEAT 10000 { REPEAT 1000 { REPEAT 100 { REPEAT 10 { RY 0 TICK MY 0 DETECTOR rec[-1] } } } RX 1 TICK MX 1 OBSERVABLE_INCLUDE(5) rec[-1] DETECTOR rec[-1] } } )CIRCUIT"); uint64_t inner = 10 * 100 * 1000 + 1; CoordFilter empty_filter; CoordFilter obs5_filter; obs5_filter.use_target = true; obs5_filter.exact_target = DemTarget::observable_id(5); auto slice_set = DetectorSliceSet::from_circuit_ticks(circuit, inner * 10000ULL * 50ULL + 2ULL, 1, {&empty_filter}); ASSERT_EQ(slice_set.coordinates, (std::map>{{0, {}}, {1, {}}})); ASSERT_EQ( slice_set.slices, (std::map, std::vector>{ {{inner * 10000ULL * 50ULL + 2ULL, DemTarget::relative_detector_id(inner * 10000ULL * 50ULL + 1ULL)}, {GateTarget::y(0)}}, })); slice_set = DetectorSliceSet::from_circuit_ticks( circuit, inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL + 1ULL, 1, std::vector{empty_filter, obs5_filter}); ASSERT_EQ(slice_set.coordinates, (std::map>{{0, {}}, {1, {}}})); ASSERT_EQ( slice_set.slices, (std::map, std::vector>{ {{inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL + 1ULL, DemTarget::relative_detector_id(inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL)}, {GateTarget::x(1)}}, {{inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL + 1ULL, DemTarget::observable_id(5)}, {GateTarget::x(1)}}, })); slice_set = DetectorSliceSet::from_circuit_ticks( circuit, inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL + 1ULL, 2, std::vector{empty_filter, obs5_filter}); ASSERT_EQ(slice_set.coordinates, (std::map>{{0, {}}, {1, {}}})); ASSERT_EQ( slice_set.slices, (std::map, std::vector>{ {{inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL + 1ULL, DemTarget::relative_detector_id(inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL)}, {GateTarget::x(1)}}, {{inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL + 1ULL, DemTarget::observable_id(5)}, {GateTarget::x(1)}}, {{inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL + 2ULL, DemTarget::relative_detector_id(inner * 10000ULL * 25ULL + 1000ULL * 100ULL * 10ULL + 1ULL)}, {GateTarget::y(0)}}, })); } TEST(detector_slice_set_text_diagram, repetition_code) { CoordFilter empty_filter; CoordFilter obs_filter; obs_filter.use_target = true; obs_filter.exact_target = DemTarget::observable_id(0); CircuitGenParameters params(10, 5, "memory"); auto circuit = generate_rep_code_circuit(params).circuit; auto slice_set = DetectorSliceSet::from_circuit_ticks(circuit, 9, 1, std::vector{empty_filter, obs_filter}); ASSERT_EQ(slice_set.slices.size(), circuit.count_qubits()); ASSERT_EQ("\n" + slice_set.str() + "\n", R"DIAGRAM( q0: --------Z:D12---------------------------- | q1: -Z:D8---Z:D12---------------------------- | q2: --------Z:D12--Z:D13--------------------- | q3: -Z:D9----------Z:D13--------------------- | q4: ---------------Z:D13--Z:D14-------------- | q5: -Z:D10----------------Z:D14-------------- | q6: ----------------------Z:D14--Z:D15------- | q7: -Z:D11-----------------------Z:D15------- | q8: -----------------------------Z:D15--Z:L0- )DIAGRAM"); ASSERT_EQ( "\n" + DetectorSliceSet::from_circuit_ticks(circuit, 11, 1, std::vector{empty_filter, obs_filter}) .str() + "\n", R"DIAGRAM( q0: --------Z:D16- | q1: -Z:D12--Z:D16- | q2: -Z:D12--Z:D17- | q3: -Z:D13--Z:D17- | q4: -Z:D13--Z:D18- | q5: -Z:D14--Z:D18- | q6: -Z:D14--Z:D19- | q7: -Z:D15--Z:D19- | q8: -Z:D15--Z:L0-- )DIAGRAM"); } TEST(detector_slice_set_text_diagram, surface_code) { CoordFilter empty_filter; CoordFilter obs_filter; obs_filter.use_target = true; obs_filter.exact_target = DemTarget::observable_id(0); CircuitGenParameters params(10, 2, "unrotated_memory_z"); auto circuit = generate_surface_code_circuit(params).circuit; auto slice_set = DetectorSliceSet::from_circuit_ticks(circuit, 11, 1, std::vector{empty_filter, obs_filter}); ASSERT_EQ(slice_set.slices.size(), circuit.count_qubits()); ASSERT_EQ("\n" + slice_set.str() + "\n", R"DIAGRAM( q0:(0, 0) -X:D2--Z:D3--------------------------------Z:L0- | | | q1:(1, 0) -X:D2--|-----------------X:D6--Z:D7--------Z:L0- | | | | | q2:(2, 0) -|-----|-----Z:D4--------X:D6--|-----------Z:L0- | | | | | q3:(0, 1) -X:D2--Z:D3--|-----------|-----Z:D7------------- | | | q4:(1, 1) -------------Z:D4--X:D5--X:D6--Z:D7------------- | | | q5:(2, 1) -------------Z:D4--|-----------|-----Z:D8--X:D9- | | | | | q6:(0, 2) -------------|-----X:D5--------Z:D7--|-----|---- | | | | q7:(1, 2) -------------Z:D4--X:D5--------------|-----X:D9- | | q8:(2, 2) -------------------------------------Z:D8--X:D9- )DIAGRAM"); } TEST(detector_slice_set_svg_diagram, surface_code) { CoordFilter empty_filter; CircuitGenParameters params(10, 2, "rotated_memory_z"); auto circuit = generate_surface_code_circuit(params).circuit; auto slice_set = DetectorSliceSet::from_circuit_ticks(circuit, 8, 1, {&empty_filter}); std::stringstream ss; slice_set.write_svg_diagram_to(ss); expect_string_is_identical_to_saved_file(ss.str(), "rotated_memory_z_detector_slice.svg"); } TEST(detector_slice_set_svg_diagram, long_range_detector) { Circuit circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 2) 2 QUBIT_COORDS(3, 3) 3 QUBIT_COORDS(4, 4) 4 QUBIT_COORDS(5, 5) 5 QUBIT_COORDS(6, 6) 6 QUBIT_COORDS(7, 7) 7 QUBIT_COORDS(8, 8) 8 QUBIT_COORDS(9, 9) 9 H 0 1 2 3 4 5 6 7 8 9 TICK MX 0 9 DETECTOR(0) rec[-1] rec[-2] )CIRCUIT"); CoordFilter empty_filter; auto slice_set = DetectorSliceSet::from_circuit_ticks(circuit, 1, 1, {&empty_filter}); std::stringstream ss; slice_set.write_svg_diagram_to(ss); expect_string_is_identical_to_saved_file(ss.str(), "long_range_detector.svg"); } TEST(detector_slice_set, pick_polygon_center) { std::vector> coords; coords.push_back({2, 1}); coords.push_back({4.5, 3.5}); coords.push_back({6, 0}); coords.push_back({1, 5}); ASSERT_EQ(pick_polygon_center(coords), (Coord<2>{3.25, 2.25})); coords.pop_back(); coords.push_back({1, 6}); ASSERT_EQ(pick_polygon_center(coords), (Coord<2>{(2 + 4.5 + 6 + 1) / 4, (1 + 3.5 + 0 + 6) / 4})); coords.push_back({7, 0}); coords.push_back({1, 5}); ASSERT_EQ(pick_polygon_center(coords), (Coord<2>{3.25, 2.25})); } TEST(detector_slice_set_svg_diagram, is_colinear) { ASSERT_TRUE(is_colinear({0, 0}, {0, 0}, {1, 2}, 1e-4f)); ASSERT_TRUE(is_colinear({3, 6}, {1, 2}, {2, 4}, 1e-4f)); ASSERT_FALSE(is_colinear({3, 7}, {1, 2}, {2, 4}, 1e-4f)); ASSERT_FALSE(is_colinear({4, 6}, {1, 2}, {2, 4}, 1e-4f)); ASSERT_FALSE(is_colinear({3, 6}, {1, 3}, {2, 4}, 1e-4f)); ASSERT_FALSE(is_colinear({3, 6}, {2, 2}, {2, 4}, 1e-4f)); ASSERT_FALSE(is_colinear({3, 6}, {1, 2}, {1, 4}, 1e-4f)); ASSERT_FALSE(is_colinear({3, 6}, {1, 2}, {2, -4}, 1e-4f)); ASSERT_FALSE(is_colinear({0, 1e-3f}, {0, 0}, {1, 2}, 1e-4f)); ASSERT_TRUE(is_colinear({0, 1e-3f}, {0, 0}, {1, 2}, 1e-2f)); } TEST(detector_slice_set_svg_diagram, colinear_polygon) { Circuit circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 2) 2 QUBIT_COORDS(0, 3) 3 QUBIT_COORDS(4, 0) 4 QUBIT_COORDS(5, 1) 5 QUBIT_COORDS(6, 2) 6 R 0 1 2 3 4 5 6 H 3 TICK H 3 M 0 1 2 3 4 5 6 DETECTOR rec[-1] rec[-2] rec[-3] DETECTOR rec[-4] rec[-5] rec[-6] rec[-7] )CIRCUIT"); CoordFilter empty_filter; auto slice_set = DetectorSliceSet::from_circuit_ticks(circuit, 1, 1, {&empty_filter}); std::stringstream ss; slice_set.write_svg_diagram_to(ss); expect_string_is_identical_to_saved_file(ss.str(), "colinear_detector_slice.svg"); } TEST(detector_slice_set_svg_diagram, observable) { Circuit circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(2, 0) 1 QUBIT_COORDS(1, 1) 2 REPEAT 3 { C_XYZ 0 1 TICK CX 0 2 TICK CX 1 2 TICK M 2 TICK R 2 } DETECTOR rec[-1] rec[-2] rec[-3] M 0 1 OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] )CIRCUIT"); CoordFilter obs_filter; obs_filter.use_target = true; obs_filter.exact_target = DemTarget::observable_id(0); std::stringstream ss; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, circuit.count_ticks() + 1, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, {&obs_filter}); expect_string_is_identical_to_saved_file(ss.str(), "observable_slices.svg"); } TEST(detector_slice_set_svg_diagram, svg_ids) { Circuit circuit(R"CIRCUIT( QUBIT_COORDS 0 QUBIT_COORDS(1) 1 QUBIT_COORDS(2, 2) 2 QUBIT_COORDS(3, 3, 3) 3 R 0 1 2 3 TICK M 0 1 2 3 DETECTOR rec[-1] DETECTOR(1) rec[-1] rec[-2] DETECTOR(2, 2) rec[-1] rec[-2] rec[-3] DETECTOR(3, 3, 3) rec[-1] rec[-2] rec[-3] rec[-4] )CIRCUIT"); CoordFilter empty_filter; auto slice_set = DetectorSliceSet::from_circuit_ticks(circuit, 1, 1, {&empty_filter}); std::stringstream ss; slice_set.write_svg_diagram_to(ss); expect_string_is_identical_to_saved_file(ss.str(), "svg_ids.svg"); } TEST(coord_filter, parse_from) { auto c = CoordFilter::parse_from(""); ASSERT_TRUE(c.coordinates.empty()); ASSERT_FALSE(c.use_target); c = CoordFilter::parse_from("D5"); ASSERT_TRUE(c.coordinates.empty()); ASSERT_TRUE(c.use_target); ASSERT_EQ(c.exact_target, DemTarget::relative_detector_id(5)); c = CoordFilter::parse_from("L7"); ASSERT_TRUE(c.coordinates.empty()); ASSERT_TRUE(c.use_target); ASSERT_EQ(c.exact_target, DemTarget::observable_id(7)); c = CoordFilter::parse_from("2,3,*,5"); ASSERT_TRUE(c.coordinates.size() == 4); ASSERT_TRUE(c.coordinates[0] == 2); ASSERT_TRUE(c.coordinates[1] == 3); ASSERT_TRUE(std::isnan(c.coordinates[2])); ASSERT_TRUE(c.coordinates[3] == 5); ASSERT_FALSE(c.use_target); } TEST(inv_space_fill_transform, inv_space_fill_transform) { ASSERT_EQ(inv_space_fill_transform({0, 0}), 0); ASSERT_EQ(inv_space_fill_transform({4, 55.5}), 339946); } TEST(detector_slice_set, from_circuit_with_errors) { CoordFilter empty_filter; auto slice_set = DetectorSliceSet::from_circuit_ticks( stim::Circuit(R"CIRCUIT( TICK R 0 TICK R 0 TICK MXX 0 1 DETECTOR rec[-1] )CIRCUIT"), 0, 5, {&empty_filter}); ASSERT_EQ( slice_set.anticommutations, (std::map, std::vector>{ {{3, DemTarget::relative_detector_id(0)}, {GateTarget::x(0)}}, })); ASSERT_EQ( slice_set.slices, (std::map, std::vector>{ {{3, DemTarget::relative_detector_id(0)}, {GateTarget::x(0), GateTarget::x(1)}}, })); } TEST(circuit_diagram_timeline_text, anticommuting_detector_circuit) { auto circuit = Circuit(R"CIRCUIT( TICK R 0 TICK R 0 TICK MXX 0 1 M 2 DETECTOR rec[-1] DETECTOR rec[-2] )CIRCUIT"); CoordFilter empty_filter; std::stringstream ss; ss << DetectorSliceSet::from_circuit_ticks(circuit, 0, 10, &empty_filter); ASSERT_EQ("\n" + ss.str() + "\n", R"DIAGRAM( q0: -------ANTICOMMUTED:D1--X:D1- | q1: ------------------------X:D1- q2: -Z:D0--Z:D0-------------Z:D0- )DIAGRAM"); } ================================================ FILE: src/stim/diagram/diagram_util.cc ================================================ #include "stim/diagram/diagram_util.h" using namespace stim; using namespace stim_draw_internal; std::pair stim_draw_internal::two_qubit_gate_pieces(GateType gate_type) { if (gate_type == GateType::CX) { return {"Z", "X"}; } else if (gate_type == GateType::CY) { return {"Z", "Y"}; } else if (gate_type == GateType::CZ) { return {"Z", "Z"}; } else if (gate_type == GateType::XCX) { return {"X", "X"}; } else if (gate_type == GateType::XCY) { return {"X", "Y"}; } else if (gate_type == GateType::XCZ) { return {"X", "Z"}; } else if (gate_type == GateType::YCX) { return {"Y", "X"}; } else if (gate_type == GateType::YCY) { return {"Y", "Y"}; } else if (gate_type == GateType::YCZ) { return {"Y", "Z"}; } else if (gate_type == GateType::CXSWAP) { return {"ZSWAP", "XSWAP"}; } else if (gate_type == GateType::CZSWAP) { return {"ZSWAP", "ZSWAP"}; } else if (gate_type == GateType::SWAPCX) { return {"XSWAP", "ZSWAP"}; } else { auto name = GATE_DATA[gate_type].name; return {name, name}; } } size_t stim_draw_internal::utf8_char_count(std::string_view s) { size_t t = 0; for (uint8_t c : s) { // Continuation bytes start with "10" in binary. if ((c & 0xC0) != 0x80) { t++; } } return t; } void stim_draw_internal::add_coord_summary_to_ss(std::ostream &ss, std::vector vec) { bool first = true; for (const auto &c : vec) { if (first) { ss << ":"; } else { ss << "_"; } ss << c; first = false; } } ================================================ FILE: src/stim/diagram/diagram_util.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_DIAGRAM_UTIL_H #define _STIM_DIAGRAM_DIAGRAM_UTIL_H #include #include "stim/circuit/circuit.h" namespace stim_draw_internal { constexpr const char *X_RED = "#FF4040"; constexpr const char *Y_GREEN = "#59FF7A"; constexpr const char *Z_BLUE = "#4DA6FF"; constexpr const char *EX_PURPLE = "#FF4DDB"; constexpr const char *EY_YELLOW = "#F1FF59"; constexpr const char *EZ_ORANGE = "#FF9500"; constexpr const char *BG_GREY = "#AAAAAA"; size_t utf8_char_count(std::string_view s); /// Splits a two qubit gate into two end pieces, which can be drawn independently. std::pair two_qubit_gate_pieces(stim::GateType gate_type); /// Adds each element of a vector to the string stream starting with ':', separated by '_' /// adds nothing if the vector is empty void add_coord_summary_to_ss(std::ostream &ss, std::vector vec); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/gate_data_3d.cc ================================================ #include "stim/diagram/gate_data_3d.h" #include "stim/diagram/gate_data_3d_texture_data.h" using namespace stim; using namespace stim_draw_internal; constexpr float CONTROL_RADIUS = 0.4f; std::shared_ptr> texture_coords_for_showing_on_spacelike_faces_of_cube( std::string_view name, size_t tex_tile_x, size_t tex_tile_y, bool actually_just_square) { constexpr size_t diam = 16; float d = (float)1.0 / diam; float dx = d * tex_tile_x; float dy = d * tex_tile_y; Coord<2> v00{dx + 0, dy + 0}; Coord<2> v01{dx + 0, dy + d}; Coord<2> v10{dx + d, dy + 0}; Coord<2> v11{dx + d, dy + d}; if (actually_just_square) { return std::shared_ptr>(new GltfBuffer<2>( {{std::string(name)}, { v10, v00, v11, v00, v01, v11, v11, v10, v01, v01, v10, v00, }})); } return std::shared_ptr>(new GltfBuffer<2>( {{std::string(name)}, { v00, v01, v10, v01, v11, v10, v00, v00, v00, v00, v00, v00, v10, v00, v11, v00, v01, v11, v01, v11, v00, v00, v11, v10, v00, v00, v00, v00, v00, v00, v11, v10, v01, v01, v10, v00, }})); } std::shared_ptr cube_gate( std::string_view gate_canonical_name, size_t tex_tile_x, size_t tex_tile_y, std::shared_ptr> cube_position_buffer, std::shared_ptr material, bool actually_just_square) { return std::shared_ptr(new GltfPrimitive{ {"primitive_gate_" + std::string(gate_canonical_name)}, GL_TRIANGLES, cube_position_buffer, texture_coords_for_showing_on_spacelike_faces_of_cube( "tex_coords_gate_" + std::string(gate_canonical_name), tex_tile_x, tex_tile_y, actually_just_square), material, }); } std::shared_ptr> make_cube_triangle_list(bool actually_just_square) { Coord<3> v000{-0.5f, +0.5f, +0.5f}; Coord<3> v001{-0.5f, +0.5f, -0.5f}; Coord<3> v010{-0.5f, -0.5f, +0.5f}; Coord<3> v011{-0.5f, -0.5f, -0.5f}; Coord<3> v100{+0.5f, +0.5f, +0.5f}; Coord<3> v101{+0.5f, +0.5f, -0.5f}; Coord<3> v110{+0.5f, -0.5f, +0.5f}; Coord<3> v111{+0.5f, -0.5f, -0.5f}; if (actually_just_square) { v000.xyz[0] = 0; v001.xyz[0] = 0; v010.xyz[0] = 0; v011.xyz[0] = 0; return std::shared_ptr>(new GltfBuffer<3>{ {"cube"}, { v000, v001, v010, v001, v011, v010, v011, v001, v010, v010, v001, v000, }}); } return std::shared_ptr>(new GltfBuffer<3>{ {"cube"}, { v000, v010, v100, v010, v110, v100, v000, v100, v001, v001, v100, v101, v000, v001, v010, v001, v011, v010, v111, v011, v101, v101, v011, v001, v111, v110, v011, v110, v010, v011, v111, v101, v110, v110, v101, v100, }}); } std::shared_ptr> make_circle_loop(size_t n, float r, bool repeat_boundary) { std::vector> vertices; vertices.push_back({0, r, 0}); for (size_t k = 1; k < n; k++) { float t = k * 3.14159265359f * 2 / n; vertices.push_back({0, cosf(t) * r, sinf(t) * r}); } if (repeat_boundary) { vertices.push_back({0, r, 0}); } return std::shared_ptr>(new GltfBuffer<3>{ {"circle_loop"}, std::move(vertices), }); } std::pair> make_x_control_mesh() { auto line_cross = std::shared_ptr>(new GltfBuffer<3>{ {"control_x_line_cross"}, {{0, -CONTROL_RADIUS, 0}, {0, +CONTROL_RADIUS, 0}, {0, 0, -CONTROL_RADIUS}, {0, 0, +CONTROL_RADIUS}}, }); auto circle = make_circle_loop(16, CONTROL_RADIUS, true); auto black_material = std::shared_ptr(new GltfMaterial{ {"black"}, {0, 0, 0, 1}, 1, 1, true, nullptr, }); auto white_material = std::shared_ptr(new GltfMaterial{ {"white"}, {1, 1, 1, 1}, 0.4, 0.5, true, nullptr, }); auto mesh = std::shared_ptr(new GltfMesh{ {"mesh_X_CONTROL"}, { std::shared_ptr(new GltfPrimitive{ {"primitive_circle_interior"}, GL_TRIANGLE_FAN, circle, nullptr, white_material, }), std::shared_ptr(new GltfPrimitive{ {"primitive_circle_perimeter"}, GL_LINE_STRIP, circle, nullptr, black_material, }), std::shared_ptr(new GltfPrimitive{ {"primitive_line_cross"}, GL_LINES, line_cross, nullptr, black_material, }), }, }); return {"X_CONTROL", mesh}; } std::pair> make_xswap_control_mesh() { float h = CONTROL_RADIUS * sqrtf(2) * 0.8f; auto line_cross = std::shared_ptr>(new GltfBuffer<3>{ {"control_xswap_line_cross"}, {{0, -h, -h}, {0, +h, +h}, {0, -h, +h}, {0, +h, -h}}, }); auto circle = make_circle_loop(16, CONTROL_RADIUS, true); auto black_material = std::shared_ptr(new GltfMaterial{ {"black"}, {0, 0, 0, 1}, 1, 1, true, nullptr, }); auto white_material = std::shared_ptr(new GltfMaterial{ {"white"}, {1, 1, 1, 1}, 0.4, 0.5, true, nullptr, }); auto mesh = std::shared_ptr(new GltfMesh{ {"mesh_XSWAP_CONTROL"}, { std::shared_ptr(new GltfPrimitive{ {"primitive_circle_interior"}, GL_TRIANGLE_FAN, circle, nullptr, white_material, }), std::shared_ptr(new GltfPrimitive{ {"primitive_circle_perimeter"}, GL_LINE_STRIP, circle, nullptr, black_material, }), std::shared_ptr(new GltfPrimitive{ {"primitive_line_cross"}, GL_LINES, line_cross, nullptr, black_material, }), }, }); return {"XSWAP", mesh}; } std::pair> make_zswap_control_mesh() { float h = CONTROL_RADIUS * sqrtf(2) * 0.8f; auto line_cross = std::shared_ptr>(new GltfBuffer<3>{ {"control_zswap_line_cross"}, {{0, -h, -h}, {0, +h, +h}, {0, -h, +h}, {0, +h, -h}}, }); auto circle = make_circle_loop(16, CONTROL_RADIUS, true); auto black_material = std::shared_ptr(new GltfMaterial{ {"black"}, {0, 0, 0, 1}, 1, 1, true, nullptr, }); auto white_material = std::shared_ptr(new GltfMaterial{ {"white"}, {1, 1, 1, 1}, 0.4, 0.5, true, nullptr, }); auto mesh = std::shared_ptr(new GltfMesh{ {"mesh_XSWAP_CONTROL"}, { std::shared_ptr(new GltfPrimitive{ {"primitive_circle_interior"}, GL_TRIANGLE_FAN, circle, nullptr, black_material, }), std::shared_ptr(new GltfPrimitive{ {"primitive_line_cross"}, GL_LINES, line_cross, nullptr, white_material, }), }, }); return {"ZSWAP", mesh}; } std::pair> make_y_control_mesh() { auto gray_material = std::shared_ptr(new GltfMaterial{ {"gray"}, {0.5, 0.5, 0.5, 1}, 1, 1, true, nullptr, }); auto black_material = std::shared_ptr(new GltfMaterial{ {"black"}, {0, 0, 0, 1}, 1, 1, true, nullptr, }); auto tri_buf = make_circle_loop(3, CONTROL_RADIUS, false); auto triangle_perimeter = std::shared_ptr(new GltfPrimitive{ {"primitive_triangle_perimeter"}, GL_LINE_LOOP, tri_buf, nullptr, black_material, }); auto triangle_interior = std::shared_ptr(new GltfPrimitive{ {"primitive_triangle_interior"}, GL_TRIANGLES, tri_buf, nullptr, gray_material, }); auto mesh = std::shared_ptr(new GltfMesh{ {"mesh_control_Y"}, { triangle_perimeter, triangle_interior, }, }); return {"Y_CONTROL", mesh}; } std::pair> make_z_control_mesh() { auto circle = make_circle_loop(16, CONTROL_RADIUS, true); auto black_material = std::shared_ptr(new GltfMaterial{ {"black"}, {0, 0, 0, 1}, 1, 1, true, nullptr, }); auto disc_interior = std::shared_ptr(new GltfPrimitive{ {"primitive_circle_interior"}, GL_TRIANGLE_FAN, circle, nullptr, black_material, }); auto mesh = std::shared_ptr(new GltfMesh{ {"mesh_Z_CONTROL"}, { disc_interior, }, }); return {"Z_CONTROL", mesh}; } std::pair> make_detector_mesh(bool excited) { auto circle = make_circle_loop(8, CONTROL_RADIUS, true); auto circle2 = make_circle_loop(8, CONTROL_RADIUS, true); auto circle3 = make_circle_loop(8, CONTROL_RADIUS, true); for (auto &e : circle2->vertices) { std::swap(e.xyz[1], e.xyz[2]); std::swap(e.xyz[0], e.xyz[1]); } for (auto &e : circle3->vertices) { std::swap(e.xyz[0], e.xyz[1]); std::swap(e.xyz[1], e.xyz[2]); } auto material = std::shared_ptr(new GltfMaterial{ {excited ? "det_red" : "det_black"}, {excited ? 1.0f : 0.0f, excited ? 0.5f : 0.0f, excited ? 0.5f : 0.0f, 1}, 1, 1, true, nullptr, }); auto disc_interior = std::shared_ptr(new GltfPrimitive{ {excited ? "excited_detector_primitive_circle_interior" : "detector_primitive_circle_interior"}, GL_TRIANGLE_FAN, circle, nullptr, material, }); auto disc_interior2 = std::shared_ptr(new GltfPrimitive{ {excited ? "excited_detector_primitive_circle_interior_2" : "detector_primitive_circle_interior_2"}, GL_TRIANGLE_FAN, circle2, nullptr, material, }); auto disc_interior3 = std::shared_ptr(new GltfPrimitive{ {excited ? "excited_detector_primitive_circle_interior_3" : "detector_primitive_circle_interior_3"}, GL_TRIANGLE_FAN, circle3, nullptr, material, }); auto mesh = std::shared_ptr(new GltfMesh{ {excited ? "mesh_EXCITED_DETECTOR" : "mesh_DETECTOR"}, { disc_interior, disc_interior2, disc_interior3, }, }); return {excited ? "EXCITED_DETECTOR" : "DETECTOR", mesh}; } std::map> stim_draw_internal::make_gate_primitives() { bool actually_square = true; auto cube = make_cube_triangle_list(actually_square); auto image = std::shared_ptr(new GltfImage{ {"gates_image"}, make_gate_3d_texture_data_uri(), }); auto sampler = std::shared_ptr(new GltfSampler{ {"gates_sampler"}, GL_NEAREST, GL_NEAREST, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, }); auto texture = std::shared_ptr(new GltfTexture{ {"gates_texture"}, sampler, image, }); auto material = std::shared_ptr(new GltfMaterial{ {"gates_material"}, {1, 1, 1, 1}, 0.4, 0.5, false, texture, }); auto f = [&](std::string_view s, size_t x, size_t y) -> std::pair> { return {s, GltfMesh::from_singleton_primitive(cube_gate(s, x, y, cube, material, actually_square))}; }; return std::map>{ f("X", 0, 6), f("Y", 0, 7), f("Z", 0, 8), f("H_YZ", 1, 6), f("H", 1, 7), f("H_XY", 1, 8), f("SQRT_X", 2, 6), f("SQRT_Y", 2, 7), f("S", 2, 8), f("SQRT_X_DAG", 3, 6), f("SQRT_Y_DAG", 3, 7), f("S_DAG", 3, 8), f("MX", 4, 6), f("MY", 4, 7), f("M", 4, 8), f("RX", 5, 6), f("RY", 5, 7), f("R", 5, 8), f("MRX", 6, 6), f("MRY", 6, 7), f("MR", 6, 8), f("X_ERROR", 7, 6), f("Y_ERROR", 7, 7), f("Z_ERROR", 7, 8), f("E:X", 8, 6), f("E:Y", 8, 7), f("E:Z", 8, 8), f("ELSE_CORRELATED_ERROR:X", 9, 6), f("ELSE_CORRELATED_ERROR:Y", 9, 7), f("ELSE_CORRELATED_ERROR:Z", 9, 8), f("MPP:X", 10, 6), f("MPP:Y", 10, 7), f("MPP:Z", 10, 8), f("SQRT_XX", 11, 6), f("SQRT_YY", 11, 7), f("SQRT_ZZ", 11, 8), f("SQRT_XX_DAG", 12, 6), f("SQRT_YY_DAG", 12, 7), f("SQRT_ZZ_DAG", 12, 8), f("X:REC", 13, 6), f("Y:REC", 13, 7), f("Z:REC", 13, 8), f("X:SWEEP", 14, 6), f("Y:SWEEP", 14, 7), f("Z:SWEEP", 14, 8), f("I", 0, 6), f("C_XYZ", 1, 9), f("C_NXYZ", 6, 10), f("C_XNYZ", 7, 10), f("C_XYNZ", 8, 10), f("C_ZYX", 2, 9), f("C_NZYX", 9, 10), f("C_ZNYX", 10, 10), f("C_ZYNX", 11, 10), f("H_NXY", 12, 10), f("H_NXZ", 13, 10), f("H_NYZ", 14, 10), f("II", 15, 10), f("II_ERROR", 15, 11), f("I_ERROR", 15, 8), f("DEPOLARIZE1", 3, 9), f("DEPOLARIZE2", 4, 9), f("ISWAP", 5, 9), f("ISWAP_DAG", 6, 9), f("SWAP", 7, 9), f("PAULI_CHANNEL_1", 8, 9), f("PAULI_CHANNEL_2", 9, 9), f("MXX", 10, 9), f("MYY", 11, 9), f("MZZ", 12, 9), f("MPAD", 13, 9), f("HERALDED_ERASE", 14, 9), f("HERALDED_PAULI_CHANNEL_1", 15, 9), f("SPP:X", 0, 10), f("SPP:Y", 1, 10), f("SPP:Z", 2, 10), f("SPP_DAG:X", 3, 10), f("SPP_DAG:Y", 4, 10), f("SPP_DAG:Z", 5, 10), make_x_control_mesh(), make_y_control_mesh(), make_z_control_mesh(), make_xswap_control_mesh(), make_zswap_control_mesh(), make_detector_mesh(false), make_detector_mesh(true), }; } ================================================ FILE: src/stim/diagram/gate_data_3d.h ================================================ #ifndef _STIM_DIAGRAM_GATE_DATA_3d_H #define _STIM_DIAGRAM_GATE_DATA_3d_H #include "stim/diagram/gltf.h" namespace stim_draw_internal { std::map> make_gate_primitives(); } #endif ================================================ FILE: src/stim/diagram/gate_data_3d_texture_data.cc ================================================ #include "stim/diagram/gate_data_3d_texture_data.h" std::string stim_draw_internal::make_gate_3d_texture_data_uri() { std::string result; result.append("data:image/png;base64,"); result.append("iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAdnJLH8AAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAIABJREFUeNrsnXdUVLnbx79DlSaCIlWKAiIIorgWsPxsrGLDgg2xYFkL9l5AwYqKvWJbC1bWrruKiuDq6iq6KDZQUSyAgiIIAjPwvH/sO/c4SxF17h2EfM6ZI95k5klyk5vvTZ4kIiIiMBgMBoPBqFQosSJgMBgMBoMJAAaDwWAwGEwAMBgMBoPBYAKAwWAwGAwGEwAMBoPBYDCYAGBUMs6cOYM3b96wgmAwGAwmABiVidOnTzMBwAAAvHr1Cjdv3uTdTkxMDFJTU8tFnj09PeHu7s5uPkPhDBkyBMeOHas8AuDDhw9wcnKCo6MjMjMzBbN7+/Zt6OvrY8yYMQCA/Px8NGzYEPb29vj48aNg6di2bRtGjx4tcy00NBTjxo3j1W5ubi4CAwPRrFkzhIeHw8vLC1ZWVhg5ciSuXLkid3vr1q2Dq6sr3N3d0a5dOyQkJJQaPyEhAQ4ODnj//j0v+c/Pz0dISAjq16+P2rVrw87ODmvXrhW8/icnJ8PU1LRcdEDbt2/HmjVrUL9+fd5t1atXDwEBAdizZ4/C83379m3cunWL9T4MhZKfn4/IyEh06tTp679MPygnT54kAASATp8+LZjdjRs3EgCysLAgIqKEhAQuHXFxcYKlY8SIEbRz506Za4MHD6awsDDebGZmZpKTkxN5eHhQZmYmjR07lu7evUupqanUrVs3AkCfPn2Sm71Vq1aRmpoaJSYmEhGRp6cn2djYkFgsLjZ+fn4+NWnShKKjo3nJf2FhIXXu3JmqVKlCV69eJSKiuLg4qlq1KoWGhgpa/8+dO8fVO3mW+dcyd+5cmjhxoqA2CwoKqHfv3rRw4UJB7T548IBq1KhBPj4+9OnTJxoyZAh16dKF8vLyyMfH"); result.append("h/T19QV9BkgkEpJIJMSo3Jw4cYJ8fHy+6bs/7AiAjo4O97eamppgdg0NDQEAVlZW3P9FIhEAwMjISLB0/P3332jatKnMtatXr6J58+a82Vy7di3u3LmDhQsXypR/zZo1sW/fPpiamsrNVmFhIYKDg+Hk5ARLS0tuyDUhIQFHjhwp9juzZ89Gp06d0LJlS17yHxkZidOnT6Nv375cOTs4OKBbt24lpokvjI2NuX+rVKmikDa4bds2hIeHY8WKFYLaVVJSwo4dO7B161YcPnxYMLv6+vrQ19fHnj174OnpicaNG8PFxQV9+vTBnj17ULVqVejp6QmWniVLlmDVqlXsFbiSc/DgQfTt2/fb2tKPmulatWpxf5uZmQlm197eHgDg5OTECZHatWtDW1sb1atXFyQNOTk5ePHiBezs7Lhrb9++RWZmJidM+ODGjRsAgIKCgiJhWlpa3zYEVQKvX79GSkoKatSowV0zMTHhhl7/y7lz53D9+nX4+/vzlv87d+5wHdDnpKenw8DAQND6b2trC1VVVTg6Oiqk/T19+hQTJ06Ev78/lJWVFfICMHfuXAwdOhQvX74UxGbNmjXx8OFD3Lp1C61bt8b69etx8OBBNGnSBH/99ReePHnC1VEGQwhyc3Nx+fJldOjQoXIJADMzM+7N29zcXNAHr6amJicAAKBBgwZo0KCBYGmIiYlBo0aNuPxL3/6bNWvGq11pJ7d8+fJiw8eNGwdVVVW52FJRUQEASCQS7pr02Ir/jvi8efMGEyZMwN69e3ntjKRi5Pr16zL3IioqClOnThW0/qupqcHOzk6mHgrJ3LlzIZFI0L17d4U9A/r27QuJRIKFCxcKZjMqKgpr165FaGgo7O3tYWFhgbCwMOzatQuXL19mPRJDUM6cOYN27dp98yi4yo+acTU1NdSsWRMSiQSampqC2VVSUoKjo6NMh9+gQQOkp6fzanfWrFnYtGkTAODTp09QVlZGtWrVuPDPr40ePRpLliyRexoGDRqEbdu24dChQ9DQ"); result.append("0CjyJizPzsjIyAh16tTBq1evuGvPnj0DADRs2FBGFAwdOhRBQUG8C0HplMv9+/dx+/ZtvHnzBtOnT8fp06fh5ORUxAteVVWVV2EotPCU8uLFCxw8eBBt27aFlpaWwp4BOjo6cHFxwY4dOzBv3jxuWoQv4uLi0LZtWygpKeHAgQOIj49HVlYWhgwZAm9vb2zZsgWxsbEKG5VhVD4OHTqEoUOHfvsP/MjOD40bNyZnZ2fB7QYGBlJOTg73//Pnz9ORI0cEs9+zZ0/67bffZK517dpVEGfI4OBgzvkMAK/5Dg8PJ2VlZYqJiaH8/HxydXWlFi1aUGFhoYyj4PDhwwUrezc3NwJAxsbGNHfuXMrKyuLCli5dypVLv3796Ny5c7ymZceOHXT//n3B6//y5csJAE2ePFnhz4AxY8YQAEGcMDMzM2nKlCl04cIF7vnTuHFjIiK6cOECzZw5kzIzMwXL+4IFC2j58uXMC66Skp2dTRYWFiU6RZeFH1oA9OjRg7p161bpbrylpSU9f/5c5pqxsTGlpqYKYv/w4cNUrVo1AkDKyso0d+5c3ryR//jjD+rfvz8NGjSI5s+fL+Pxfvv2bWrQoAFlZ2dzXtErVqyg3r17k7e3N928eVOuKwB27dpFlpaWXCdf3IoLHR0d6tq1a4Wufx4eHgSAtmzZovC0hISEEADq3r274LYNDAyoRo0aCss7EwCVm4MHD9LIkSO/6zd+aAEwfvx48vPzq1Q3PT09nQwMDGSuvXz5kszMzARNx+vXr6lJkyZcZ9i6dWt6+/atoOq3QYMGdPv2be6ar68vOTk5UW5uLkVERJCGhgZdv379u22lpKRQu3btqHnz5vTkyRNydXUlAGRoaEjv3r3j4iUlJZGOjg69fPmyQtdBMzMzAlBkFEpRD0EAZGtrK7jtiIgI+v3333m3c/LkSdLS0iryUVNTIzU1tWLDTp48yXrICk7Pnj250ahv5YfeCbBWrVqCOgCWB2JiYuDi4iJz7caNG2jcuLGg6TA2NsZP"); result.append("P/2EgIAA6OrqIioqCi1bthRsM6Tx48dj6NChcHZ2BgDcvXsXO3bswIABA6Curo727dvDwMAAs2bN+i47r169QtOmTZGeno6IiAjUrl0bK1euhEgkQmpqKiZOnMjFDQkJwcqVK+W6HLI88vbtW24OXtFI/X9SUlIEt92+fXt07NiRdztdunTBx48fi3z8/f2xaNGiYsO6dOnCJsgrMB8/fuRWo3wPKj9yIXy+FLCiI3UCzMvLAxHJOADm5uZCJBJx1/hyAiwOLy8veHt7o3379nj48CFWrFiB+fPn82rz8OHDSE5OxtatW7lrf/zxBwCgTp063DVra2tERUUhOzv7m53VhgwZgufPn2Pjxo3cbzRt2hSjR4/Gxo0bsXv3bnTq1Am1atVCUlISVq9eXeHr4ucrMxSNhoYG90BkVHzy8/PRtm3bYsMuXrwo6J4wiuT48ePw8PD47lVPP7QA+O+bcEVmyZIlWLJkCXr27AkfHx/06NGDC+vcuTOmTJlSYsOQp+rU1tYuct3W1hYbN25E165dedkO+HOeP3+OefPmISoqSmYZpPQN8PMVIVpaWigoKMDbt2+/SQDcvXsX58+fBwBupEHKihUrEBUVhXv37mHUqFGwsLBAREREpaiLBgYGSElJQU5OjsLT8unTJwBA1apVWe9YCSgsLCzxGVNYWFhpyuHQoUOYMmXKd//ODz0FYG1tDWtr60rVAK5fv15kvX9MTIwgUwC//PLLF8WYdP0+HxQUFMDHxwdr1qwpsvGOrq4uAEAsFnPX8vLyAPy7gcu3EBcXx/39+PHjIm+ehw4dgpaWFj58+ICsrCwZ21Lu3btX4eqgdIojIyND4WnJysoCANSuXZv1jpWAKlWq4P9914p8FLUjptB8+PABd+/eRYsWLSqvACAirFu3Dhs3bqw0lf/FixdQVVWVWe+cmJiIGjVqCPIGlJqaisjIyGLDrl27BuDfKQG+CAoKQrNmzYrd9apVq1YAgKSkJJmykW7c9C1ItyAGgICAAOTm5sqE"); result.append("P336FAYGBlBVVUViYiLc3Nxkyufo0aO4dOlShauH0q2WExMTFZ6W58+fA4DgPjCVnZcvX2Ls2LFYu3ZtpXrz/pw1a9Yo5CCwY8eOoVu3bkX2YfnWjvSH5NKlS5wHujw8vX8EDh06RH379pW5duDAAfL19RXEfrt27cjMzIzOnj1LRMQdBnT37l0yMzOjQYMGUUFBAS+2o6OjqWnTppSfn1/iMr3//e9/1KZNGyosLKSYmJgSl+p9DZ6enlw9s7e3p4CAAFq+fDm1b9+e6tevTw8fPqQ//viDdHR0uHhmZmZUp04dsrW1FXRduFBIDyLq37+/wtMyePBgAkBnzpypdF7gS5YsoVWrVinE9qBBg7j6HhISUunK/s8//+Tyf+XKFUFtd+rUiTuM7Hv5YQXAmzdvyNbWlurVqyezFKsiM2XKlCINfvLkybR582ZB7N++fZsmTZpEjRo1IkdHRzI1NaVmzZqRh4cHr0vC3r17R/b29hQfH19qvIyMDBo4cCC5u7uTs7MzrVmz5rtti8ViWrduHTk7O5O2tjbp6upSixYtaNOmTTIbcCQmJtLAgQOpevXqpKenR15eXkX2aqgoiMVisra2JisrK4WnxdbWlszNzb9rMxTG17NhwwbS1tYmZ2dn6tChQ6XLf1paGtnZ2VHdunUpLS1NMLvp6elUp04dmc3QvgcR0f9vsM5gfCV+fn4YNWqUIOfAM8oXYWFhGDhwIO7du8cdkCU0CQkJsLW1xdatWzF8+HB2UxRAdHQ0Nm/ejH379rHC+AFRYkXA+FY8PDy+2cGO8WPTv39/tG/fHuvWrVNYGtatW4dWrVrB19eX3RAFsWfPnlKdgxnlGzYCwGAwvol3797B1dUVu3bt4g5KEooHDx6gW7duiIyMFPQ4cMa/5OTkIDg4GCYmJkwAMAHAYDAqI0lJSRg5ciTWrl0LW1tbQWy+fv0avr6+gtpkyBIeHg4XFxdYWVmxwmACgMFgVFays7Oxdu1adOjQgffleDdv3sSJEycwZcoU"); result.append("bu8HBoPBBACDwVAghYWF8lmbrGAbDAYTAAwGg8FgMCosTEozGAwGg8EEAIPBYDAYDCYAGAwGg8FgMAHAYDAYDAaDCQAGg8FgMBhMADAYDAaDwWACgMFgMBgMBhMADAaDwWAwmABgMBgMBoPxowmA9+/fY9y4cWjYsCEaNWqEAQMG4NWrV4ImPDc3F+vXr4e5uTkyMjIEs5uTk4MZM2bA0tISampqMDc3x/jx4/Hu3TtB7EskEoSEhKBevXrQ0NCAhYUF5syZA7FYrLBKNGzYMIhEIuTm5gpmc+bMmRCJREU+//zzj6B5T05Oxty5c9GpUyeMHTsWu3bt4tVe06ZNi8239GNhYcF7nnfv3o2WLVuiQ4cOcHd3R8uWLbF7927ByvzkyZNo06YNmjVrBktLS3h5eeHRo0fsac6oFJw4cQK+vr7w9fWFvb09HB0dERwcDIlE8vU/Rl9JamoqOTo6kre3N4nFYiIimjNnDpmYmNDTp0+Jb3Jzc2n16tVUp04dAkAA6P379yQEBQUF1KpVK1JWViZra2uqUaMGl4Y6depQcnIyr/YLCwvJ29ub6tWrRz4+PuTq6srZ9/X1JUVw8OBBLg2fPn0SxGZ6ejpVrVqVlJWVZT4dO3YUNO/Lli0jHR0dWrx4MeXl5fFu79atWwSAlJWVqXr16mRoaCjzEYlENG7cOF7TMG3aNDIxMaFHjx5x1+7fv096eno0c+ZM3stg+fLlZGJiQnfv3iUiog8fPlCHDh2oatWqdO3aNWIwKjKBgYHk7e1NBQUFREQkFotp5MiRBIAGDBjw1b/31QKgW7dupKOjQxkZGdy1vLw8MjQ0JDc3NyosLOS1AMRiMb17945SU1NJVVVVUAGwZs0aat++PSUmJnLXDhw4QJqamgSAvL29ebW/f/9+Cg4OlrkWGhrKdcB8C5D/kpiYSHXq1KGqVasKKgDmzJlDS5cuVVgjlEgk1KdPH1JTU6M//vhDMLujRo2iJUuWUHZ2drH3AgD9+eefvNmPj48nkUhE"); result.append("q1evLhI2Y8YMEolElJSUxJv9v/76i5SUlGj79u0y19PS0khTU5MsLCyKLRuGsG0jPDycYmNjK0yebt++TUeOHOE6XUWRkZFBqqqqtGzZMpnrOTk5pKenRwDo77//5k8AREVFEQDy8vIqEubr60sA6Pjx44IViJmZmaACYNCgQZSTk1Pk+qpVqwgAaWlp8Wq/pJtra2tLAOjBgweClb1YLCZXV1c6efIkmZqaCiYA3r17R1ZWVpSVlaWwhiit6//tiPgu740bN5YYHhwcTGZmZrwK8P379xOAYtOxbt06AsDrW7inpycBoMePHxcJ8/HxIQC0du1a1gsrgPT0dFq6dClZWlpS9+7d6dmzZxUmbwkJCfS///2PrKysKCQkROblV0iePn1KAKh27dpF2rl0NDg0NPSrfvOrfAAOHjwIAHBxcSl2bhIA73Ogn6Ompibo3Iufnx80NDSKXO/Xrx8AQCwWg3g8XPGnn34q9nrNmjXh4OCAunXrClYW8+bNQ5MmTdClSxdB78Hq1auRkpKCHj16YNmyZUhOThbU/u7du7Fjxw60bdsWvr6+gtlVUVHB6NGjSww/dOgQ+vTpA5FIxFsaTE1NAQCbN29Gfn6+TFhsbCyMjY3RoEED3uxfvHgRAGBoaFgkrHXr1gCA48ePs0liAYmLi8PIkSNRv359vHnzBhcvXsSxY8cE8UURCmtra0RGRuLo0aOIi4uDtbU1xo0bh/j4eEHTYWlpic6dO0NFRQWFhYVF/PI+b6O8+ADUrl2bANC+ffuKhEVERBAAMjQ0FEwRSf0AhBoBKG3YSyQSUcOGDRUyLFSzZk2KiYkRzGZkZCQ1btyYm/cWagQgIyODqlWrxk15AKAqVarQ/PnzBRme+/jxIxkbGxMAOn/+fLl5Q3ny5AkBoOvXr/Nqp7CwkBwdHQkAdevWjRtuv337NlWrVo1+//133mzn5uZy9/zFixdFws+ePUsAyNjYWLARmVatWpGZmZmMP4RQfPjwgZycnKh27dr08uVLQW0X"); result.append("FBTQsWPHqG3btmRnZ0cbNmygjx8/8mbv5MmTpKWl9VWfo0eP8paeN2/eUFBQEJmYmJCHhwedO3dOoe0/OTmZVFRUyMLCgnJzc/mZAigsLCRlZWUCQJcuXSp2eFraQDMzMyuVAIiLiyMAtGrVKkHtZmVlUZcuXWjbtm2C2UxLS6O6detSfHw8d00oAZCZmUlXrlyh48eP04wZM8jS0pKrc7169eJdBOzZs4frZKKjo8nb25tsbGzI2tqaRo0aRampqQqpf4sXLyZLS0tBbMXHx3MiyNnZmc6cOUMtW7akGzdu8G5bS0uLABT7cD99+jQBIG1tbcEeuiKRiADQrl27BL/nsbGxXN0/ffq0YC8bISEhVKdOHerUqRP98ccfvPt8lWfy8vJo9+7d5OLiQvb29rR582aF+KBMmzaNlJWVv+mlpMwCIC0tjatwxTX2e/fuceF8OgKVRwEwZ84csrCwKNY/gA9evHhBCxcu5HwglJWVafbs2YLY7tatG+3evVvmmpA+AP99KwwKCuIexCtXruTVXo8ePTgBEBQURLGxsXT79m1ubtrS0lIhIsDZ2ZlmzJghmL3ExERycHDg2rtQwrd79+4EgH7++eciYVJn2Fq1aglWDnv27KGgoCBBVoAUR2hoKIWEhPAufJOTk2nMmDFkbGxMfn5+MuKf8S+XL1+m3r17U82aNWnGjBmUlpYmiN2UlBTS1NQs1T9ILgLg5cuXXIO/c+dOqYpUqIdgeRAAqamppK+vTxEREYJ2fImJibR3715ycXHhyn3Lli282l27di0NGjSoyHVFCQApK1euJABkZmbGq526desSANqwYYPM9fz8fLK3tycANHjwYEHzHh8fTwDo1q1bgtm8fv069e7dmwIDA0lJSYkA0OjRo3nviO7cucOtuJkyZQp9+PCBMjMz6cCBA9yzoHPnzqw3kjPPnj0jT09PMjIyooCAAEpJSWGF8h+ysrJo3bp1ZG5uTj///LMgS+KJiCZNmkTTpk375u+XWQB8/Pix1BGA"); result.append("q1evEgASiUQkkUgqjQDo1asXLVy4UGH2JRIJDRo0iACQvb09b3ZiY2PJycmpWO97RQuAwsJCatiwIQGg169f82ZHV1e3xCHo9evXEwCqWrWqoMOiCxYsIFtbW8HsRUREkImJCbfk9LfffqMqVapwIoBvrl27xoleJSUlqlevHq1bt46srKwIAG3atIn1Rjzx9OlTmjx5MtWsWZN8fHwE8zs6deoU6erqftVHqNVojx8/pokTJ5KpqSmNGTOGHj58KOiLoIeHx3f1t1/lBCh90Bfn7HPq1CnBh+AULQBWrFhBI0aMUHjDTEtLIzU1NVJVVeXNhnTpW1k+0k1ahCQwMJAA8OoQJZ37Lq7+379/n8v/hw8fBMu3o6Mj+fv7C2Lr3bt3pKenR9OnT5e5/vvvv3N7cgi1GU96ejo3zHrjxg0CQHp6eoKWfWV/27W1taWWLVtSeHi4YC995YXz589T165dydraWmFLA69evUqHDh36rt9Q+ZoVA61atcL+/fvx5MmTImHPnj0DALi7u1eK5S+HDx/GzZs3ERYWpvC0VK9eHS4uLrxuh9q4cWNkZ2eXuDXlp0+f4OXlBSUlJVSrVk3wMjAxMUH16tVhZGTEmw07OzskJyfjzZs3RcLMzMwAAOrq6tDW1hYkz48ePcLdu3exf/9+Qezt378f79+/h6urq8z1jh07IjAwELNnz8aJEye4JcF8oq+vz/3t7+8PAFi4cCGqVq3K1ubxjLa2Nvz8/DB27FicOXMGa9aswdSpU+Hn54dhw4YppP0LQU5ODvbu3Yu1a9fC0NAQ48ePR9euXaGkpJgjdZ4+ffr9be1r1ILU07a4eeDhw4cTADp16lSFHwE4deoU9ezZk/Lz84sdklcE9evXp379+inEtqKnAIiIfvnlF5oyZQqvNtauXUsAaNSoUUXCXrx4UaKDGp+jHg4ODoLZ8/f3L3EKJDk5mQCQn5+foPdduhV17969K7VHuqK5d+8ejRw5kgwMDGjs2LEKWxHDB2/fvqXp06eT"); result.append("qakpjRgxguLi4spFuuRR3796K2BXV1eqXr26zMM+Ly+PDAwMqGnTpoI2Qum+BEIKgJMnT1LXrl2LXW/56tUr8vHx4c12fn5+sQLj2rVrpK2tTffu3avQAuDp06d04cKFYvPv4ODAez3Iyckhc3Nz0tPTK+ILcejQIRKJRMUukeULe3t7CgoKEsxeZGQkAaCpU6cWCXv48CEBoBMnTgiWnuvXr5OmpiZ5enoqRHxu376d5s+fr5BVAO/fv6fJkydTUFDQV6/95ntqZunSpfTXX39VGAHw559/0tKlSyk9Pb3cpCk/P58WL1783UvAv1oAPHnyhAwNDbmDPwoKCsjPz49MTEzoyZMnghaC9DCe58+fC2IvLCyMVFVVycXFhdzc3LiPq6srOTk5kYqKCq9LoszNzUlXV5dmz55NCQkJ9O7dOzp06BDZ2NjQ2bNnFVYZhRIA0u0uW7ZsSYcPH6bo6GiaP38+NWvWTOZ8Bj6JiYkhHR0dGjBgACfGXr58STY2NrRo0SJB37gACL4JzYgRI6hKlSoUHR3NXcvOzqauXbt+02Ek38qvv/5KNWrUoEWLFilkj/Znz55xPh979+4V3H5AQABnn+8DoBjlj/DwcO7+f88zAN/ypcTEROrduze5urqSm5ub4EM+GzZsoN69e3MF4OrqSkuXLuW1Azpy5Ai33rykj4qKCq/lEBgYSKampqSqqkpVq1YlZ2dnmjlzpsKX5QglAKKjo8nFxYU0NDRIT0+PWrduTaGhocVOxfDJ3bt3qXPnzuTs7ExeXl7UpUsXQc/AkHYAzs7Ogt/rwsJC2rJlCzVp0oQ6duxIAwYMoC5dutDmzZt5H/3bt28fTZ06lbp160bTpk1T6H7zubm51LRpUzIzM6OEhATB7R8/fpx0dHTIxcWFbGxsWI9YyXj69CmZm5tT48aNv2vzIRERj5vXMxgMBoM3kpKS0K9fP1y9epUVBuOrUWJFwGAwGD8me/bswahRo1hBML4JFVYEDAaD8WMhkUiwceNGiMVi"); result.append("+Pj4sAJhfBNsCoDBYDB+MP744w+YmprC0dGRFQaDCQAGg8FgMBhlh/kAMBgMBoPBBACDwWAwGAwmABgMBoPBYDABwGAwGAwGgwkABoPBYDAYTAAwGAwGg8FgAoDBYDAYDAYTAAwGg8FgMJgAYDAYDAaDwQQAg8FgMBgMIfnqw4DEYjFOnjyJM2fOQCwWQ11dHUSEnJwcqKio4KeffsLgwYOho6PDSpfBYDAYjPIKfQW7d+8mNzc32rBhA2VkZBQJLygooN9//508PT3pjz/+IL4ZO3YsXbx4kVcbFy5coJkzZ5Kuri4BIACkoaFBtra25OLiQpaWlmRra0sDBgygs2fPkhBERUXR4MGDqU6dOmRsbEwNGzak5s2b04IFC+j58+d05coVWrhw4XfbefToES1cuJDq1avH5R0AqampkbGxMenr65OlpSW1adOGZs2aRf/88w8v+T18+DBNmDCBNDQ0CACpq6uThYWFzMfU1JTU1dUJAPn7+8vV/oMHD2jBggVkZWXFlYGJiYmMfX19fS5swYIFvN37t2/fUkhICLVo0YIcHBzIycmJmjRpQm3atKFVq1ZRUlISjRkzhvLy8uR2/+vWrcvlzdjYmGbMmEE3btzg4h07dowmTJhAampqXLz//e9/tHz5csrOzpZr/u/cuUOBgYFkaWnJ2TI3N6f58+fTnTt3BGl/0vpQu3Ztmfowb948iomJkZsdafnXqVNHpvznzZvHtbWMjAxauHAhaWtrEwASiUTUpUsXOnDggNzScfDgQRozZgypqqpy6XBxcaGAgAC6ffu23Mv34cOHNHPmTDIyMuLsbd++vczfHzhrtvZMAAAgAElEQVRwoEw9DA4O/up6mJubS4sWLSI3Nzfut/r3719i/GfPntGiRYvIycmJAJCTkxMtWrSIXrx4IdeyiYmJoV9++YXs7e3J1taWLCwsyN7eniZOnEhPnjz56t8rkwDIzs6mHj160LBhw8pUkBKJhGbMmEGhoaG8NcKMjAzS1tamHj16CNLo"); result.append("V69eTQBIS0urSNjly5fJ0dGRAJCPjw+JxWJe0pCZmUl9+vQhZWVlmj59OiUlJXFhOTk5tGvXLjI1NSVlZWWaNGmSXB+60kYQHh5OEomEC4uNjaXx48dzDwcfHx/6+PEjL/kfPXo0ASA3N7cSG+3gwYNp4sSJvNj/+++/uXJ48OBBsQ/shg0b0syZM3mxf+jQIdLR0aEWLVpQdHQ0FRYWcmGpqak0Z84cUlFRIQByffDExsZy+T5x4kSJ8aZNm0YAqEaNGpSfn8+7CJamKTIykhTBP//8w6Xh9OnTvNmJiYnh7Jw8ebLYOI6OjmRoaEhRUVG8pcPX15cAkKGhoVwE5pc4e/Ysl+969erJ1PeSePnyJfcs0tbWlks9nD9/PpeO4ODgL4oXAJSYmCjXssjOziZfX19SU1OjJUuW0Lt377iwhIQE8vb25sLkKgByc3OpRYsW3/RWNWTIELp37x4vlSMkJIQAkLKyMj1//pz3ynjs2LESBQARUXp6OqdYQ0JCeBE89erVIyUlJTp27FiJ8ZKSksjMzIwGDx4sV9vSBvD5m9/n/Pnnn6Snp8epbj46gMDAwFIFgPQNecSIEbzUgZcvX5YqAKRiacKECXK3vWDBAu4tpKCgoFSRAIBu3bolN9tpaWlcvkt7w5W2SUdHR97b4+PHj7k0fcubjzxISUnh0hAbG6swO8uWLSNLS0t6+vQpr/mVdoRNmzYVpHyTkpJITU2NG1k6fvz4F78zdepUbjSkTp06ckuLdARYSUmJfv/991I7agAyL0nfS25uLrVu3ZoA0KFDh0qM5+fnRwBo/PjxZf7tLzoBjhkzBqampggKCvrq6YXVq1fD399f7tMWhYWFWL9+PbS1tVFQUIBNmzbxPlWirKxcari+vj769u0LANi9e7fc7Q8fPhwPHjzA8OHD0b179xLj1apVC1u2bMH79+8FyzsAuLm5ISwsDABw6dIlLF26VO5loKT0ZZ/VGjVqoGvXrgqpAwDg6OhY6v35FiIiIhAQEAArKyvs"); result.append("3Lmz1HLw8vLCoEGD8ObNG17yXZptaVhZ7pNQaaoIaSjNzq+//ooNGzYgMjISVlZWguRXRUVFkPJVVVWFhoYGBgwYAABYtmxZqfGzsrKwbds2DBs2jJd01qtXD4WFhejfvz8SEhJKbQNleVaUlQkTJiAqKgo9evSAl5dXifGCg4NhaGiItWvXYu/evWV7ppYWGBkZiWPHjmHjxo3flHBdXV3UqFEDz549k+uNOH78ODQ0NBAYGAgA2LZtG3JzcxXuTyG96ampqXL93d9//x3h4eEAgGnTpn0xvoeHB8zNzQXPf6dOndCxY0cAwIoVK5CVlaWQ+8CXACgrbdq0kdtv5eTkYPDgwSAiTJ8+Herq6l/8zvTp0yGRSJiDUwVn586d8Pf3x/nz52FpaVlh8zl16lSIRCJcuXIFV69eLTFeaGgoWrVqBTs7O17SsX//fpibmyMjIwPdu3cX5PkWFxeHrVu3AgBGjx5dalxNTU0MHDgQADBr1izk5eV9nwAICAjA9OnToa+vX+xb+NatW9G3b19MmjQJgYGBOHXqFFq0aIFjx47JdEbnz5+Xa6GsWbMG48aNg6+vLzQ1NZGWloYDBw4otJLm5OTgyJEjAIAmTZrI9bdDQ0MBADY2NrC2ti7Td+bNm6eQchg0aBAAIDMzE6dPnxbU9v79+/HPP/8oJN9isRizZs2S+++GhYUhOTkZANCrV68yfcfBwQGdO3dmPWQFZt26dfD398eFCxfK/Ez4UXFwcOBeLEoaBZBIJFi7di2mTp3KWzoMDQ1x/PhxaGlp4cGDBxg4cCCIiNe8h4WFobCwECoqKmjRosUX47du3RoA8PLlS0RGRn67ALh//z5u3LiBkSNHFgnLy8tDr169EB0djb1792LVqlVwcHDAhAkTcPXqVTRv3pyLa2FhUeJwybcQGxuL2NhY+Pj4oFq1avD29uYahFAUFBTI/H39+nV06tQJz549g76+PhYvXixXe9Ib6eDgUObv1KhRQyGNtVmzZtzfN27cEMzu27dv"); result.append("sWHDBoWJv8WLF+PTp09y/+2TJ08CAExNTWFgYMB6PgYWLFiApUuX4uLFi7C1ta0UeZaOfJ44cQKPHj0qEn7w4EEYGxujZcuWvKbD2dkZe/bsgUgkwokTJxAQEMCrvcuXLwMAzM3NoaGh8cX49vb2Rb5bGiVOkhw/fhxt2rSBnp5ekc6vc+fOeP/+Pa5evQpVVVUAgK2tLZ4+fYqffvoJhoaGXHwtLS25Ds+vWbMGw4YNg5aWFgDAz88PW7duxa1bt/DXX3/JiA8+yM7OhpGREQwNDZGdnY2XL19yw61dunTBqlWr5KrI09LS8OHDB4V26l+rkqWkpKTwYuPWrVsyw3zZ2dl49eoV72r8cxo0aACRSAQAyM/PBxFhwoQJcrfz4MEDAED16tVLjbdhwwZcuXIF7969k0njuHHjYGZmJrf09OjRo8RpCHn6nTCKZ8aMGThz5gzc3Nzkel/LO23atIGLiwtiYmKwfPlybNu2TSY8JCQEc+bMESQtPXr0wIIFCzB37lwsWrQIzs7OZR6d+1qko3/VqlUrU/zP40m/+00jADdv3ixWTQUHB+PixYvYsWOHzINAOs//36HH169fw9jYWG5veQcPHoSfnx93zcnJiUunEKMAWlpaePv2LeLi4pCYmIh3794hPDwc9evXx7lz5zBz5kwkJSXJzV5+fj73d1kUoKL53ElJTU2NFxuNGjXCw4cPuc+LFy/w5MkT1K9fX7B8xsbGIjc3F7m5ucjJycHcuXN5sSO9/5mZmaXGGzt2LJYvX46oqCicPXsWGhoaCA4OlnsncfToUZmy//zDxxQIo6jwVFZWxpUrV9ClSxfk5ORUmrxLh/f37t0r07lduHABmZmZ6NGjh2BpmTNnDvr37w8iwuDBg3H37l1e7X0+6lwanz9zy+KIWKIAeP78OerVqydz7cmTJwgMDISnpycaNGggE3bx4sViBUB8fLzcHFQ2b94Md3f3Ir83duxYAEB4eDhvb50loaOjg169euHmzZtwdnbGb7/9hmbN"); result.append("msnNC1tfX5/rVN++fVvuG+nn+TYxMRHMrpWVFUaNGqWQPFepUgWBgYG87H4pHVF5/fo1CgsLS41ramrKOX82bNiQ9ZYVkAEDBmDPnj1QVlZGZGQkunbtysvUU3nEy8sLlpaWyMvLw5o1a7jrK1aswOTJkwVfDbJjxw40adIE2dnZ6N69O9LT0+Vuw8jICEDZR9c+d0w0NTX9dgHw8ePHIsMOK1euRH5+PiZNmlREnRw7dgwGBgZwcXGRCTtz5gw6dOjw3QUhFouxadMmxMTEwM7OTubj7+8PFRUViMVibNmyRSGVU11dnZv7T05Oxvr16+XWuUjfbO/fv1/uG+nff//N/e3m5iaobXd3d67BKGLkQ7pcSZ5IR7fy8/Px559/fjG+dEqOr9EXRvHIc9nXl+jfvz/27dsHFRUVXLx4Ed26dasUIkBZWZnrezZv3oysrCzcu3cPMTExGDp0qEKE/7Fjx2BqaorExET07du3zG/qZUX6DH3x4sUXRwGlL+lSpA6B3yQANDU1ZZYSEREOHToEY2PjIt6IYWFheP78OX7++WduXhT4d/5aIpF8cf6yLBw6dAg1a9ZEUlJSkaHH+Ph4zJgxAwCwZcsWiMVihVTQz73/5dlZ9+vXDwBw584dJCYmluk72dnZgs6Jf14XpG//7du3F9S2jY2NwgQAgCIjZvJg2LBhXJuSli2j/KGrqyuovT59+nAi4Pz58+jevXu5WAot5eHDh7z87rBhw6Cvr48PHz5gy5YtWLFiBcaMGaOw6VFjY2OcOHECmpqauHDhAqZMmSL3ER9p/1sWr37pMsk6deqUySGyRAFgYWEhM5yemJiItLQ0Gecn4N/lBtL5T3d3d5nfWLx4MTc8/72sWbOm1MIdO3YsVFVVkZycjN9++00hleHzIXoLCwu5/a50MyYAmD17dplGS2bMmCHjPyAEly9fxokTJwAACxcuVNhbaF5eHqZPn14hOhZ7e3uMGTMGwL9DjrGxsay3LWfo6OjIOL8KhZeXFw4cOAAV"); result.append("FRVERETA09OzXIiA/Pz8Mm9E87VoaWlxU30hISE4evSo3PqYb6VRo0b49ddfIRKJ5D4C7ezszL0AfmnDO4lEgp07dwIAli9fXqaNkEoUAD/99BNu3bol81AFgIyMDO5aSkoKgoKC0LZtWwCAq6srF3bu3Dm8fv2aW7/5PURHRyMpKYkriJKUmKenJwBg1apVcr/JZRlVCAkJ+bdQlZS43ajk9XZx4MABaGpq4sCBAwgKCirx7T4vLw8jR47EiBEjyrRpjLzyHhcXh759+6KwsBAjRozgZUhOOrz2pZGN+fPn8zIH/vkcvLyH+kpjxYoV8PDwgEQiQf/+/fHixQtBH3Blzbc0TIiyUcS9SE9PR926ddG1a1cUFhZydjt37szrFMB/lx1/Tq9evXDw4EGoqKjg7Nmz6Ny5M28b1EifA196HgQEBKBx48bfbU8ikRTr9zJ+/Hioq6sjJSUF/fv3L7I8Vjpy/SWfGXmk5XMxxteSwNDQUDg6OuLs2bOljgLOnTsXjx49wuzZs8vuEFnSHsFxcXFkbW0tsx9xjRo1CAD98ssvNHfuXOrWrRu9f/+eO33p3r17lJeXRytWrCAPDw/uUJjr16/T5cuXv2kfZLFYTE2bNiUvL68vxt2yZQu3Z7Y8T8MiIlqxYgUBIE1NTfrw4UORPeKlB9UoKyvzdghSVFQUWVhYcPvh7927l+Lj4ykjI4MeP35MW7dupQ4dOtBff/0lV7u3bt3iyvW/py+mpKTQ4sWLSUdHh7S1tb94WMb3MGzYMAJAlpaWxZ418OnTJ1q0aBFpaWnxciDRtWvXBDn8pTjy8/Np6tSppKamRkZGRhQaGko5OTkyed+1axfp6emRurq6XOv/54feHD16tMR448aN4w4D4utALCmXLl3i0hQdHS3IPbh79y5nc+/evXT+/HmqWbMm73vw37hxg7N76tSpYuOMHDmSi+Po6MjL2QRDhgwhAKSrq0sJCQkyZ1Lk5OTQ/fv3afTo0aSpqSmXUyCvXLlCIpGoyPOW"); result.append("iGj48OGkpKRE8fHxJR5KpaOjI5c9+d+/f1/qOShSCgsLycvLi/B1h+yWOQ1dunQhVVVVCg4OpszMTC7s6dOn5OPjQxoaGrRmzRr5HQbUtm1bmQfdmTNnyNTUlMzMzGj+/PmUm5tLRESRkZFkampKJiYm5O7uTmFhYVzlKCgoIHd392Jv4pf4/fffqVmzZtwRvKNHjy7xJixZskTm2FJ1dXUaMWJEsRXka7hw4QJNnz6dO2ACnx3LaWdnR+bm5qSrq0sODg40bNgwunv3Lq8Pg+zsbFq/fj21bduWjI2NuaN5W7RoQRs2bJCpGN/Lo0ePaMGCBWRjYyOTdwMDA3JwcCB7e3uysrKizp07U0hICKWlpfGS58OHD9PYsWNJSUmJS0P16tWpTp063MfMzIw7Baxv375ytf/gwQMKCgoia2trzr6hoSEFBATI9fjXspCYmEiLFi2iVq1aUe3atcnJyYnq1atHlpaW5O7uTitXrqTk5GS53v/P25WRkRH5+/sXOQ7Yz89P5rhYIY8DtrKyosDAQEGOA16wYAEZGBiQoaEheXt7f/fzpTTu379Pc+bMkTmG2sjIiGbMmCFT77Zs2UK1atWSaaPKysrk4eFBmzdv/u50HDx4kEaNGkXKysoyNkr6dO/e/bvrXVBQEHcMspubGy1dupTevHkj0yZ79eol870zZ87QpEmTqEqVKlxaOnToQMuWLfumevjmzRtasmQJNWnShDuRcOHChfTo0aMSv5OTk0MuLi681YmIiAjy9vYmGxsbcnBwoHr16lGDBg1o5syZ33QCqIhKGU+9desWBg8ejJs3b37zcPKCBQtgbGyM4cOHs8lCBoPBYDDKCUpfcm7w9fXFkCFDvmk+JTQ0FMnJyazzZzAYDAbjRxIAADBp0iQ4OzvD09OzzJvbfPz4EX5+fkhMTJTbengGg8FgMBjyo9QpgM+Jjo6Gv78/WrZsiUGDBhV7CMXjx4+xf/9+REdHY/bs2XI9FpXBYDAYDIYCBADw7/Krc+fOITw8"); result.append("HM+fP4eqqiqUlJQgEolQUFAAKysreHl5oVWrVjJ7BTAYDAaDwfiBBQCDwWAwGIyKgRIrAgaDwWAwmABgMBgMBoPBBACDwWAwGAwmABgMBoPBYDABwGAwGAwGgwkABoPBYDAYTAAwGAwGg8FgAoDBYDAYDAYTAAwGg8FgMJgAYDAYDEY55927d7C2tgbbQBa4ceMGkpKSKr4AiI+Px8KFC2FnZweRSMR9NDQ0oKenBzs7O/j6+iImJob3BN+/fx/jx4+Hvb09zMzMUL16dTg5OWHmzJl49eqV3O1dvHgRs2bNgp6eHpdvZWVlGBoaomrVqrCwsICHhwcOHz4sSKO4f/8++vfvD0NDQ+jq6sLGxgbjxo3DlStXsHLlSly6dEnuNsPCwmTue1k+ffv2/W67586dw6xZs1CtWjXudxs2bIglS5bg5cuXMnETExOxePFiODk5QSQSoVatWpg/fz4eP378zfajo6PRs2dPmXzVq1cPixcvLjb+6dOn0aJFCy5ut27d8OTJk6+2u2zZMpibm3O/U7t2baxcuRIAEBcXB19fX+4MDpFIhI4dOyIsLEzmNy5fvoyRI0dCSUkJqqqqGDFiBJKTk78qHUlJSZg0aRJq1aolUwaGhoaYM2cOsrOzubi//fYbevfuzcWpX78+goKCvuv+R0ZGol27djK2DQwM4O/vjxcvXsjEffLkCUaNGsWVS9WqVTFt2jS8fv1aLm0gKipKpv1ra2sX+SgrK0MkEkFFRQVXr17l7RmQm5sLkUgENTU11KhRg/v8t0z4QF9fH7a2trh27ZpCOqwXL17I5FlNTQ0ikQi5ubmCpkMsFmPQoEGYNGnSj61i6Cu4efMmASAAdOnSJSIiSk9PpwULFpCysjKJRCJatWoV8UFBQQFNnz6dVFVVacqUKZSUlERERBKJhKKiosjNzY20tLRo69atvNhfu3YtAaCqVatSdnY2ERF9+vSJtm/fTtra2gSABg8eTIWFhcQXly5dIg0NDRowYAC9fPmSiIiSkpJo"); result.append("1qxZpKKiQgAoMjJS7nY3b95M9vb2dPPmTcrLy+PyLq0Lf/31F3cvHj16RJ6enuTh4SE3+8HBwZytlJSUUuMmJCQQALp+/brc7M+cOZOzf/HixVLjisViUldXp7Fjx36XzUePHpGSkhIBKLZNTZkyhUvTo0ePSvyd+vXr08KFC78rLbm5udSvXz/O3tSpU4uNl5aWRgBo7NixlJ+fL7fynzZtGmc7ICCg1LjNmzcnCwsLio+Pl2sbOHz4MNWtW5f+/vvvYtv4vXv3qEqVKgSAZs+eTXwibXvt2rUjRbBz506aMGEClQd+/vlnAkCfPn0S1O6yZcvIx8eH6tevT+fPn6cfla8SAKmpqVxDvHv3rkzYnDlzCACJRCK5PnyJiAoLC6lPnz4EgDZs2FBsnLy8POrYsSMBoODgYLkX1LFjxwgA6erqFgnbtm0bVy779+/n5UZJJBIyNzen+vXrk0QiKRJ+5MgREolEvAiA5cuXc538fx9CnwuAz8O8vLzkZj88PJwAkKam5hfj5uXlEQB69+6d3Oy/e/eOtLS0CACtWLGi1LgPHz4kXV1dysjI+G677u7uBID69OlTJOzt27ekqqpKAOjIkSPFfj8nJ4eqVasml7IQi8XUqlUrAkA1a9ak9+/fF4nj5+dHffv2lXv9E4vF1LBhQwJAdnZ2JBaLi42Xnp5Oenp6dO3aNbmnYePGjXTq1Kliw/Lz87n0NWrUSK7ipzwKgPfv35OVlRWvLzvlWQC8fv2azMzMKCUlhS5evEgODg4l1skKJQDevn1bogB49eoVFzZy5Ei5JnLRokUEgP73v/+VGi8pKYk0NDRISUmJzp07J9c0nDx5skQBIJFISF1dnQCQp6cnLzfq9u3bBIB69+5dYpxOnTrxIgD27t1bpLGXJgCIiHbt2iU3+0ePHi2x7IvrLABQVlaWXMtgzJgxBIDs7e1LjTdnzhyaOHGi3ModAGlpadHHjx+LhHft2pUA0MCBA4v9/pEjR6hr165yK4OXL1+Snp4eAaAh"); result.append("Q4bIhO3fv58cHR250TE+6r90lGvp0qXFxhk7dixNnjyZF/tLliwpMW/SEaIqVarQvXv3eH9oK1oAEBF16dKFoqOjK6UAGDBggMyonJeXF61Zs6ZyCwAi4t6Sfv75Z7klMCUlhTQ1NQkAhYeHfzF+z549CQA5OTnJVaGWJgCIiOrUqUMAqEWLFrzcqFu3bnGdQUkPmR07dvAiAEp7CJUkAORJeRAA8fHxJBKJCACdPXu2xDdBY2PjUofkv4bs7GxueiksLKxI+Pr16wkAaWtrF9s59e7dm/bt2yd3MSi972fOnCEiort375KxsbHch92LE1cASENDgxISEmTCrl27RlZWVsUKJT65fPkyN1WzevVqQdueIgXAnj17yM/Pr9IJgOjoaKpfv77MG//z58/JxMSE3r59W3kFwIcPH7iwoUOHyi2By5Yt4363LEOZ27dv5+JfvXpVEAHw6dMnTqSMGjWKlxslFovJ1NSUS8Ovv/5aJM6rV6/o+fPnTADwIACkbz0AqGPHjsWG79+/n9q3by9Xm4MGDSIAxfpU9O7dm7sH/+3oMzMzqUaNGry8kffo0YMAkJmZGT1//pxsbGxKnIaQJ7m5uVSvXj1uNFAq8PPz88nR0bHEIXq+yMzMJCsrK64zFmpIvDwIgMzMTLK0tKSCgoJKIwAkEgk1aNCALly4UCQsKChI7iPfP5QA2LRpExdW0hvS99xgU1PTr3pTBvDdzk9lFQALFiwgAKSmpsbrW9D58+c5RyMA1Lx5c4qKilJIxamMAuDChQucn8v9+/eLhLds2VLuHWFERAQBIBUVFXrz5o2M4K5WrRonAv4rEH799Vfy9vbm5X6kpqZSjRo1OKfYadOmCVbvrl69yr1xSx1+Fy1aVKyfBN8MHTqUAFC1atXoxYsXgrc9RQoAIiJPT88vOsVWJAGwdu3aEn2bPn36RNbW1nTr1q3KIQBu3LhBRP965x8+fJh0dHQIgNzmP6XY2dkRAHJ0dCxT/BcvXvDii1CcAEhKSqIpU6aQ"); result.append("SCSi6tWr04kTJ3i/YdevX6e6detyeQRAnTt3LrZDYgJA/jRo0KDYuhUXF0e1atUq1kHzeygoKOBGftatW8dd37lzJ/Xs2ZOioqKKFQju7u50+vRp3u7J4cOHuft/8uRJQevexIkTuY43OjqajIyMKDk5WdA0SOtkSdMzlUEA7N+/n7cRz/ImAN6+fUumpqaljrAeO3aM3NzcKocA6NGjB3l6elKjRo3IwcGBvLy8eBmCMzc3JwDk4uJSpvg5OTlcGvv37y93AaCiokLu7u7k4OBAAEhJSYk2b97MW4dTHHl5eRQcHEy6urpcXlVVVWnRokVMAPAsAHbu3MnNQ6elpXHXR48eTQsWLODFpnQZXLNmzbhrHTp0oKNHj1JhYSFZWloSAFq7di0R/es3Y2hoyKtn8tWrV0lZWZmbCvjw4YNgdS87O5sbeldRUaHNmzcL+tBMSUnhRkD4WPXwowiAjx8/koWFhdxFb3kdAaiIyNUJkA9cXFwIANna2pYp/rt377g0jhkzhrcRgIyMDKpduzYBoBEjRijk5qWlpdGkSZO45WBlWSf9IwqAEydOlFkA5ObmEgDKycnhTXwZGhoSAE5wZWVlUfXq1b+4R8G3cufOHa6sHz9+TCkpKWRgYMDtyTB79mwCQE2aNCEiojVr1tDo0aN57QAtLS0pPDycE6HDhg0TtO7v2bOHE2JCL0fz8PDgpiXludz0RxMARP/6oURERDAB8INS7rcCtrKyAgC8fv0ahYWFX4z/9u1b7u8GDRrwli5dXV0cPnwY6urq2Lp1K/bv389rOeTk5OD+/fsy16pXr46VK1ciNjYWdevWBQAsWbIEaWlpFWrLTQ0NDa4M6Au7LWZnZ0NZWRlVqlThJS1qamoYM2YMAGDDhg0Qi8XYvXs32rdvD0NDQ15sOjo6cnV53759OHDgAHr16gU1NTUAgI+PDwDg77//xuPHj7Fv3z4MGDCAl7RIJBL07dsXkydPRq9evRASEgIA2L59O86dOydYnahWrdq/W5n+"); result.append("/85/QrFp0yacOXMGIpEIO3fuhJ6eXqXeDrdv3744ePAg2yO5Im8FrEi6dOkCAPj48SMePXr0xfixsbEAAGVlZXh4ePCatkaNGnFbtP7yyy9ISEjgzVZmZia2bt1abFi9evVw8uRJqKioQCwW4/bt2xWqktasWZPbfjM1NfWLW4WamJjw2imMHj0aVapUwevXr3Hw4EFs2rQJY8eO5bUMpJ18WFgYwsLCMHDgQC7Mzs4OLi4uAICgoCCkpqbCzc2Nl3RMnz4dxsbGGDduHABg2LBh6NChAwBgxIgRyMrKqrAPy4SEBEydOhUA4Ofnx+W7pHpYGejcuTPOnTsHiUTCelMmAORPjx49uA4gPDz8i/GPHTsGAOjXrx/MzMx4T9+YMWPQt29fZGVloU+fPrzuSX3s2LESR0FsbGxgZ2fHjU5UJGxtbaGlpQUAX9yDPCIiAs2aNeM1PQYGBvD29gYATJkyBQDQsmVLXm0OGDAAysrKePToEdLS0op08FJBsPe5hhUAACAASURBVGfPHvTr148XAXTo0CGcPXu2iBDdunUrtLW1kZSUxJVHRUMikWDgwIHIycmBnZ0dgoODS4wrFouxadOmStGBaGhowNXVFefPn2e96Y/I18wXJCcnc3ORsbGxgnqbAiAjI6NSt1hNSEggdXV1MjQ0lLtXsNQRTUdHp0hYZmYm2djYEADy9fXlpQykZV+So9+bN29IQ0ODbGxsBFmbm5WVxdWFK1eu8G4vICCA22ipJOe227dvk66uLsXExPCenri4OC7/GzduFKQdSLcG9vf3L3ZeXuqUd+fOHbnbvnHjBhkYGJS42kS6KREAQVbDSNujhoaGIGU/b948zulQugKqJLZs2UIrV66sFD4ARP/uOPnfnSGZD0AFdAL8+++/uUYu9KYb0g2BevbsWWwH8P79e3JxcaHq1at/sYF+C6tXr+bWgBfn8fzPP/9wa/QnT54sdw/sz8XXwIEDOQFWWFhIt27doiZNmpC+vr4gnR8R0YMHD7j0HDx4"); result.append("kHd7ubm51KtXLwJArVu3psjISMrMzKSsrCy6desWTZs2jbS1tWnHjh2C1ckOHTqQjo6OYCtApI5vJe002LFjR6pfv77c7Z48eZL09PRKdfTLy8vj1ufr6enxviXuunXruPqXnp7Oq63r169z2xAHBQWVGC8jI4NCQ0NJS0uL1wNiypsA+PTpE5mbm3NOqUwAVDAB8OjRIwoKCiJra2uu0dWqVYsCAwMF63CI/l1nWatWLWrUqBHt3buX7ty5Qzdv3qQ1a9aQmZkZtWvXjp48eSJXmxcuXKAZM2Zw+xwAIFdXVwoODi4yyhAaGsrFMTc3Jz8/P3r9+rXcBMD48ePpxo0btGDBAnJ1dSVjY2OqWrUqWVhY0KhRowTZjOTZs2e0cOFCql+/PpdXExMTmjdvHi/C63MKCgpo586d1KFDBzI0NCQVFRXS1dUle3t7GjNmjOB7IZw5c0auK02+xMePH6lt27YlhoeFhdHixYvlZu/06dPUrl077j7XrFmT5s6dS7m5uTLxoqOjZXYlxP9vWT1q1KgiW/Z+L9HR0TRr1izuTAIA1LRpU1qyZAmlpqbyUu6f13VNTU3S0tIq8tHQ0JDJP19pKY8CgIho4MCBgu8HwQTA9yMiEuAQezkiFouxZ88eTJw4kXM4UldXx4ULF3hzfGIwGIzyQm5uLjQ0NNCuXbtKP/fesWNHnD17Fp8+feJt5Q9zAixHqKqqwtfXF5cuXYKpqSkAIC8vDxcvXmR3k8FgMBiMiioApDRq1Ah37txBnz59AACBgYE4deoUu6MMBoPBYFRkAQAA+vr6OHjwIKKjo+Hm5oZevXph1apVyM/PZ3eWwWAwGIxS+OF8AErj/v37OHToEJ4+fQpbW1u0b9+e9zXhDAaDISRSHwA1NTWZnQhv3LiBWrVqVei8v3jxAg0bNuT+n5mZCbFYXKF9AGJiYtC0adOv+s6VK1fK9J0KJQAYDAaDwWCUDSVWBAwGg8FgMAHAYDAYDAaDCQAGg8FgMBhMADAYDAaDwWAC"); result.append("gMFgMBgMBhMADAaDwWAwmABgMBgMBoPBBACDwWAwGAwmABgMBoPBYDAB8EMQERGBLl26oGfPnqwwPiMjIwMrVqyAhYVFpTueVCwWY+nSpejQoQO0tbXRqFEj/PbbbxUyr7GxsRg6dCgsLS3LRXr69+8PQ0NDxMXFKcT+wIEDYWRkhHv37gli78aNGxgyZEi52O735cuX8Pf3h7GxMf755x/2EPxRoW/g2rVrpKGhQQDo4cOHVNEpLCykCRMmkLm5OQGg7t27E+Nfbt68SV5eXqSkpEQAKCIiotLkXSKRUPv27Sk0NJSIiK5evUpVqlQhAHT+/PkKldf169eTs7MzASBDQ8NykSZLS0sCQEeOHFGIfenzIDw8nHdbO3fupPbt2xMAql69ukLL/cqVK+Tj40MikYgA0O3bt9mD8AcF3/rFw4cPk0gkomXLllWawgoPD2cCoATc3NwqnQDYsGEDAaCsrCzu2u7du8nIyIj++uuvCpffx48flysB8OTJEzp9+rTC7MfHx9OJEyeosLBQEHvJycnlQgBIcXBwYALgB+ebpwB69+6NJUuW4Pjx45VmtKR69epsyKgEatSoUenyvH//fqiqqkJbW5u75uPjg+Tk5Ap5CmV5q/+1a9eGh4eHwuzb2Niga9euEIlEgtj7/OS/8kB5Sw9DYB+AGTNmwNHREW/evKkUhaWiosJqDCsbjocPH0JVVZXdY4YgKCsrs/Qw5Numv/cHNm3axEqRUSnJyMiAuro6KwgGg1H5RgAUQWFhIbZu3YrWrVujR48eqFu3Ln766SeEhYUJmo68vDzMnDkTRkZG0NHRgZeXF5KSkgSxnZubi8WLF8PV1RVNmjRB7dq18csvvyAlJUUQ++fPn4e7uzvatGkDNzc3+Pr64v3794KV/YcPHzBz5ky0aNECZmZmMDY2xvDhw5GamipIp29nZwc7OztIJBLk5ORw/x8xYoQg+d+5cyfc3d0xatQoODs7QyQSyXwCAgIEScfZs2fx008/QVNTEy1atMDj"); result.append("x48FqwOJiYmYM2cOjI2NcfPmTcGfQ0lJSQgICICpqSn+/PNPhT0P8/LyMGDAAIhEIhgbG2PmzJmIioqqEJ2TRCLBkSNH8PPPP3Mrr44ePQoXFxdoa2ujTZs2XJ17+vQpPD09oaOjA1NTU2zcuFHu6bl8+TK8vb1hZ2cHALh48SKaN28OTU1NNG/eHAkJCYKUy6lTp9C5c2d07NgRNjY2cHNzw969e7/tx340p4Xx48cTALp37x4REWVnZ5O9vT0BoD///JNX25cvXyb8H3vnHRbV0TXw39JZmiAiRUFEVBCwK/auscbEGqxR7L2baDQx9tiiSey9RI1GjRpfe+81iqIIilIVkN5h5/vDZT9Q7OwicH/P4/PmnTvsmTt37p0zZ845A6JNmzaiZcuWon79+qJ169Yqz29bW1sREhKi1jbExsaKmjVrCm9vb5GWliaEEGL37t0CEI6OjiIuLk6t8lesWCFMTEzEqVOnVGVTpkwRgEacACMjI0W1atXEgQMHhBBCZGZmimXLlqnu/8WLFxobi9ra2sLIyEij43/ChAlCV1dX+Pn5CSGESEtLEw0aNBCA6NWrl0hISBAZGRlqkR0fH69yAly5cqXo0KGDWLRokWjTpo0AhKenp0b64MyZM6Jv376qMXf16lWNPoNLly6JgQMHqrzgz549qxG56enpuToBjhkzRjRt2lSjY18IIRo1aqRWJ8Dvv/9euLi4qByvp0+fLvr16ycWL14sPD09BSAqV64srl27JurVqyfmzp0rpk6dqopQO3PmTJ61ZdOmTaJbt24CEA4ODmLlypWiVatWYubMmaJFixYCENWqVVN7n0+bNk04OTmJwMBA1ZgYNmyYAIS3t7fmogDyCxMTE6Grq5ujbOrUqQIQc+bM0YgCULJkSXH+/Pkc3sC2trYCEF5eXmptQ8+ePUWpUqVEUlKSqiwlJUUj4Wc3btwQOjo6YsGCBTnKMzIyROnSpTWiAHh5eYmpU6e+Vu7m5iYA8fPPPxdaBeDevXtCJpOJ"); result.append("pk2b5ig/ePCgAIS5ubla5WcpAHp6emL9+vU5nr+dnZ0ARHBwsMb6w93dPV8UgCxq1aqV7wrAhAkTRIcOHURKSorG71/dCoAQ/x955eDgIG7cuKEqT0xMFKampgIQI0eOVC2GhBBi/vz5AhDDhw/P07YEBQUJQBgaGuYY/+np6cLKykoA4vHjx2rri6NHjwpA/Pnnn6+Ni6zv39q1azUTBZBfjBkzhokTJ+YoMzExASApKUkjbfD09KRu3bqq/+/s7MzcuXMB2Lt3L+np6WqRGxISwrZt22jZsiWGhoaqcn19fU6ePMm6deto3Lix2u572rRpZGRk0L179xzl2traVK9eXe39/uLFC3bs2MHhw4fp2LFjjn96enpUqFBBI9sA+cWFCxcQQmBlZZWjvEqVKgBER0eTkpKi9naYm5vTt2/fHM/f1dVVNUY1RX5HJVhYWOTrVuigQYMIDg5m9+7dhdYXpVixYqoxXrVqVVW5XC5XjbnevXvncMZ1d3cHIDQ0NE/bkjXPWFlZ5Rj/Ojo6VK5cGYCwsDC19cXs2bMBaNasWY5yHR0dhg4dCsCcOXM+6DcLnFvvTz/9BEBaWho7d+5kz549BAUFqV6K/KJLly706dOHpKQkgoODcXR0VMsEoFAocs0E5unpqdbQs8TERA4fPoy+vj52dnavXdeER/D169fJzMxkzJgxfPPNNxQ1sia8V309siYiMzMzDAwM8qVtWQqpJhQQTY65z1G+EIIePXqwZ88e/P39C3V0xtv62MjISNUf2cl6B1JTUzXWFlNTU9W8pA4SEhI4derUGxXfhg0bAuDv7094eDjW1tbv9bsFMhXwunXrqF27NsnJyWzbto3evXvne5sMDAzeu9M/ZQWsbi3zTQQGBpKeno6WVv4Nmaz7f/ToEUWRVq1aYWdnx61bt4iPj1eVP3nyBICuXbvmW9uyYuHzUwkvKshkMkxMTFQOgBkZGVKnfCa8qozkFcHBwarfzvoOZqdUqVKq//4QS3iBUwD69+/P5MmT"); result.append("+eeffxgwYMBnZfpSKBTo6elhY2Ojlt83MzNTWQLehLrykmdpv8nJyURERORL/2Yl3Nm/f/8b61y9erXQflwMDQ05dOgQJUuWZOrUqaoxN2vWLFxdXZk3b570BS4iLF26lCpVqnD27FkmT54sdUghJ+vbn13hz07WPKijo5OrhbZQKABnzpxh3bp1tG3b9rM4ECM7UVFRPHv2jFatWqnNDFujRg0AfHx8OHjw4GvXT58+rbaDURwdHVVm3rdNwOpcAWbtAV6+fJnt27e/dt3Hx4cTJ04U6g+BXC7H2NiYpKQkvv32W7y9vXF1deXSpUtSZrYihIGBAbt27cLMzIyFCxeyZ88eqVMKMTY2Njg7OwPk+qyzFmWtW7f+oEVxgVIAspw67ty5ozKHZGZmcvv2beD/93wiIyM13ra1a9eir6/PzJkz1SajXLlyNGnSRGUJOX/+vOra0aNHGTVqFO3bt1eLbH19fby8vICXzoCvbkMkJCQAL2P01YWtrS3t2rUDoG/fvvz222+qPefLly/To0cPjfkGCCFQKBRkZmZqbIwlJyfTokULvLy8WL16NevXr2fdunVMnjxZ5aCk7nvOizqabE9h4tX7dXJyYt26dar34cGDB/nansL+jN9ncaPO9k6aNAl4mQckMTHxtcWflpbWh1uDClIIYEBAgNDV1RWAaN68uZg4caKoVauW6Nq1qwCEk5OT6NmzpypHQF7j4+MjDAwMhFwuF6tWrVKFnuzatUtYWFiIffv2aaQPbGxsVDHQpUuXFpaWlkJXV1ecPn1arbKjoqJE+fLlBSDs7OzE0qVLxe7du0WvXr2Eg4ODAISbm5uYPn262g5ICQ4OVskChK6urjA2NhaAWL16tUbHYlYbnj9/rhGZd+/eFYDQ1tYWjo6OokKFCsLFxUW4ubkJT09PMWjQIFV+AHXw5MkTAQgjI6PXnm/Tpk01djJeFh4eHgIQR44cyZfvUdu2bTUaBph1GJC+vn6OXA8DBw4UgHB2dhbh4eEau/+sw4DU"); result.append("GXqcFQbYqFGjN4ZhnjhxIkf5P//8IwBRr169PG3LgwcPBCCKFSv22vjPOqlRneNfoVCIXr16qfIixMTECCGEuH37trC3txfz5s0r/HkAtm7dKhwcHIRcLhcdOnQQgYGB4tGjR8LGxkZUqlRJXLhwQa3yAwMDxahRo4STk5OwsLAQlStXFr179xYPHjzQWB88ffpU9OzZU5ibmwtDQ0PRvHlzcenSJY3IjoyMFIMGDRKWlpbC0NBQNGvWTFy5ckV07txZNGnSROzcuVOkp6ertQ3Pnj0TgwcPFtbW1kJPT09UqVJF7Ny5U2P9P3/+fFUMOiDq1KkjFi9eLHx8fNQue+rUqcLW1lbY2NgIuVyuOoY565+5ubkICwvLc7l79+4VDRs2VMnp3LmzOHTokLh9+7YYP368KimOi4uLWLdundrzIYwYMULVFnd3d7Fhw4Z8UwCy5wRRF5s3bxZNmjRR3XO3bt3Ev//+K5KTk0WnTp1yLAhmz56tmhzUwcOHD8Xw4cNVMitVqiR+++23PJezcuVK4ezsLAChpaUlxo4dK27evCmuXr0qhgwZkuP5L1myRAghxIIFC1SLFC0tLTFq1CgREBDwyW3Zt2+faNy4sUpm9+7dxeHDh8WdO3fEhAkTVOO/YsWKYsWKFWpVAlatWiWqVasmihcvLqpVqya++OKLj1aCZaKo2dEkJAooERERfPPNN+zatUsVH51FSkoKgYGBDBo0CG9vb3r16iV1mJpp164dBw8e5L///sPDw0PqEIkCh5bUBRISBQMvLy+aNWv22uQPL53CKlasSJMmTYrk0cz5tSespaWllpwfEhKSAiAhIQHAtWvXOHbsGGfPnn1jsp3//vuPS5cu0bJlS6nD1EBMTEyO5DIJCQk0aNBAIw6YEhLqQDrgW0KiAODi4oKHhweHDh3C0dGRdu3aUaFCBeRyObGxsVy7do3IyEh27NghndOuBlJSUihbtiy6urr8+eefVKpUCT8/v7eGxEpIfO5IPgASEgVoElq5ciV/"); result.append("/fUXPj4+JCYmYm5uTrVq1VQhkIU5LWx+M2DAAHbs2IFCoaBRo0b89NNPqtwcEhKSAiAhISEhISFRIJB8ACQkJCQkJCQFQEJCQkJCQqIoIG0YSkhISEhI5AOyrGM0JQuAhISEhISEhKQASEhISEhISEgKgISEhISEhISkAEhISEhISEhICoCEhISEhISEpABISEhISEhISAqAhISERGEmICCAX3/9lcTERI3J9Pb2ZsuWLRq9z/3797N//34yMzOlhy4pABISEhJFFyEE06ZNY9myZfTp0wcjI6NCfb/t2rVDW1ub1q1bExgYKA2AT0RKBCTxSZw9e5YGDRpIHSEhkQ/MnTuX8+fPc/z48SJxvzKZjDZt2pCamkqLFi24efMmxsbG0kCQLAASmubmzZusW7dO6ggJiXwgLi6On3/+mSZNmhS5e2/cuDH+/v6sWLFCGgiSAiChaVJSUhg0aBDSYZISEvnD9evXSU5OJioqqsjde9Y9nz17VhoIkgIgoUlSU1Pp1asXV69elTpDQiKfyEojf+vWrSJ371n3rKUlTWEaUQAUCgUHDx6kY8eOtG7dGiEEc+fOpXTp0sjlcr744gvu3bunkUbfuHGDLl26UKtWLcqXL0+dOnVYs2YNtWvX5tSpU2qXf+HCBfr06YOzszNCCCZMmICZmRnt27dHoVCoXf7Zs2dp06YNHTt2pHz58shkMooVK6aRvhdC0LdvX65duwbAP//8Q5UqVahSpQqhoaFqk7tgwQLc3NyQyWR4enqqys+fP0///v2RyWTIZDLu37+vFvl//PEHVlZWKjn9+/cnODhYdX337t24u7tjbm7OqlWr8kTmvn37cHBwUMmcOXMmAIcOHaJRo0aq8g4dOqhWQpmZmUycOBEtLS08PDy4c+dOnrRl165d1KhRQyXTw8ODu3fvkpqaSufOnZHJZFSrVo0jR46opf9nzJiBoaEhMpkMHR0dJk+eTGxsrOr6oUOHcHFxQV9fX9VPavlgamlhbm6Ou7u7atxXqVIF"); result.append("U1NTZDIZ9vb2GrOKVahQAQAfH58iN3HdvXsXAFdXV2kW/8QP+nsxa9YsUblyZQGIZs2aiZEjR4oOHTqIAQMGCCsrKwEICwsL8eTJE6FO1qxZI6ytrcWpU6dUZVu2bBFaWloCECdPnlSr/GXLlok6deoIQNjZ2Ykff/xRfPnll0JbW1toa2uLyMhItcp/8OCBsLa2FiEhIUIIIRQKhZg1a5YwMzMTmmTPnj0CEH369NGYzAsXLghA1K5d+7Vrrq6uAhC+vr5qk3/z5k0hk8kEIF68ePHadW9vb7F+/fo8lXn37l2hpaUlDA0NRXp6uqo8ISFBWFpaCkD4+fnl+JukpCRRvHhx8fz58zxtS3JysqhVq5YAxNdff60q//XXX4Wnp6dITExU6/P/448/BCCsra1zvd6jRw8xZcoUtclPT08XlSpVEsnJyTnK79y5IwwMDIS2trY4c+aMRt9DGxsbYWFhIfKD/v37i82bN+eL7B9++EEAYvfu3aIgU2AUACGEOHr0qABEiRIlxNatW1XlISEhwt7eXgCie/fuauuss2fPCm1t7Vwfer169TSiAAghxJMnTwQgDAwMxO+//676UJ87d07tsmfOnCmsra1FRkaGqkyhUIi6desWegXA19f3jQpA1vNXpwIghBCtW7cWgNiyZctrk66rq6tIS0tTm8yjR4/mKB8zZowAxIIFC3KU7969WwwePFgt9x8QECCMjY0FII4cOSKCg4OFs7OzSiFVJwqFQnh4eAhAnD17Nse1lJQUYWVlJZ4+fao2+UlJSWL69Om5PndAzJgxQ+MTSLVq1XIoY0VFAThx4oQAxMWLFyUFQFM+AFnhFu7u7nh5eanKbW1t+emnn1Rmy7S0NLU0dtq0aRgbG9OxY8fXrllbW2us07LM7cbGxgwePFhliqpXr57aZaelpREeHk7//v2JiYlR7QVOmDBBMmdpgBEjRgDw+++/5yjfsWMHX3/9Nbq6unkus1+/fgBs2LAh1zG/evXqHOXr1q3D29tbLfdf"); result.append("tmxZfvnlFwCGDRtG3759WbRoEba2thrZ8548eTLwMvzt1S2K2rVrU7p0abXJNzQ0ZMqUKTnKRo0axb1792jSpMlr19RNeHg4ERERr/VFUaBJkyZ069aNkydPakTe06dP6dixI3Z2dtSpU4cZM2bw4MGDXOuuW7eOR48eFa4tACGEuHjxomoL4FUiIyMFIADh7++f55pSXFyc0NbWFtWrV8/1eqdOnTRmAYiPj1dtAWgaf39/YWJiIgBhbm4upk6dmuemXskC8PZVqLOzswDEtWvXVOV169YVQUFBapGZmpoqLC0thVwuF7GxsUIIIdLS0kTlypVFjRo1BCBOnz4thBAiLCzsje9IXtKiRQsBiJYtW2p03GVkZAgnJycBiFu3bqnKGzRoIA4ePKjRtuzcuVMAwtLSUiMWkOx9cPbsWTFkyBBx7969fFu95qcFIGtLZuLEiWLZsmUiKipKre98o0aNxObNm4Wvr6/4+++/Ra9evYSxsbGoVauWWLp0qWrr+9atW6Jp06YiMzOz8FkA3kbx4sUxMTEBICMjI88bGhQURGZmplp+uyDh5OTElStXaNKkCdHR0cycOZNy5cqxZs0aaXmuAWQyGcOGDQNg2bJlwEunVGtra0qVKqUWmXp6evTo0YOkpCR27twJwNatW/nyyy8ZPnw4gMrxcOPGjfTt21ft/TB69GgATpw4oXII1QTa2toqa9fs2bMB8PX1JSgoiC+++EJj7Xjy5AkDBw5EJpOxYcMGjVhAsjh37hyLFy+mU6dOuLi4FNl3McsZNCgoSK1WEH9/f1q1akXPnj2pWLEiX331FZs2bSIsLIyhQ4eyb98+nJycMDQ0pHv37ixcuLDgRCfklQVACCGMjIyElpaWapWSl/j4+AhAmJqaFmkLQHaOHz+ucsrStENMflgA7t+/n+8WACGEiI2NFcbGxsLAwEBEREQIb29vcezYMbXKvH37tgBE/fr1hUKhEDVq1BDPnz8XiYmJwszMTBgYGIioqChRpUqVXB0U83r8"); result.append("V61aVUyePFkAolKlSiIlJUVj4yAlJUXY2NgILS0t8eDBAzFy5Egxa9Ysja48sxyBx4wZk2/v/7x588TYsWOFQqEokhaAa9euiVatWomIiAi1yklOTn7N8fNV0tLSPqodBdICkFuoW0REBImJidSsWRNTU9M8b6ijoyM6OjrExcWxf//+Iqv1rly5ktTUVACaNm3KxYsXGTVqFACbNm0q1Peup6cH8NYDTzQRhmlqakrv3r1JSUlhwYIF3Lx5k2bNmqlVpru7O9WrV+fcuXMsWbKEmjVrUqJECeRyOT169CAlJYWhQ4dSqVIlzM3N1dqWYcOGMXLkSObMmcMXX3zB3bt3mT59usbGgb6+PqNHj0ahUDB9+nS2b99O//79NSZ/+vTpXLx4kerVq7+28nz48KHG2jFx4kT+/vtv1q9fX+S+g3FxcbRp04bhw4djaWmpVlkGBgYYGBi8tY6urq7a2/HZWABcXV1fu7Zq1SoBiF27dqlNE+vYsaMAhJOTk3j8+LGq3M/PT5QuXVrjFgAbGxuNa72TJk16TevOao86IzBe5eDBgwIQX375pRDipTe0ukNAExMThZaWlpDL5TnCLbdu3SrMzc1z9Q5XF/fu3ROAkMlk4tdff9WIzN9//10AQldXVzx8+FBVfvPmTZUV6MSJE2ptw+bNm4WXl5fq/wcFBQkTExOhra0tLly4oLHxFxcXJ4oVKyYA0aVLF41a3bS0tISJiUmOZ5DFTz/9pNHvgYeHh3BzcytyFoDly5dr9H1XFwVSAQDEmjVrVOUPHz4UdnZ2YsCAAWrtrEePHqlinw0NDUWbNm1E27ZthZeXl/jyyy81pgCEhoYKQOjp6Yn4+HiNKwBmZmY54o2PHDkidHV1NfoyZJnj5XK5+PXXX0Xnzp1FeHi42uVmOZ9VqFBBjBw5UtSvX1/MnDlTtQVQs2ZNMXfuXI30QfPmzYWRkZGIiYnRiLzo6GhhYGAgOnfu/Nq1GjVqCCcnJ7Wag2/duiVsbGxEdHR0jvKZM2cK"); result.append("QJQtW/a1a+pkypQpAhDHjx/XiLyIiAhha2srgBxh0Fn4+/uLNm3aaHQrQl9fXxgZGRU5BWDixIkCEMuXL5cUAE0rAJ6enmLo0KGibdu2olmzZqJWrVrijz/+0MhelJ+fn2jXrp2Qy+WiVKlSYtasWSIjI0NjPgA7d+4UDRo0UClCnp6eYtu2bRpVobhQ7AAAIABJREFUALJkV6lSRXTs2FG0bdtWXL58WeODd9q0acLY2Fi4u7trJAeCEC9zTrRo0UIYGBgIFxcXVd83atRIdOjQQfzvf//T2J7ovn37xMCBAzXa515eXrk+65UrV4rZs2erTe6uXbuEpaWl0NbWzhHvfufOnRx+KG5ubmL79u0a6YurV6+K8uXLa7TvsywwtWvXzvHP3d1d6OnpiVatWmmsPVl+Ic7OzkVOAViyZIkAhLe3t6QAfC5OgPmJJp0AJSQk8p9x48aJhQsXFtn7P3z4sMa3QD4XBeD06dMC0KjFpTAqADpSYJeEhERBIyEhge3bt3P79u0i2wclS5YEimY+/KzwR00mgCuMfJACkKWwiM/wCFghHUsrIVGoOXjwIHp6ejRs2JBJkybRrVs3LCwsimx/eHh44OHhQVJSUpG79+TkZAB69eolvRiaUgCyTt/KfgrX50J0dPRn2zYJCYlP4+zZs7Rr1w54eSJfxYoVOXfuXJHuE5lMxubNm+nSpQujRo3Czs6uyNz7rFmzmDRpEo0bN5Zejk/gvfIApKSkMH36dFW8+fXr1xkwYACnT5/O9xu4c+cO48ePV7Vl0qRJRTI3toREYcbNzY2aNWtiZmaGl5cXJ0+eVHu+g4JiBTh48CA///wzv/zyiypHSGHl1KlTDB06lDp16kjf+bxQIoVkO5eQkJAo8CQmJqKvr4+OTuF17YqLi1NLorl8m4BlMpmkAEhISEhISBS1FXg+KwBa0iOQkJCQkJAoeuigdJ7LN44dk55CfqI8RU4in1YA0vjPV0Tz5vnbgIEDP+v+2XbuHF7166tPQH73f5FX"); result.append("ACQKHQkpKbiPG8f+yZNxK11a6pACir2BAWPs7elcsiSl9PVV5c/T0lgTEsLswEASMzMB6GRlxTfW1nSysgLgbmIiO589Y8ajR5J8iY9CIQSXHz5UrwIgka9IWwCFUavT1iYyPh4dLenxFmSepqQwxs+PcufPs/3ZM1X5prAwpgQEqCY/gN3PnzPI1xeA34OCqHrp0idPfkVdflEn8PlzyigVKglJAZAoAAgh0NfRYXLHjlS0s0Mh+XgWeFIVCnr5+HBGuV3X28aGYrl4ev9Ytiw7nj1j+IMHpOfhcy/q8osqviEhuHzmuQWCQ0P5dvhwho4fT8uvv6bX4MFkZGQwf+lSflm2jAZt2nD91i0A/vPxYdbChXw3YwZf9epFkjKZkKQASBQKMhUKrLy9qTJxIn5hYXSYNw8DLy+uSyuhAk+GEHj5+BCdno6Vnh6Ly5fPcb17yZI0Mjen3717knyJPFUANp0+Tc3vvkPWtSu633zD8iNHVHX+vnwZK29vKo4ezZazZ4lNSmLB/v3YDhqErGtXHIcN46gyXXNSaiqLDhxA1rUrrWfP5qzSYvMplLK1pUzp0jzw92fPli38MGECKzdsoLyTExNGjKB39+4MGD2a5JQUhk+cyKRRo5gzbRrp6en4PnggKQDSMC88aGtpcX3ePG7On09iSgo7x47l4dKlVHV0lDqnEBCSmsoI5Uerr60trYsXB8DN2JhF5cvT6fZtkrKZxSX574dfUhLf3ruH7NgxfnnyJNc6cRkZmJ48ieP58+yLiMA/KYnRfn7Ijh2j4bVr9Lt3jxpXrjAnMBABvEhPZ01ICNrHj1PhwgW8792j7tWrDPT1JTo9vUCMt6eRkdhbWtK7USPOzphBHaXS1bpqVVWdxpUq4VqqFJdnz6ZngwaYyeWMb9+e8z//jIWxMfq6ujR1cwNArq9PDScnejZowKHvv6eBMp//p2JkZIS7qytGcjnlnZz43/HjPHz0iA3bthETG4ujgwPXb93CSC5X5Ug4sH071atUkRQA"); result.append("6bNauLC3tORJRAS7L1/m1N27OJQogVb+hppK5CFbw8PZ8/w5AKtcXbE3MOBvDw+GPXjAQw3khC+M8svL5XxfpgyGWlosffo01+2DNaGhZAhBcwsLvixRgnJyOcNLlQJghpMT61xdWV6xIlP8/Zn9+DEWurp429lho6fHN9bWrHF15VDVqvwbGUnXO3c+u3EVFBVF6iuKiUKhICtM3UBXlz9HjUKup8ewNWuAl9uNw9auZcWAAZjJ5Tn+1tHKirVDhvAgNJRFBw4AEBUfz/x9+1g1aJB6rWUZGVTz8KCvlxcTRoxg26pVKBQK/AICcpwZ8ywiQlIApE9q4aNMiRIYGxjgbm8vdUYhZPD9+0Smp1NKX587np7sjYhQTYqS/I9DVybjG2trwtPS2B4enuNaphCciY7Gw8QE7WzlOq8o1jVNTXEzNubPbH+fvY6Zjg5fW1lx7MULIt/TCrBNzecdRMXHM3bjRtrPncvaEydyXHs1R41DiRL80qsX/968ybZz55i7dy/tq1en4hv8BDrWrMk39eoxfedOHoaFMXj1ahb27o2hnl6e34dCoVD9d7NGjRj13Xdcv3WLp8HBLFmxgqoeHsTFxzN70SKSkpPZsWcPzyUF4O0KwNPgYMZMmUJpNzdkFhaqfyUrVGDKzJkkZtO4d+/fT+c+fVR13OrWZcb8+QW6c6ITExm7cSNlhw/HpHdv7AYNovuSJey9epVz9+/z8+7dn6V8mUxGQxcX7N7jpLSkzExmPn5MtcuXkR07huzYMXrfvfvebVwbGqr6O+cLF5jx6BF+H7kSO/HiBd/5+2N+6pTqN7WPH6fkmTOYnjyJw7lztLl5k7+ePaMou3g9T0tjiHL/1FRHR+UcJ8n/NEobGNDZyoqFT5/mKN8TEcFXH+ANb/KOVLxaMhlG2trvntSUYXjqRFdHh/EdOrB34kQWHjhAWkYGAOExMdjkctbCwObNaebuzrC1awmKinpniOCyfv0wMTSkztSpfFWrFhVsbXO+82fO0GPgQPRK"); result.append("lswxx/QbMYKb2Y56PnHmDL0GD1Zd92zRgrVbthD+/DlnLlzg1Llz+Pr5ATBy4EDq1a5Ns44d+bJHD9q0aIGJsTHb165l/bZtlKlcmZjYWNyVxyhHx8Tw3YwZ/LJsGTWbNSMhMZEvOnemWceOxMTG0mvwYKo0bEhwaChBISE0bNuWsGfPOHXuHIv++IPWXbqw8c8/AUhNTWXukiX8NG8eX3TuTHRMDMvXraN+69YsXbkSBw8PegwcmENh+WwVAPtSpVg8axb+16/T/euvVeW9u3Vj1tSpGGUz+3Rq356VixcDMMzbm5unTzNt4sQC+5ENj4mhxuTJnPH1Zde4ccRt3IjvkiU0d3fHe8UKGkybRqYaH+Knyq9Wtux7yZFrazPV0ZHTNWqgrwwb3B4eTlBKyjv/VgCLsu2Zbq5UiWlly1L+FXPg+9LUwoI55coxw8lJ9XGPb9yYZw0b8rxRI6aXLcvZmBi63rnDt3fvFmklICQ1lUylOXO5iwumGs7/Xljlj3Nw4L/4eI69eKEq2x4ezjclS77zb4+/eIFPQgKD3rAiDktNZeezZ/SytsbwPUJ0NRGGZ2poiK25OWVKlKChiwsbTp0C3h4BML9nT2ISE4lOTHzn7xc3MWHSl18SFR9PfC5e900bNmTrqlVcOXaM4soFi4W5OeuWLaOqh0eOeisWLQLguzFjuHD4MP179sTayop/tm3j9rlzuCh9FPT09Fi5eDExgYHcPH1aNdE3b9QI/+vXee7nx6C+fVW/fejYMUqWKMGEESMYM2QIxkZGzJk2jeiYGIqZmfHjpEm8iI7G1toaPT09Bvbpg5mpKWu3bGHs0KGsXLyYYRMmEBcfz9JVq2hUrx7TJ03C1saGxcuX06ppU/wCAmjbsiV3zp/n7MWL7Prnn89fAchCX1+fzStW0LBuXQA27dhBTC7H7v44bx7dvvqK3+bPR1dX970acD8khGk7dmDl7Y2sa1e0u3VjxLp1HPnvP1WdP8+fp/uSJci6dkXWtSsVRo1i7t69"); result.append("PFfj0b/jN2/mSUQE+ydNopqjIzKZDFNDQ7ybNePK7NlYGBur9cF8qvzSSgep9161aGtjp6+PsbY26UKw+JVVUG78GxnJ02yKQikDgzy5d3vl78iUCgqAgZYW/WxtWVKhAgAbw8LYkS02vChRUk+PbW5udLtzh9iMDErp67PoFa94Sf7HUcPUlAbFirFAqdheiYujmokJem+ZsPdGRDDz8WO2hoezt3Jl+r6yyr0aF8eip0+ZGhDAd2XKsEY5Ib0LTYfhff/VV8zft4+MzEx8g4NzlS2EYNGBA4xs3Zrt58+z//r1t/5mVHw85x88oHXVqkzcsoXgqKhc61Vxd2fH2rXIZDJeREezc+/e1+rMXbKEbl99xewffkArD3OceNaowc8LFtB/xAgaKy0aVT08SE1N5YG/P9f/+w8Lc3NOnz/PP4cO8WWbNty+e5eIyEg2bNvGiTNnaNGkCVEvXnD89Gn+8/Fhw7ZtlCxRAkMDA/T09DA1McHJ0RFTExM6d+jA1Rs3Co4CAKCjo8O21asxL1aM5xERjJkyJcf17X//zenz51n3228f1ICKdnbM6NaNKUoLQ5tq1VjWrx8tK1dW1fmmXj22jx6No1IbXj5gAJM7dsTKzExtHXPg+nUsjI1zNYOVLVmSSV9+qdYH86nyLU1MPtwcKJPhrXzpV4eEEKM0B76JBU+eMCDbR0Inj5wNtd/yO31tbFSWih2v7NV+DPcTExl2/z6yY8do/paXMjQ1Fb3jx7E8fZr1oaGEpKayJiQEk5MnMT55ko7//cdX//2Hy8WLjPbzU5s3vI5Mxg53dxY9fcru588ZpzR79re1peUHKn2S/NwZ6+DA4agofBISWBEczCCls9+b6FiiBFMdHVnn6kqHEiVeu17T1JSx9vasdXVllL39e78nmg7Dc7axoWa5cmw6cwb/8HCcrK1fn4T37qVT7dos6tOHao6ODFm9mtg3bPkJIRi+bh0LevVi5cCBKIRgiNKBMDeaNWrESKWD4NgpU4hPSFBdO3n2"); result.append("LDv37GH1r7/m+ZhyKF2aO+fPk5ScTLVGjVSLW6/Ondm+ezchYWGMGjSIzTt3Ep+QgImxMRkZGRgZGdHXy4u+Xl7s2bwZW2trMjIzqVe7Nn29vJgzbRpjhw59TZ6FuTmmH/F9zlcFAMDOxoZl8+YBsGHbNg4p85j7+PoydsoUdm/ciNzQ8KMakrWiNXuL+dhU+dvmRkZq7xghBBFxcWw8fTrX613q1Pms5Zt85HPwsrbGTl+fhMxM/ggKemO9G/Hx3EtMpLeNjUYHrLZMpkoLG5kH4VQVjYxYUqECOjIZx1+84L/4+Fzr/R4cjIKX2xTf2tpip6+Pt50dnmZmVDQyYm/lyuypXJnVLi78FhREXzXFo893diYsLY1lymezNjSUo0pz9WoXF0zeY29Zkv92OlhaUk4uZ/zDhxhpa1P8Pa2ZeU1+hOFN+fpr5uzZQ3JaGrqv9OXJu3eJjI/nq1q10NbSYs3gwTyLjWXC5s25tn/m33/TtU4dHK2sKF28OHO8vDhw/fpbHRtnTZ1KGXt7QsLCmDJzJgDPIyL4dvhwtq5ahYkaLK+7/vkHYyMj/lyzhspubjxWWn+8Onfm97VrqV65Mp06dGDfv/9SXrk96VGpEqfPn2f91q08i4jgj7VrSUpOplHdugwdP56HAQH4+Pry1759ACQkJKgiEHz9/GjbsmXBUwAAenTpwlft2gEwcPRongYH83Xv3vz+yy84KzvnY/iQUxE1cYJi1kvW748/mLx1K8lpaTmuO1pZ8YUa40g/VX6LbPtnH2oFGKmMHlgaFETqG/wMfgkMZHjp0hhoON1wikJBmLIv3PLoY6Ark9HE3BwLXd3XHMAAkhUKrsTGUsbAAL1Xxp7+K/dfv1gxqpmYsOf5c9UedV7RtWRJWhUvzoBXlIsB9+6RkJmJvYEBC9Voii/M8jOEIEP5vLRkMkaVLs2RqCiGZztLIzNbnay/yf6/7/rdt/G5hOG5lS6Nu709EXFxOcofhIYyY9cu5nh5qcqqOjoyuEULVh8/"); result.append("zr83b+acVC9dIuTFC76qVUtVNrRVKzwcHBixbt0btwKM5HLVXv/va9Zw5cYNeg0ezPABA6iRTfHJS+ITEmjbrRu/r1lDtcqVqeLu/rIPHRzo1L49DevWxdTEhG5ffUWrpk1fLkZNTNi0fDk/zZ9PlQYNKGllhXmxYowbPhw7GxuqN2nCdzNm0LFtWwBS09JY8Ntv/L5mDXVr1aJaNgt3gVIAAFYsXIhl8eIEh4biXq8eHdu0USkFhYVFffrgUKIECiGYt28fFUePZsOpUzlS63o6OxdK+YPs7DDV0eFZWhobw8JeX5mkpPBvVBRD32EaVQcLnjwhKTMTPS0txuZhmKNcW5tBdnZsDw8nNDU1x7XNYWH0+gBLR0xGBiX09N66lfGh1DA15bcKFeh8+zYJr2wvPElJYbK//8vJ0M6O9paWed7vhVm+f1ISvwYFcTAyUuX8962tLT1tbKggl5OUmcmWsDDuJSZyPDpalQhoqdIKsS40lFuvWI5epKezMiSEsLQ0DkZGcuQNE97nGIY3tVMnXJTvdlJqKjN27aLmd9/xLCaGG48fq+r5hYXxWBl++c2SJczft497wcEMXLmSbosX8zw2lsBsoXZn7t0jOS2NFwkJNP/55zdaAlo1bUrPrl1RKBQ079gRgHHDhqntm+Ldqxdn//2XYd7ezJk2LUe/L1+4UPXffyxYkMO3rU2LFgT+9x9h9+/TqX37l98RQ0O2r11L3NOn7P/zT4yV1uriFhZMGDGCYd7eDPP2/mzmuY9SAKxKlFB1TFx8vMo5sDBha27OpVmzVCvxp5GRfPvHH3iMH8+hV7TdwibfTEdHtbe/8MmT184TWPz0Kb1tbDRqGg1KSWH8w4dMCwiguK4uu9zdcf7IaIM3kbXaW5Zt60MAO549o/t7eIGnC8H0R494kpLyWqraT6GdpSVHqlbln8hIfN/geb06JET1nDZWqoRrHm6TFXb55eRyllWowM3atWmu9EQ30tZmU6VKKuWwp40NiU2a8LhePVUioKUV"); result.append("KiCaN2ebmxtVXtnTtdDVZZCdHZnNmnGzdu03+ifkdxheblRzdGRAs2Yv711fn2mdOxO3cSP3Fi/Osegob2PDgcmTETt3ErtxIxO//BLXUqVYNWgQmTt28Pf48ZTJ5hPRuFIl/H79FbFzJ/eXLHlr25fMnk0JS0viExJoWLeuRqy+RZGPtt/a2digrdwjGjJuHHFv2Dv9UA7fuoXnlCm5/nuYB05fH4J1sWL8+913/D1+PM7KFeDdoCDazJlDt8WLSXiPULmCKn+0vT26Mhl+SUnszabFx2ZksCE0lDEaSDKUmJlJq5s3cbt4Eftz51j89CnLXVwIrF+f9rk4W32y0qWvTzdra1aGhKhOmjscFUUTc/O3eoGHpKQw8sEDSp45w6noaO56etLtPRSGd9HG0pJj1aqxv0oVzHV1aW9pyc9OTq9tOzQoVoytbm6qjI/murpcqVWL5RUrUu4TlKSiLl8T5HcY3puwV4MV50MwNDSkuFIBmr1okWpfvqChUCjY/c8/hD97xvnLlz+79n1U8OyziAi8Bgxgx7p19B8xguDQUMZOmcKapUs/uUGtqlRhy4gRuV6rMmEC/+XDQPiqVi3aVa/OqmPH+HHnTiLj49l58SLP4+I4Pm2a2lPt5of8UsrJcEtYGPOfPOFrZQTGiuBgWhQvTtmPdDL8EIy0tTlctSqxGRlUu3yZR8nJXI+Le2OcdV4wxt6eLWFhrA8NZXjp0qwMDmb1O8K27AwMWFqhAulCsDksLM9WK/9GRvJvZOQ7652NieFsTEye90VRl69pvv/qK1rPnk2/Jk3wDQ5WOe9lJ3sY3tJDh/CqX5/21au/8TdfDcNrW60apfIwWiM+ORn38eOZ8vXXKqtBnljjJk6kS8eOHDt1iotXrzJk3Dj+t2tXwVtha2kxavBgRg0eXDgsABkZGXTr14+xQ4fSqX17Fio9Nddu2cKRkycLjWnkWkBATlOdtjbDWrXCb+lSvqxZE4BTd++y/9q1QikfYIKDAwCXY2M5Ex1NuhAs"); result.append("CwpivLJcU5jp6PCXhwf6WlqsDgnJkWo1r6lmYkJDc3N+DQrCJyEBKz09LN9zq2OBszPWenr0vXtXOoZZ4oPJ7zC8j0FXR4fYpKQ8XYRs27WLhwEBTJ84kZWLF6Ojo8PhEyf4U82ZV4siH6wATJw+HZuSJRkxcCAA/Xv2pEXjxgAMGDUqR+xmQWah0nP2VcyNjNgxejTllSb5K0rno8ImH8DD2Fi1dzn/yRO2hYdTTi6nlqmpxp9HNRMTVbKXQb6+aj14Zqy9Pf5JSXS5c4dRH7DVYaStzYZKlbgQG8ui90ikJFEwOR8TQ6fbt5EdO4bF6dMcVzoOvkhPZ7K/PyYnTzIvMJC4d+TRyI38DsP7UAx0dRncogXd69XLk997GBDAdzNmsG31arS1tXF3dVXF0o/5/nuiC4Glp8AqADv37uXwiROvJWNY/euvGBsZ8TQ4mHFTp2qs8clpaUzYvBlZ1670/f13YpR7Yg9CQ3EYOpSJW7YQl5zMF7NmMWr9eqZu387U7dtxGzcOt3HjSH9Lspb/AgPxf8NKU19XV5WoKCv8ZtelS7iNG4esa1cmbtlCUmoqCiFYc/w4Zn368Pvhw6+F8r2ND5WvLrJW+/9GRvJDQMB7r/53PX+O28WLyI4dY+LDhyRlZr7sj5AQzE6d4vegIJI/MJXy0FKl6FayJPGZmXS9c4eUV/7+UXIykx4+ROvYMUqfPavyzA5ITqbhtWu0vXWLm7n4qmQKkSNxT3tLS5wMDXEwMMjhTJYmxGthkakKBSnZ/rZ+sWKMs7fne39/LqgxU6VE/lGvWDF2e3jQw9qalMxMVeprC11dZMDeypWZVKbMR6Unzu8wvPflf7duoe/lRbG+fbn+6BHt585Fq1s3Gk6f/tG/mZqaSrf+/Vkyezals23zTZ80iTL29jyLiGDiJ/x+XhEdE8PcJUvQtbKiVrNmBIWEAPDH2rU4eHhw4PDhwqcAXLt5k+ETJ7Jr40ZVaEMWDqVLM1f5YFZv2sT+//3vgxuSlSRBvMV0"); result.append("+qpZ1VBPj1969aK5uzs62toUU7arTIkStK9enfk9e6rS5/767bfM7N6db5s04dGzZ6wYMOA1DftVWSPXr881375QHtKhp6NDJ09PADp7enL6xx+xt7QkJjERub4+WjIZkfHxHJg8mWGtWn3QKVgfKj8vyBCCV6W1sLCgiokJAjDW1qbtK85B2WOcsz+fzlZWnK5RA3sDA2IyMpBra7/sj/R0DlSpwrDSpd+YDz3rN3Mzo692dcVZLudWfDzD7t/Pca2soSHznJ2Z7+xMZHq66gNsoq1NebmcfypXpuor3toPkpL43t+fS7GxrA0NJSYj42UcuL09o5Wr/+DUVBY8eUJQSgono6PZoMwEuCokhEuxsdxPSuK3oCDClOGDPzs5UV4up9WNG3zv70/IK2GFEoWDFS4ulNTXZ4hyHB6MjMRCV5dm73EI19vI7zC896GctTWj2rTh3uLF1K1QgWPTprF+6FA6f8L3aMj48dSsWvW1kHK5oaEqAd2azZs5qnSUzC/MixVj8ujRzP7hB8KfP6eE8puYkJjIvzt30q5VqwIzhmXixYt3blYeOHyY3kOG8HW7dm909EtLS8PQ1haFQoF5sWKcO3QIV2Xe9reizCa46MABxm3aROuqVfn3u+9yrWo9YADPYmM5Pm1aDgeZe8HBVJs0iatz5uBub8+Sgwf5smZNVerg+ORkVWa8VrNmYWdhwbohQ97arIqjR/MgNJRa5coxs3t3mlSqhI62NqHR0Xy/bRtbzp5l5cCB9FcmhlC9ZL6+NPnxR47+8AOZCgUPw8IY+hED4kPl77p0iR//+ou7QUFM6NCBH7t0wUBPj3UnTjBu0yZme3nRr0mT15WQVauAlyFs5qdOsd3dnXavTPJbw8Pp6ePDWldX+r0SRvRvZCRtb90C4HKtWq9tD5yJjqbJjRscrVqVTOBhUtI78wf8+vQpo/38kAExjRu/tpL6Lz4ez6tXSVEoGGtvzzxn5xzpVQXQ8sYNMoTgSLVqDPH1ZUH58hTT8IE17/UC"); result.append("Ksc/QDMLC9a5uhKSmso/yg+3oZYWPW1scDp/nmomJiytUAFnuZw1ISEU19OjkpERPwQEcEp5It771HkXnmZm9Le1JSg1lVSFArmWFpZ6eiwLCkJPJmOeszNVTUxYotzm0JbJ6GJlxbf37uVqYXkXJtratCtRgm1ubmwND8dHuY1op6+Pnb4+X9++zRfFizPP2ZlRDx5wPS7unfXfe+HRvPknPb/jL17Q/MYN5pQrh29iIhsqVeKDdsOVW6mvkpUF8HMnPjmZiqNHs6B3b775mG2A5s1JTklh7JQprFi/niAfH0q9IVSxhLMzkVFRWFtZceX48RxWgvwgMzOTGk2b0rFNG9p/8QUXrlxh+IABH/b+W1jka3yj9o+TJv34pov/Hj3KkPHjmbVwISkpKYSEhREXH0+9WrXQyfYxPXvxIpN+/JG7Sk04JSWFjX/+SVBICBWdnbHIJZ5VtQI7d471J0+y6MABElJSePTsGRFxcejr6lJWGUr118WLzN27l4vKvN9X/P1JSU/H2cYGI319Spia8jwujq1nz9K6alUu+PmpHOWyTOYA28+fZ+2JE+ybOBG5Mp3sm7j5+DGbRozAwtiYzWfOMHnbNmb9/Tcrjx7Fulgx1g4ZQocaNV77O4cSJXiRkMD8f/5BIQQ/dunyUQ/mQ+W7lipFt7p12X7hAqUsLPi6dm1kMhlHbt9mcseOdPb0zNXikXTlCouDgpgeEMDDpCQuxMQQl5GBqY4ONso+cjUy4nBUFIvLl1dNtHeUedJ/fvxYtdd5LiaG2IwMrPX1VTkCHAwNeZGeznxlPoEf33JK4YkXL1gZEsL8wEDSlKv/M9HRRGVk4CSXY6xsv7W+PiX19NgfGcnF2Fg2hoXxKDmZqiZD1imsAAAgAElEQVQmmOjoIAMam5vzQ0AAh6OimF62LI4aiFr4GH569Ej134+Tk2lqbs79xES+DwjgXEwMp6KjScjM5GZ8PGFpaZQ2MMBSVxcvHx8OREbiLJezuHx5fg8OJlWZJfFddd5G"); result.append("JysrFleoQC8fHw5FRXE+JobT0dF4WVvjk5DAtfh4SurpUcHICC8fH84pPfBPREdjrqub43Co9yVNCHwSEhhnb8+8wEBWhYRwLiaGQ1FRGGlrczM+Hv/kZL4vU4Y9ERH4JSW9s/778uN7npr5JsoaGhKamsr8J0/Y7u5OiQ896/4NHvxmn3n4YnZFZfaePTjb2NBcmUHvQ1h8+DB9hg7lmHJV/yQoiNJ2djkm9/sPHzL5p59UYXQJiYls3rmTZ8+fU9/TE718StWspaWFR6VKDB47lpSUFGZ8//0HRwD9NG/eT5+9BUCtZFsBfdK+TGIi5UeOpKqjIzvHjFFtB2QRp9RUf+7W7bVVe14Tm5SE9YAB9GvalN/799dod36wBUJpAVBrf2RkYH3mDP1sbfm9YkWN9cX3/v4sDQrCx9OTMvmoADxKTmbiw4dEpqfzv6pVCUhO5oeAAJZVqECps2dz1N1buTLBKSkMf/BAVWaopaXyl5jq6Eg7S0s8r14FXqbH3eHuToULF/BTOka+T53cMNXR4Wn9+gz29WX7KyctltTTw9XIiJPR0Yy2t8fbzg63ixdz1Mnezo8hpnFjvO/dY5fSrP3qb96vW5fBvr4qS8a76mvCAgAwJSCANSEhNDY3Z8eHToJvsAAUFBRCYNyrF6sHDaJHgwYfZQEo6DTr2BEzU1P+3rTpwyfgfLYAaFFIMDcyom/jxpQqXvy1yR/g+23bKGtlRb8mTdTell/++YfFffuy4sgRzrxy4pa6aejiwvAvvqDf8uXsvXr1o7Yf8rw/njxhcfnyrAgJ4cx7mqHzYtLNEIKapqZ4a/gZ5LZK3OLmRlxGBgHJyRx/8YJtbm7YvcMKBS8jMSq8IaudXFsbbzs7TkZHvzEq4n3qZNHe0hIzHR2O5/KMnqWlcfINz05HJuMba2uSFQqqm5pypFo1Zjk5cb5GDba5ufF9mTL41a1Lf1tbIhs1wuM9z3DobWPzQZN5Vn0DLa3XZI53cOBenToMLlWKwPr1c5xi+Sns"); result.append("i4iglL4+K11c2PnsGfuy7bkXBbRkMlzs7HDXQGKwz5Hzly/Tunlzjp48WSDD4HUK08PQ19XNNR71WkAAa06c4OqcOSoTTaZCwYuEBErkcUjbn+fPU9nBgS516nDz8WP6L1/O7QULPsgB8FOZ0a0bq/LIsvLJ/REeTmVjY7qULMnN+Hj6+/py29PzjQ6AeUGyQsHMx4/5o2JFQlJT8bh0idUhIXn20f8YDLS0WO3iwhc3bnCyevW3HqJUzdSUyWXKqCbWHj4+Oa6X0NPjuzJlGOvgwKzHj/kjOJhXzXjvU+dVsqwkUe8RrWKpq8vkMmVeKp3FinFEGQp3PS6OVIWC0gYGtLh5k1L6+pjo6DCtbFkuxMbS4No1Hr0lI11HKyvKyeUYaWvT09qaTbmcRfGu+ikKBYdfvMgh83FyMj84OpKmUFD36tX3OqDnfZTM/0VFsVxp1epkZcXQ+/dpZG7+WfqbqIty1taUUfpbFSXiExLYe/Agv8yYQUZGBiMnTeLO+fM5zgv47BW4wvRAFEK85jmeqVAwePVqRrZunUNLPXTz5munb30q1x894vaTJ6qjen/p1YuU9HTGbtyo2RV3PlogcvRHXBy3ExLoovTl+MXZmZTMTMYqfTnUgQDGPHjAD46OGGhp4WRoyEwnJ8b5+eGvxtwB77taqmFqyu5sJuvcuBEXx9zAQGY+fkzPVyZ/gIi0NOYEBnI1NpZ6xYqRlssq+X3qvEq4MlrB7D0mr8j0dOYGBjI3MJCOt2/zPJvSkJiZyY34eJIyM/FLSiIxM5MUhQLfxER8ExPf6oew9/lz5gYG8kNAADOyebx/aP1XZaYoFCQrFNyIjyc0NTVHez+GmIwMxvj58Uu23Pi/VaxIXEbGa9EphR0rMzNMDAyKnAIwfc4cJo4cCcDYoUPJyMzkl2XLCpYFp7A8DN+QEE7dvcsVf39uBQaqylcdO8b1R49Iz8xU5QEYtX49w9ety7OUmEmpqSzcv5+mP/2EhbGxKpTxeWwsNsWKseLoUSZv"); result.append("3Uq4BpJYZFkgBrdogXezZvRfvvyD8g/kSX9kZrLwyROa3riBha6uauX5PC0NG319VgQHM9nfn/A8btfVuDha3bjBhdjYHEfxagHxmZm0u3WLw58Y//yxRKWnczM+nu3u7mx/9uyNh9q8ys34eG7Fx2OUiwNnv3v3aGxu/taTCt+nThZHX7wgVaGgxRveC+s3WLHSFAq2hYfn2sZPYX1oKMB7/+6H1v9YtoSFqVJTP85mzbibkIC+lhbbwsMZ5Oub41phxsbcvEgd1vM0OBivAQM4feECqcpvWERUFFaWlvw4bx4r1q9H8Qm+MJqk0NipXOzsuKBMS5ydIS1bMqRly9fKf/322zyTLdfXZ1z79oxTHgmZ3TR2Zc4cza24lRaIrGQhv/TqRaWxYxm7cSPLPzA85ZP6Q1ubcQ4OjHslaVA5uZwr2RKT5DU1lfvPrzLK3v6DMvrlNbcTEhj14AFb3NzQ19KivaUl3e7cYVsuud5z+4yW1NOjZfHibA4LQ0smU21zhaelMdDXlw2urlyOjVU5+L1PnVw/bCkpzHz8mPnOzlyNi8sxgX1jbc0JpZk/tzZqy2QMtLNjsTI0UOsjVhq5/W5TCwti0tO5kYtn/9vqJykUucrMixVPTxsbeuaiUDWzsCCyUaMitxIu/p4+HYUF+1Kl2LZ6dY4yOxsbLhSgBECFTgEoyiSlprL8yBFm7NrF1E6dEEIgk8lyWCDM5HJGt22LdbFiUodpGA9jY05mC/ea4eTEDCen1+q1Kl6c6qamlJPL+a5MGYRSmeppbU2Da9eoZmJCKwsLysvltLW05H9RUex5/pz2lpacqF6daQEB3EtMfGedreHhbzTDz3z8mKCUFDZXqsTTlBQeJScTnZHBX8+e8SwtjSomJrSxtMTBwIAfHB1JFwJdmYxWxYvze3AwbsbGuBsbU0JXl30RETxNSaGzlRUmOjr0tbVlg3KVnh0TbW2+trLCVFkn6wS/knp6NDQ3p/rly9QxM8NOX5/WxYvzIDGRlsWLv7G+"); result.append("55UrTCpTJofM1sWLY66jQ28bGx4lJxPzEWl6JXKnoIQsSuSidBeWMECJj0QDYYASb3kBpfGfr4j8DkMr4GGAAPuvX3/riYRvpRCEAX7S+5/PYYA60eb52wHHuiCRn/O/1P/5/AWQuiA/aXE0n+f/z7x/zm07R32v+m+v1KU6f33k7zeXhmC+Im0BFEJSElIY5z6OyfsnU9qttNQhBRTzJuY4TnXEoun/55ZPj0wneEUwIatCSAn6/6x7hk6GlJlQBruBdiCDjPgMQlaG8HTxU1JDUyX5Eh+MUAgeXn74bgVAQlIAJD4ftHW0iY+MR0tHS+qMAkz0yWiiT0bjPN8ZhwkvHSqD/gji0fRHr9VNDkjGd7Avxh7G6Nvqc6PFDZIeJknyJT6a54HPsSpjJXVEIUaaIQqb1i4EOvo6dJzcEbuKdgiFkDqlgOP/vT/xN196wZfsWhKZTu77BroWusgryrnT7U6eTn5FXX5RJcQ3BDsXu8+6jaHBoQz/djjjh47n65ZfM7jXYDIyMlg6fynLfllGmwZtuHX95WFlPv/5sHDWQmZ8N4NeX/UiOSm5yD9jSQEoRCgyFXhbeTOxykTC/MKY12EeXgZePLr+SOqcgqzUZQju9buHyBAYVTTCYZxDrvXKzihL2PowYi/HSvIl8kQBKOlUkr9++gsvfS+6yrqyvN9ywv3DVXX8LvoxxnUMfcz6sH/hfsIDwvmt9290lXWlq6wr+xfsJyn2/5Wxc9vO0cuoF6Mrjuby35c/uY22pWwpXaY0/g/82bJnCxN+mMCGlRtwKu/EiAkj6N67O6MHjCYlOYWJwycyatIops2ZRnp6Og98H0gKgDTMC9HD1NZi3vV5zL85n5TEFMbuHMvSh0txrOoodU4BJ/5WPIHzAl9OdNPLIi+XM/TKrLYZlm0sCZgWIMn/QJL8krj37T2OyY7x5JcnudbJiMvgpOlJzjueJ2JfBEn+SfiN9uOY7BjXGl7jXr97XKlxhcA5gSAg/UU6IWtCOK59nAsVLnDP"); result.append("+x5X617Fd6Av6dHpBWLMRT6NxLqcNV2md6HHvB4AlK9THuty1qo65euUp3Sl0nx/6Hvaj2uPtZM1wzcNp+aXL09jrdGhBnKz/39WdbrUwbaiLTMvzKT217XzpJ1GRka4ursiN5LjVN6J4/87zqOHj9i2YRuxMbE4ODpw6/ot5EZy1Sm22w9sp0r1KtKcIX1aCxeW9pZEPIng8u7L3D11lxIOJZBpSa7mhYHHPz8m0TcRLUMtXFa7qCIIZLoyXFa78GDEAzITMyX5H4i8/P+xd97xNV9vHH/fmXmz9yaSEIkQO2oXra01SqlRFS1FjRq1tUUptUfNKqrjZ7SlI7ZQe2YIkb1k73nv/f1x4xIZkkiCyOf1yovX/T7f85zv+Z7veZ7znGdo4zDHAaGWkPC14Sjzix+bRW+LRlmgxOhNI0z7maLdQBubiTYAOC52xHWHKw03NeT+F/cJ+ToEiZEE67HWSC2lWAy1wHWbK82ONSPhaAK3B99+6eZWYkQi+blFFROFQqHO8NdzUk8atGrAzwt/Jjvtsen8wdUHGNsY4+LlUuTesRvHoqWnxQ/TilbI+2vDX7w79110jaoveVBBQQFNPJswbNQwPp3xKVv3bUWhUBAcFKzO0goQHxf/2q8pdQpALYSpgymauprYudvVDUYtgiJXgf+H/igVSgw7GWL9oep81n6GPZkBmST8mVDHv5IQSARYDLUgLzaP2J9ii1xTypUkn0lG1kQGT2QZftoXQa+lHrpuusTujy2RRqwvxuwdM5J8kshPKJ8V4Ny+c9VrWUpMZ/fU3Szrs4wT208UHZMn0vsKhAK8v/cm7WEa++bsU42LQslvS35j8KLBxdo1tDJk2NJhXP3jKv/9+h8AydHJ3L94n1YDqj4b6JOpdzt27cjsybO5cfUGkeGRbP5uM02aNSE9LZ1VX68iOyubgwcOEv+wTgEoUwE4e/Is/bv2x0hgpP5zMnXi63lfExURVVQ7Dw5h6vipGAuNMRIYYadnx/wZ84mNjq1052KC"); result.append("YvjfV/9jSsMp6jMlbytv9s7aS/CVx6a+y4cvs2vKLvU51WDBYBZ1XsTvK38nN6vyIUCZyZnsnrqbifUn8oHsA7ytvfnuve+4fOgygecC+W3Jb9X6cirLXyAQ0KhDI4ysjZ7JQ54lJ+TLEC56XsRH4IOPwAe/D/zK3cfo7dHq+847nefB4gdkBVXOASvpRBL3Z9/nlOEpdZvHRcc5Y36Gk3onOWd/jus9rxP3Sxy8pr6NqRdSiVgbAYDTCicM2htg96kddyffreP/nNC01cRsoBnh34YX+T3+YDxmA8rvDS+WlR1cJRAKEOk8u17BozC86oRYIqbv9L58fuhz/vj2DwryVBkSU2JTMLQsmiTGvok9vaf25p9N/3Dv4j3+2fwP7Ya2Q0tPq8S2u4/vjnNbZ3ZO2kl2Wjb75uxj6NdDi9Ds2rKLJvZNisiYUYNGceLvospIfFw8c6bMwURkgpHACBcLF5YtWEZMVAznz5zn3KlzBAWoioyNmzSO1u1a079rf97v9z7denZDV6bL9p+2s2/nPjwcPEhNScXV3VX1rMkpLJ69mHUr1tG1ZVcyMzIZ+NZA+nftT2pKKuNHjKdD0w5ER0YTFRFFrw69iIuJ49ypc2xctZFBbw9i/+79AOTm5vLdsu9Yvmg5A98aSEpyCjs27eDtN95my9otNLFvwrj3x700tQIEScpnZwJc8PkC1q1QVTn6fP7nzFo0q1TaHl49iI2O5X///g9HJ8dndsCHZ2dCC7sVxgyPGQDMPDKT5n1Kzjr14+c/cmTFEWQmMrZGb0UkqXxRkJTYFOa1m4eOoQ7eW71xaOZATnoO538+z75Z+0hPTGfQgkEMWlg9mXSel/+BeQcYsmTIM/lsRZUJUJ4u57TpaRS5CgQSAe2C26Fp+4wKX0q44HaBTH9VYZuWF1qi30b/uZ89Yl0EdyfdRawnpn1Me0TaIhQ5CmL3xXJ38l3kGXIsR1rSeGfjVz6Rjo+g4pkARdoi2txpg1Y9LZQFSgInBhK1JarG+lyb"); result.append("+L+pVKWiyQ7NJmZXDCa9TbjU8hKe/3pi9KZKgb418BZu+9y42uEquk11abS5kfoe33q+ND/ZHMNOhiQdT+Jat2u47nDFapSVagfvcA6rUVbUX1if3JhcLja7iPFbxjTe1VglrMpIBRT3II4rh6/Q67NeNTKuG0dvxLmtM2+OexO/k35kpmQW263nZecxzW0aEk0Jtm62fHbgs7K/5TsRfO75OQ1aNcCzlycDZg8oOv68SWpKKl1bduXB/QdoaGoQnRVdanGh3h17kxCfwCGfQ1hYWVTJc/+671fiH8bz8ZSP+XXfrwwcNpBb128x6cNJnLp2ipDgEPp27svN0Jskxidy8t+T9HmnD595f8bmPZuJDI+kjWsbAqID2LVlF23eaEPLti35dMynWNlYMXTUULq36c7fF/7GxNQELzcvlqxcQv/B/TESvNhMgOU6Apj39TyaNGsCwMGfD1JQSh7t5KRk7gXeY8eBHeUS/uXFkzvZsna1BhaqPPeGlobPJfwB9kzfQ3xYPDN/n0k9z3oIBAK09LToOrYrX1/6ulrPsKqCv7FtxSodimQiNKw1EOmKUOYrCV8d/sx7Eo4mkBP+OBmLpk3VlATVtCtsR6Ba7AGEmkKsxljh8p3qrDFmdwxxB+JeSyuAPEvOg/mqyA5lvpKorVF1/KsIei30MGhvQNhKlTNg2qU0ZJ4yhNLSl8r4Q/GEfBlC7N5YPA55qIX/I6RdTiN8VTjBc4NxmO2A6zbXcvWlpsPwBswZwOFvDiMvkBMZEFkib6mWlMGLBxPpH0n3j7s/s01bN1s6jezE/Uv36Tu9b4k0+gb6rN+5HoFAQG5OLseOHCvZIpqRSVBAEN/v+77KhD9AizYtWLlkJZ9++ClvdFIlPWrSrAm5ubncv3ufm1dvYmhkiO9pX44dOUbPfj3xu+VHQnwC+3bt48yJM3Tu1pmkxCROHz/NnZt32LdrH6bmpmhqaSKVSpHpyajnWA+Znoy+A/ty7fK1l2ItKZcCIBaLWbdjHWKxmHuB"); result.append("99jw7YYS6ZbOX8qw0cNo3rp51XZSJCxiPivLtPYsmvLi6h9X0TXSLWYGAzCvb06/mf2q9cU8L3+Ziazi5iCJAOuxqo8+6vsoClLKLpgStjIM648eLxKlxWdXuB+i0tuxHGWJUEM1H2IPxD43r8zATAInBOIj8OHam6V/lLnRuRyXHue0yWmid0aTG5VL1LYoTspOclL3JDf73+TmgJtcaHSBoClByLPk1To/8lPy1WbiF3EcUpv520+1J/HvRDLuZBC5ORIbb5sy6U37m1Jvbj1cd7hi2te0uFLRUg+7qXa4bnfFbrJdub+Tmg7Ds3SypEHLBpz54Qyx92OxcCxZyMqMVWuLVFNarufQNdZFKBSWuSlr80Yb3h/zvtrinFdCqfB1K9YxcNhA3Ju6V+n7trW3xfe2L9lZ2XT07EhqiiqMdOCwgfz202/ERMXgPdmbn/f8TEZ6BroyXQoKCtDR0WHYqGEMGzWMPQf3YGFlgbxATut2rRk2ahjzl87nk6mfFONnaGSITE/Gy4ByOwG6N3Vn8szJACxftJwH94vGll+9eJV/j/7LnMVzasUuS6lUkhafxundp0u83nZQ25eav5ZMq1J8LYZZoGGtgTxDTsTGiFLp0q+lk+mfieUHljX6XgQiARo2GiohkPD84VQ6DXVw+c4FgVhA0vEk0m+ml0gXuSESFGDUxQir0VZoWGtgPdYa/Tb66DTUweOQBx4HPWj0fSMi1kfgP8qfOryaMOlrgnYDbe5Nv4dIR4TEWPJC+vEiwvDe+eIdDi49SF523nNbUSuKhcsXYmRsRHBQsPrI+RFCH4Syf/f+Mo+fK4sjvx5BR1eHbfu34ebhRlhImFoB2L5hOx7NPej7bl+OHj6Ko7PKst24SWN8T/uyd+de4uPi2b5xO9lZ2Xh19GL6J9MJvhdMwJ0ADv9yGICMjAx1BEJQQBDde3V/KeZ6haIAps+bjnMjZ3Kyc5jy0RT1A+Xn5zP5o8ksX7ccbZ3aURqy2dvNANg4ZiN7Z+0lL7uo"); result.append("RmpWz4ymbzV9afk36dakcgJWIsBukip6IGJtBIrckp1VQleEYjvRFqFmzQaSKHIU5MWoxkLXrWqOYQQSAYadDZEYSYo5gAEoshWkXkpF00ETgbTo7u2RNeIRDN4wQOYp4+HBhyjldVkYXxmFv0CJskCptiDaTrYl8Z9EbCc+rqWhlD+meXTPk/8+q92y8LKE4dm62WLnbkdafFqpfX3kKPh0f8uiL8gvKBKCVxKMjI1Y9M0iAL796lvCQx9/i7Mnz2bWolno6etV+bvPSM9gSK8hbNuwDQ9PD7WFwb6ePX3e7YNXBy9kejIGDBlAlx5dVFYQPRmbftjEN4u+oX3T9piZm2FgaMDEaROxtLakc/POLJ69mF79Vf4bebl5rF+5nm0bttHKqxUenh6vngKgoaHBuu3rEAqFnDt1jh+3/6g2zTg3cn5ptJqqwMhVIzG1N0WpUHJ4+WGmNJzCqV2niqTWdWrjVCv5W3tbI9YTkxeXR8zumGLXc8JzSDyaiM0nNjX+XsJWhiHPkiOUCrGbWnVhjiJtEdbe1sT+FFuseEzMnhgsR5Tf0lGQUoDUVFrmUcZzKy3iqjvuet35Z93PImJNBAl/JpDkkwSA1WgrLIdbou2ijTxLTsyPMWT6Z5J8PFmdCOhRNEL0jmjSbxS1HOUn5RO1JYq8mDwS/kwg8Z/Eki1pL2EY3rtz38WmUcnf9p0Td/hr/V8A/PndnwSeCyxd+VEoObfvHJcOXkKpUPLT3J+IuRdTJu9ho4fRul1rcrJzmD15NgB///E3yUnJvPfBe9Uyl0aMHcHRs0cZO2Es85fOLzLu3276Vv3/lRtXIpE8tgZ169mNm6E3CYwJpM+7fVSWV20ttv+0nfC0cPb/vh8dXR21cvPpjE8ZO2EsYyeMfWnkXIWLAbVs2xLvSd5s+m4T82fMp4FLA7au28qZ62dqpMMrBqxAolGySS4zObPK+BhaGfLVf1+xacwmrh+7TkJ4AhtHb+T3lb8zfMVw9Q69uvAi+Yv1xVh/ZE3Y"); result.append("t2GEfRuG1VirIgtt+OpwLD+wRGIsIS8+r0bee05EDhFrIghbFYbEWILrTle0narW2mQ70ZawlWFErIugwdIGhasYxB2Io+mxpjxYXHZKZWW+kpAvQ8gJy6HxD42rdTweOVwKtYRIjCTkJ9VsdrnaxF+7gTYu64ruoEU6IvU7FGmLsBxuieXwokqgy1oXXNa6lNimxEiCtbc11t5lO/E9CsPrOakni7supuvYroil4jLD8I6sPEKHER14cPXBM8Pwzv54lp2TduLR3aPEMLySUM+zXqk+RG5d3HDr4lY+JU0o4I1hb1SomqBAIODbTd/SybMTx44c4/fffmfx7MVsP7C91MiAOtSQBeAR5n41F/t69qSmpNKvSz9mLZyFmUXNVI2acXAG3wV+V+Jf/9n9q5SXgYUBs4/OZvr/pmPppPr4I/wiWNpzKauHrCYnI6dan/VF8rebYodAIiArKIv4Q48TZhSkFhC9Kxq7z6o/yZA8U871Hte54HaBc3bnCF8dTqNNjXgj9A1M+5hWOT8NKw0shlgQtSVKnVEu8e9EDDsblukFnhOVw91JdzljfobkU8m08WuD+RDz6pkT7Q1o8HUD6i+ur/6t6dGmOMxyQGomrfZ38rrzr2po6WlhaGWIqYMpjTo04tSuU0DpEQCDFg7CzMGMTWM24X/aH68hXmUKYO+t3qQlpPF1z6+xcrHCrF751mkTO5MXNiau7q6MnzIegA/f+5BO3Tqpo9BeNSgUCo78doS42Dgu+l586fpXKQVAS1uL2YtnqzXYkeNG1motqdWAVqzyW8WH6z9Ua8YXfr7A8j7La6Ta3ovgr2GjEoYAYd88zo8euTkS427GaNXXqvbnFumIaPZ3M1r6tkSrvhZKhZK0q2mIdKvPOcnuMzvyk/OJ3hmtet4tkdiML/uoQ9NaE5e1LpgPMSftalq17lRSzqZwf859ThudVidLutzmMqHLQsl7WP3WmNedf3XiRYXhPQ+y07OZ4DCB498fr9J2Zy2chVgspqCggA8/"); result.append("+fDV3WELhYyfPJ7IjEhat2v98vWvsjfqG+irH7A2mmaezDQIIJKI6DGhB2uD1qo9bP1O+XHl9yu1kj+grsGeejGV5DPJKPOVRKyLwH66fY2+C7G+mCa/NEGoISTq+6giqVarGjJPGYYdDIlYE0HGnQykZlIkJuXzAnda6YTUQorfKL+6Msx1qDBeZBhepb9NiZis1Kwq9wXR1tFGJFL199G/dXiJFIDajj++/aPE33UMdZhyYAqWziqT/P1L92slfwDdJroYdzdWWwFi98Wi3UAbvVZ6Nf4+ZJ4ynFc5AxDgHVCt9d7tptqRdT+L24NuYze5/EcdIh0RjXc1JvV8KuGrwus+olqKuANxnLE4g4/Ah3sz7j0OR1WqnFR9BD4EfhJIbmTF05C/yDC8ykCiKaHb+G60e69d3cSoUwBqD0JvhhZJuFFk0mtI8OiuCuPQ1tcmPzefIyuOMEQ0hElOkwi5FqKmve1zm/c13+fAvANkJGVUC//qxKPdfsLRBILnBZdr96/IVRC2IkxVCtXpPOnXHntIJ/kkcULzBMHzgivsuEiXZ9kAACAASURBVGXziQ3mQ8yRp8u5Pfg2ipyiIYrpN9K51PoSPgIfgucHI89QnePnJ+Rze/Bt/nP/j+RTycXaVcqVRRL3mPQxQctRC017TXRcdR7T5SmLhUUqchXIcx7fa/CGAXbT7Lg/5z6p5+vq0tdGmA8xx/0ndxCAlqPWYwuRAAw7GGI7yZaGGxuq81VUBC8yDK8iuPHXDYZpDGOUwSgeXH3Asj7LGCIcwoIOC2r9+4+OjGbgWwMxEhixec1m9e8+x3yw0bVRR8fVagXgUTrgmihq8KQ5VSEvnd+ja2XRVITnzkk7S2xLqVQV6RBLxbR5tw0SDQl9Z/Sl92e9yUrNwtzxsQOYkY0Rvaf2ZsiSIRVKH1wR/lU2zgVKeIqdUTcjZE1loASRrgiTXibF73nqPQk1hNjPsMfuMzsKUgvQcnzsL6Bho4HdVDsclzgiMZKU3o+n3vsjuH6v"); result.append("8v5Pv5FO4ISiIUiypjI8/ueBWF+MSEuk9hWQmEiQmklp9k8zDDsV9azOupulEtb/pRK9PZqClAIEQgF2k+2wm6La/edG5hK2MoyciBySTyYTvaswE+DWKFL/SyUrMIuI9RHkxqh2fI5LHNF21uZaj2vcn3Of3Khc6lC78KgaYfC8YPITH1sAwteE0+DrBs/V9osMwysvLBpY0HNyT1b7r8bFy4X5PvP5ZOcntBlYdeuRXC6vMRlTEVjZWLFt/zZMTE0wMn6cmt7c0pwFyxYw/MPhr8w8Flf2xuhIlZNUTnYOyUnJGBoZVlsnEyMSi/y/fvP6JdIlhKnKgabEpiAvkCMSP58J7fqx68z1mst7X75H486NEYlFJEcns2/OPkKuhTBuy7giwn7IkiFcPnyZvTP38tHmj1Qf6eo/Gb1mdLXyz8/N59jaY+ydtRfz+uZ8duAz6nnWU1sglvVeRt8Zfen1Wa9SlRBlvpK82Dxyo3ORecqKWQHuDL+j2v0/ddT3ZC2A3MhcNKwe73oclzgSfzieezPvqQuohK8Ox2WNS5nPnR2mSnQiz5BTkFaAWO/xNBXJRLj/4s7lNpeJ3hGN2ECM03IndVy4hrUGTiucuDv5LuZDzNGqr0Xy6WRknjI0LIvvyLRdtHFa7oTT8qI5FWw/tS2itNhPty9m/bAeZ431uOKOWkINIW3vVDxTpFFXI1x3uJIblUv8EVXkhVBLiOVwS3wdfZF5ynBZ64K2kzZR26KQGkvRaaxD8LxgtWWjPDTPgn4bfaw+tCI3IhdFrgKhthCpiZSIdREIpAKcljshayYj/DvVMYdAJMBskBn+o/1Jv55e4ecWyUSY9jbFbZ8bsXtjybiToX6XGtYa3HrnFsZvGeO0XPVe066mPZO+JuC0won4P+K5N/0erjtdid4Rjfkg83JV+isLLzIMryIKwPBvhpOdno3P9z5YuVjRcWTHKms//mG8epMZGx2Li6vLSyU4DQwN+OLLL/jyiy/pN7AfGpoaHNhzgIXLF75S"); result.append("iqxo5sKZFerxhbMX2LVlF+tWrCMnR7X4+572JSkhCUdnR3R0dCrUgQeUHlsdExTDP5v+4cD8A6QnqhYW/9P+ZKVloSXTwshKpX1dPnyZvzf8zb9b/kWpUJKXlUfg2UDSE9JxaOqAWFJxPSfkegif/vApuka6nNlzhn2z9vG/r/7Hv1v+xcDCgI+3f0yLvi2KDqZEhL2HPTsn78Stixv+p/xxbuusPq+vLv4isQiXdi7kpOdw/9J9Bi0YhERTojb/aWhr8N5X7yHVKu40dCnrEhGrIwheEEzWvSxSzqeohe4jganjqkPi34k4r3ZWC9qM26o86SFLQihIKywhei6FgtQCNCw0kBhLEEgE6HroEjQ5CKMuRiSfSka/rT7aziUfWySdSCJqSxSh34SizFPt/pPPJFOQWIC2o7Z6R69hoYHUXErC7wmkXkglZncM2Q+ykTWTIZaJ0WuuR/KJZBL+TMB8iDlhy8OoP6/+S1k58MGix/M/OyQbwy6GZAZmEjwnmJRzKSSfSkaeISf9ejp5MXlo2moiMZFwZ9gdEv5IQNtJG+fVzkRuiESRqygXTVkwe9cMl9Uu3Blxh8RjiaT4ppB8OhmLYRZk3Mkg/Uo6UnMpOi463Bl2h5RzKaScTSH5RDISQ0kRhbDclqc8JRl3MrCbZkfo8lCitkaRci6FxGOJiHREpF9PJ/t+Ng5zHIg/GE9WUNYz6cuL+gvrV958qilE006T4LnB6DbRJcknCYdZDhVqozkl102p7qO9qkJCeAIHvz6IpZMl7m9WPEd/fYqOf3ZWNhtXbWTpgqXERKmsFZfOXyI9LR1be1u18/nLgCbNmnBgzwFSUlLIzcnF1t4Wp4YVS862fNHyRS/yGcpVDrg6UZ5ywK8avh//PXdO3MGztycjV9VciGRedh7Tm0zHvau72gKx1Xsro9eMVisET+NROeDqRMD4AJJPJGPS20TtyFfdyH6QzX/u/6HXUo+Gmxui01Dnhc2HjNsZXO95nXpf1MNmvA15D/O4/d5t"); result.append("Gm1pxHnn80VoPQ55kBOZw92Jj+vbC7WEKLJVgrve3HqY9DbhcpvLKrPjYHPcD7hz3uU8WUFZ5aYp0RyoJ+aN8DcIGB9A3E9FKy1KzaXouOqQfDIZuyl2WI+15oLbhaIC8Yl+VgadUjrhP9afh78+LLFNr0Av1VwqtGQ8i748eFQO+Hlwo/cNknySaBvYFi2HioXHllUO+FWAUqFkhO4IvL/3pv377St8/5u8+Uo//4WzFxjcczBDRgxh5caVFbf6vQrlgOtQMQxaOIiYezE17hkr1ZIyftt4fL73IeBsAKd3n6bt4LalCv+aguNCR7LuZWHxnkWN8dSqr4XFCAuE2sIXKvwBdN11abSlEQl/qI6o0q+l02hTo3JlMtRtoouOS8n9F2mLsB5rTfLJ5FKjIspD8wgmfUwQ64tJPl78qCAvLo/kkyUfIQjEAiyGWqDIVqDXXA/Pfzxx/MqRFr4tcNvnhsMcB7yCvLD60IqOCR3RbVI+XxjLDywrJMwf0Qs1hcV42k+3p61/W2zG2/BG6BtFqlg+Lww7GSLSFVVY+NcGCIQCrBtZY+dux+uItu3b4tzQmRZtWryS/RdThyrHI4H7IvKku3Z05c2P3mTzh5vx7O1ZpedyldYyHxUMqmF1U6QpemG56osJ154mxO2PI2pbFAKRAOO3jEul1fPUw2GWg1qw3nn/TlFFz1SKw2wH7KfaE/JVCJEbI4uVxC0PTTGlqVCA5SU+O6GOxESiNncbdDAg6R9VDv20q2kochVo2mpyvdt1NGw0EMvE1J9fn9TzqVxpf4XsB9mltmvW3wztBtqIdERYDLcg5oeyndZKolfkKEj6O6koz5Bs6s2rhyJPwWWvy+Uq0FOH8sGigQVmDmav7fNLNaQIha/mXrpOAagGPPJef1HJYAYtHMS/W/59aWJz1eOgqHm+L1NCHufvnLngdoE3wsp2ykq7lkboslAAEv5MKL4bj88jdGkoBu0NMGhnoHbGqyjN08iNVUUriPXFFCQXlEmbn5Cv7qNw"); result.append("lRCzgY8FgDxTTvq1dORZcrKCstBpqIMiR0FmwLNrdTw89FBt0i9LUXgWvTxTXoynIltB+rX0YsWennuelbPiX22Fvpk+mjLN1/b5FQrFSxepUO7NWZ24rlqkJ6ZzcudJAHz3+5IYmfhaWSCKCYrEx2l1Y/fHVio5SmWQejGVlDMpZNzIKLUSW42/F2MJYn1xmXUFis2n6+mk30gv0bPcf4w/hp0My6xUWB6aR0j6NwlFrgLjbiVbJ6QWJWeeU+QpiN0X+9ze70/j0bwpb7sVpa8KJJ9K5uHBhxSkFhCxLqLGimO9TDC0NHxtC/WcOXGGe4H38DnmQ2R4ZJ0F4HWHzFhGn2l96DOtz2trgXha6NlPs8d+Ws2mD9ZvrU/rGy9X7u20K2nkxeaR6Z9ZJMFQEZSwjkrNpRh3NyZmTwwCoUCt2OXF5hEwLgDXXa6kXkxVO/iVh6Yk5ITnEPJlCE7fOJF2OY3skMc7aouhFiSdSCq1jwKRAOtx1oSvDi95a1EenaeEdo26GJGfkl8kmVR56BVZipJ5VvGWx7CTIa0utnqt1zxdY93X9tk7dOnAg6QHr2z/6xSAWmiBeFRRzHe/L4ZWhhjbGNcNzEsAvRZ6dErpVOp14x7G6DXXQ7uBNg6zHVTJl7RVZ9tX2l9B5inDqIcR2s7amPQyIfGvRB4efIhJHxOan2hO8PxgMv0zn0kTuze21HDAkC9DyInIofGexuSE55D9IJuC5ALifokjLy4PWVMZJj1N0LTXpN68eijzlQgkAox7GBO5IRJdN1103XWRmEqIPxxPTngOZgPNEMvEWI2yInpXdDGeIpkIs3fMEOupaLQbaKsVH8MOhlxsfhH9tvpoWGtg/LYxmXczMe5uXCr9pTaXcJjpUISn8dvGiA3FWH5gqXqmlIK6CVlFeFVCFutQgg5dFwb4eqMmwgDrUMb8F9TN/xeJqggDfB686mGAAFd/v0rzPs0rN/6veBjg8+JFhwGKSTZ8sSPgM6huFXqhGkDd+L9gHbxuCF4k"); result.append("uv37Yvm/AvL/7Nm9NGjQEkvLknN4NGcQ/FJZDez1nn7KqizQUCkFoA61Djk5GUyb5s6sWb9ja+tWNyC1AIaGHWne/FThoqFAoSjuIS8UaiEQCFEq5Vy50oHU1PN1/Ovw3AgKOk+rVv3rBuIlwO3btzl48CA7d+4kNDQUADs7O8aMGcM777yDu3vFsjHWKQC1ECKRmPT0BITCutdbWyCRmJKVdZc7d0aQlnaFp4P6dXRcad36KgKBJqGhy6tc+L3u/F/vDUUmGho6dQPxEsDd3R13d3c6depEx46qHC+7d++mU6dOlWqvLgywlkGpVCIWa9C//yysrRuiVCrqBqUWQCo1JShoGmlpl4sJP4FAgpvbjwiFmqSnX+PBg4V1/OtQh1oMa+vHmSzt7CqfhbFui1iLoFDI+egjC4yMrHBwaMry5X25ceMvvvrqAvXrN68boFcYYrE+ycknS7zm6LgYmawZCkUOd+6MQKnMr+NfhypBZmYyurqFRdcuH2b16sG4uHihrf24KE9q6kOCgi5gaenMihU3kEq1+PzzZmRnp2Fj44pQ+Dgvg7//GTIzk/n44+107jzmufp26NDPLFu2EG/vSXz33TKmTfuCfv0GsXXrOsRiMX/99Ttff72a5s1bk5mZwebNazA0NOLIkV/58MNP6NPn3Vf2vYhEj8f0ebIQ1ikAtQhCoYjly69ibGzDqlWDmTr1Z1JS4jAxsa0bnFccoaHLSvzdwOAN7O0/B+D+/VlkZvrX8a8gsrKCCA1dSnT0LpycvsHefkYxmoKCNM6etUEqNcbZ+Tt0dBoTGbme8PA1GBi0R1u7ARkZtzAzexcHh1nk5yfz8OH/CAz0RkurAQYG7cnM9EdX140GDZYjkRi+EvMuMjIAGxtVKe/09ASmTfuN5s17F6FZurQnAoGQTz7ZiVSqSidtZeXCxIk/IBY/Th4VFHSBK1d+p2nTt55b+AP06zeIyZM/QiqV8vff5xGJxMydO41PP52Os3Mj9PT08fYezpUr95g1azJD"); result.append("h47Ey6sDVlY2/Pzzj6+0AlBlMqNuaa1dMDGxIz4+jIsXf8PP7xSmpvYIBHWvuTZCJJLRuPEPCARCkpKOEx6+to5/JaCt7YyDwxyEQi3Cw9eWaEGIjt6GUlmAkdGbmJr2Q1u7ATY2E9UWCFfXHTRsuIn7978gJORrJBIjrK3HIpVaYmExFFfXbTRrdoyEhKPcvj34lZljUVEBWFs3KhxvcTHhf+LEdq5fP0bv3p/h4uKl/r1Zs7eLCP+8vGw2bBiFlpYMb+/vq6RvAoEATU0tmjTxxMLCClNTM06c+JsrVy6yb98uMjMzaNiwMTk52Rw58iuNGzcB4K23+rBjx4G6BaROAaidMDV1QFNTFzs797rBqMVwcVmDllY9CgpS8PMbxTOr/dTxL0OYSLCwGEpeXiyxsT8VuaZUyklOPoNM1gQQPXFPUQOqnl5LdHXdiI3dXyKNWKyPmdk7JCX5kJ+fUO6+nT27l5iYoGody5s3/+GLL9py586JUhWAjh2LljZPTIxk9+6pWFo6M2TIkiLXnqbdv/8LYmKCGDlyNcbGNtX2HNnZWXTq9CbDho1i0qTP2bnzF6RSDeRyOYGBfmq6hw9j6xaQiigAvr6nMTISYGQkwMREhI2NbrE/ExMRRkYCTE3FXLpUO71wIyP9WbNmKB99ZM7IkfpMmuTEjh2fcveuL3/8sQo/v1NVzvP69aMsWtSFkSP1GT3aiJkzPfnttyVERNxh9eohJWrGjRp1wMiociVPs7Lucv/+bM6cscTHR4CPj4Do6B3lvt/Pb4T6vqtXOxMW9g1yeVal+pKUdIL792dz6pShus3jx0WcOWPOyZN6nDtnz/XrPYmL+6XGBdCLVfL6Y2U1GoDAwAnk5kbW8X9OaGraYmY2kPDwb4v8Hh9/EDOzAeVuRyyWPUPZECISld+rPijofKW/5fIJ/7+JiLhDly4f8uuvi4tcS0tLQCYrOZPo5s1jycnJYMKEXWrTf0m4e9eXo0fXFJr+R5dIs3z5IrV8MTeXlihfHl13"); result.append("dbUmJeVxaeonC/F07tydcePeJzDQj4iIMNau/QaALl2688UXU4mKiiAuLoYDB/ao70lJSWbx4tmsW7eCrl1bkpmZwcCBb9G/f1dSU1MYP34EHTo0JTo6kqioCHr16kBcXAznzp1i48ZVDBr0Nvv37wYgNzeX775bxvLlixg48C1SUpLZsWMTb7/9Blu2rKVJE3vGjXv/pSkeVG4FIDExngYNXDh+/BLx8QVERmYU+Tt+/BISicrkM3nyTFq18qp1i66//2lmzWqBQCBk+fJr7N6dyoIFJ9DSkrFwYSd++GFalfM8fPgbli3rQ7Nmb7NlSxQ7diTw8cc7CA29ybRp7ly48HOJ99Wv71lpntraLjRosJTGjXerfwsLW1kuAZubG0Vs7IFCk6Eunp7/YG//OSJR5dKFGhl1oUGDpTg6Li5cXPXo1CmdDh3i6NjxIfXrLyAl5Sy3bw/Gz2/0a6EESKXmuLqqzKhxcQeIjd1Xx7+KYG8/jfT0myQlPc7QGBv7E+bmQ8uhrB4nI+MO1tbepXwbMcTF/YyFxQiEQq1y96m6w/A8PHrQu/dUunQZQ3JyDAEBZ555z/Hj27h582969/4MZ+e2pdLl5WWzcePoZ5r+ExPj6dWrP7duhREXl1dMvixfvla9udmwYScGBob4+BwjNTWZAwd+IDU1pVCRWIehoRHdurXhgw/eoUeP3giFQlas2ICRkTFt2rjy8ccjGTx4uJq3j88xTE3N+fTTGXz88Wfo6Ogyf/5SUlKS0dc3YObMhSQnJ2FhYYVUKmXkyHHo6enz44/b+eSTqaxevYUZMyaQnp7G1q1radeuIzNnLsDS0opNm1bTpUsPgoOD6N69F76+t7lw4SxHjvz6Uqwl5XYCTEiI58svv6VZs5bFruXn5+PtPZzc3Bw8PDyZOXNhhTrh67ufc+f2c/Xq72rzkZfXEJo1exuACxd+4dy5fVy+fAiADh1G4OU1BE/PXjU2UAqFnPXrP8Dc3JGJE39Qe7YaG9sydOjXODq25Ntv"); result.append("q9apJDr6Lvv3z6FbN2/69n3smOTg0JRp035l164pHD26psR7jY2f3/FPR6cRQqFKqcvMDCA+/ndMTfuWeU94+HcIhRrI5flIpeYIBJIqGQtNzUehLgK1MiEUamJlNQZQ4u8/lpiY3ZiYvIW5+XvlbregIIWYmB8IC1tJTk4EMllTWre+/gwz4wPOn3dGqZRjbj4Ic/P30NV1JyHhD0JCviQ/Pwmp1BxtbWfk8gzy8xORyTxxcJiJvn6b5x4LV9cdSCQm5OZGERDwcY0vGrWZv55eCwwM2hMWthIjozdJS7uETOap/g5KQnz8IVJSzpGd/QAPj0PFvpG0tMuEh68iI8MPB4fZ2NpOeCkVS4FAyIABs/n11yXMm/cveXnZaGholyALwvnhh2lYWbnw3ntfltnmvn2ziYm5x8cf7yjT9J+ensa6dTswMCjuHBkWFsLs2VMAGDt2Ap07dwfgzTffJja2aHVRExNT9uw5WKwNc3NLfv75aIm8W7RoQ9euLfH3v80XX6iOMpo0aUZubi7379/lzp2bGBoa4et7mpCQ+7zzznv4+d0iISGefft2FVoeupGUlMjp08fR1ZVx795dTE3N0dTUQiqVIpPpUa+eIwB9+w7k2rXL9O//4n1Byq0ApKWl0r595xKvLV06n1u3rqOhocnmzXuQSCq26LdrN5SmTd9i9Ggj9PXNmTBhV5HrbdsOonnz3gwfro2OjgETJ/5Q4wMVHn6bhIRw2rQZWCSs5RFatRpA06ZvVSnP69ePoVDIsbVtXOL1YcOWcubMnhKvyWQmz28eEkoQCrUwMxtAdPQuwsK+KVMBkMvTiYrahrX1h4SHryl2Rvp8i1PpJV4tLUcRGDgBhSKX2NgDFVIAxGIDbG0nIRbr4+c3ivT0GyQmHsPY+O1S7wkLW4lSKVcLI5FIVQ3Nzu4zsrNDiIhYh5PTSiwthxd+O5e4fv0trlz5k2bNjmJkVPn8pzY2H2Ni0hNQ4uc3moKC5Br9Dl4H/vb2U7l5cwAZGXeIjNyM"); result.append("k9OKMulNTftjaNipDKWiJXZ2UyvVl5oOw2vffji//LKIoKALSKVaWFm5lGr6/+STnUgkmqX2PTDwHMeOraNZs7dLNf0/gp2dQ4nCX6FQ8PHHH5CRkY6TU0MWLfqmyt+3ra09vr63mTt3Gh07enLpUiD6+gYMHDiM3377CT09Pby9J/Pzz3to1MgNXV0ZBQUF6OjoMGzYqMK1eBS5ubnI5QW0bt0OV1f3QqtPLomJ8UX4GRoa8YIzAD9e48tLOGXKLLS0imuD//13Tn3OsmDBMlxcXCu5w5MV/qtbitlPC6FQ9MIyUj16YdevHyMysuRQozZtqjqvvornv/9uITs7vcQxedor9xG0tGRVuCBOBwSkpPiWmWEtMnIrhoYd0NZuWMM7FxEaGjaF1qiESrUhkRijp9cCgJCQpWWYNB+SlHQcqdQMgUCkFv6P2zEqQQC0wsFhNkplPsHBCyr9nNraTjg5rQQgImI9SUn/lvE9VX3o5+vC38SkL9raDbh3bzoikQ4SyYurpllSGN6CBSeZMeOQ+k9Hx6DEMLzVqwOYOfN3NV2/fjPJykotMwxPJBLTv/9Mfv11cREHwMfm8q3cuvUvvXtPLdH0n52dVij4sso0/T+ie4TZsxeX2J81a5bz33/nEIvFbN68B01NrSof4yNHfkVHR5dt2/bj5uZBWFgIAAMHDmP79g14eDSnb993OXr0MI6OqnoIjRs3wdf3NHv37iQ+Po7t2zeSnZ2Fl1dHpk//hODgewQE3OHwYVWRhIyMDLUMCQoKoHv3XrwMeK4ogIyMdD7++AMUCgUdO3bF23sStRV2du4YGVmTm5vJ3LlenD69uxhN06Y9MDevX2U8PTx6IBAICQ+/zZw5rbh3779iNN27l2wCbdKkW5X1Q0enMcbGKutGaOg3pShIBURErC1UFmoWCkUOeXkxAOjqVr72gYFBOwwM2pGScpaUFN8SaSIi1mJj80mFjza0tBoUKhCV8z4WCMS4uf2ISKRNZmYg9+7NLINWgo1N1ZrG"); result.append("azt/pbIApbKg8H4htraTSUz8B1vbiU/QyNU0j+558t9ntVsZvIgwvE6dRhMefpvTp39QKx+gMv3v2TO90PS/pIRv4w6hoTcAlek/NvY+I0euLtGB8cyZH5/57LduXWfZMpXCPGPGfJo1a1Et60dGRjpDhvRi27YNeHh44u7etHDjU48+fd7Fy6sDMpkeAwYMoUuXHoUWVj02bfqBb75ZRPv2TTEzM8fAwJCJE6dhaWlN587NWbx4Nr169S8c/1zWr1/Jtm0baNXKCw8PT14GPJcCMHv2ZMLCQtDXN2DDhl0IBLW3splIJGbChN1IJJpkZaWyYcMo5s71KuIwY2hohYmJXZXxtLFxVX9oUVGBzJ3rxbp1w4mLe6CmcXJqUyPP7+Cg8kGIjz9CVtbdYtfj4g6goWGJgUH7Gn83YWErkcuzEAqllTa1Pn7OWYWKTnErgFyeQVzcAaytx1a43UdZ7AwNO1eqX/XqzUVPrxVKZQF+fiNKLIbzCFZWo8nLi6/SMa7N/LOy7hMRsYaEhD/Vzn9WVqOxtByOtrYLcnkWMTE/kpnpT3LyceLjDxfeo3JMi47eQXr6jSJt5ucnERW1hby8GBIS/iQx8Z8y+/AyheFJJBr06TOdwMBzGBnZqC2gmzZ9SE5OZomm/4KCPPbtm42trRsBAWf466/STf8BAWeJjb1fZh9yc3Pw9h5Ofn4+zZu3ZurUOdW2fowYMZajR88yduwE5s9fWkSOffvtJvX/V67cWOR4u1u3nty8GUpgYIw6qZCWljbbt/9EeHga+/f/jo6OykJoZGTMp5/OYOzYCYwd+/w+IE9GEcjl8kq3U+lD2j//PMTevTsBWLFiA1ZWNtR2uLt3ZdGi06xf/wHR0XcJCrrAggUd8fTsxYgRK4qZy6oCAwbMwcjIml27ppCZmcLZs3u5cOEXunUbz6BBC9Tng9UNQ8PO6Ok1Jy3tKqGhK3B13faUEP6WevW+qNH3kZMTQUTEGsLCViGRGOPquhNtbafnatPEpFehQ9+f"); result.append("ZGTcQle3yROL8fdYWLxfoRAuuTyLiIh1RESsx9CwA05OyyvcJz29VuqxffBgcWExnBI+ZrE+5uaDcXZexc2b/apsnGs7f23tBri4rHtK4dehceMfCv+vjaXlcLVPxyO4uKzFxWVtKULUCGtr71IjAooK/7+JiPBTh+G5uXVRX6upMLyn8eab47h920ctDM+c+YHbt33Q0THk8OHlxYR/aOhNQImOjgEbN45BqVSSzNBwjAAAIABJREFUmZnCihVFqwimpcUTFPQfH320qUz+CxfO5O5df7S1ddi8eU+R1Ld1gIiICPX/o6OjcXR0rDkFID4+jilTPioUUEMYOHBYlT1YSkpssUnz2Jz24mMnGzRoxcqVt/jzz+84ePBrsrJSuXbtT27e/IfBgxcyYEDVa6odO46kadO3+emnuZw8uYOCgjyOHVuLr+9+PvlkZ41FQ9jbT+f27aHExv6Io+MSNDQsAVX4U0FBGqamA6q9D3J5Jtev9yAnJ4rMTD8EAiGNGm0qFMy6VWFsxt7+c/z8RhAaugw3t32Fcy+fyMgttGzpW65WYmJ28fDhzyQm/o1EYoKnpw+Ghp0qlZXR1XW72qHSwWEWDg7Fzd8CgbBIaFlGxu0qG/PXnX91w8OjBx4ePVAqFRw5soKAgDM0atShzHseheH16TOtSsLwnoaGhjajR68psgY9bVVQ7dQzmTbNne7dP+bdd+cCsG7d/ecaj9Onfdi6VaWQLVmyEkdHJ15VKBQKjhz5jbi4WC5e9KV163bP1d6T5YAfYeTIkYwaNYoBAwbUTDngiRPHkJiYgKWldRETSVXAwMCCGTMOlXjtvfdejtIFYrGUfv0+p0uXD/nf/77ir7/WI5fns3//F+Tn5zJ48KIq56mvb4a391Z69/6MH3+cydWrv5OWFs833/RjzpxjVXrmXxrMzAahpTWb7OxQIiLW0KDBssLd/0rs7afWSMphkUiHZs3+pqAglYsXPcnOfkBa2tVy7bTKCwuL93jwYB5xcT/j6LgELS1H"); result.append("YmP3YWzco9wOYZaWo7C0fJ9r17qRlHSc/Pz4So/Pf/+92IyOrzv/msKLDMMrCebmz95V5uZm8fBhCJGRflW0AUzmk09GoVQq6datJ6NHj3+l36lQKGT8+MmMHz+5Stp7VA54/vz5VdO/it6wY8cm/v33aJGEDK8DcnOzinn/y2TGjBy5ipUrb6rDZQ4eXEp6ekKV8IyJuUdWVmqR36ytGzFz5hE+//wwmpq6KBRyfvzx8xpaoETY2X0GQGTkZuTydDIz/UhPv6rOylZzSpg+TZr8glCoQVTU90XSrz7/c4qxs5uGUikvdHpUEh7+Hfb2FU30JKBx4z1IJCYEBHiTkxNGHepQFtq3H05s7H2Cgi4QHX23xsLwKgs9PVOcnNrQokXVHPlMm/YxMTFRGBubsG7d9roJUd0KSkWIg4PvMW+eysv7o48m0qlT6bvOqKiIWjVQ2dlpHD9esglNJZR/RyQSI5fnExJyvUp4PnhwtdTUwi1a9GXMmHWFO/Cb5Ofn1sg4WFl9iERiREFBKpGRWwgLW4mNzScVymxWVZDJPHF2XgVAQIA3WVn3qqxta+sPkUpNiYnZTVTU9+jquj+RjKj80NCwpHHjnRQUpHLnzvvq/AF1eLWRkPAnZ8/a4O8/hoCA8QQEjOfmzf74+AgICKj8rvVFheE9D9q2HUSLFn2eu51fftnLwYOqLKKrVm3BzMzitZEvL70CUFBQgLf3cLKzs3ByasjChaU7M+Xn57Njx6ZaN1iXLh0q1Q/B0tIJKytV/PuTSTqeFxcv/lbqtebNVR+dVKpVJOSnOiES6WBjM75Q8fiWhw8PYmPz4jKb2dh8grn5EOTydG7fHoxCkVM1H4ZQC1vbSSgUuQQGTijx3LlsPE70YWLSG1vbiaSk+PLgwYK6VacWID8/mZYtz+PquoNGjTbTqNFmlMp8tLTq4ey88rnaflnC8MrCsWNrGTJExMiR+ly+fIi1a4czZIiI0aMNiYoKrHB7UVERzJihWkeGDh1Jnz7vlErr73+b"); result.append("s2dPvtD3L5fL8fR0ZOHCmXz11Vy++moutrYypk59tY4syq0AfPvtl1y7dqlcCRn27duJiYlphTqSmZlcqLlmlrIDT0ehkJOTk/HCBis+PpRDh0quS56WFk9cXDCWlk44OlZdvKqv70/4+58u8VpQ0AUAvLzeq5YQTFUMc3GFx9Z2EkKhBnl5sVhYDEUqNS12nwqKKu2L6t/ibbq6fo+2thPp6TcIDKycMlJQkEpBQepTysUERCIZxsZvoaPTuIhwl8vTUSrlyOUZT7WTUiggkor87uS0Al1dd0JCviYmZk+dBH3Foa/fsohFKCrqexIT/8LVdddzO6O+DGF4z4KDQzN6957K2rVBNG7cmU8/3cOcOcfo0GEEBgYWFfy2lUyYMIq0tFRsbe1Ztqzsss6rVy9VZ9p7UUhKSmT9+p0sXLicL774EienhmhpaTF//tJXah6Xy6vu2rVLfPvtV0DZCRnS0lI5dOhnvvhiKnv3Hi53J86fP8D58yrTT0pKLJs3j6VNm4Hq1LoXL/6Gr6+qROejGHwvr8Ho6Zmxf/8c/PxOsXz5Vezs3Ll924c9e2YwevQadHWNWb9+BHl52cyZ8xempvakpj7km2/64eU1hG7dvMsMnykJ+/d/QWRkAH37zsDevglKpZLQ0Bt8//14pFItpkz5qUqd4eTyfL76qgf9+8/mzTc/wtDQCrk8n+vXj7FlyzgcHJrywQcrq2Vy5OSEI5dnUFCQhlisp/5dKjXH0nIE0dE7Soy7z8kJL1TmYlEq5WWm8S0vsrPDCsejeH9EIhnu7r9w+XIboqN3IBYb4OS0vFypiAsKUomLO0BExHpyciLQ1XXH2LgnOjoNkUgMsbEZVyS6ITHxLx4+PERBQVrhYjoOM7OBhaGDv6uFe0SEqiaCmVl/pFILhEJN3N1/4uLFFvj5jeThw/9hafl+kb4YGXXF1XUHublRxMcfUVsiLC2H4+vriEzmiYvLWrS1nYiK2oZUaoyOTmOCg+eRnHwKoFw0zxZubbCy+pDc"); result.append("3AgUilyEQm2kUhMiItYhEEhxclqOTNaM8PDvAJVviJnZIPz9R5OeXvHjL5FIhqlpb9zc9hEbu5eMjDsAaGhYo6Fhza1b72Bs/BZOTsu5e3cyaWlXn0lf3dDWdnliboYSFDQNW9vJGBp2qJL2X3QY3rPQqFF7GjVqT25uFr6++9HQ0KZfv5l4eHSvcFsbN67izJkTCIVCNm7cjUymVyJdTEwUGzeu5vDhX1i/fucLFZx6evq0bKk6gklNTWHevOksXrzyuXzijh8/zpgxY7C2tqZv376FcyubH3/8keDgYK5du8akSZO4d+8eY8eOJTExET8/P5YsWUKnTp0KZfWzaZ6EICnp2UmJ27VzJyBA9ZFpaWmXuNtUKBTk5DxOznH3bhympmbPfGgfn+c1xeUwd247unQZQ48eE/jtty/p3v1jdezsw4chzJ7dkoULT2Fr60Zycgy+vvvp3btiCWNSUmI5eHApHTqM4MaNv7hx4xgPH4aQk5OJrq4hTZu+zTvvfFGlta5VSo8SPT0zrl37g1u3/iUjI4ns7HRMTR3w8hpCnz7TKqzEPImtW4v/lpUVRFzcAaKjd5OdHYyBQTtMTPpgZTVGvdvPzAwkOHguTZr8+oRwPEZi4r9ERm5Sm+KNjLphbNytcDdd8YqASUknSEr6h4iIjcjl6YUCygszs35YWn6AVGpRZBcWEDAOUBUPMjXti4PDHHW44ssIH5+i35KHxxFycsK5e/dxBjorqzHqcsz16y/A2PhtLl9WJYBq0OBrbGwmcu6cjVopKQ9NaTAzexdHx0VcudLpibTKAtzcfiQiYgOpqeexs5uKtfUYLlxwe0IgOiOVmpGScu45TN9p+PuP4eHDX0t89jfeCMfP7wO1IvMs+vIJ2ufNya7k6tUu5OXF0rr1dYRCzQrdPW5c6dfi4oLL5Yn/IhEXF8ynnzagVasBTJ/+vwrf37hxFJ6ejuTm5iIQCEpMN69ScvLJy8sDwNm5Ef/95//SjMHUqeO5dy+Q338/VeF7DZ/S"); result.append("F/r27YudnR3r169X/7Zjxw7GjFGlbl60aBHHjh3jv/9UWWHnzJnD+vXriYyMRE9Pr9w0FbIA+Pq+vDG1EokmU6b8xLx57YiPD6Njx5FFEmeYmdVj+PBvWLt2OEuXXuKffzYycGDFQygMDCzUcbGOji3UMa/ViXbtHhe1cXfvWmNjqq3tTL1686hXb16pNDo6DYsIfwBj47cxNn5b7ZhXFTAy6lJYEnjZM2mtrT/C2vqjV9y4XPyI48kIh6edCNPTbyAWy5BKLdTCvTw0JZoDxXq4um4nIGD8UzUVlAQFTUVHx7XUPmZlBZGTE1Gtz65QZFWIviYQHr6GlJRztGx5vsLC/1l42YW/an2tj6amLvXqNavU/ZaW1sTE5LyyX+vVqxfZv383p09fq5L2hMLi1uOhQ4c+YS0rak1t2rQp6enpxMbGqoV7eWjU/KgFsLR04s03x3Ht2p9YWDQodr1z5zGYmdVjyZJueHkNQSSSUIc6vArQ1W2Cjo5LiddEIm2srceSnHyy1AiI8tA8golJH8RifZKTjxe7lpcXp05nXMyMKBBjYTEUhSIbPb3meHr+g6PjV7Ro4Yub2z4cHObg5RWEldWHdOyYUCS7Ytnf9QdlpvwtjV4o1CzG095+Om3b+mNjM5433gitEkUxK+su9+/PwcFhFnp6LV/L+SkQCLCzc8fBoelr9+xyuZypU8czYcJUnJ0bVQuPW7ducffu3VLmXxbbtm2jc+fOODk5VYqmVigAERF+mJs7Ym3diH37ZpVI07PnJHJzM7G1daMOdXiZoafniYPDLOrVm4u7e/EdrVRqioPDbN54I4yEhKNcv/4WT0YdlJfmaWhpORQK+8Rn9lEiMSnMyjcLD48jSKXmAKSlXUWhyEVT05br17vx4MFCkpL+RlPTjtTU81y50r7EWhKPd5T9cXCYhaPjEurXf3ZCrZLoFYqcYjwjItajoWGNQpHH5ctexMcffq53pFTKuXPnA3R0XKhfv6hFMS3t8jPHujbBysrllbBWVDW2"); result.append("bFlDWloq06c/tgY/fBj73O1eu3aNZcuW8eWXXxbZ/T9CfHw8S5cuxd7enp49e/LXX38VO5YvDw08Ry2AlwWpqXFcuXKYAQPm0LJlP6ZPb0KTJt1o1qzn07pqnWSpwyuBtLRrhIaqjjwSEv4sYTceT2joUgwM2mNg0E7tjFdRmqeRm6tavMRifQoKksukzc9PUPdRKFyFmdnAJ3ZGmaSnX0MuzyIrKwgdnYYoFDlkZgY8sw8PHx5Sn+lnZz+oNL1cnlmMp0KRTXr6NXJzo5/7HYWGLiUj4watWl0pVhkyJmbPa2UR0NMzrbGaJC8LoqMjWbp0ATt2HCgSEffnn4eeO3uhp6cns2apNrK9ehVP825qasrs2bM5e/Ysvr6+TJkypVI0r7wF4OrVP5g/v4M6IYZEokHDhm+wdu1wzp3bp6bLzEzh1q1/SUgIJzDwHHWow6uC9PTrpKffKLEAkb//GAwNO2FpOaLU+8tD8whJSf+iUORibFxygq8nHS6fhEKRR2zsvgoVSSrfIqvy9C5vuxWlrywyMwN58GAxGhpWhIevwd9/bOHfaC5ebEZubtRrNUd1dAzQ1NR9rZ557txpaGpqcunSeXUegAkTRnHixN9VyqdZs2Y0bdqUzMzi4fE7duzg1KlT7NlTeljxs2heaQtA8+a9i9TH1tDQYcqUn0qcoEOHfsXQoV/VSZQ6vOQQlCB4zTE27k5MzB4EAqE6zDQvL5aAgHG4uu4iNfUiWVlBqhbKQVMScnLCCQn5Eienb0hLu0x2doj6moXFUJKSTpTaR4FAhLX1OMLDV5eytxBW6tmNjLqQn59Cevq1CtGrHAaF1bLn0dFpSNeueXVT9Yl1t6SaBbUZO3YcqJZ2lSUE5cXFxfHPP/8wYsQIFAqFuhSwhYUFW7duZdSoUbRu3RpnZ+dChfzZNLVCAahDHWoTjI17oKfXHG3tBjg4zAaUiETaWFgM58qV9shknhgZ9UBb2xkTk16FOQkOYmLSh+bNTxAcPJ/MTP9n0sTG7kWh"); result.append("KDl1dEjIl+TkRNC48R5ycsLJzn5AQUEycXG/kJcXh0zWFBOTnmhq2lOv3jyUynwEAgnGxj2IjNyAru7/2TvvuCrL94+/z4HDlD0EGYIg4iAHguLelpor98AytCxLzVXOMtNSy4a/0kzN1CL3yPymGTkKQVFQUZbsLRvZHM7vjwcOHDYKgnk+r5cvD89zP8/nnPsZ93Vf93V9ri60auWERGLCw4enyM+PxtR0EqqqOrRp8yrx8T9W4VRR0cHUdCKqqrq0afMqWlr2csPHwGAAPj7O6Om5oa5ugZHRS+TkBGNkNKLG9r6+vbGxWanAaWT0EqqqBpibu5f+pgzlDddI0NLSeyqFwP7r+OOPP/Dz8yMsLIzNmzcjEonIzc3l4MGDXLlyhZs3b/LHH38QEhLC2bNnefHFF5kwYQJnzpxhyJAhbNiwgU6dOtXZZubMmairqwsmdH10AJoST6oDoMSToTodACWe5v2vjE1pTjy5DsCToTYdgGcF3t5HcHOb/Jj9/3zffwbNXEtPJJPJmjlc9Ugzs09uVv4poud7AGj22+85h+h5v/+aeQQafqGZO6CZv8CwYZ81K39ZsN3zimdyCeDy5fvs3fsXV68GkZtbiJmZPhoaEkaN6o67+0BiY1Px8gpk9eqmkQS9f/kyf+3dS9DVqxTm5qJvZoZEQ4Puo0Yx0N2d1NhYAr28mLh6tXKEUUIJJZRoAB4+fIifnx9+fn5kZwvqn5MmTaJnz/rVWPn111+5dUuQpG7Xrh0dOnSgT58+SCRK/ZfKeKYWbrKz85g6dTtDhnxE69b6eHl9SHz8Lm7e/IyLF9dhbW1Mnz5rGDBgPamp2Y3On5edzfapU/loyBD0W7fmQy8vdsXH89nNm6y7eBFja2vW9OnD+gEDyE5NVd5dDcCePXvQ19fH19f3ueRXQgklBJiYmPDiiy8yZcqUCpO+y/XyFmZlZXH79m0A1NTUeP311xk4cKBy8H/WDYDMzFx69VrF0aPXOHZsKZ99NhMrq3LJX01NNdzd"); result.append("B+Lt/Qnm5gakpTVu1cDczExW9erFtaNHWXrsGDM/+wwjKyv5fjVNTQa6u/OJtzcG5uY8SktT3l0NgKamJvr6+vLglDKEhoYyefJk+vTpQ7du3VBTU0MkEiESibh79+5/hl8JJZRQhKmpKSoqKqioqJCcnMz9+3XrSFy9elUuhaujo1NFFrclQVUVPv0Uvv0W1NRgxgz46y+wtIT//Q/efRcmToS1a0FPT9i3bBns3l01dmLjRihbzdPVhQsXYMECKFMWLjt+9Woh7uSHH8DY+BkyADw8dnL/fhweHkMZN65mkQ0rKyN27ZpPenpOo/Lv9PAg7v59hnp44DJuXI3tjKysmL9rFznp6fU+d9++fTl27FhpZcFITp8+zeHDh7l+/TqHDx+mf//+8rY6Ojq4u7uTnJxMZmYmP/74o/zfiRMnKCoqQk1NDQcHBzZu3IhMJiMuLo5Tp07h5+fH+fPn6du3b4viB5gxYwaRkZF07dpVvi0wMBBnZ2eGDx/Ov//+i7+/PzExMUyYMKHR76/m5v8vws7Ojl27dnHixAn5tiVLlnD48OHngl+JJ5ydisVIJBK6dRNkhi9dulRr+4KCAq5fv46Li4v8+JaM4mK4exdu3oTCQvD1hfBwiI2FsDBhwD5+HLZvh8xMCAmBkyeFv5cuLT+Pvj688AIMHFjmBYHgYLhyBUqzAeXHHzsmBH4fPSqc55kwAM6du8XRo0Jlo+XLx9bZftSo7lhbGzca/61z57h2VFAbG7t8eZ3tu48ahbG1db3P/88//7B9u5A/vXDhQsaOHcuUKVPo378/0dHRXLp0iSVLlgCQnZ3NTz/9xNWrV0lISODVV1+V/5swYQJLliyhVatWhISEsHbtWkpKSjhw4ADjxo2jd+/eFBUVcfHiRbp06dJi+GvCtm3bMDU1ZX6FUOnWrVvz66+/KgzUTYXm5n9acHNz48KFC8hkMjw9PfH09MTb25txtRi69UFCQgJSqRRNTc0Kz/I5du/e3aL4lWjZGDBgACKRiKio"); result.append("KKKiomps5+vri62tLaampv+J3923L7z2WvnAXoa2bSE6usJ40x08PKAa1eAaceECDBnyjBgA338v5Aq2b2+Ovb1ZvY5Zv77xovv/LM2VM2/fHjN7+3odM3n9+gZx5OfnV7tt2bJl/PLLL2zdupUePXrI95WVxqyMvXv3kpVVVhVORlFRkXxfUVERX375Jerq6sycObNF8VeHpKQk4uLiCAlRFK+RSCS8+eabTX7fNTf/04K3tze//ioIm0ybNo1p06Zx7Ngxjh8/ruD9aShyc3OJjY1V2BYUFMSFCxdaFL8SLRutW7eWC9jU5AUoKSnhn3/+eaL7pbnQrRuMH1/Vrf/PP7BvHyQllW8bOxbeeUfRA9ChA/TpIxgGreopyCiTQX7+M2IAeHkFAtC5s2W9jzE21mk0/kAvoQqaZefO9T5Gx7jxPBAbNmxARUWFd955p9Z2r7zyCqamphQXF9dy4WXymXxL4Y+NjeXjjz/GxsZGXsMa4MUXXyQ/P5++ffvi6amo8Dh69GhatxYK0Hz++eeoq6sjEon48ktB837//v2YmZkhEomYNWsWoaGh8sHG0dERNzc3+eDQ3Pwtwx1ZXMWQE4vFTJ069YnOW6ZI1tL5lWj5XgCA+/fv8/Dhwyr7AwIC0NHRwdbW9pn7bf7+gmu/Jk2cu3eFdX2A06chMVFw+QM4OQnHnjwprOtXiJuUQ0sLjIwUtw0ZIngBGmwA3Llzhw0bNmBraysPhmrbti0fffQRd+7cafTOSUnJJjMzt3RQ133qFyc7JYXczEwAdBtxUG8IgoODSUlJoXfv3grbzc3N5evvp06dqjJIVYaGhgbLly8nMzOTgwcPthj+wMBArly5UsW9t3DhQmbMmEFKSgrTp0+nd+/eXLwolKq1srLCxMQEgKVLl8qLXQwfLujYz5kzh48//hiAqVOnykthurm5YWZmxunTp7G0tGwR/C0ZZYbaqlWr2Lp1K99//z0nT55EU1OTdu3acerUKfz9/QFwdHTk77//5rfffqv2"); result.append("XB07dmTv3r0Ka/ItnV+JlgE7OzssLCyQyWRcvny5yv4rV64wsLKvvIVDVVWY/XfqJAQBdu8OtrZgYQF2djBqlBAEuHkzqKiAoyP06CF4AD76CMaMEYICy4L/MjNh8WKhXYcOwpKAuzt8+aUQC+DoCC+/DJMnw4gRsGLFYxgATk5OrFu3jv3798u37d+/n/Xr1+Pk5NTonVRYWD4z0NRUe/ozowqubrUKa4lPG6mpqVXWtiquwY8bN45PP/202mNdXV1ZvXo1P/zwAyEhIfTo0YPoiotIzcw/cuRIRo0aVeU4sVjMoUOHOHToEJaWlvj4+DBs2DBGjhxZxS3/5ptvIhKJOHTokHzbpEmTEIlE7Nu3T74tKCiI9u3bywfvlsDfErF48WLy8/PZv38/jo6OrF27luXLl/PGG2/g5ubGsGHDCA8PVxhMg4KC+OOPmouhhIWFkZWVpbAm31L5lWi5XoBbt24peBDDwsIoKCigcwM8tC3D6yYM4O+9JwQBHjkCQ4dCXBy89BJs2SIEAS5ZAunpMGgQHD4MOTkwfDj89hvMmQMJCcL5LlwQPANBQcL+1avhp5+EqP+y47duFXhWrBCCBR97CcDCwkL+2boBAW8NhaFhK8RiwcR5+DDrqV+kVoaGiEqjSbOqcT09LRgYGJCSklJrm7Nnz1a73dfXl08++YRZs2bxzjvvEB4e3uL4K6ffVcSMGTMICQnh008/RV9fn/Pnz9OzZ08Fd72trS1Dhgzhp59+QiqVAnDy5Ens7e05c+YMCaVPyZ49exSC+loKf0vBwoUL2bx5M6ampri6uhIUFERYWBgjR45ELBbz0ksvIZPJ0NWt3htXW652UVERSRUXNFsA/8W0NCYEBCD680+0vbxIqxCzUh3WPHiA6M8/sbpyhe9iY+tsXxfSLqYRMCGAP0V/4qXtRVFa7ed7sOYBf4r+5IrVFWK/i62zfV0QKht+yF9/afDnnyJCQhbXeUx8/I/8+aeIixdVCAv7gMzMa0/l3nRycsLAwIDi"); result.append("4mKuXi2v6nr58mX69ev33KtaPg4e2wComF/ZlOkWGhoSunQRDIx7957+mqlEQwPr0oj12Hv3muUi2dvbY2pqWq3rqyKuXbtGZGTkM8lf3cNbJugheH80WblyJWFhYYwbN47s7GwWLFig0H7evHnExcVx/vx5ZDIZR44c4ddff6W4uJg9e/ZQVFTEnTt35GlCLYm/pWDHjh188MEHvPnmm/IlveLiYgwMDPj000+JjIwkJSXlsV+2dYm5PG3+oYaGeDo5oSISkSuV8n1czaV8C0pK5PvnWliwwNISwycUmDEcaoiTpxMiFRHSXClx39fMX1JQIt9vMdcCywWWSAyfjF9b25F27T7EzEwIIY+L201RUW2GvoyoqG0A6Oj0wN5+M3p6vZ/OYCUW069fPwB8fHwoKCiQB+rWVyVQ8XwlzZ6Hr/h9BPe8VCrM0vfuFb5HPePOFTBlipBKWD45g+peO89EEOC0aX1KX8hRREQk19OyLWg0nfk+06YBEHX7NskREfU6piAnp9H4V61aRWFhoTxVry7Mnj27Ufu/ufh3796tkEUAYGRkxOHDh7GxscHf318heGzChAkYGRnJ13lHjx5N9+7d6d27N7t37+bUqVMNSi1rbv6Wgr59+/LZZ5+xcuVK7lUygqVSaZOLrTQ1v7pYjKOWFsYSCTtiYiiq4bn9OTERy1JPkU4j/maxuhgtRy0kxhJidsQgK6qeP/HnRNQtBX4Vncbtc4nECF3dnkiluURHf11ju4cPf0NFRVhCUVXVe+r3oouLC1paWuTn5+Pj48Ply5dxc3N7LKW/khJxs+fhK34fYeBPTYXPP4e5cyEmBn7+ueH9dOaMYMiU4d13hWDDZ9IAeOutkVhYGJYORr/U2b6oSMrKlQcV4geeBCPfegvD0iWPX1atqrO9tKiIgytXKsQP1PlJX/OeAAAgAElEQVQSqsYFraqqyvr165k1axYeHh4KLz+JRFLtTT98+HDMzMzks1qJRFKvNc/m5q8O2dnZCmvqZVBTU8PK"); result.append("ygobGxtUVVUVts+ePZvTp0/zf//3f8ydOxeA+fPnEx0dzQcffFCv9MOWwv80UfY7Kv6eMvTs2RMtLS10dHTo0aMHxsbGaGlpYWtrS2JiIra2tlhYWNCxY0cGDhyIiYmJ/N4oCxSu6Gmpbvbe3PwaKiq8YWlJXEEBR2pYptgZG8uCJgrcVNFQwfINSwriCkg6Uj1/7M5YLBc0XeCopeVbqKjoEBu7A6m0+iyhqKgt2NisbLb7VE1NjV69egFC4F9gYCBubm5NZHg2fR5+9YZJ+eerV4UgwYYiL0/x7wcPoLrVqmfCANDT08LTczFaWup4ev7Dhg1Ha5xdFxQUMX/+LubNG4a6euPoP2vp6bHY0xN1LS3+8fTk6IYNNfIXFRSwa/58hs2bh6SWdeWK6NevHytWrABg06ZN7N+/nx9++IELFy5gZWVFjx49OHDgQKnbTYd58+YxZMgQbG1tOXLkiDwS/8yZM/z222+cOXMGBwcHNm3ahFgsZvz48cyYMaNGK7m5+aFch6CyvsDChQsV1tUBfvnlF65du8YXX3xR5TweHh4UFhYyZMgQueExdepU9PT0GDBgQI1rx0+b/+7duxhX8AE+fPiQRYsWsXXrVlxdXZk1axZFRUWsWbMGMzMzYmJiuHbtGnp6enz++efyY0aPHo23t3fpzCNLno1QhsjISPr378+YMWPYsGEDQ4cOJTAwUKGNm5sb00vfXsuXL8eqgsQ1wLFjx3j06BF3796lZ8+e/PXXX8ydO5ecnBwuXrzIxYsXuX37Nq+++iqXLl0iMzOTgQMH0rZtW0aOHEnXrl3p378/tra2DB8+HCcnJ4WMkubmL8PblpZIRCK2VxMgeyk9HQdtbczr+Uw/1gD8tiUiiYjo7VX50y+lo+2gjbp50/FLJAZYWs6nqCid2NhdVfZnZv6Lioo2rVp1eyrv/ZKSkmrfs3379kVVVZXs7Gy6du2KtrZ2Fa8Q1L/SaHPm4deFIUPK0wM3bxai/M+cgX79hKWGXbugLKFq6VIh"); result.append("ZbAynJ3Bx0c4porh/ay4Ifv1c+TcuVW4u+9g/frDnD8fwIIFI3B1tcfUVI+UlGy8vO5y+LA3GzZMpWvXto3K79ivH6vOnWOHuzuH168n4Px5RixYgL2rK3qmpmSnpHDXywvvw4eZumEDbRugFHf16lWFoJa6ZqW7d++ul5rZBx98wAcffNDi+X///Xd++OEHALZu3Yq2tjbOzs4A5OTkMGfOHN577z3s7e3Jzc3F1NSU8+fPM2jQoCrn6ty5MyNGjODtt98uN+C0tJg9ezazZs1qMfxRUVGkVigYtWPHDvr168fkyZNZuHAh27ZtQyKRsHr1ar777jvU1NTo3bs3s2fPlr/gTExMGDRokHwGdOjQIX7++WfWrFkjNy5sbGxwdnbGxsaGxYsXs379epYtW8a5c+fk3N7e3gwdOrTG6xMbG0unCtOQ70uFscpQeVmjYjZI5T4aUs20p7n5y2Curs6U1q05lJjI1YwM+unry/d9FRPDKhsbEhvg1WvwUoS5Oq2ntCbxUCIZVzPQ71fOH/NVDDarbChMLGzS96y19XvExHxDdPQXWFm9g1isXsGY/Awbm6dXPjcjI4PCwkIKCgoUPJStWrWie/fu3Lhxo1rhn4yMDPm7qqSkpM4YtbI8fHt7qC6UoHIefpcugsv/33/L8/ATE4W0vilThLV7hQmkFlR2gpbl4deEF1+EwYMFT8O2baCtLWQIuLpCWprgmXj9deEc48cLx5w6JWyvDD+/Wjx/PEMYMKAj9+59wb59f3P8uA/Llx8kNTUbIyMd7OxaM316X44dW4qOTtOk+XQcMIAv7t3j73378Dl+nIPLl5OdmoqOkRGt7ezoO306S48dQ1NHByXqj1GjRlWbhlfmWWgoqksF++abb1oU/9ChQzE0NKwwC+nGokWL0NLSYvTo0bi7uwNC8OGECRPw9PSU7z948CArVqzA39+f7t27y2dL6enpzJgxg127drG6hlLUjx49ok2bNsqbrgYssbbmUGIi26Oj5QZAZF4eqUVF"); result.append("9NTV5bc6MmGeeABeYk3ioUSit0fLDYC8yDyKUovQ7alLym9Ny6+u3gYzs9nEx+8hIWE/FhbzSw3h+xQWJmNgMIjc3LAm/Q4pKSkEBARw8+ZNZDIZe/fupVOnTvTs2VM+2x8wYAB5eXkKXrTg4GBCQ0Pl2TkFBQXs27eP9u3bVxsnIBaX0K2bEHxXUx6+gwP07w8bNijm4Z88CV99JQTtvf9+mYcE1q0TDIOyPPzgYGHmvXJleR6+k5MQkFfqdK0W//sfXKuUXNG/P0ydKhgBDXVEVV4SeCYNAMGaUuftt0fy9tsjm4VfXUuLkW+/zcgKM7zmRIcOHVi3bh3+/v5s3br1qXL379+fr776ivbt23P79m0WL17M9evXlaNIDUhOTmb8+PHY29uzaNEi+SAPQgChVCrlrbfeol27duzaVe6CdXd3Z+nSpSxYsAAzMzOkUin+/v54eXmxaNGi0pnJacaOHYuWlhaDBw9m5cqVCuvpgYGB7N27F11dXdY3UKb6eYKzri599fU5+fAhEXl52GpqsiM2loVPSbRJ11kX/b76PDz5kLyIPDRtNYndEYvlwqcnGmVjs4KEhH1ERm6hTZvXEYlUiIraQtu2K54Kv7GxMUOHDq3VK2RiYlLFo9ehQwc6dOjAmDFj6sVTUiJWGISPHBH+gZCHX4bjx8u8SeXbSvW+qKg5VZaHX3E/CLn4lY8v46kvtLXhl1+EWb9UWj7rf9I4c7HykX92YW5ujouLCxMnTnzqZS8tLCzYunUr3377LcuWLaNt27b88ccf8gDApkJAQAATJ05k8eLFvPjii7i6uvLXX38BwtrfqVOnGD16NG+88QaBgYH07duXVq1a0a9fP7liXEORmprKvHnzeOONN5g5cyaOjo4KM/rr168zb948unfvTmZmJtOnT0dHR4eOHTsqqCNmZ2cTERHB9evXOXjwoFwbACAmJoZJkyYRHBxMv379GD58uFzGtn///qSmpvLll18yevRoZs+eLRfiKnNvXrhwgRs3bnD5"); result.append("8mUMDQ05duyYwm/o3Lkzc+fOZf369TXGQSghYLG1NSUyGd/ExJAjlXIhNZWJT7HAjPVia2QlMmK+iUGaIyX1QiqmE58ev5aWAyYmE8jLe0BS0mEKCuLIyvLD1HT8f/J6W1gIbvKlS4WBdelScHMTZumZmTBrFuzZA6++WvXYb78VVPrKYGgoqPTNmQPe3oIrv1s3yMgQzj1+POzcWcegLFY8JwgBiSYm8PAhtGkjtGnVCrKzy6P9u3atutRQF1SVj/uzi4SEBA4ePMjatWufOveQIUMYPXq0fB3bx8eHW7duMWLECH4qM3kbGbm5uQwdOpR33nlHPovt1asXM2fOJCEhgdDQUKKjo/n9998ZNWoUn332GYsWLSIgIIDPPvuMgQMHcufOnQYLV82ePZu8vDy8SmtCrFy5knfffZdhw4bRunVroqKiOHbsGPr6+ixfvpwRI0YwcOBA1qxZw/Tp09HQ0GD8+PHY2dkpDPojRoyQfz569CivvfYa+vr6fPzxxxw4cID8/Hy0tLTk9QTOnDnDihUrmDVrFu3bt+fff/8FBGW00aNHy5cxzMzM2LBhwxPr6Jdh9OjRbNu2DSMjI/m1FYlEdOvWDX9/f5ZWjIiqzyxXV5eFCxcyePBguXTy0+JXU1Pj66+/ZsqUKTx69EjItaqECSYmtNXQYE98PKZqasw0N0flKYrMmEwwQaOtBvF74lEzVcN8pjkilacrcmNj8z7JyceIjPyUrKwbWFsvBv6bQjtxcRARAZcuwY0bwjYdHWFwTU8Xguz+/hsCAqDiimDHjmBuDhMmCGl9ICwbFBbC/v1CsF63bkKMQUaGsGwAUKomXu3A/8orgm7/pElC2mCZ9tzNm8L2P/4QvA5du4KVFVy+LAQHXrsGO3YIrv6+fYXURHV1GDlS+G329sLnf/9VzDJQGgD/ARQ9oRrZ4+DXX39ViJj39/cnPT2dgoKCJuPMy8tDKpXK170BevToga+vL7m5uTg6OtKuXTveffddcnJyOH36NCoqKkyZ"); result.append("MoW8vDy2b9/Ojh072LJlS4N4MzIy6Nu3rwInQEREBB07dmTSpEl88cUXBAcHs3HjRrlkcps2bRg3bhybNm1i/HjF2dOJEycUqtKlpqbSu3dvZsyYQUFBARs2bEBLS0u+393dXe5dsbS0ZP78+XTv3p2EhAQWL17Mxo0bFVyo3t7ebN26lYkTJ3Lr1i1iYmKYNm3aY3lozp49y0svvUS/fv1YtmyZfLtIJKqzQFR1sLOzw9LSst5yyI3Jv2TJEm7dusWOHTuYNm0aZZESFSPGVUQiFlpZsTw0lM2RkURWuPZNhYr8IhURVgutCF0eSuTmSPpG9n3qz7eubk8MDYeSlnaR4uIM7O03/+ffo25ugjfAza18Xb8M9vZQWs9LYdvrrwt5+mUGwLlzgn5Ax45CPECpcxIVFcEbYGwszNyr8wKU6QBUtzyQkiLEI5ShYkhRabwyUJ4RIDwf5Z9rWsF6bAOgYpWtiilSSjwfqJwu16pVK6RSaa1a7E8KIyMj0tLSEIlEhIWFceDAAfksuLCwEC0tLblL3N7eXmFZ5J133mH79u34+Pg0mPeff/5BJBKRnp7OgQMH+N///lelD8RiMQYGBgr1EsaOHYu1tTV+fn5VBGtaVcoX2rhxo8IgXhn29vbYV5AE++qrrwBhGahyidSePXsqDCg1lVBtCKqr8CiTydizZ0+Dz3Xr1i2uXbtGnz59njr/rVu3OH/+PABr1qxh9bBhSGWyKtH9HhYWfBQezgwzMwwqBI9lln6PzOLiRruvZVJZleh+Cw8Lwj8Kx2yGGRKDcv7izGKF/xsLxcWZFBdnVvECpKVdxMrqXcRiNYW2wv8Z/6l3mre34AFITy/fJpEIyn1TpgjFd8pgaiqk/amoCDN+V1dBSCg1VcgkWLBAEALy8BCMAqlUCOwDKC1pUCPatoVPPxVSC8uSrUxMBDXB6pYhqjeyhXP88IPgNagJjx0DEBMTI/8cHx//xJ0fHp7EypWHEIunYmW1AH//SAAePEhiwID1jB69mVu3"); result.append("Ijh58jqtW89DVXUax4+Xv8yDg+Pp2HEJr7zyOSEhCTx4kISDwyJWrDjImjWerFnjSf/+61BTmy4/d2XscHdnh7s7nmvW4LlmDQdXrGC6RMIXkycT6e/Pql69mCIS8eu6deQ/egQI1QK3T5nCUicnAv/+m1AfH7aMG8cUkYhPXnyR1NKSr6HXrvG6sTEHli+Xb/svYcaMGXzyySfyFJymQkxMDNOnT+fAgQNyN3J90LZtWyQSyWN5KPLz81mxYgVLly5l2LBhDdLyt7e3p6SkpIqXxtXV9Zm/5m+++SY5OTmYm5uzbds2bt68yZw5c0hJScHd3b3KtorBWY1Rpvdx+MsG/zL8nZ7O/Pv3iS8oYGlICNdKK3/qq6ryWps2LCrVJCiWyfghLo4tpVLX+xMSGqUWQPrf6dyff5+C+AJCloaQeU3gV9VXpc1rbbBaJPDLimXE/RBH5BaBP2F/QqPUAsjNDSEq6nOSko4SFbWtVApYGAENDYdhaDgcS8s3ykwV4uP3EhYmRM5lZ9/iwYN1ZGZeo6gohVu3RhIT8zUxMTsIClqAl5cuubmhte6riLi4OL777js2btzIb7/9xo8//sjBgwfJzFQ0TAoKCqrEuACkp6dz8uRJ3n//fY4ePcpff/3F6dOn+fHHH7lbXYJ8Dbh8WfAECN5VYRB++FAxiK9vX8HlfvKkIBW8ZImwfWRpbPrXXwtZANWV5614/uoQFSUYDTExgsTwxo2waBH8/ntDDDqwtlb0AjSKB+DOnTucOHFCocLZnDlzePXVV5kwYcJjVwRs1641n302ExMTXdau9URXV4hm0NHRwMHBnF275qOiIqZ7d1tUVcW8/PJnmJmV58na2pri7NyO/fvfRkVFzLVrofz22/s4OJiXGinpfPfdeT76aArdutlU+x2chg5l4Jw58r8Pvf8+uiYmzNu5Ex0jI5YdP857nTujpqmJRukMTsfYGF1TU9Z89RUG5gLX8pMn2TJ2LMkREeiX1owvzM9n3MqVjF2+/D83+JuY"); result.append("mNCzZ0/eeOONJuUJDw+nV69efP755woR9PWFSCRqcL3woqIiBg8eTKdOndhbmuBbuRJgXZxmZmZoaGgobNfT06tSXbGlw9zcXJ5j3759e3R1ddm5cycpKSk8ePCAefPmER8fz9tvv82NGzfQ1tZW2Pak5cIbm18kEjHIwIBBBgbsqUZu7esOHcpflCIRHhYWeFR6c8uAgwkJLAsNpb++Pvs7dyZbKmXy7du8bmFBb11dNkdGsj8hAR9XV1x1dfk+Lo6zKSl84eCAwSADDAYZ0GlPVf4OX5fzi1RFWHhYYOGhyF+SX8KDtQ+I2BhB99+7Y/SSEcVZxdyZdgf9fvoYDjEkcHYgmvaadDnUBYmhhLQLaYQsC6Hz/s7oaDnQtu1S2ratPo6iR4+KBpOINm3m0qbN3GqM5Bi6dDmERGKMVJqDr29POnbchZZW+1r3KXg9LCzkXq4xY8ZQUlLC999/Lzf2y+Dn54efnx/Dhw9XCGg1MDDghRde4Nq1a4wfP16eBRMXF8f3339PWlqavKKg4n0FNjaCi97SUhg4k5OFtXNjYyFt79VXhTV9U1O4c0dYoz9/XqjMl5MjBPe9/LIwS//9dzh0SFij//prITPAyEhIGSwuFtICv/22Lg971W2nT9f/WYmKErQJ6kKDDQAnJyd5SeCmwNKlY/jjD39ef30n58+vYdWqX9i2bTYqKuXOijFjnJkxox9vvPE9N29uQSJR4fPPz7B69UR5u44dLdDTK19DnTv3WxwdLVixomYtdpcK67RBV69yZts2Vp45g46RkWARW1gwe+tW9i1aRJ+pU2ndrh33Ll2iXY8e8sG/7MXy1r59vNe5M8c2bmT4G29w7cgRXv+///vPDf4SiYSVK1eydOnSRqt9UBN+/vlnUlJSqi38UXlGWXmJIjQ0lMLCQiZNmtQgTh8fH3x8fKo1OOriLCkpISgoqEZOGxubZ+paJyQk8H7p4qhIJJK/A4qKikhMTCQjI0MhrqG6bS2Jf8aMGYoyb48BETDL3Bxb"); result.append("TU1m3r1LsUxGcE4O79vYMKo0R31f584kFRZyNSODnjo6JBQUcMTJCbVGKKIm1hBj97EdjwIekROUg9FLRoglYgz6G2DzgXB/dTnUhdtTbiOWCHwFCQW8cOwFtOy1Gu3e0NAoV28MCnoTff3+8gJDte2rzmCW/zaxmHbt2nHp0iVkMhkikQiZTEZ6ejpWVlb4+PhUCSKtTvTHwsKCESNGcO7cOZydnasoByYkVC8ABIrKfhVidrlypfxzWJhi9H3FdfgyVJSGqVDBut545RWhjoCmplBQqEMHIQVQJhNiE8r+Li4WihpB/VIEW1waoEgkYs+eBfj5hTNkyEe8/fZI9PW1q7T76qvXSErK5LPPThIcHE9JiYyOHS0qzLDKb+4dO/7H1atB/PTTQgVDojK09ITiFnnZ2exwd2fovHl0r5gQCgydNw8HNze+f+MNigoKuHLoEIOqkV/SMTZm3s6dnNi8mb3vvsuMzf+9IBoVFRXWrl3Lli1b5PW5NTU1m6w6pHmpkbVixQrOnz/P6tWr5S/348ePc7QsEgdBJ7xizfCPP/6YUaNGMXHixAZxlgXNbd++nbNnz/LNN9/IiyJdvXqV/6tg1MXFxSmkGu7ZswexWFxj3n3rUu/QswiZTMYvv/yi8HdlA7C6bS2Fv127dtjZ2TXa9+mrr880MzNeu3ePgEeP5IN/mZGwp1MnPo+K4oOwMDwsLBpl8K8Ix+8cidoWRV5kHrG7FGsG6Lrq0npya0LfD6UwsZCSvJJGHfwV3fg/kJ0dQIcOXzVoX3WQSqWEhYXh6OgoNwyCgoLo3LkzAwYMwMfHp97xZ506daK4uJjg4OBn5hlr00aQ/l2+XIj0ByHK39dXUCP08BCWHyr+/eGHDeNokVkA1tbGLFz4Il9//TsGBtWLKxsb6/D116/x6qvfcvduDPv3Vy/MExKSwMqVh/jyy1exs6vfC/fHxYtRUVXFfdu2ave/sXs3y5yc+GTkSObt3FljaVLXCROw69mTxLAw1LS0mqy/VFRU"); result.append("mrQkc02c33//PT4+PvKoeF1dXcaNG9dkBW9mz57NH3/8wblz50hJSeHTTz+lV69ezJgxA29vb7777jt5WysrK9566y0MDAyIjo7Gzs6OH374ocFlZO3t7dm0aRNbt25l8eLFLFmyhIMHD9KzZ0+uX7/Oe++9pzCgf/vtt2hqapKamkpRURFXr15VUCurCP0KUrPPIkJCQtDS0mpy7YfG5jcxMcHd3Z2PPvqIj2oRm2ko1traYn75MnOrUVpso67Om5aWnE1JYfPj1HetA+oW6rT7qB23X7mN7RpbVPUVX+12G+y41u0aJQUldNzZsUmux6NHdwkL+4CePa8gFmvWe19l5OTk4OfnR3JyMp07d1Yo9hMbG8vw4cORyWScO3eO27dvK2QF1YSyoNtHpbFbzwLi4+HLL4XP4eHl23NzheI+WVnCP2trxb+feQMgPDyJ4mIpLi72eHjs5M8/q89znzatLx9+eIQePWyrLfxTXCxl1qyvGTy4M/Pm1e9Bv37qFJf27+fjq1dR19aufubWrh0DZs8mJToaC0fHGs/le+IEfadN4+jHH3Ny82ZeaeR8fT09PaZPn46trS1jx47Fz8+vSaPwK+LQoUNMnTpVXvGuDJ988kmTcaqpqXH48OFqXjyPKlxzITrawcFBru//pKiupkFSNa5jLS2tKjr1tcHAwOCZeRmJxeJqjad169bJr3l1YlQ1CVTVVJWvqflNTEzYtGkTW7ZsoW3bxq0Xsic+noNduvBWUBC3e/dGr4ISY1R+Phbq6hioqvJFdDTLGpkbhMyBoDeDMJ1QNbZErCnGfJY5MqkMkWrj5/NLpTncuTMZB4dtaGuXvxPT0i6ip9e7xn3VoWItjopITk4mLS2Nv//+W34tvb2962UA5OTkAEIxs2cRZTGPjT2PbHEGQF5eIRs3Hufbbz2Ii0vjhReWsXv3xRoHcA0NSY2z348/PkZERDKnT6+s5IpKk5cXrojM5GR2zZvHhA8+oH2FamGP0tJQ19JCUiGQS6KhgaiW"); result.append("WXd8cDBhvr7M2LwZXRMT/u/VV3GdOBGrzp0bra8yMzPZuXMnO+uSlmoCTJs2jWnTpqHE48OoNLakpWPcuHGMHDmStm3bsmPHDvLz8xGLxXTt2pWcnBy0tLSYNGkSFhYWLFiwgJ07d2JqalplW5k73snJibFjx9Y7ILOx+PX19bly5QodOnTAw8NDOHk9hIjqg+PJyfTW08NVV5e/0tN5JziYn0qf9VyplJ8SElhra8swQ0N6+voyytiYTjVMMJoMTajjExy8EKk0l6KidKKjvwRkZGZ6Y27uXuu+huD27dtMnjxZ/r7Py8vjk08+ITY2Fss6pJrv37+PRCLBoWIyfQtGdbbxmDFw61aZQVzZQK7fOVq0ASCTyViyZD9r176ChoYEO7vWbNw4jaVLf2Lw4M7Y21d19ZWUyKpNKfL1DWPTphP8+utihWyB9PQczp27hYdHVYNip4cHRlZWTKoU4Oi1dy9jKrh6AWQlJchqSGXKSU/n2MaNLCjNUe47fTr/eHryzaxZfHLtWr3LBCvRcJSl+RUWFj513oZyPiuSvKdOneLUqVO1tpk1a5aCNntSUlKVbWW4c+cOkydPfur86enpOFby2Mkq14Bt6ISlpITvYmM5kZzM6dIKoIMNDBgfEIC1hgajjI1ZHhLCW6XphHqqqnTR1uaV27fZ26kToNdo1ynltxRkUhnJR5MxnaToBShIKCDLN4uSohIK4gpQt2jcd1CnTvuq2Srkxhkbj6lxHwRWGQOqi9t49OgRMplMYbKnqamJo6Mj//zzj1z1srpjk5OT+fPPPxk3blyVAMCWiLZthbLDjo6wapWQEWBkJBQrGjVKkCru1k34OzBQ2Fb2d5mB8MILwvHDhwulgCtqG7RIA+D69QesXv0LiYkZSKUlFSwbEdnZeYwZ8ylfffUaI0cKD1lxsZTff79FeHgSf/wRwIsvduOFFwS3WlGRlNmzv8HIqBU3b0Zw82ZE6Uu6iLNnb7JtW1XL89L+/fidOYPb5Mkc+egj+faHkZEk"); result.append("hYfzcgUFslAfH+5dvkxWcjIB58/TtUJ46LWjR/ll1SrsXV0pyMlBVU2NnPR0dIyMuHH6NF9MmsSMzZux6tJFOVo3Mnx9feVBeWfPnmXHjh289tprTfrQx8bGsnv3bm7fvk1RURFr167l1VdfrVeA2bPwMlKidmiKxbxnbc17FeSlx5mYKBgW/7i4yD/rqaryVzXu7caA8RhjhsmqN2jUzdXperpri+7LuLg4QkNDSUpK4v79+/Lgv6ysLE6cOIGqqiqZmZnolQZrZ2Zmkp+fT2BgIFZWVnTo0IGAgABAqMipq6tLTk4OqampzJgxo1GDPpsSUVFCymBNeO894V9NfwveEiEzoE5Pg6ypc7fqxJFmZp/crPxTRP9Nfe2GeH2UaD6Invf77wk9AE+K4ReauQOa+QsMG/ZZs/K/X1nz93l7/mXDhsmUD0Dz4QLDUfZ/M/b/hSMo8fwa4M87Jh9p5glYc1/+Zv4Ck5v59yuLASmhRANx+fJ99u79i6tXg8jNLcTMTB8NDQmjRnXH3X0gsbGpeHkFsnr1RCV/E+D+5cv8tXcvQVevUpibi76ZGRINDbqPGsVAd3dSY2MJ9PJi4urVSv4nQMe0I3YAACAASURBVEhCAke8vTlw+TLBpXLv5gYGuA8YwKTevelZ6lI/df06XoGBfHf+PIWlWTiDOndmdI8evDViBFqPGfOUEJKA9xFvLh+4THywwG9gbsAA9wH0ntQbu54C//VT1wn0CuT8d+cpLhT4Ow/qTI/RPRjx1gjUtR6TPyEEb+8jXL58gPh4QT/AwMCcAQPc6d17EnZ2gnrQ9eunCAz04vz57yguFuKAOnceRI8eoxkx4i3U1bVa7LtMaQAooUQ9kZ2dh4fHTo4d82Hp0pfx8voQKyshkj8vr5AjR7zp02cNiYkZvPvuS0r+RkZedjY7PTzwOXaMl5cu5UMvL4xKg+sK8/LwPnKENX36kJGYyEvvvqvkf0I4mJuzeuJEXnZ2pmuphPmu+fN5uVIMwzgXF8a5uKCm"); result.append("qsrW06cx1tHh/Jo1SGpIAa0vzB3Mmbh6Is4vO7O8q8A/f9d8nF9W5HcZ54LLOBdU1VQ5vfU0OsY6rDm/BhXJE/KbOzBx4mqcnV9m+XIhfmL+/F04O7+syO8yDheXcaiqqnH69FZ0dIxZs+Y8KiqSFv9OkxsAuVIpH4WHcykjg6KSEu7l5JBfGuWePXgwrVRUOJSYyO64OC6lp6MpFuOorU1eSQnqYjETTUxYbmODplhMUE4Ox5OT2RARQUFJCW01NDBVUyOpsJDuOjq8b2NDbz3F6FdprpTwj8LJuJRBSVEJOfdyKMkX+AdnD0allQqJhxKJ2x1H+qV0xJpitB21KckrQawuxmSiCTbLbRBriskJyiH5eDIRGyIoKShBo60GaqZqFCYVotNdB5v3bdDrXYlfmkt4+EdkZFyipKSInJx7lJTkC/yDs1FRaUVi4iHi4naTnn4JsVgTbW1HSkryEIvVMTGZiI3NcsRiTXJygkhOPk5ExAZKSgrQ0GiLmpophYVJ6Oh0x8bmffT0eivwK/u/efu/LmRm5uLmtprg4HiOH1/GuHEuCvs1NdVwdx/I4MFd6NNnDWlpjSs48rzz52ZmstrNjfjgYJYdP47LOEVJbzVNTQa6u9Nl8GDW9OnDo7Q0JX8jwcLQsNrPlWFWKmxlbmDwxIN/RRhWSNk2tKiZX78028vA3OCJB38FfkOLaj9X4dc3k3sJnoXBHypIAY8NCCCmoIBLzs749epFbP/+vFKpWMlMMzM2lrp95lpYcLNXL+65uTHH3Jz14eGMvnULGeCorc0qW1v6ld4QN3r1wtfVlX9cXAjJzWXAjRtcqHSDBowNoCCmAOdLzvTy60X/2P6YvqLIbzbTDLuNAr/FXAt63eyF2z03zOeYE74+nFujb4EMtB21sV1li34/gb/XjV64+rri8o8LuSG53Bhwg7QLlfgDxlJQEIOz8yV69fKjf/9YTE1fUeQ3m4mdnVCy1cJiLr163cTN7R7m5nMID1/PrVujARna2o7Y2q5C"); result.append("X7+fwN/rBq6uvri4/ENubgg3bgwgLU1x7VvZ/83b/3XBw2Mn9+/H4eExtMrgVxFWVkbs2jWf9PScRn1Qn3f+nR4exN2/z1APjyqDX0UYWVkxf9cucmrKe1LyNxgqFVLvxLUEjZbtEzdyYKm4gny7SFzzucv21dbmsfjF5caESFSz9kvZvtratEgD4HpWFhfT0lhta4t66cU2kkj4uUsXHCpJD+mrKq4aiIAl1tZ01dHBKz2d31NSamxrqa7OJnt7imQyVoWFybdnXc8i7WIatqttEasL/BIjCV1+7oKWgyJ/ZYlLRGC9xBqdrjqke6WT8ntKjW3VLdWx32SPrEhG2KoK/FnXSUu7iK3tasRiYb1IIjGiS5ef0dJSFI5QVa0s3yrC2noJOjpdSU/3IiXl9xrbqqtbYm+/CZmsiLCwVfLtyv5v3v6vC+fO3eLo0WsALF8+ts72o0Z1x9rauNEe0ued/9a5c1wrrfNQn2qa3UeNwrhCWp6SXwklajEAkksFTK5UshrVxGJmVahyVxu6lOY0R+TlNbhdYbLAn35FkV+sJshX1gfaXYTz5kXkNbhdYWGywJ9+pZLlp4a5+az68WsLef15eRENbqfs/+bt/7rw/fd/AtC+vXm1YlTVYf36xgvvfd75/yyVVzZv3x6zeuroT66hAJOSXwklKhkAvfX00FJR4Z3gYDZFRFBcITd7sqmpfFZaG0JzcwHo3KpV7e1KB56K7fR666GipULwO8FEbIpAVlzObzrZVD4rrQ25oQJ/q8618+eF5lVpp6fXGxUVLYKD3yEiYhMyWXE5v+lk+ay0Vv7cUOG8rWqX+s3Lq9pO2f/N2/91wctLUCvr3Nmy3scYGzee5vjzzh/o5SV4sBogo61jbKzkV0KJOqAKgrt5X6dOzLp7l9UPHnAwMZEt7dszxtgYx3qole2MjcU3K4vRxsYMrqXASWJhIR+EhSERiRQqYkmMJHTa14m7s+7yYPUDEg8m0n5Le4zHGKPtWDd/7M5YsnyzMB5tjMHg"); result.append("mvkLEwsJ+yAMkUSE/eYK/BIjOnXax927s3jwYDWJiQdp334LxsZjFIpX1Mgfu5OsLF+MjUdjYDC4Zv7CRMLCPkAkkmBvX14eWNn/zdv/tSElJZvMzNzSQe3pS/c+7/zZKSnkZmYCoNsMg9rzzl8ZE7ZuRV1SfYBbek5Ok/NvnbAViXr1/DnpT4F/6wQkkuonJDk56Y3Od+fOHU6cOMG+ffuIjIwEwNramrlz58pLm9e238nJqW4DAGBK69Y4aGkx7/59bmRl8bK/P8MNDdnVsSO2mlXLN3pnZLA8NJTo/HyKZTK+dXRkvkX1EZIbwsMpAcJzc3HT0+NXJyc6VFrbbj2lNVoOWtyfd5+sG1n4v+yP4XBDOu7qiKZtVf4M7wxCl4eSH52PrFiG47eOWMyvnj98QziUQG54Lnpuejj96oRWh0r8raegpeXA/fvzyMq6gb//yxgaDqdjx11oalYtWpKR4U1o6HLy86ORyYpxdPwWC4v51fOHbwBKyM0NR0/PDSenX9HSUtRpVPZ/8/Z/zUZDuTdCU1Ptqb9wn3f+4gr1FdQ0NZX8zYwTy5fTzcam2n1fnj3Lkv37m5R/+Ynl2HSrnv/sl2fZv6SJ+ZefwMamW/X8Z79k//4ljcrn5OSEk5MTgwYNYuDAgQDs37+fQYMGKbSpbX+9DACAbjo6+Li4sDc+njUPHnAhLQ1XX1+8XVywrzRguOnrs7V9+3qRrGvXDmNJ3WkROt10cPFxIX5vPA/WPCDtQhq+rr64eLugZV8pGM5Nn/Zb68ffbl07JMb14NfphouLD/Hxe3nwYA1paRfw9XXFxcUbLS3FtTd9fTfat99aP/5265BI6rbelf3fvP1fHQwNWyEWiygpkfHwYdZTf+E+7/ytDA0RicXISkrIevhQya/EcwmLCpM762oCPOvaXxPEILiGU4uKhA0iER4WFgT16cM4ExNSiopY8+BB084yEgspShX4RWIRFh4W9Anqg8k4E4pSiniwpon5CxMpKkoV+EViLCw86NMn"); result.append("CBOTcRQVpfDgwZom5Vf2f/P2f23Q0JDQpYvwQN27F6vkf8qQaGhgXVo4K/bePSW/Es8lVCroKoiriQmra3+tBkBkXh4XK+WF66uq4unkhL6qKv7Z2U364/Ii80i7qMivqq+Kk6cTqvqqZPs3MX9eJGlpFxX5VfVxcvJEVVWf7Gz/JuVX9n/z9n9dmDatDwC3b0cREZFcr2NycgoardDR887fZ9o0AKJu3yY5on7ZGwU5OUp+ZaEtJepjAADsT0ioav2LxVhpaNCumrWnhtxc9WmbsL8qv1hDjIaVBprtngJ/QtW1I7FYAw0NKzQ12zU5v7L/m7f/a8Nbb43EolSBbNWqX+psX1QkZeXKgwrr50r+x8fIt97CsNTF+cuquvUbpEVFHFy5UmH9XMmvhBK1GAC/p6SwOCSER1KpfOfPiYmE5ubyYYU6ypmlxR7Si+t+uBvSNuX3FEIWhyB9VM6f+HMiuaG52H1Yzl+cKZyrOL3uczakbUrK74SELEYqLZcwTUz8mdzcUOzsPiw/Z3Fm6f91R3w2pK2y/5u3/2uDnp4Wnp6L0dJSx9PzHzZsOFqjUVFQUMT8+buYN28Y6uqNIwf6vPNr6emx2NMTdS0t/vH05OiGDTXyFxUUsGv+fIbNm4fkMYvQKPkVUVKBS1oqT16t4VG6r7Y2jwNZSTl/ibTmc5ftq63NY/HLys9XUiKtmb90X21tWhoUggC/io7mh7g4OmlrUyiTYSKR8LezM666QvrPocREvoqOBsAzMZFfEhMBmG9hwUobG9ppapJZXMzGiAi2R0cjLb1xFgYF8aalJRMrSdtWRvRX0cT9EId2J21khTIkJhKc/3ZG11XgTzyUSNTWKOHzL4kk/pKIXm892n3YDqORRvLzRG6OJGJjBNJc4ULcn38fq3etMJ1YB3/0V8TF/YC2didkskIkEhOcnf9GV9e1dEA6RFycIMqRlHSEpKQjaGk5oKpanvMslebx6NFtRCIVuSRkSMhS2rR5DVPT2quj1dT/nbS12RQR"); result.append("wZcxMTwsteoPJyVhKJGw1NoaW01NfkpIYN2DB0Tl5zPG2BhrDQ0uZ2QAsDQkhEEGBnwSGcl2Bwfm1CAuVFP/S0wlBLoHknCg3EuQfCKZ6C+iMZlggqatJul/pxO6LJQsvyx0uunQ6oVWZFwW+EOWhmAwyIDITyJx2O6A+Rzzx+r/iIhP5PEASUmHycj4B4nEELFYndzcUIqK0jAzm46t7TqSk4+RkXEZAD+/IYhEYvr0CUMsfrxI9n79HDl3bhXu7jtYv/4w588HsGDBCFxd7TE11SMlJRsvr7scPuzNhg1T6dq1baM+qM87v2O/fqw6d44d7u4cXr+egPPnGbFgAfauruiZmpKdksJdLy+8Dx9m6oYNtO3aVcnfSIhJTVX47NyuXbXtokpVSBMzMiiWSlFtpHoAqTGpCp/bOVfPnxIl8GckZiAtlqKi2kj8qTEKn9u1c65hEiOMTRkZiUilxaiotPxaeyLZsGGP5R+9++gR/W7cILO4mGsuLvSqUFzmkVRKJ29vTnftSjed2gVBHqccfElhCdd7XSfbP5uOuzti4VE1/SxgfAB6vfSw+cCGRv8CQEDAOBwcvkBT005he3DwO8TE7MDO7mNsbesOXrvA8HpzZhUXM8jPj1vZ2Xxqb8/KSuk4Q2/eZKaZGXPbtKly7MmHD5kQEMCytm0Vsgca8vOD3g4i9ttYzGaa0eVgl6oD+JfRpF1Io+uprohUFfW4H558SMCEANoua6uYPdCAL5Caep7U1P/h4PCFwvb8/Ci8vbugoqKNm1sgEomRwn4fn248enSXgQPTUFVVzGW/cKFh9dBzcwvYt+9vjh/34f79OFJTszEy0sHOrjXTp/dl9uwB6Og0XbrWf43/CA1TDCzIzeXvffvwOX6cuPv3yU5NRcfIiNZ2dvSdPp0Bs2ejqaPTZL//v8Y/+UjN939IQgKH//2Xg1euyMsBm+nrM2/oUMb27KlQDvjPO3fYdeECRaUezPqWAz5Sy+VPCEng38P/cuXgFXk5YH0zfYbO"); result.append("G0rPsT0VygHf+fMOF3ZdQFok8Ne7HHAtXyAhIYR//z3MlSsH5eWA9fXNGDp0Hj17jlUoB3znzp9cuLALqVQIpq5vOeDJ9bz9IyMjsbW1LZ0IRWBT6d1f1/5GNwAAfk1KYtqdOwwyMMCrQonIVwMDGWtiUueM/wnGX7JvZePr4otmO0163+2NWK088jE/Kp+7s+/i/Ldz3YUhHvMLREd/gbX1ewrb0tMv4ec3GB2d7ri6+iAS1W0BNsQAAHiQl0fXa9fQEIsJ6tNHnt73Y3w8t7Kz+apD9fntj6RSzC9fxsvZmZ66uo/186WPpHh39qYgoQC3u24KdQJkUhk3+t3ghWMvoN5GvdpjL5tfxtnLGd2euo/V/4mJv2BoOBg1tYpytDJu3hxGWtpfvPDCsWq9LOHhH5GVdZ1u3X6r2v8NNACUaFw01ABQonFRmwHwVK5/c1/+Zv4CzW0APFHZoqmtWzPR1JS/09PZW2oh7ouPR1dVtV6D/5NAp7sOVu9akRuaS9SWKEXLdWkIDtsdGr0qVEWYmc1UHOCkOdy7NxeRSJXOnffVa/B/HNhparLJ3p7UoiLeCwkBIDAnh30JCVV0AUpkMsYGBDDtzh2Cc3J4y9ISiUjE0pAQXHx9ufuoYSVbVVqp0OGbDsiKZAS9FaToJtwRQ+sprRUGf1mJjICxAdyZdoec4Bws37JEJBERsjQEXxdfHt1tGL+h4ZBKg7+gApiW9hetW09VGPxTUn7D19eVmJgdGBu/hJHRCJKSPLl5cyghIUtQQgkllHje8cR1C//P0REDiYRloaFcTEtjb3w82+opUPPEg+EGOzSsNIj4JIK8B4LGfOq5VNRM1dB1blrZUjW11gp/h4WtJC8vnHbt1tKq1QtNyr3Q0pI+enocSEjg5MOHvH7vHns7dUKtUv6nDHiQm4tfdja/JCXxsKiIC2lpeGdmEpSTQ1qp9kBDYDLWBJPxJqRdTCPxZyEGpDCxkOQjyVi9Y0XlL5D7IJdsv2ySfkmi6GERaRfSyPTO"); result.append("JCcoh6K0oifq87y8SEJDV6CmZoqj4w6FfUVFqeTmhpCWdp6UlP+RlxdBWtpFHj26S05OsPLJV0IJJZ57PPE01UxNjc/bt2fuvXu87O/P7d69qwxETYWyGWnA+ACCFgbR9URXwjeE0+33bk+1E9PT/yYm5lt0dLpjY/NB01ttIhF7OnWim48Pr9y+zY+dOmFXTaqgikhEoJub/O+hN2/ydYcOLGv7ZAFajt84knYxjZD3QjAebUzo8lDsNtlVWfcXqYhwCyznvzn0Jh2+7kDbZY0RICbj/v3XkUof0bnzj1WU/szN52BuPqfUG3CWrCw/HBy207HjbuVTr4QSSijRGB4AgNfatKGTtjZ5JSUEl1ale1owGSfMSFP/l8qtF29h4WGBxEDy1Pgruv47dWo613+VQVhbm9fbtKFEJuN2PVz5R5KS+CstjfWNoCqobqmO3cd2FCYVEjAuAERgMMCg1mOSjiSR9lcaD9Y3jqpgbOx3pa7/KZiavlJr29DQFURGbpaXHVZCCSWUeJZQUiG1UiqVNnh/kxoAR5OTcdTWRkMsZkFQkEIu+1MZDL9xRKQqIj82nzZz2zxV7tDQFeTlRWBruxodna5PjfdBXh6BOTl01NZme3Q0N+tQC1QRCbNzQ0njGEdWC63Q7qRN+qV07D6xq7O9SEXglxg+OX9eXgShoStRUzPB0fH/6uYWqQAiJBJD5ZtECSUqISAqCvcdO9CePZtbFZQGS2QyfvPzw3rBAs74+RGamMjKQ4cQT52K1YIF+JdWn3uQlMSA9esZvXkzPqGhbD19GpWpU2n/7rvcrHC+P+/cQWPmTNb++itplSYt/v/zZ3Xv1Sx2XExuZvkkMic9h3Nfn2Ol80rCfMMI9Qlly7gtTBFN4ZMXPyE1VkgRDL0WyuvGr3Ng+QFSY1NJepDEIodFHFxxEM81nniu8WRd/3VMV5tOpH9klT7w9/8fq1f3ZvFiR3JzM8v5c9I5d+5rVq50JizMl+vXTzJvXmumTVPFx+e4vF18fDBL"); result.append("lnTk889fISEhhKSkByxa5MDBgyvw9FyDp+ca1q3rz/TpakRGNlzZNCYmpgJXfIP314Qnnq6G5ebyTUwM57t3Z0tUFOsePGB1WFiN0ehNAXVLdcTqYiT6EhA9vQcnPd2L2Njv0NHphq3tqqfGW1BSwtx79/ihY0cSCwsZeOMG8+7dw9fVVT7QV0aXVq0A6N5IKUoiFRGatprk3Mupl8elVReBX6f7k/LLuHdPcP136rS3XkV+WrXqglis8dS8M0oo8Syha9u2/LRwIWdv3mT81q3c+PRTTHR1EYtEjHF25rebN3m5NMvrs5kzMdHVZa2nJ7qly446Gho4mJuza/58VMRierVvT1JmJj9duoRd6/K4HUtDQ94bM4aPp06t8h26vdgNA3MDVnRfwf+3d+dxUVf748dfwzDMMCqbwgjIpoC7ueUSeutqy7eyxBQt03LLn6V1Na9paZq5Ua6ZaZnltTIrr2ZZ1yVNb6mZmIoLBsq+oyIg2zDb7w8QQYYBXJq6vp+Ph49H8Vne8znnfM7n8znn8zln5dMrmf7tdBQOChq5N6LfuH5cSL5AcI/yCcGmbZvG24+/TU5iDm46NwDKSssYOH0gj097vPKGYMZ3M/AOLR9z5HLGZXav2c3QuUOtzibYufP/4e7uzSuvdGHlyqeZPv1bFAoHGjVyp1+/cVy4kExwcPl4JA4Ojrz11mO4uV17IdnLK4iWLbsxceIGHByUnDt3mBkzvsPbO7TiWpHB7t1rGDp0bq2zCVpTdTrgq5599llGjRrFoEGDAGwur2s64JtqASitciFSOzgwPSCAto0asSotjV/z8/+nTxqTqZCYmLEVTf//QqGoeREsKro9k3f8IzaWCb6+hGi19HVzY7SPD8euXGF5xSBN1gQ7O6NxcKCVVmuX9HIOdsZB44C21c3FT01dzeXL+9DpItDpan5DU1qajMlUvRuqUaMONcZruNaCk8nIke8SFvY6paXlLyWWlRlZt24vzz77HufOZbFmzW602hGMGbOGkpIy"); result.append("8vKKCA9fzPz5W8jMvMzSpdtRKIayZMl2zBWjlq1fv49evWZy8mQyW7b8ygsvrGPVqp2sWrWT+++fR+fO0ygtNZCfX2xz/1FR523+vtTUS7c1flxcBiNGvItSOYzDh8+V34DqDfy//7eWsWPXcOZMKosXf4tO9xzx8dmV6XrwYCz33juH2NgMm/Ezz53j3ZEjeT0sDENpKVA+Be7edet479lnyc/O5vNXX2XLvHnsXLWKnatW8UJAAB9OmEDqmTNM79qVKW3bciG5/Eug/JwcZvbuzfcrVpCXnc3uNWsYodWyZswYykpKKMrLY3F4OFvmz+dKxQA3te3/9wMHePXuu9nw8rXPfQtzc/nw+efZsXIlCb/9dlvj13V8p/butfn7Lmdl1Sv+VeF3342niwtDli6t/J4fwPG6d7qmDhhAnzZtGPv++xhMJl7btIklI0eirLLevGHDcNVqmb5xY+Xfln//PbOHDKn9YqR04JF/PELswVi+mPXFtb87OKCo8mCjUCh4Yf0LFFwoYMv8LVzOuMzhzYcrL/4Avm19Ky/+AKvHrMa3jS8DXxlYe3wHJY888g9iYw/yxRezao3frdsA+vQZztq1/6/yu//t25fyxBMzcXAoH3zI17dt5cUfYPXqMfj6tmHgwFcaVN917NiR2bNnk5iYiMViwWKxkJCQwOzZsyunCra1/La2ALwYG8uEFi0IqbioODk48EHbttx79Cjjzp7ltx49/rAXAv9oV5v+W7acY7Xp32DIJTd3D40atbulcTdmZWEGnmp+7e5zcUgI3128yOz4eMI9PWtMHQzlLw62dHamxS0aHrTBLQYOCpxbOqNucePxS0oSOX++vOm/dWvrTf8ZGRtqDMCk1YbUOhxwSIg3U6c+xqBBixk58l2++moKTk6OhIf3wGg0ExLSnJCQ5jRr1oQZMz7HaDSRmnqJxx/vzpgxfy+vEKc+RmJiDseOJeBQ8enpxYtX+OabV9DpXDGZzAwePA6A06dTmTdvCz///CYajQqNRsXz"); result.append("zz9Y5/5r+31+fk1ve/z161/gzJlUTp5MplevEFQqRzw8GrNgwVM4OCho396PjRt/5pFHFnLo0HyaNm1CWFhrHnigE61b+1BcrK81vndICI9NncriQYN4d+RIpnz1FY5OTvQID8dsNOKq09F76FCCunQBYO+6dTRyc2PUihWoNBqmbtnCq3ffTWlFF5jZZKJ3RASPTp4MwIPPP0+TZs34fMYMTEYjl1JT6f744/x9zJjKMmBr/32efpr/vPMOXkFBPPziizT28KBj//74deiAb5s2tz1+Xfu39fvcmzevV/zKm3QnJ7555RXufvVVJv/rX7w3dmwtXWoKPnr+eTpMnUq/uXNZOXo0bo0a1djXugkT6Dd3Lk/36UNCTg5De/dGU0cXpE9rHyZ/OZnIRyMJ7BxI76G9ra7XpFkTnnv/OZYPW07qmVRe+PiF6ue867U6cOeqnfx+4HeWRC/BQWn7euTj05rJk78kMvJRAgM707v3UKvrjR79DlOmtGPbtrfo3TsCi8WMr2/bKnXOtYHxdu5cxe+/H2DJkujKG4Q/ixu+Oi9MTCSltJThzat/l93XzY3HPT05XVjItHPn/pCDsBgsmEvMlWPP3265uT+SlvY+TZrcRVDQzBrLzeYSYmNfsjqJzc3Yk5vL9HPnWB4aWu3vHioVrwYGUmI28/Tp0+hrGYu7hUZDI+WtK4CVY/3XM901LTQoG91ofAsxMWMwmYpo3XoVTk6eNdbIz/+Fy5f3Vg7BfJWTk2ed/f9LljxDTk4+r7zymdXlERG9uf/+jowatZqtW3+tvDhW3oQtHsmxY4l89dUv/PrrOUJCvNHpyiuBLl2CKlqE9ERELGPZsmcIDfVu0P7r+n23M75KpWTjxpeYOXMTCQnZfPjhHsaPv7/yZgNgyJBehIf3IDx8MXp99c876xP/mSVLyM/J4bNXaj4hXb04pp4+zabXXmPKV1+h0mjKm16Dghjx9tusHDECY1kZu1ev5uEXX6y2fe+ICDrefz+rR43i161ba1z8"); result.append("bO0fYPq337ItMpKj335b47fd7vj12b+t31ef+FX5eniwbdo0Pv7xRz7cu7fW9fybNWPS//0fxxMTca/oXrzeve3a8dz99zP2/feJTkqifz2eSAHuevAunln2DKtHryY5OrnW9XoM6kGr7q3IOp+Fk9b6EN+ZcZlsnL6RUctHoWulq1/8ux7kmWeWsXr1aJKTo63fgDRpxujRK9m6dT5ffTWHxx77p/X4mXFs3DidUaOWo9O14s+mwTcAe3Nz+ftvvzEzPp7YoiK+zqn+ZvW/c3KIJVFxMgAAIABJREFUrnjBY2VqKsNPn+ZoQcHtuxj/mMvZ8WexmC0Uny8mflY8V47dzulryz8/AwsGQx5Hj/YlKqpX5b9ff+3GTz95k5W1kUaN2t+SiOeLixkdE8ODx46RXVbG6rQ0qg7feKSggO0V43AfKSjg3t9+Y2tOzTfeb9XTf9HZIhLnJZL/a3k3T9yUOC5+d7HO7W7m6T8j419cvrwfhUJJSsqyamkeFdWLQ4dCiYoKQ6Op2b/n6OiKUmn73QO12pFvvnmFHTtOsGbNbqvrvP32CHbuPEHLljUrEmdnJz777EVeeuljdu48QXj43TXWmTBhLX36tOHpp/s2eP91/b7bHb9duxbMnPkETzyxhEaNNAQF1RzoKzJyOAEBnjz77HtWJ6uxFd9RreaVb77hxI4d7F6zpsZyfVERyyIieHbZMnyue7/o72PG4BUUxLwHHuCeYcNQWnnKHPH225zYuRNdLePY29q/V1AQM7Zv54Px44k/erTGtrc7fl37r+v31Sd+tQtrcDAfv/ACkz76iEOx1sfMSMjOxmgycXdwMOPef7/Wfb0REcG5zEyeDAtr0Pn+8IsP03dEX94e+DYFF61fP458fYSwJ8PITc9l26JtNbtpjSZWjlhJ+7+3p/9z/RsW/+EX6dt3BG+/PZCCAut1W1jYk3h6BhIU1BWVysropyYjK1eOoH37v9O//3N/ypbsBncB9PfwoL9H7U9TQ7y8GHKbRwGs9vTb"); result.append("zwOPfh60W9/uD4qoICws8Q/NpGCtlvXt2rG+nfVj7OHiwt6uXevcT/NbdAPQqG0jgl4PIuj1oAZtp25+4/F9fEbj4zP6hrZVKpugVDaqcz03t0bs2PEaffq8jtbK+OGrV+9i27ZpPP30Sv72t7YEBFRvhejevRWtW/vQzcpkJevW7eXEiSSOHFlUa/y69l/X77vd8f/xj0eYMmWD1ZuLq03D69e/wCOPLOTVVz+ncWNNg+I3cnPjtR07eL1PH9TXdWOtnTCB0Hvuoe+IEVa3feSll/h02jT8OnSwunzX6tVM27aNlU8/Tdu//Q3P68bCqGv/QV27MmnDBpYMGsQj//hHjTi3O35d+6/r99UV/3pPhYVxJjWVwUuX8re2bat3xZWVMX/rVlaPG0d6bi6d/vlPPty7l+f617zIXm3yd1A0/O3ssavGMv/B+awYtoLQ3tVbPTNiMzh/5DzDFw3HxdOF90a9R48neuDX/tpgZFvmbSEnMYfp306v/tCYnouHb91fBI0du4r58x9kxYphhIZa74pQqTQ41NLNvWXLPHJyEpk+/dvrWpDT8fDw/Wt3AYi/Hg9H+74F7+hhn/hKpQalsn4vH/r5NeW772Ywbdqn1f7+wQc/EB7egwce6MTUqY/x9NMrMRpNVi+C1zt9OpVXX/2cr756GWfn8qbKixevEF2lebO++6/t9/0R8RX1qMRVKiVbtvyTXbuiOXcuq97xr2rq58eM777j02nTrrU6rltH0vHjjHn33cq/ndm3D0vVri4bv+2HDz6gR3g4nR54gMemTmXl009jqjJFdr32D9z10EM8OX8+X8yaZS3hb2/8eqR9bb+vrvhXGa77fHvesGHc07o18dnXXu60WCxM2bCB1wcPRqNS0UqnY/6TTzL1k084XzE7bFVXpxI2W+qecsZsNlf7nl2pUjJ1y1TysvOqt0BeLmLL/C0MnVvePx/2VBid/68z7454F0NF99P5I+f5euHXjP9gPG7N3apte3zH8frFV6qYOnULeXnZ"); result.append("tbcHW6pvU9lqe/4IX3+9kPHjP6j2tUBR0WWOH9/x1+0CEH9dLva+AXCxT3yFwgkHB43VZYWFpezeHc2uXdFculTeddSxoz9ffjkFtdqRzMzLTJ36Cd99dww/v/JZBvv378DBg7E888yqam++HzuWSHx8Njt3nqjcl15vICJiGZ06BbBr1wlWrPiexYu/5dFHFxEU5FXn/k+dSrH5+6q6HfGvHp/JZGbr1l8B2Lz5FwyGaxeL3buj+fHH05w/X34BcHFx5j//eRVvb7c645cWFhK9ezfRu3ZVvpXu37EjU778Eke1mvTff2f9Sy/ROiyMPWvX8v2KFWyZN4/vli1DUfHkVZSXx8kffuBiSgq/HzhQ+bsuZ2byydSpHPvuO5r6lT8Zdujfn9iDB1n1zDNkx8fb3H9eVhbnDh/ml82bMVUMm33vs88yZPbs6hek2xS/zuPLyLD5++oTH6BIr+fzAwfYc+oU+8+cqXbD98mkSXSpmGQmKj6ehxYs4FBsLKYqFz0HhYIrJSUMiIxkV/S1PvNLV66wft8+ADYdPEjadV8dVHU54zIHNh7g+H+OkxaTVvn3xh6NmbF9RuVLfYf/fZjXer4GFtAX6Ssv6k2aNiHpRBLLhiwj6UQS7458l8ZNG5N4LLFyHIBPp33KrLBZePjUfPq/fDmDAwc2cvz4f0hLu/b1VuPGHsyYsb3aS31Xm/ePHv2W7OwEoqN3kZx8ssoyA+++O5LGjZuSmHischyATz+dxqxZYXh4+Pxprgk3NRvgrXCjswH+r/yAhs4GeDM2ZWVV+3rgjz78rE1ZNH+q+R+e/gZDLgUFUTRt+lDN9JfZAO1KZgO0L5kN8K8xG6DcAMgNwP+mOzz/H/jhgTs7+e/0AviAnH53dPrb+filC0AIIYT4Ezp16hRvvvkmQUFBKBQKFAoFAQEBzJ07l1OnTtW5vC4yNqoQQgjxJ3R1tL/77ruPe++9F4ANGzZw3333VVvH1nJpARBCCCH+onx9r3026O/v3+DlcgMghBBC"); result.append("/AUpq4zgam3cgbqW16ZGF0ChycSKlBQO5eXRXK1GqVDgolTSzcWFAqORoTodW3NymBIXh4XyoX9LzGYKTSYmtmjBaB8f4oqL+SQzkwWJifio1XR3cSGttJSmKhVzWrYkzM2t1h9kKjSRsiKFvEN5qJurUSgVKF2UuHRzwVhgRDdUR87WHOKmxIEF3Pq6YS4xYyo00WJiC3xG+1AcV0zmJ5kkLkhE7aPGpbsLpWmlqJqqaDmnJW5hNuKbCklJWUFe3iHU6uYoFEqUShdcXLphNBag0w0lJ2crcXFTAAtubn0xm0swmQpp0WIiPj6jKS6OIzPzExITF6BW++Di0p3S0jRUqqa0bDkHN7faR8Wyd/rbPX6hiRUrUjh0KI/mzdUolQpcXJR06+ZCQYGRoUN1bN2aw5QpcVgs0LevGyUlZgoLTUyc2ILRo32Iiyvmk08yWbAgER8fNd27u5CWVkrTpirmzGlJWJit4y9kRcoKDuUdorm6OUqFEhelC91culFgLGCobihbc7YyJW4KFiz0detLibmEQlMhE1tMZLTPaOKK4/gk8xMWJC7AR+1Dd5fupJWm0VTVlDkt5xBmI//tXf7tnf52P/8LC0lZsYK8Q4dQN2+OQqlE6eKCS7duGAsK0A0dSs7WrcRNmQIWC259+2IuKcFUWEiLiRPxGT2a4rg4Mj/5hMQFC1D7+ODSvTulaWmomjal5Zw5uNkYFc/+9Y+9y/+dnf5/tGo3AKmlpTx4/DiPNWvG9s6dK6eWzS4rY8CJEwz09MRDpWKcry8bs7IoMZvZUTGO9cLERMbExGC0WHjO15d5rVqxKCmJkd7eRAYHY7BYCI+Opv+xYxzt0aNyetqqSlNLOf7gcZo91ozO2ztXziFfll3GiQEn8BzoicpDhe84X7I2ZmEuMdNlR3n8xIWJxIyJwWK04PucL63mtSJpURLeI70JjgzGYrAQHR7Nsf7H6HG0R+X0tNXil6Zy/PiDNGv2GJ07b6+YRx7KyrI5cWIAnp4DUak88PUd"); result.append("R1bWRszmErp0KR/UITFxITExY7BYjPj6PkerVvNISlqEt/dIgoMjsVgMREeHc+xYf3r0OErjxjVH9LJ3+ts9fmopDz54nMcea8b27Z1RVuR/dnYZAwacYOBATzw8VIwb58vGjVmUlJjZUZH/CxcmMmZMDEajheee82XevFYsWpTEyJHeREYGYzBYCA+Ppn//Yxw92oMOHawdfyoPHn+Qx5o9xvbO21FW5H92WTYDTgxgoOdAPFQejPMdx8asjZSYS9hRkf8LExcyJmYMRouR53yfY16reSxKWsRI75FEBkdisBgIjw6n/7H+HO1xlA5W8t/e5d/e6W/38z81leMPPkizxx6j8/btKCqeqsqyszkxYACeAwei8vDAd9w4sjZuxFxSQpcdFef/woXEjBmDxWjE97nnaDVvHkmLFuE9ciTBkZFYDAaiw8M51r8/PY4epbGVEf3sX//Yu/zf2elvD5VtBRZg+OnTuDk68lZISLV55XVOTmzt1InCKiNFqa9rZng5IAClQsHHGRkAKABVlX2oFAom+/ujN5vZaGXEKCxwevhpHN0cCXkrpPLkB3DSOdFpaydMhdfiO6irxw94OQCFUkHGx+XxUYBCVWUKSZUC/8n+mPVmsjZaiY+F06eH4+joRkjIW5WZD+DkpKNTp62YTIVVmlmqD8UaEPAyCoWSjIyPr0asNkWwQqHC338yZrOerKyN1g7frulv9/gWGD78NG5ujrz1VkjlxQdAp3Ni69ZOFFbJf/V1+f/yywEolQo+rsh/hQJUVfJfpVIwebI/er2ZjRutHb+F4aeH4+boxlshb1VWfuXHr2Nrp60UVsl/9XX5/3LAyygVSj6uyH8FClRV8l+lUDHZfzJ6s56NVvLf3uXf3ulv9/PfYuH08OE4urkR8tZblRef8vg6Om3diqmwyvl/3bDaAS+/jEKpJOPjj6+e8CiqjNmvUKnwnzwZs15P1saNf8L6x97l/85Of7vfAPx0+TIH8vIY7eODtUEn/TQam2P8X92miY3Z"); result.append("5mytc/mny+QdyMNntA/WfoDGT4PXEBtzDFRso2yivKF1Ll/+iby8AxXjzdf8ARqNH15eQ6hr57Ynnal9HXunv93j/3SZAwfyGD3ax+qop35+GobYyP+r2zSxkf+21vnp8k8cyDvAaJ/RKKykgJ/GjyE28v/qNk1s5L+tdexd/u2d/nY//3/6ibwDB/AZPdrqsLsaPz+8bMxlf3UbZZMmN7SO/esfe5f/Ozv97X4DsKNimMZuNhKwu4tLrcsWJiVhsliY6OdndXmp2czi5GRcHR0Z4e1dY/mlHeXxm3SrPb5L99rjJy1MwmKy4DfRenxzqZnkxck4ujriPcJK/Es7KiqnbrXHd+lee/ykhVgsJvz8JlqPby4lOXkxjo6ueHvXnPDD3ulv9/gV+d/NRv53t5H/CxcmYTJZmFhL/peWmlm8OBlXV0dGjLB2/Dsqjr+bjePvbuP4F2KymJhYS/6XmktZnLwYV0dXRljJf3uXf3unv93P/4qm5CbdbJz/3W2c/wsXYjGZ8JtYy/lfWkry4sU4urribWXCH/vXP/Yu/3d2+ttL5TsAaaWlQPnc8vWVqdcTmZREQkkJerOZfd26cZ+7e7V1juTnsyAxkbNFRYRqtaxp0wZ/Tc1x2UvTyuOrPOofX5+pJykyiZKEEsx6M932dcP9vurx84/kk7ggkaKzRWhDtbRZ0waNv5X4pWkVTZUe9Y+vzyQpKZKSkgTMZj3duu3D3f2+6vHzj5CYuICiorNotaG0abMGjabmZxr2Tn+7x6/If48G5H9mpp7IyCQSEkrQ683s29eN+67L/yNH8lmwIJGzZ4sIDdWyZk0b/P2tHX9axfF7NOD4M4lMiiShJAG9Wc++bvu477r8P5J/hAWJCzhbdJZQbShr2qzB30r+27v82zv97X7+p1Wc/x4NOP8zM0mKjKQkIQGzXk+3fftwv+776/wjR0hcsICis2fRhobSZs0aNFY+07J//WPv8n9np7/dbwC0Fc2yV0ymem/srVYzIzDQ5jo9XF2Z"); result.append("GVT3tLFKbXl805X6x1d7qwmcYTu+aw9XgmbWI37FbHEm05X6x1d7Exg4w3Z81x4EBc2sc1/2Tn+7x6/I/ysNyH9vbzUz6sj/Hj1cmTmzPsevrTj+Kw04fm9m1JH/PVx7MLMe+W/v8m/v9Lf7+V8x/bDpSgPOf29vAmfUcf736EHQzJl/gfrH3uX/zk5/e6nsArjHtXy2o2MFBXb5Ia73lMcvOGan+K73lMcvOGaX+PZOf7vHr8j/Y8fsdfz3VBz/sTuy/Ns7/e1+/t9Tcf4fs1P+273+sXf5v7PT3+43AEN1Olqo1axKS8NkZe5ms8XCJmtv798iuqE61C3UpK1Kw2KqGd9itpC16TbG1w1FrW5BWtoqLJaaTyEWi5msrE23Lb6909/u8YfqaNFCzapVaZis5L/ZbGHTptt5/ENpoW7BqrRVmKzkv9liZtNtzH97l397p7/dz/+hQ1G3aEHaqlVYrLSCWcxmsjZt+h+uf+xd/u/s9Lf7DYBWqWRzp07EFhUx7NQpssvKKlfKMxp5IyGB/lX6Z/RmMyU2mostgMFisblO9SYgJZ02d6IotohTw05Rln0tvjHPSMIbCXj0vxbfrDdjKrGxbwtYDBbb61zXBNSp02aKimI5dWoYZWXX5nk3GvNISHgDD4/+VSpEPSZTCbZ+gMViqGOda+yd/naPr1WyeXMnYmOLGDbsFNlV8j8vz8gbbyTQv0r+6/VmSmzkrcUCBoPF5jrVj1/L5k6biS2KZdipYWRXyf88Yx5vJLxB/yr5rzfrKbGRtxYsGCwGm+v8mcq/vdPf7ue/VkunzZspio3l1LBhlGVXOf/z8kh44w08+lc5//V6TCU28tZiwWIw2F7nT1X/2Lv839npXxez2Vz53yYrdWpdy2tTbSCgXq6uRPfqxZsJCfQ8cgQvJycCNBqCtVqmBQTgoVKRazCwOSeHqIIC9GYzq1JTGezlhXeV7zLjiotZn5GB2WJh24UL9HR1JUKnq/ZduNVmmF6u9IruRcKbCRzpeQQn"); result.append("Lyc0ARq0wVoCpgWg8lBhyDWQszmHgqgCzHozqatS8Rrshdr7WvziuGIy1mdgMVu4sO0Crj1d0UXoqn0XbL0ZqBe9ekWTkPAmR470xMnJC40mAK02mICAaahUHhgMueTkbKagIAqzWU9q6iq8vAajVl97s7i4OI6MjPVYLGYuXNiGq2tPdLqIat+FWmPv9Ld7/F6uREf34s03E+jZ8wheXk4EBGgIDtYybVoAHh4qcnMNbN6cQ1RUAXq9mVWrUhk82AvvKvkfF1fM+vUZmM0Wtm27QM+erkRE6Kp9l279+HsR3SuaNxPepOeRnng5eRGgCSBYG8y0gGl4qDzINeSyOWczUQVR6M16VqWuYrDXYLyr5H9ccRzrM9ZjtpjZdmEbPV17EqGLqPZd9J+x/Ns7/e1+/vfqRa/oaBLefJMjPXvi5OWFJiAAbXAwAdOmofLwwJCbS87mzRRERWHW60ldtQqvwYNRV/mypTgujoz167GYzVzYtg3Xnj3RRURU+y79z1n/2Lv839npb0tqamrlf2dkZNCqVasGLa+NwnL//RZ7NkE8cIdPSP3Dn2BGdDsnwB2d/w/88MCdnfx3egF8QE6/Ozr96zj+U6dO8fXXX7N+/XqSkpIACAoKYtSoUQwaNAjA5vKOHTvWvwVACCGEEH8OV6cDnj17ts11bC23xeq0Qel6PW8nJ+O4dy/u+/fzUUYG+UZjjfX2X77Mk6dOodizB8WePUyOi+OiwQDAr/n5hEVF4b5/P4uSkihuQL+EPl1P8tvJ7HXcy373/WR8lIExv2b8rM+z+Nn3Z/Yo9hAVFkXez3mVy0qSSogeFM1e5V5iX4pFn6FvUMLo9ekkJ7/N3r2O7N/vTkbGRxiN+VbXPXkygoMHg4mOHsTJk0M4eXIIJ048yp49Cn75pT1mc8NixxUXM/3cOdQ//ohizx4eOn6cM0VFACSVlDAuJgbHvXuZFBtLgpU+rvrm362MedPbxxUzffo51OofUSj28NBDxzlzpmL7pBLGjYvB0XEv"); result.append("kybFkpBQc/vDh/Pp1SsKhWIPzZv/xOefX3thrLjYxMyZ8SgUe3jkkeMcPWr9TfP9l/fz5KknUexRoNijYHLcZC4aLlaU518JiwrDfb87i5IWUWwqtrqPiJMRBB8MZlD0IIacHMKQk0N49MSjKPYoaP9Le/T1LAs3WrbNejMZ6zMqt014M4GS+Pr1Q37+eRa+vj+jUOwhLCyKn6vETEoqYdCgaJTKvbz0UiwZVWLm5xtZsiQZH5/ybYOCDvLDD7mVab9sWQoKxR4efvh4tX3eqmMuTSnlzDNn2KPYwx7FHpKXJFerL7I+z2Jfo30canOInK05tcY36/UkzJ3Lj2o1exQKYsaMofj8+WvH+csv/NKuHftdXUleuhRzlfdkAIrOnOFHtZpjDzzAySFDKv8dDApij0JB5qefNqgeSE19j//+tyknTjxWWa+cPDmEffsa8+OPWoqL42oeg1lPRsZ6fv7Zlz17FCQkvElJSXy9Y95I+Y0rjmP6uemof1Sj2KPgoeMPcaboTMW5n8S4mHE47nVkUuwkEkoSbMY/GRHBweBgogcNqky/E48+yh6Fgl/at8esrxk///Bhonr1Yo9CwU/Nm5P1+eeVy0zFxcTPnMkehYLjjzxCwdGjdabBjdbnN5Jf9ma1BcBXreaVgADWpafTWqtlrI+P1Y3vc3fnPnd3fNRqlqek0L1JE5pV9LP0dHWlmZMT+9u04a4mDRv6UO2rJuCVANLXpaNtrcVnrPX4zYc3RxuiJap3FM6Bzrj1vTbLl3OgMx79PHDt5Urg9MAGJ4xa7UtAwCukp69Dq22Nj8/YWtfVaPzo0OFTHBw0VS5ok1EoHGnffkONcaPrEqrV8lZICL3d3HgiOho/tZr2jRoBEOjsjL9Gw/tt2jCuyhzQN5J/tzLmTW8fquWtt0Lo3duNJ56Ixs9PTfv2FdsHOuPvr+H999swbpz17Xv1cmX37i506HAYB4fyt9qv0mqVPPmkjuPHC/j++y7U9irCfe73cZ/7ffiofViespzu"); result.append("TbrTTNWsojz3pJlTM/a32c9dTe6qNR39NH582uFTNFXKwuS4yTgqHNnQfkONMdRrc6Nl20HtgM9oH/J/ySdrUxYtZ7esd7kbPrw5ISFaeveOIjDQmb5VYgYGOtOvnwe9erky/bqYrq6O/POfAQwe7EX37kdQqxX06+demfbduzdhxAhvPv20/W05Zo2/hvaftMdYYOTCNxfwfNwTR9drVZsuQkfy0mS6/tDV5kBDDmo1LefMwdHVlbgpU3Dt3RttcPC14+zdm0bt29Nu3brKz9aqKrt4kfaffIJu2LAqNycpHO7YEc+BA/EeObJB9YDZXESPHr/h7HzteC9c+IacnC2Ehi5Hqw2teQwOanx8RpOf/wtZWZto2bJhT4Y3Un5DtaG8FfIWvd1680T0E/ip/WjfqH3FuR+Iv8af99u8zzjfcXXG1/j50eHTT3GoMlhY3OTJKBwdab9hQ405AKD83YEuu3dzuEMHcHBAN3Ro5TKlVovuyScpOH6cLt9/D3W8h3Qz9fmN5Je92Zw42EmhqDHpizVvhYTQ3cWF6efPVz5pLk5OZqhO1+CLf1UKJ0WNST+u53K3C/5T/Mn+IpuCI9ee7EyFJnJ/yCXgnwE3lUAKhVOdF/BmzQZUKyyXL/+XlJSVBAZOtzl8ZF3CPT15yd+f9ZmZHM4vb304lJ9PVllZrRfSG8m/WxnzprcP9+Sll/xZvz6Tw4crtj+UT1ZWWa0X/8qy4OLImjVtSE4u5Z13Uqoti4xM4oMP2tbn/OetkLfo7tKd6eenk1/R6rM4eTFDdUNtXvwBBjQbUK3y/O/l/7IyZSXTA6fbHEr1VpdtByeHOs8da+6+24UpU/z54otsjlSJWVho4ocfcvmnjZhBQc589FE7YmOLWbasPP0vXTLw9tvJrF3b9rYfc5vVbXB0cSRuavUnrdT3UgmaFVTvUQb9XnoJlx49SHjjDYxVxsUo+O03NC1aWL34AygcHfEMD7/2B4uFmDFjUKhUtP3ggwbnRZMm3atdTAyGi5w9"); result.append("Ox43t774+b1ku2J3cGrwg8fNlt9wz3Be8n+J9ZnrOZx/uOLcP0RWWVa9Lv4AzQYMqHbxv/zf/5KyciWB06fbHArY0cWFNmvWUJqcTMo771RblhQZWZ7+9Tn5b6I+v5n8+lPeAFgz4vRphp06Ve1vKoWC9e3acdFgYNq5c/ycl0dSSQlPN29+y3/w6RGnOTWsevyWc1ui8ddwdvxZLMbydxoT5iYQNCuo2qxit4LFYuDgwVakpa2u/JuHR79rFZWpkJiY0TRu3JGgoNk3HW9hq1YEaTSMi4khXa9nQWIiS0Nr3kmuTU8n8MAB9FU+B7ndMa2VhYZsX2v8ha0ICtIwblwM6el6FixIZOlSK/FHnGbYdWXh0UebERGhY86cBJKTy4eX3b79Al26NMHPT1Ov+CqFivXt1nPRcJFp56bxc97PJJUk8XTzp62kwQiGnbr2xNevSlkoNBUyOmY0HRt3ZPYNloX6lO3Ck4X81+O/XDl+5ZaU8blzW+Lvr2H8+LMYK2LOnZvArFlBlbMErl2bTmDgAfR6c40buKeeas6cOfGcO1fMhAlnWbo0FGdnh1t6zOlr0zkQeABzlfhqHzXBi4K5+N1Fcv5d3tSvz9CT/2s+XoO86n/T7+BAuw8/pCwnh/jXXis/781mEufNo+Xcude62tau5UBgYGWztFtYWLUn1NT33iN3717avPceTjpdg/Ohar0CcPbs85hMRbRvvx6F4lp6pqev5cCBwAZ3NVpT3/K7Nn0tgQcCa3QJLGy1kCBNEONixpGuT2dB4gKWhi6t/zH3q1KXFhYSM3o0jTt2JOi6Pu7r0x6g2aOPoouIIGHOHEqTk8ufwLdvp0mXLmhqmaOkrnS3VZ+fPj2CU1XO/frm11/6BsBbrcbHSjNMh8aNmRUUxLr0dGbHxzeowm9Q07y3GrVP9fhKrZI2q9twJfoKKctTKDxZiFlvxqWHy234BUo0Gv9ax4yOi5vZXodwAAAP5UlEQVRKaWlaRVOR001H0yqVfNSuHTFFRYRF"); result.append("RbE8NBRnK0/17o6OBDg746hQ/GExaysL9d2+1vhaJR991I6YmCLCwqJYvtz6BcTbW42PT834K1e2RqVS8MILv1NcbOLDDzOYPLlh4293aNyBWUGzWJe+jtnxs2utxLzV3viorXexTI2bSlppGhvab8DpBstCfcq2g9YBTYAGZSPlLSnhWq2S1avbEB19heXLUzh5shC93kyPKjHd3R0JCHDG0bFmeXv33dY0aeJI795RDBrkRevW2lt+zI7ujjgHOKO4Lr7vBF9ce7sS+1IsxgIj5187T/DC4AanQeNOnfB/+WXS1qwh/9dfSX//fZo/9RSOLlV/gzvOAQEoHGv2pJbEx3N++nS8hgyp1iVwo7KyNpGT829CQt7G2bn6J16Oju44OwegUNzad7ptlV93R3cCnANwvC6mVqnlo3YfEVMUQ1hUGMtDl+Ps4HxD8eOmTqU0La286d+pevza0r71ypUoVCp+f+EFTMXFZHz4If6TJ99wGtiqz9Vqb9S1nPu28qs2e/fuJSAggHvuuYfIyEgiIyOZM2dO5Sd9x44do0+fPuh0OmbOnMmECRPo27cv+/fvr9xHfdaplo4NTZDFISG1LpsRGMg7KSmklJZittyerwtDFluP3/Thpuie1JHwRgK5e3Pp+EXH2xJfoXCgW7d9VpddurSL9PS1tGw5lyZNOt+ymPe6uzPA05OdFy/W+oQfodMRcQNPGTcT01ZZqM/2NuPf686AAZ7s3HmxxlNmZfxaykLz5k5ERoYwYcJZHnroOJGRwVYvVHWZETiDd1LeIaU0BbOltjRYbPXvuy7tYm36Wua2nEvnmywLdZVtbbCWnsd73tJy/vDDTXnySR1vvJHA3r25fHFdzIgIHRER1stb06Yqpk8PZOrUuAbNLdCQY9ZF6NBZia9wUNB2bVt+7forJx45QbNHm+EcdGMXoFZvvEHOv/9NzJgxNO7QgY5ffnndb4hAFxFRs5XQbObMs8+ibNyYtmvW3HRe6PWZxMZOwsOjHy1aPF9juU4X"); result.append("gU4XcUvzv67yG6GLIKKWmPe638sAzwHsvLiz3i+91qhLd+0ife1aWs6dS5PONePXlvZOzZsTEhnJ2QkTOP7QQwRHRlq9QavXb6ijPg+p5dyvK79q079/f+666y78/f2ZUWWOg4CA8m6vrl278sADD2A0GlmwYAEAr732Go8//jhpaWm4uLjUa52bagGwZUVKChNatCCptJRZ8fH80UKXhGIqNuHayxVHtz/2C0ejMY+YmLE0adKVoKDXbum+D+Xn46tW4+nkxNiYGKtD9d5qNxvzprc/lI+vrxpPTyfGjo2xOjytLePH+xIaqkWpVBAW5naD5XkFE1pMIKk0iVnxs+q9XZ4xj7ExY+napCuv3aKyYI+yvWRJKMXFJnr1csWtATEvXTJw8GAeDz/clFdeOUdamv4PPebGHRrj86wP+Ufyb+odIAdnZ1q9+SZFMTG0eL7+FXnK0qXkHTxImzVrUDVrdtP5cPbsc5jNBtq1+xhrc9Xfajdbfg/lH8JX7YunkydjY8ZaHVrYZl2al0fM2LE06dqVoNcaHt93/Hi0oaEolErcwsL+8Pr8ZvLLwUpL6VNPPXWtdUxZvZWvc+fOXLlyhawqw7TXZ51bfgPwc14e2WVlzG/VikktWvBOaipH/uCJZVRNy1/ycdD88f0tsbEvYjBcoH37Dbe0Ke5CWRmLk5J4JzSU99q0IaqggOUpKbf1WG425k1vf6GMxYuTeOedUN57rw1RUQUsX96wY1YowN1dheYGy8LPeT+TXZbN/FbzmdRiEu+kvsORgiP12vbF2Be5YLjAhvYbajSR/pXKdtOKmA1JQ4sFJk36nSVLQvjgg7aYzRaef/7sH37MqqYqFA6KOkf/q3s/TSt+Q/3eHymKiSH+9ddpPnw4Xk88cdN5kJHxERcvfk9o6FI0moA/JN9vpvxeKLvA4qTFvBP6Du+1eY+ogiiWpyxvWF364osYLlyg/YYNN/b0rlCgcnevd57dyvr8VufXyZMniY2NtbqsuLiYdevW8fe//52Q"); result.append("Wlpj61rnltQm2WVlLElOZmFFX8WC4GD81GrGnDlD2S14Ke3P7sKFb8jM/IyWLefSuHGHastKS1PIzz90Q/s1WyxMjI1lWWgoTg4OhHt6MtjLi9nx8ZwvLr4tx3KzMW96e7OFiRNjWbYsFCcnB8LDPRk82IvZs+M5f774D8nP7LJsliQvYWGrhRXleQF+aj/GnBlDmbnM5rbfXPiGzzI/Y27LuXS4riyklKZw6AbLwl/F/PmJDB2qIyjIGT8/DYsWBfPddxerjcvwv8piNHLm2WdReXjQ+t13ayxv6GQ2paUpxMW9TNOmD+Hr+1zNcpr95S0/hpspv2aLmYmxE1kWugwnByfCPcMZ7DWY2fGzOV98vn516TffkPnZZ7ScO5fGHa6rS1NSyD90+8+fG63Pb1V+HTt2jMjISObPn1/t6f/a77vAokWLCAgI4JFHHmHnzp0ornv3qz7r1HkDoLdYMFzXdDs5Lo5JVe5ICk0mnjp1iuUVFT5AY6WSpaGhnCkq4vWb6Aqw6C1YDNXjx02OI3aS9Tsic1n5zYZZf+tuOiwWPRaLocr/mzh6tC+ZmeWDelz91MPVtScBAdOu35qEhDk4O4fcUOwpcXGEe3oS5HytD3Nl69aYgFExMRir5M2mrCz6HD1arb/dWv7dypjXl4WGbm81/pQ4wsM9CarSb7tyZWtMJhg1KqbyrXSAyZPjmFRLWQAoKzPX+v5AbQpNhTx16imWhy6vfPGpsbIxS0OXcqboDK/Hv37d+TCZSbGTALhouMj4s+Pp6dqTadeVBQsW5iTMIeQGy4Ktsl0cV8zhTocpqhg46ep61587DVVWEdNaGm7alEWfPkerLfv3v3NITy9lUJU37l94oQWdOjXmxRdjG9wVYOuYszZlcbTP0VrPdXNZxfHfZG/Z1cF+rA1Ak7VpE0f79KlclrhwIQVHj9J27VpUHh41WgauHD/ekJqHmJgxgIJ27dZZedL8V2X1nZW1iaNH+1T7CsBsrl5v1UdDyu+mrE30OdqnWh//"); result.append("lLgphHuGE+QcVOXcX4kJE6NiRmG02B6MzHDxImfHj8e1Z08Cpk2r0bSUMGcOzhVPsdenvbV8q22Zzd/QgPo8Lm4ysRXnfkPyqy5du3ZlxowZzJo1i88++6zGck9PT1599VXuvvtuDh48iJOT0w2tA7W8BJiu1/NldjYJJSVcMhhYm57OMJ0OV0dHMvR6DBUXmU8yM5mbkECGXk9UQQEtKyr9AqOx8hvwxcnJlJrNTPb3r3ZRsHnjka4n+8tsShJKMFwykL42Hd0wHY6ujugz9JgNNU/6ojNFpK1NK7/T+jKbRu0aWX1JqL70+nSys7+kpCQBg+ES6elr0emG4eCgobQ0BYPhUkUhmEJZWQ4ajR8nTw6uWgQpKvodozGfdu3WN7D5OY858fHsu3yZmY6OFJtMaCv6dXZeuoQCOJiXx+MnTjAzKIgwNzdyDQZSSksxWixctJF/tzJm1bJwI9tXi/9zHnPmxLNv32VmznSkuNiEVlux/c5LKBRw8GAejz9+gpkzgwgLcyMjQ4/BSlnIySnjq6+yOXOmCJVKwYcfpjNkiBfu7ra/A/8k8xPmJswlQ59BVEEULZ1bVpTngsrvmhcnL6bUXMpk/8kEOQeRoc/AYDZUVoA5ZTn4afwYXKUsmDHze9Hv5BvzWd/AslCfsm0qNFGaWoqx0IhZbyb7q2wufn8RY4GRhDkJeD/jjXOrhr0Id+ZMEWsrYn75ZTbt2jWq9tJfbq6BlJRSjEYLGRklLFqUxEcfZTBwoCdJSSUEBpbH++mnPEpKzOTmGrj//t+YPbslw4c3v+ljNuQaKE0pLf9MsMqHIBazhewvsrnw9QUsZgvxs+LxHuWNNkTb4HTP/fFHUletKn/6XbGivE+5T58qvyGX0pQULEYjRYmJJM6fj7JRI9LXrSN93bWLgOnKFfIOHSJ02bIGNCV/TG7uXjSaAH7/fdJ1dVMmBQVH6N07puKilUtpaQoWixGzGbKzv+Lixe8xGgtISJiDt/cz9XoTvSHlN9eQS0ppCkaL"); result.append("kSN5R5gTP4d9l/cx03EmxaZitEptxbm/EwUKDuYd5PETjzMzaCZhbtb75eOmTKEsJweNnx8nBw+u2ixI0e+/Y8zPp9369TXSnipfIpXl5JD91VcUnTmDQqUi/cMP8RoyBJW7e73SvSH1uV6fgbni3G9IfjVEly5dys+HoiIaVQysdtXHH39Mx44d+fTTTxlZyyBTda0jkwHJZEDYOQHu6PyXyYDu8AIokwHd2el/3fEPHDgQPz8/VlXceJZ3HWSze/duRo4cyZtvvsl3333HkSPl7yN9/fXXjBo1iqioKEIrPr2vzzr16gIQQgghxO23a9cufvvtN/bt28eiRYuIjIxk9uzZ9O7dm379+nHs2DF27dpFXFwc33//PSaTiUGDBjF48GD69evHxx9/zOHDh+tcR1+la0RaAKQFQB5BpAVAWgCkBUBaAP4ELQBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBC"); result.append("CCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEHe6/w9fw3EBXkQ95QAAAABJRU5ErkJggg=="); return result; } ================================================ FILE: src/stim/diagram/gate_data_3d_texture_data.h ================================================ #ifndef _STIM_DIAGRAM_GATE_DATA_3d_TEXTURE_DATA_H #define _STIM_DIAGRAM_GATE_DATA_3d_TEXTURE_DATA_H #include namespace stim_draw_internal { std::string make_gate_3d_texture_data_uri(); } #endif ================================================ FILE: src/stim/diagram/gate_data_svg.cc ================================================ #include "stim/diagram/gate_data_svg.h" using namespace stim_draw_internal; std::map SvgGateData::make_gate_data_map() { std::map result; result.insert({"X", {1, "X", "", "", "white", "black", 0, 10, 0}}); result.insert({"Y", {1, "Y", "", "", "white", "black", 0, 10, 0}}); result.insert({"Z", {1, "Z", "", "", "white", "black", 0, 10, 0}}); result.insert({"H_YZ", {1, "H", "YZ", "", "white", "black", 22, 12, 0}}); result.insert({"H", {1, "H", "", "", "white", "black", 0, 10, 0}}); result.insert({"H_XY", {1, "H", "XY", "", "white", "black", 22, 12, 0}}); result.insert({"H_NXY", {1, "H", "NXY", "", "white", "black", 22, 10, 0}}); result.insert({"H_NXZ", {1, "H", "NXZ", "", "white", "black", 22, 10, 0}}); result.insert({"H_NYZ", {1, "H", "NYZ", "", "white", "black", 22, 10, 0}}); result.insert({"SQRT_X", {1, "√X", "", "", "white", "black", 24, 0}}); result.insert({"SQRT_Y", {1, "√Y", "", "", "white", "black", 24, 0}}); result.insert({"S", {1, "S", "", "", "white", "black", 0, 10, 0}}); result.insert({"SQRT_X_DAG", {1, "√X", "", "†", "white", "black", 18, 14, 0}}); result.insert({"SQRT_Y_DAG", {1, "√Y", "", "†", "white", "black", 18, 14, 0}}); result.insert({"S_DAG", {1, "S", "", "†", "white", "black", 26, 14, 0}}); result.insert({"MX", {1, "M", "X", "", "black", "white", 26, 16, 0}}); result.insert({"MY", {1, "M", "Y", "", "black", "white", 26, 16, 0}}); result.insert({"M", {1, "M", "", "", "black", "white", 0, 10, 0}}); result.insert({"RX", {1, "R", "X", "", "black", "white", 26, 16, 0}}); result.insert({"RY", {1, "R", "Y", "", "black", "white", 26, 16, 0}}); result.insert({"R", {1, "R", "", "", "black", "white", 0, 10, 0}}); result.insert({"MRX", {1, "MR", "X", "", "black", "white", 0, 14, 0}}); result.insert({"MRY", {1, "MR", "Y", "", "black", "white", 0, 14, 0}}); result.insert({"MR", {1, "MR", "", "", "black", "white", 24, 16, 0}}); result.insert({"I_ERROR", {1, "ERR", "I", "", "white", "black", 0, 10, 0}}); result.insert({"II_ERROR", {1, "ERR", "II", "", "white", "black", 10, 10, 0}}); result.insert({"X_ERROR", {1, "ERR", "X", "", "pink", "black", 0, 10, 0}}); result.insert({"Y_ERROR", {1, "ERR", "Y", "", "pink", "black", 0, 10, 0}}); result.insert({"Z_ERROR", {1, "ERR", "Z", "", "pink", "black", 0, 10, 0}}); result.insert({"E[X]", {1, "E", "X", "", "pink", "black", 0, 10, 0}}); result.insert({"E[Y]", {1, "E", "Y", "", "pink", "black", 0, 10, 0}}); result.insert({"E[Z]", {1, "E", "Z", "", "pink", "black", 0, 10, 0}}); result.insert({"ELSE_CORRELATED_ERROR[X]", {1, "EE", "X", "", "pink", "black", 0, 10, 0}}); result.insert({"ELSE_CORRELATED_ERROR[Y]", {1, "EE", "Y", "", "pink", "black", 0, 10, 0}}); result.insert({"ELSE_CORRELATED_ERROR[Z]", {1, "EE", "Z", "", "pink", "black", 0, 10, 0}}); result.insert({"MPP[X]", {1, "MPP", "X", "", "black", "white", 0, 10, 0}}); result.insert({"MPP[Y]", {1, "MPP", "Y", "", "black", "white", 0, 10, 0}}); result.insert({"MPP[Z]", {1, "MPP", "Z", "", "black", "white", 0, 10, 0}}); result.insert({"SPP[X]", {1, "SPP", "X", "", "black", "white", 0, 10, 0}}); result.insert({"SPP[Y]", {1, "SPP", "Y", "", "black", "white", 0, 10, 0}}); result.insert({"SPP[Z]", {1, "SPP", "Z", "", "black", "white", 0, 10, 0}}); result.insert({"SPP_DAG[X]", {1, "SPP†", "X", "", "black", "white", 0, 10, 0}}); result.insert({"SPP_DAG[Y]", {1, "SPP†", "Y", "", "black", "white", 0, 10, 0}}); result.insert({"SPP_DAG[Z]", {1, "SPP†", "Z", "", "black", "white", 0, 10, 0}}); result.insert({"SQRT_XX", {1, "√XX", "", "", "white", "black", 0, 10, 0}}); result.insert({"SQRT_YY", {1, "√YY", "", "", "white", "black", 0, 10, 0}}); result.insert({"SQRT_ZZ", {1, "√ZZ", "", "", "white", "black", 0, 10, 0}}); result.insert({"SQRT_XX_DAG", {1, "√XX", "", "†", "white", "black", 0, 10, 0}}); result.insert({"SQRT_YY_DAG", {1, "√YY", "", "†", "white", "black", 0, 10, 0}}); result.insert({"SQRT_ZZ_DAG", {1, "√ZZ", "", "†", "white", "black", 0, 10, 0}}); result.insert({"II", {1, "II", "", "", "white", "gray", 0, 10, 0}}); result.insert({"I", {1, "I", "", "", "white", "black", 0, 10, 0}}); result.insert({"C_XYZ", {1, "C", "XYZ", "", "white", "black", 18, 10, 0}}); result.insert({"C_NXYZ", {1, "C", "NXYZ", "", "white", "black", 18, 8, 0}}); result.insert({"C_XNYZ", {1, "C", "XNYZ", "", "white", "black", 18, 8, 0}}); result.insert({"C_XYNZ", {1, "C", "XYNZ", "", "white", "black", 18, 8, 0}}); result.insert({"C_ZYX", {1, "C", "ZYX", "", "white", "black", 18, 10, 0}}); result.insert({"C_NZYX", {1, "C", "NZYX", "", "white", "black", 18, 8, 0}}); result.insert({"C_ZNYX", {1, "C", "ZNYX", "", "white", "black", 18, 8, 0}}); result.insert({"C_ZYNX", {1, "C", "ZYNX", "", "white", "black", 18, 8, 0}}); result.insert({"DEPOLARIZE1", {1, "DEP", "1", "", "pink", "black", 0, 10, 0}}); result.insert({"DEPOLARIZE2", {1, "DEP", "2", "", "pink", "black", 0, 10, 0}}); result.insert({"PAULI_CHANNEL_1", {4, "PAULI_CHANNEL_1", "", "", "pink", "black", 0, 10, 0}}); result.insert({"PAULI_CHANNEL_2[0]", {16, "PAULI_CHANNEL_2", "0", "", "pink", "black", 0, 10, 0}}); result.insert({"PAULI_CHANNEL_2[1]", {16, "PAULI_CHANNEL_2", "1", "", "pink", "black", 0, 10, 0}}); result.insert({"MXX", {1, "M", "XX", "", "black", "white", 18, 18, -6}}); result.insert({"MYY", {1, "M", "YY", "", "black", "white", 18, 18, -6}}); result.insert({"MZZ", {1, "M", "ZZ", "", "black", "white", 18, 18, -6}}); result.insert({"MPAD", {1, "M", "PAD", "", "gray", "white", 18, 12, -6}}); result.insert({"HERALDED_ERASE", {1, "HErase", "", "", "#800000", "white", 8, 10}}); result.insert({"HERALDED_PAULI_CHANNEL_1", {4, "HERALDED_PAULI_CHANNEL_1", "", "", "#800000", "white", 14, 10}}); return result; } ================================================ FILE: src/stim/diagram/gate_data_svg.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_GATE_DATA_SVG_H #define _STIM_DIAGRAM_GATE_DATA_SVG_H #include #include #include namespace stim_draw_internal { struct SvgGateData { uint16_t span; std::string body; std::string subscript; std::string superscript; std::string fill; std::string text_color; size_t font_size; size_t sub_font_size; int32_t y_shift; static std::map make_gate_data_map(); }; } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/gltf.cc ================================================ #include "stim/diagram/gltf.h" #include using namespace stim; using namespace stim_draw_internal; void GltfScene::visit(const gltf_visit_callback &callback) { callback( id, "scenes", [&]() { return _to_json_local(); }, (uintptr_t)this); for (auto &node : nodes) { node->visit(callback); } } JsonObj GltfScene::_to_json_local() const { std::vector scene_nodes_json; for (const auto &n : nodes) { scene_nodes_json.push_back(n->id.index); } return std::map{ {"nodes", std::move(scene_nodes_json)}, }; } JsonObj GltfScene::to_json() { // Clear indices. visit([&](GltfId &item_id, const char *type, const std::function &to_json, uintptr_t abs_id) { item_id.index = UINT64_MAX; }); // Re-index. std::map counts; visit([&](GltfId &item_id, const char *type, const std::function &to_json, uintptr_t abs_id) { auto &c = counts[type]; if (item_id.index == UINT64_MAX || item_id.index == c) { item_id.index = c; c++; } else if (item_id.index > c) { throw std::invalid_argument("out of order"); } }); std::map result{ {"scene", 0}, {"asset", std::map{ {"version", "2.0"}, }}, }; for (const auto &r : counts) { result.insert({r.first, std::vector{}}); } visit([&](GltfId &item_id, const char *type, const std::function &to_json, uintptr_t abs_id) { auto &list = result.at(type).arr; if (item_id.index == list.size()) { list.push_back(to_json()); } }); return result; } void GltfSampler::visit(const gltf_visit_callback &callback) { callback( id, "samplers", [&]() { return to_json(); }, (uintptr_t)this); } JsonObj GltfSampler::to_json() const { return std::map{ {"magFilter", magFilter}, {"minFilter", minFilter}, {"wrapS", wrapS}, {"wrapT", wrapT}, }; } void GltfImage::visit(const gltf_visit_callback &callback) { callback( id, "images", [&]() { return to_json(); }, (uintptr_t)this); } JsonObj GltfImage::to_json() const { return std::map{ // Note: saving space by not including names. //{"name", id.name}, {"uri", uri}, }; } void GltfTexture::visit(const gltf_visit_callback &callback) { callback( id, "textures", [&]() { return to_json(); }, (uintptr_t)this); sampler->visit(callback); source->visit(callback); } JsonObj GltfTexture::to_json() const { return std::map{ // Note: saving space by not including names. // {"name", id.name}, {"sampler", 0}, {"source", 0}, }; } void GltfMaterial::visit(const gltf_visit_callback &callback) { callback( id, "materials", [&]() { return to_json(); }, (uintptr_t)this); if (texture) { texture->visit(callback); } } JsonObj GltfMaterial::to_json() const { JsonObj result = std::map{ // {"name", id.name}, {"pbrMetallicRoughness", std::map{ {"baseColorFactor", std::vector{ base_color_factor_rgba[0], base_color_factor_rgba[1], base_color_factor_rgba[2], base_color_factor_rgba[3], }}, {"metallicFactor", metallic_factor}, {"roughnessFactor", roughness_factor}}}, {"doubleSided", double_sided}, }; if (texture) { result.map.at("pbrMetallicRoughness") .map.insert( {"baseColorTexture", std::map{ {"index", texture->id.index}, {"texCoord", 0}, }}); } return result; } void GltfPrimitive::visit(const gltf_visit_callback &callback) { position_buffer->visit(callback); if (tex_coords_buffer) { tex_coords_buffer->visit(callback); } material->visit(callback); } JsonObj GltfPrimitive::to_json() const { std::map attributes; attributes.insert({"POSITION", position_buffer->id.index}); if (tex_coords_buffer) { attributes.insert({"TEXCOORD_0", tex_coords_buffer->id.index}); } return std::map{ // Note: validator says "name" not expected for primitives. // {"name", id.name}, {"attributes", std::move(attributes)}, {"material", material->id.index}, {"mode", element_type}, }; } std::shared_ptr GltfMesh::from_singleton_primitive(std::shared_ptr primitive) { return std::shared_ptr(new GltfMesh{ {"mesh_" + primitive->id.name}, {primitive}, }); } void GltfMesh::visit(const gltf_visit_callback &callback) { callback( id, "meshes", [&]() { return to_json(); }, (uintptr_t)this); for (auto &p : primitives) { p->visit(callback); } } JsonObj GltfMesh::to_json() const { std::vector json_primitives; for (const auto &p : primitives) { json_primitives.push_back(p->to_json()); } return std::map{ {"primitives", std::move(json_primitives)}, }; } void GltfNode::visit(const gltf_visit_callback &callback) { callback( id, "nodes", [&]() { return to_json(); }, (uintptr_t)this); if (mesh) { mesh->visit(callback); } } JsonObj GltfNode::to_json() const { return std::map{ {"mesh", mesh->id.index}, {"translation", (std::vector{translation.xyz[0], translation.xyz[1], translation.xyz[2]})}, }; } void stim_draw_internal::write_html_viewer_for_gltf_data(std::string_view gltf_data, std::ostream &out) { out << R"HTML( Download 3D Model as .GLTF File
Mouse Wheel = Zoom. Left Drag = Orbit. Right Drag = Strafe.
JavaScript Blocked?
)HTML"; } ================================================ FILE: src/stim/diagram/gltf.h ================================================ #ifndef _STIM_DRAW_3D_GLTF_H #define _STIM_DRAW_3D_GLTF_H #include #include #include "base64.h" #include "stim/diagram/coord.h" #include "stim/diagram/json_obj.h" #include "stim/mem/span_ref.h" namespace stim_draw_internal { constexpr uint64_t GL_FLOAT = 5126; constexpr uint64_t GL_ARRAY_BUFFER = 34962; constexpr uint64_t GL_TRIANGLES = 4; constexpr uint64_t GL_TRIANGLE_FAN = 6; constexpr uint64_t GL_LINES = 1; constexpr uint64_t GL_LINE_STRIP = 3; constexpr uint64_t GL_LINE_LOOP = 2; constexpr uint64_t GL_CLAMP_TO_EDGE = 33071; constexpr uint64_t GL_NEAREST = 9728; struct GltfId { std::string name; uint64_t index; GltfId(std::string name) : name(name), index(UINT64_MAX) { } GltfId() = delete; }; typedef std::function &to_json, uintptr_t abs_id)> gltf_visit_callback; /// A named data buffer. Contains packed coordinate data. template struct GltfBuffer { GltfId id; std::vector> vertices; void visit(const gltf_visit_callback &callback) { callback( id, "buffers", [&]() { return to_json_buffer(); }, (uintptr_t)this); callback( id, "bufferViews", [&]() { return to_json_buffer_view(); }, (uintptr_t)this); callback( id, "accessors", [&]() { return to_json_accessor(); }, (uintptr_t)this); } JsonObj to_json_buffer() const { std::stringstream ss; ss << "data:application/octet-stream;base64,"; size_t vertex_data_size = vertices.size() * sizeof(Coord); std::string_view vertex_data{(const char *)(const void *)vertices.data(), vertex_data_size}; write_data_as_base64_to(vertex_data, ss); return std::map{ {"name", id.name}, {"uri", ss.str()}, {"byteLength", (uint64_t)vertex_data_size}, }; } JsonObj to_json_buffer_view() const { return std::map{ {"name", id.name}, {"buffer", id.index}, {"byteOffset", 0}, {"byteLength", (uint64_t)(vertices.size() * sizeof(Coord))}, {"target", GL_ARRAY_BUFFER}, }; } JsonObj to_json_accessor() const { auto mima = Coord::min_max(vertices); std::vector min_v; std::vector max_v; for (size_t k = 0; k < DIM; k++) { // Double precision is needed here because serializing a float to // decimal then parsing it as a double can produce a slightly // different value (because when you use the shortest decimal // pattern that is uniquely closest to one float and serialize to // that, there may be a closer double that is then picked when // parsing). We need these values going through JSON land to end up // exactly the same as the buffer floats going through binary land. min_v.push_back((double)mima.first.xyz[k]); max_v.push_back((double)mima.second.xyz[k]); } return std::map{ {"name", id.name}, {"bufferView", id.index}, {"byteOffset", 0}, {"componentType", GL_FLOAT}, {"count", (uint64_t)vertices.size()}, {"type", "VEC" + std::to_string(DIM)}, {"min", std::move(min_v)}, {"max", std::move(max_v)}, }; } }; struct GltfSampler { GltfId id; uint64_t magFilter; uint64_t minFilter; uint64_t wrapS; uint64_t wrapT; void visit(const gltf_visit_callback &callback); JsonObj to_json() const; }; struct GltfImage { GltfId id; std::string uri; void visit(const gltf_visit_callback &callback); JsonObj to_json() const; }; struct GltfTexture { GltfId id; std::shared_ptr sampler; std::shared_ptr source; void visit(const gltf_visit_callback &callback); JsonObj to_json() const; }; struct GltfMaterial { GltfId id; std::array base_color_factor_rgba; float metallic_factor; float roughness_factor; bool double_sided; std::shared_ptr texture; void visit(const gltf_visit_callback &callback); JsonObj to_json() const; }; struct GltfPrimitive { GltfId id; uint64_t element_type; std::shared_ptr> position_buffer; std::shared_ptr> tex_coords_buffer; std::shared_ptr material; void visit(const gltf_visit_callback &callback); JsonObj to_json() const; }; struct GltfMesh { GltfId id; std::vector> primitives; static std::shared_ptr from_singleton_primitive(std::shared_ptr primitive); void visit(const gltf_visit_callback &callback); JsonObj to_json() const; }; struct GltfNode { GltfId id; std::shared_ptr mesh; Coord<3> translation; void visit(const gltf_visit_callback &callback); JsonObj to_json() const; }; struct GltfScene { GltfId id; std::vector> nodes; void visit(const gltf_visit_callback &callback); JsonObj _to_json_local() const; JsonObj to_json(); }; void write_html_viewer_for_gltf_data(std::string_view gltf_data, std::ostream &out); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/graph/match_graph_3d_drawer.cc ================================================ #include "stim/diagram/graph/match_graph_3d_drawer.h" using namespace stim; using namespace stim_draw_internal; Coord<3> flattened_3d(SpanRef c) { float x = 0; float y = 0; float z = 0; if (c.size() >= 1) { x = c[0]; } if (c.size() >= 2) { y = c[1]; } if (c.size() >= 3) { z = c[2]; } // Arbitrary orthographic projection. for (size_t k = 3; k < c.size(); k++) { x += c[k] / k; y += c[k] / (k * k); x += c[k] / (k * k * k); } return {x * 3, y * 3, z * 3}; } std::vector> pick_coordinates(const DetectorErrorModel &dem) { std::set det_set; uint64_t n = dem.count_detectors(); std::set> used_coords; std::vector> coords; coords.resize(n); float min_z = 0; for (uint64_t k = 0; k < n; k++) { det_set.insert(k); } auto dem_coords = dem.get_detector_coordinates(det_set); for (auto &kv : dem_coords) { if (kv.second.size() == 0) { continue; } coords[kv.first] = flattened_3d(kv.second); used_coords.insert(coords[kv.first]); min_z = std::min(min_z, coords[kv.first].xyz[2]); } float next_x = 0; float next_y = 0; float dx = 1; float dy = -1; for (size_t d = 0; d < n; d++) { auto p = dem_coords.find(d); if (p == dem_coords.end() || p->second.size() == 0) { coords[d] = {next_x * 3, next_y * 3, min_z - 1}; next_x += dx; next_y += dy; if (next_y < 0 || next_x < 0) { next_x = std::max(next_x, 0.0f); next_y = std::max(next_y, 0.0f); dx *= -1; dy *= -1; } } } if (next_x || next_y) { std::cerr << "Warning: not all detectors had coordinates. Placed them arbitrarily.\n"; } return coords; } Basic3dDiagram stim_draw_internal::dem_match_graph_to_basic_3d_diagram(const stim::DetectorErrorModel &dem) { Basic3dDiagram out; auto coords = pick_coordinates(dem); auto minmax = Coord<3>::min_max(coords); auto center = (minmax.first + minmax.second) * 0.5; std::set boundary_observable_detectors; std::vector> det_coords; auto handle_contiguous_targets = [&](SpanRef targets) { bool has_observables = false; det_coords.clear(); for (const auto &t : targets) { has_observables |= t.is_observable_id(); if (t.is_relative_detector_id()) { det_coords.push_back(coords[t.raw_id()]); } } if (det_coords.empty()) { return; } if (det_coords.size() == 1) { auto d = det_coords[0] - center; auto r = d.norm(); if (r < 1e-4) { d = {1, 0, 0}; } else { d /= r; } auto a = det_coords[0]; det_coords.push_back(a + d * 10); if (has_observables) { for (auto t : targets) { if (t.is_relative_detector_id()) { boundary_observable_detectors.insert(t.val()); } } } } if (det_coords.size() == 2) { if (has_observables) { out.red_line_data.push_back(det_coords[0]); out.red_line_data.push_back(det_coords[1]); } else { out.line_data.push_back(det_coords[0]); out.line_data.push_back(det_coords[1]); } } else { Coord<3> c{0, 0, 0}; for (const auto &e : det_coords) { c += e; } c /= det_coords.size(); for (const auto &e : det_coords) { if (has_observables) { out.purple_line_data.push_back(c); out.purple_line_data.push_back(e); } else { out.blue_line_data.push_back(c); out.blue_line_data.push_back(e); } } } }; dem.iter_flatten_error_instructions([&](const DemInstruction &op) { if (op.type != DemInstructionType::DEM_ERROR) { return; } auto *p = op.target_data.ptr_start; size_t start = 0; for (size_t k = 0; k < op.target_data.size(); k++) { if (op.target_data[k].is_separator()) { handle_contiguous_targets({p + start, p + k}); start = k + 1; } } handle_contiguous_targets({p + start, op.target_data.ptr_end}); }); for (size_t k = 0; k < coords.size(); k++) { bool excited = boundary_observable_detectors.find(k) != boundary_observable_detectors.end(); out.elements.push_back({excited ? "EXCITED_DETECTOR" : "DETECTOR", coords[k]}); } return out; } ================================================ FILE: src/stim/diagram/graph/match_graph_3d_drawer.h ================================================ #ifndef _STIM_DIAGRAM_GRAPH_MATCH_GRAPH_DRAWER_H #define _STIM_DIAGRAM_GRAPH_MATCH_GRAPH_DRAWER_H #include #include "stim/dem/detector_error_model.h" #include "stim/diagram/basic_3d_diagram.h" #include "stim/diagram/circuit_timeline_helper.h" namespace stim_draw_internal { Basic3dDiagram dem_match_graph_to_basic_3d_diagram(const stim::DetectorErrorModel &dem); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/graph/match_graph_3d_drawer.test.cc ================================================ // 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. #include "stim/diagram/graph/match_graph_3d_drawer.h" #include #include "gtest/gtest.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/simulators/error_analyzer.h" #include "stim/util_bot/test_util.test.h" using namespace stim; using namespace stim_draw_internal; void expect_graph_diagram_is_identical_to_saved_file(const DetectorErrorModel &dem, std::string_view key) { auto diagram = dem_match_graph_to_basic_3d_diagram(dem); std::stringstream actual_ss; diagram.to_gltf_scene().to_json().write(actual_ss); auto actual = actual_ss.str(); expect_string_is_identical_to_saved_file(actual_ss.str(), key); } TEST(match_graph_drawer_3d, repetition_code) { CircuitGenParameters params(10, 7, "memory"); params.after_clifford_depolarization = 0.001; auto circuit = generate_rep_code_circuit(params).circuit; auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, false, false, false); expect_graph_diagram_is_identical_to_saved_file(dem, "match_graph_repetition_code.gltf"); } TEST(match_graph_drawer_3d, surface_code) { CircuitGenParameters params(10, 3, "unrotated_memory_z"); params.after_clifford_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, false, false, false); expect_graph_diagram_is_identical_to_saved_file(dem, "match_graph_surface_code.gltf"); } TEST(match_graph_drawer_3d, missing_coordinates) { Circuit circuit(R"CIRCUIT( R 0 1 2 3 4 5 6 7 8 9 10 X_ERROR(0.125) 0 1 2 3 4 5 6 7 8 9 10 M 0 1 2 3 4 5 6 7 8 9 10 DETECTOR rec[-1] rec[-2] DETECTOR rec[-2] rec[-3] DETECTOR rec[-3] rec[-4] DETECTOR rec[-4] rec[-5] DETECTOR rec[-5] rec[-6] DETECTOR rec[-6] rec[-7] DETECTOR rec[-7] rec[-8] DETECTOR rec[-8] rec[-9] DETECTOR rec[-9] rec[-10] OBSERVABLE_INCLUDE(1) rec[-1] )CIRCUIT"); auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, false, false, false); testing::internal::CaptureStderr(); auto diagram = dem_match_graph_to_basic_3d_diagram(dem); std::string err = testing::internal::GetCapturedStderr(); ASSERT_NE(err, ""); std::stringstream ss; diagram.to_gltf_scene().to_json().write(ss); expect_string_is_identical_to_saved_file(ss.str(), "match_graph_no_coords.gltf"); } TEST(match_graph_drawer_3d, works_on_empty) { auto diagram = dem_match_graph_to_basic_3d_diagram(DetectorErrorModel()); std::stringstream ss; diagram.to_gltf_scene().to_json().write(ss); expect_string_is_identical_to_saved_file(ss.str(), "empty_match_graph.gltf"); } ================================================ FILE: src/stim/diagram/graph/match_graph_svg_drawer.cc ================================================ #include "stim/diagram/graph/match_graph_svg_drawer.h" #include "stim/diagram/graph/match_graph_3d_drawer.h" using namespace stim; using namespace stim_draw_internal; Coord<2> project(Coord<3> coord) { return {coord.xyz[0] * 5.0f + coord.xyz[2] * 0.2f, coord.xyz[1] * 5.0f + coord.xyz[2] * 0.1f}; } template inline void write_key_val(std::ostream &out, const char *key, const T &val) { out << ' ' << key << "=\"" << val << "\""; } void stim_draw_internal::dem_match_graph_to_svg_diagram_write_to( const stim::DetectorErrorModel &dem, std::ostream &svg_out) { auto diagram = dem_match_graph_to_basic_3d_diagram(dem); std::vector> used_coords; for (const auto &e : diagram.line_data) { used_coords.push_back(project(e)); } for (const auto &e : diagram.red_line_data) { used_coords.push_back(project(e)); } for (const auto &e : diagram.blue_line_data) { used_coords.push_back(project(e)); } for (const auto &e : diagram.purple_line_data) { used_coords.push_back(project(e)); } for (const auto &e : diagram.elements) { used_coords.push_back(project(e.center)); } auto minmax = Coord<2>::min_max(used_coords); minmax.first.xyz[0] -= 5; minmax.first.xyz[1] -= 5; minmax.second.xyz[0] += 5; minmax.second.xyz[1] += 5; auto off = minmax.first * -1.0f; svg_out << R"SVG(\n"; auto write_lines = [&](const std::vector> &line_data, const char *color) { if (line_data.empty()) { return; } svg_out << "\n"; }; write_lines(diagram.line_data, "black"); write_lines(diagram.red_line_data, "red"); write_lines(diagram.blue_line_data, "blue"); write_lines(diagram.purple_line_data, "purple"); for (const auto &e : diagram.elements) { auto c = project(e.center); c += off; svg_out << "\n"; } svg_out << ""; } ================================================ FILE: src/stim/diagram/graph/match_graph_svg_drawer.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_GRAPH_MATCH_GRAPH_SVG_DRAWER_H #define _STIM_DIAGRAM_GRAPH_MATCH_GRAPH_SVG_DRAWER_H #include #include "stim/dem/detector_error_model.h" #include "stim/diagram/basic_3d_diagram.h" #include "stim/diagram/circuit_timeline_helper.h" namespace stim_draw_internal { void dem_match_graph_to_svg_diagram_write_to(const stim::DetectorErrorModel &dem, std::ostream &svg_out); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/graph/match_graph_svg_drawer.test.cc ================================================ // 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. #include "stim/diagram/graph/match_graph_svg_drawer.h" #include #include "gtest/gtest.h" #include "match_graph_3d_drawer.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/simulators/error_analyzer.h" #include "stim/util_bot/test_util.test.h" using namespace stim; using namespace stim_draw_internal; void expect_graph_svg_diagram_is_identical_to_saved_file(const DetectorErrorModel &dem, std::string_view key) { std::stringstream actual_ss; dem_match_graph_to_svg_diagram_write_to(dem, actual_ss); auto actual = actual_ss.str(); expect_string_is_identical_to_saved_file(actual_ss.str(), key); } TEST(match_graph_drawer_svg, repetition_code) { CircuitGenParameters params(10, 7, "memory"); params.after_clifford_depolarization = 0.001; auto circuit = generate_rep_code_circuit(params).circuit; auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, false, false, false); expect_graph_svg_diagram_is_identical_to_saved_file(dem, "match_graph_repetition_code.svg"); } TEST(match_graph_drawer_svg, surface_code) { CircuitGenParameters params(10, 3, "unrotated_memory_z"); params.after_clifford_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, false, false, false); expect_graph_svg_diagram_is_identical_to_saved_file(dem, "match_graph_surface_code.svg"); } TEST(match_graph_drawer_svg, missing_coordinates) { Circuit circuit(R"CIRCUIT( R 0 1 2 3 4 5 6 7 8 9 10 X_ERROR(0.125) 0 1 2 3 4 5 6 7 8 9 10 M 0 1 2 3 4 5 6 7 8 9 10 DETECTOR rec[-1] rec[-2] DETECTOR rec[-2] rec[-3] DETECTOR rec[-3] rec[-4] DETECTOR rec[-4] rec[-5] DETECTOR rec[-5] rec[-6] DETECTOR rec[-6] rec[-7] DETECTOR rec[-7] rec[-8] DETECTOR rec[-8] rec[-9] DETECTOR rec[-9] rec[-10] OBSERVABLE_INCLUDE(1) rec[-1] )CIRCUIT"); auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, false, false, false); std::stringstream ss; testing::internal::CaptureStderr(); dem_match_graph_to_svg_diagram_write_to(dem, ss); std::string err = testing::internal::GetCapturedStderr(); ASSERT_NE(err, ""); expect_string_is_identical_to_saved_file(ss.str(), "match_graph_no_coords.svg"); } ================================================ FILE: src/stim/diagram/json_obj.cc ================================================ #include "stim/diagram/json_obj.h" #include using namespace stim; using namespace stim_draw_internal; enum JsonType : uint8_t { JsonTypeEmpty = 0, JsonTypeMap = 1, JsonTypeArray = 2, JsonTypeBool = 3, JsonTypeSingle = 4, JsonTypeDouble = 5, JsonTypeInt64 = 6, JsonTypeUInt64 = 7, JsonTypeString = 8, }; JsonObj::JsonObj(float num) : val_float(num), type(JsonTypeSingle) { } JsonObj::JsonObj(double num) : val_double(num), type(JsonTypeDouble) { } JsonObj::JsonObj(uint64_t num) : val_uint64_t(num), type(JsonTypeUInt64) { } JsonObj::JsonObj(uint32_t num) : val_uint64_t(num), type(JsonTypeUInt64) { } JsonObj::JsonObj(uint16_t num) : val_uint64_t(num), type(JsonTypeUInt64) { } JsonObj::JsonObj(uint8_t num) : val_uint64_t(num), type(JsonTypeUInt64) { } JsonObj::JsonObj(int8_t num) : val_int64_t(num), type(JsonTypeInt64) { } JsonObj::JsonObj(int16_t num) : val_int64_t(num), type(JsonTypeInt64) { } JsonObj::JsonObj(int32_t num) : val_int64_t(num), type(JsonTypeInt64) { } JsonObj::JsonObj(int64_t num) : val_int64_t(num), type(JsonTypeInt64) { } JsonObj::JsonObj(std::string text) : text(text), type(JsonTypeString) { } JsonObj::JsonObj(const char *text) : text(text), type(JsonTypeString) { } JsonObj::JsonObj(std::map map) : map(map), type(JsonTypeMap) { } JsonObj::JsonObj(std::vector arr) : arr(arr), type(JsonTypeArray) { } JsonObj::JsonObj(bool boolean) : val_boolean(boolean), type(JsonTypeBool) { } void JsonObj::clear() { text.clear(); map.clear(); arr.clear(); type = JsonTypeEmpty; val_uint64_t = 0; } void JsonObj::write_str(std::string_view s, std::ostream &out) { out << '"'; for (char c : s) { if (c == '\0') { out << "\\0"; } else if (c == '\n') { out << "\\n"; } else if (c == '"') { out << "\\\""; } else if (c == '\\') { out << "\\\\"; } else { out << c; } } out << '"'; } void indented_new_line(std::ostream &out, int64_t indent) { if (indent >= 0) { out << '\n'; for (int64_t k = 0; k < indent; k++) { out << ' '; } } } void JsonObj::write(std::ostream &out, int64_t indent) const { switch (type) { case JsonTypeMap: { out << "{"; indented_new_line(out, indent + 2); bool first = true; for (const auto &e : map) { if (first) { first = false; } else { out << ','; indented_new_line(out, indent + 2); } write_str(e.first, out); out << ':'; e.second.write(out, indent + 2); } if (!first) { indented_new_line(out, indent); } out << "}"; break; } case JsonTypeArray: { out << "["; indented_new_line(out, indent + 2); bool first = true; for (const auto &e : arr) { if (first) { first = false; } else { out << ','; indented_new_line(out, indent + 2); } e.write(out, indent + 2); } if (!first) { indented_new_line(out, indent); } out << "]"; break; } case JsonTypeString: { write_str(text, out); break; } case JsonTypeBool: { out << (val_boolean ? "true" : "false"); break; } case JsonTypeSingle: { out << val_float; break; } case JsonTypeDouble: { auto p = out.precision(); out.precision(std::numeric_limits::digits10); out << val_double; out.precision(p); break; } case JsonTypeInt64: { out << val_int64_t; break; } case JsonTypeUInt64: { out << val_uint64_t; break; } default: throw std::invalid_argument("unknown type"); } } std::string JsonObj::str(bool indent) const { std::stringstream ss; ss.precision(std::numeric_limits::max_digits10); write(ss, indent ? 0 : INT64_MIN); return ss.str(); } std::ostream &stim_draw_internal::operator<<(std::ostream &out, const JsonObj &obj) { auto precision = out.precision(); out.precision(std::numeric_limits::max_digits10); obj.write(out); out.precision(precision); return out; } ================================================ FILE: src/stim/diagram/json_obj.h ================================================ /* * 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. */ #ifndef _STIM_DRAW_JSON_OBJ_H #define _STIM_DRAW_JSON_OBJ_H #include #include "stim/circuit/circuit.h" namespace stim_draw_internal { struct JsonObj { union { float val_float; double val_double; int64_t val_int64_t; uint64_t val_uint64_t; bool val_boolean; }; std::string text; std::map map; std::vector arr; uint8_t type; JsonObj(bool boolean); JsonObj(float num); JsonObj(double double_num); JsonObj(uint8_t int_num); JsonObj(uint16_t int_num); JsonObj(uint32_t int_num); JsonObj(uint64_t uint_num); JsonObj(int8_t int_num); JsonObj(int16_t int_num); JsonObj(int32_t int_num); JsonObj(int64_t int_num); JsonObj(std::string text); JsonObj(const char *text); JsonObj(std::map map); JsonObj(std::vector arr); void clear(); static void write_str(std::string_view s, std::ostream &out); void write(std::ostream &out, int64_t indent = INT64_MIN) const; std::string str(bool indent = false) const; }; std::ostream &operator<<(std::ostream &out, const JsonObj &obj); } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/json_obj.test.cc ================================================ // 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. #include "stim/diagram/json_obj.h" #include "gtest/gtest.h" using namespace stim; using namespace stim_draw_internal; TEST(json_obj, str) { EXPECT_EQ(JsonObj(1).str(), "1"); EXPECT_EQ(JsonObj(2.5f).str(), "2.5"); EXPECT_EQ(JsonObj("test").str(), "\"test\""); EXPECT_EQ(JsonObj(std::vector{}).str(), "[]"); EXPECT_EQ(JsonObj(std::vector{1}).str(), "[1]"); EXPECT_EQ(JsonObj(std::vector{1, 2.5f, "5"}).str(), "[1,2.5,\"5\"]"); EXPECT_EQ(JsonObj(std::map{}).str(), "{}"); EXPECT_EQ(JsonObj(std::map{{"a", 1}}).str(), "{\"a\":1}"); EXPECT_EQ(JsonObj(std::map{{"a", 1}, {"b", 2}}).str(), "{\"a\":1,\"b\":2}"); } ================================================ FILE: src/stim/diagram/lattice_map.cc ================================================ #include "stim/diagram/lattice_map.h" using namespace stim; using namespace stim_draw_internal; void LatticeMap::set( uint64_t index, stim::SpanRef offsets_per_iteration, stim::SpanRef iteration_counts, uint32_t value) { if (offsets_per_iteration.empty()) { if (index >= brute_force_data.size()) { brute_force_data.resize(2 * index + 10); } brute_force_data[index] = value; return; } for (uint64_t k = 0; k < iteration_counts[0]; k++) { set(index + k * offsets_per_iteration[0], {offsets_per_iteration.ptr_start + 1, offsets_per_iteration.ptr_end}, {iteration_counts.ptr_start + 1, iteration_counts.ptr_end}, value); } } uint32_t LatticeMap::get(uint64_t index) { if (index >= brute_force_data.size()) { return 0; } return brute_force_data[index]; } ================================================ FILE: src/stim/diagram/lattice_map.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_TIMELINE_ASCII_LATTICE_MAP_H #define _STIM_DIAGRAM_TIMELINE_ASCII_LATTICE_MAP_H #include #include "stim/circuit/circuit.h" namespace stim_draw_internal { /// A map that supports writing to sets of indices that form bounded lattices. struct LatticeMap { std::vector brute_force_data; /// Write to a lattice of indices. void set( uint64_t index, stim::SpanRef offsets_per_iteration, stim::SpanRef iteration_counts, uint32_t value); /// Read the latest value written to an index, defaulting to 0 if no writes yet. uint32_t get(uint64_t index); }; } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/timeline/timeline_3d_drawer.cc ================================================ #include "stim/diagram/timeline/timeline_3d_drawer.h" #include "stim/circuit/gate_decomposition.h" #include "stim/diagram/circuit_timeline_helper.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/diagram_util.h" #include "stim/stabilizers/pauli_string.h" using namespace stim; using namespace stim_draw_internal; Coord<3> trans(size_t m, Coord<2> xy) { return {-(float)m, xy.xyz[0] * -2.0f, xy.xyz[1] * -2.0f}; } Coord<3> DiagramTimeline3DDrawer::mq2xyz(size_t m, size_t q) const { auto xy = qubit_coords[q]; return trans(m, xy); } void DiagramTimeline3DDrawer::do_feedback( std::string_view gate, const GateTarget &qubit_target, const GateTarget &feedback_target) { std::string key = std::string(gate); if (feedback_target.is_sweep_bit_target()) { key.append(":SWEEP"); } else if (feedback_target.is_measurement_record_target()) { key.append(":REC"); } auto center = mq2xyz(cur_moment, qubit_target.qubit_value()); diagram_out.elements.push_back({key, center}); } void DiagramTimeline3DDrawer::draw_two_qubit_gate_end_point(Coord<3> center, std::string_view type) { if (type == "X") { diagram_out.elements.push_back({"X_CONTROL", center}); } else if (type == "Y") { diagram_out.elements.push_back({"Y_CONTROL", center}); } else if (type == "Z") { diagram_out.elements.push_back({"Z_CONTROL", center}); } else { diagram_out.elements.push_back({std::string(type), center}); } } void DiagramTimeline3DDrawer::draw_gate_connecting_line(Coord<3> a, Coord<3> b) { diagram_out.line_data.push_back(a); auto d = b - a; if (d.norm() > 2.2) { auto c = (a + b) * 0.5; c.xyz[0] -= 0.25; diagram_out.line_data.push_back(c); diagram_out.line_data.push_back(c); } diagram_out.line_data.push_back(b); } void DiagramTimeline3DDrawer::do_two_qubit_gate_instance(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); const GateTarget &target1 = op.targets[0]; const GateTarget &target2 = op.targets[1]; auto ends = two_qubit_gate_pieces(op.gate_type); if (target1.is_measurement_record_target() || target1.is_sweep_bit_target()) { do_feedback(ends.second, target2, target1); return; } if (target2.is_measurement_record_target() || target2.is_sweep_bit_target()) { do_feedback(ends.first, target1, target2); return; } auto pieces = two_qubit_gate_pieces(op.gate_type); auto c1 = mq2xyz(cur_moment, target1.qubit_value()); auto c2 = mq2xyz(cur_moment, target2.qubit_value()); draw_two_qubit_gate_end_point(c1, pieces.first); draw_two_qubit_gate_end_point(c2, pieces.second); draw_gate_connecting_line(c1, c2); } void DiagramTimeline3DDrawer::start_next_moment() { cur_moment += 1; cur_moment_is_used = false; cur_moment_used_flags.clear(); cur_moment_used_flags.resize(num_qubits, false); } void DiagramTimeline3DDrawer::do_tick() { if (has_ticks && cur_moment > tick_start_moment) { auto x1 = coord_bounds.first.xyz[0] - 0.2f; auto x2 = coord_bounds.first.xyz[0] - 0.4f; auto y1 = coord_bounds.first.xyz[1]; auto y2 = coord_bounds.second.xyz[1]; y1 -= 0.25f; y2 += 0.25f; Coord<3> p0 = trans(tick_start_moment, {x1, y1}); Coord<3> p1 = trans(tick_start_moment, {x1, y2}); Coord<3> p2 = trans(tick_start_moment, {x2, y1}); Coord<3> p3 = trans(tick_start_moment, {x2, y2}); Coord<3> p4 = trans(cur_moment, {x1, y1}); Coord<3> p5 = trans(cur_moment, {x1, y2}); Coord<3> p6 = trans(cur_moment, {x2, y1}); Coord<3> p7 = trans(cur_moment, {x2, y2}); p0.xyz[0] += 0.25; p1.xyz[0] += 0.25; p2.xyz[0] += 0.25; p3.xyz[0] += 0.25; p4.xyz[0] -= 0.25; p5.xyz[0] -= 0.25; p6.xyz[0] -= 0.25; p7.xyz[0] -= 0.25; diagram_out.blue_line_data.push_back(p0); diagram_out.blue_line_data.push_back(p2); diagram_out.blue_line_data.push_back(p1); diagram_out.blue_line_data.push_back(p3); diagram_out.blue_line_data.push_back(p2); diagram_out.blue_line_data.push_back(p3); diagram_out.blue_line_data.push_back(p2); diagram_out.blue_line_data.push_back(p6); diagram_out.blue_line_data.push_back(p3); diagram_out.blue_line_data.push_back(p7); diagram_out.blue_line_data.push_back(p4); diagram_out.blue_line_data.push_back(p6); diagram_out.blue_line_data.push_back(p5); diagram_out.blue_line_data.push_back(p7); diagram_out.blue_line_data.push_back(p6); diagram_out.blue_line_data.push_back(p7); } start_next_moment(); tick_start_moment = cur_moment; } void DiagramTimeline3DDrawer::do_single_qubit_gate_instance(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); const auto &target = op.targets[0]; auto center = mq2xyz(cur_moment, target.qubit_value()); const auto &gate_data = GATE_DATA[op.gate_type]; diagram_out.elements.push_back({std::string(gate_data.name), center}); } void DiagramTimeline3DDrawer::reserve_drawing_room_for_targets(SpanRef targets) { bool already_used = false; for (auto t : targets) { if (t.is_x_target() || t.is_y_target() || t.is_z_target() || t.is_qubit_target()) { already_used |= cur_moment_used_flags[t.qubit_value()]; } } if (already_used) { start_next_moment(); } for (auto t : targets) { if (t.is_x_target() || t.is_y_target() || t.is_z_target() || t.is_qubit_target()) { cur_moment_used_flags[t.qubit_value()] = true; } } } void DiagramTimeline3DDrawer::do_multi_qubit_gate_with_pauli_targets(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); Coord<3> prev{}; bool has_prev = false; for (const auto &t : op.targets) { if (t.is_combiner()) { continue; } std::stringstream ss; const auto &gate_data = GATE_DATA[op.gate_type]; ss << gate_data.name; if (t.is_x_target()) { ss << ":X"; } else if (t.is_y_target()) { ss << ":Y"; } else if (t.is_z_target()) { ss << ":Z"; } auto center = mq2xyz(cur_moment, t.qubit_value()); diagram_out.elements.push_back({ss.str(), center}); if (has_prev) { draw_gate_connecting_line(center, prev); } prev = center; has_prev = true; } } void DiagramTimeline3DDrawer::do_start_repeat(const CircuitTimelineLoopData &loop_data) { if (cur_moment_is_used) { do_tick(); } start_next_moment(); loop_start_moment_stack.push_back(cur_moment); tick_start_moment = cur_moment; } void DiagramTimeline3DDrawer::do_end_repeat(const CircuitTimelineLoopData &loop_data) { if (cur_moment_is_used) { do_tick(); } auto start = loop_start_moment_stack.back(); loop_start_moment_stack.pop_back(); auto x1 = coord_bounds.first.xyz[0]; auto x2 = coord_bounds.second.xyz[0]; auto y1 = coord_bounds.first.xyz[1]; auto y2 = coord_bounds.second.xyz[1]; x1 -= 0.5f * 3.0f / (2 + resolver.cur_loop_nesting.size()); x2 += 0.5f * 3.0f / (2 + resolver.cur_loop_nesting.size()); y1 -= 0.5f * 3.0f / (2 + resolver.cur_loop_nesting.size()); y2 += 0.5f * 3.0f / (2 + resolver.cur_loop_nesting.size()); Coord<3> p0 = trans(start, {x1, y1}); Coord<3> p1 = trans(start, {x1, y2}); Coord<3> p2 = trans(start, {x2, y1}); Coord<3> p3 = trans(start, {x2, y2}); Coord<3> p4 = trans(cur_moment, {x1, y1}); Coord<3> p5 = trans(cur_moment, {x1, y2}); Coord<3> p6 = trans(cur_moment, {x2, y1}); Coord<3> p7 = trans(cur_moment, {x2, y2}); p0.xyz[0] += 0.25; p1.xyz[0] += 0.25; p2.xyz[0] += 0.25; p3.xyz[0] += 0.25; p4.xyz[0] -= 0.25; p5.xyz[0] -= 0.25; p6.xyz[0] -= 0.25; p7.xyz[0] -= 0.25; diagram_out.red_line_data.push_back(p0); diagram_out.red_line_data.push_back(p1); diagram_out.red_line_data.push_back(p0); diagram_out.red_line_data.push_back(p2); diagram_out.red_line_data.push_back(p0); diagram_out.red_line_data.push_back(p4); diagram_out.red_line_data.push_back(p1); diagram_out.red_line_data.push_back(p3); diagram_out.red_line_data.push_back(p1); diagram_out.red_line_data.push_back(p5); diagram_out.red_line_data.push_back(p2); diagram_out.red_line_data.push_back(p3); diagram_out.red_line_data.push_back(p2); diagram_out.red_line_data.push_back(p6); diagram_out.red_line_data.push_back(p3); diagram_out.red_line_data.push_back(p7); diagram_out.red_line_data.push_back(p4); diagram_out.red_line_data.push_back(p5); diagram_out.red_line_data.push_back(p4); diagram_out.red_line_data.push_back(p6); diagram_out.red_line_data.push_back(p5); diagram_out.red_line_data.push_back(p7); diagram_out.red_line_data.push_back(p6); diagram_out.red_line_data.push_back(p7); start_next_moment(); tick_start_moment = cur_moment; } void DiagramTimeline3DDrawer::do_mpp(const ResolvedTimelineOperation &op) { do_multi_qubit_gate_with_pauli_targets(op); } void DiagramTimeline3DDrawer::do_spp(const ResolvedTimelineOperation &op) { do_multi_qubit_gate_with_pauli_targets(op); } void DiagramTimeline3DDrawer::do_correlated_error(const ResolvedTimelineOperation &op) { if (cur_moment_is_used) { start_next_moment(); } do_multi_qubit_gate_with_pauli_targets(op); } void DiagramTimeline3DDrawer::do_else_correlated_error(const ResolvedTimelineOperation &op) { do_correlated_error(op); } void DiagramTimeline3DDrawer::do_qubit_coords(const ResolvedTimelineOperation &op) { // Not drawn. } void DiagramTimeline3DDrawer::do_detector(const ResolvedTimelineOperation &op) { // Not drawn. } void DiagramTimeline3DDrawer::do_observable_include(const ResolvedTimelineOperation &op) { // Not drawn. } void DiagramTimeline3DDrawer::do_resolved_operation(const ResolvedTimelineOperation &op) { if (op.gate_type == GateType::MPP) { do_mpp(op); } else if (op.gate_type == GateType::SPP || op.gate_type == GateType::SPP_DAG) { do_spp(op); } else if (op.gate_type == GateType::DETECTOR) { do_detector(op); } else if (op.gate_type == GateType::OBSERVABLE_INCLUDE) { do_observable_include(op); } else if (op.gate_type == GateType::QUBIT_COORDS) { do_qubit_coords(op); } else if (op.gate_type == GateType::E) { do_correlated_error(op); } else if (op.gate_type == GateType::ELSE_CORRELATED_ERROR) { do_else_correlated_error(op); } else if (op.gate_type == GateType::TICK) { do_tick(); } else if (GATE_DATA[op.gate_type].flags & GATE_TARGETS_PAIRS) { do_two_qubit_gate_instance(op); } else { do_single_qubit_gate_instance(op); } } DiagramTimeline3DDrawer::DiagramTimeline3DDrawer(size_t num_qubits, bool has_ticks) : num_qubits(num_qubits), has_ticks(has_ticks) { cur_moment_used_flags.resize(num_qubits, false); } void add_used_qubits(const Circuit &circuit, std::set &out) { for (const auto &op : circuit.operations) { if (op.gate_type == GateType::REPEAT) { add_used_qubits(op.repeat_block_body(circuit), out); } else { for (const auto &t : op.targets) { if (t.is_x_target() || t.is_y_target() || t.is_z_target() || t.is_qubit_target()) { out.insert(t.qubit_value()); } } } } } std::pair>, std::pair, Coord<2>>> pick_coords_for_circuit(const Circuit &circuit) { DetectorSliceSet set; set.num_qubits = circuit.count_qubits(); set.coordinates = circuit.get_final_qubit_coords(); auto coords = FlattenedCoords::from(set, 1).qubit_coords; float default_y = 0; for (auto e : set.coordinates) { default_y = std::min(default_y, coords[e.first].xyz[1] - 1); } for (uint64_t q = 0; q < set.num_qubits; q++) { if (set.coordinates.find(q) == set.coordinates.end()) { coords[q].xyz = {(float)q, default_y}; } } std::set used; add_used_qubits(circuit, used); std::vector> used_coords; for (const auto &q : used) { used_coords.push_back(coords[q]); } if (used_coords.empty()) { used_coords.push_back({0, 0}); } auto bounds = Coord<2>::min_max(used_coords); // auto center = (bounds.first + bounds.second) * 0.5; // for (auto &c : coords) { // c -= center; // } // bounds.first -= center; // bounds.second -= center; return {coords, bounds}; } Basic3dDiagram DiagramTimeline3DDrawer::circuit_to_basic_3d_diagram(const Circuit &circuit) { auto num_qubits = circuit.count_qubits(); auto has_ticks = circuit.count_ticks() > 0; DiagramTimeline3DDrawer obj(num_qubits, has_ticks); auto all_used = pick_coords_for_circuit(circuit); obj.qubit_coords = all_used.first; obj.coord_bounds = all_used.second; auto minmax = obj.coord_bounds; // Draw an arrow indicating the time direction. auto y = -2 * (minmax.first.xyz[0] - 1); auto z = -2 * (minmax.first.xyz[1] * 0.5f + minmax.second.xyz[1] * 0.5f); obj.diagram_out.red_line_data.push_back({0, y, z}); obj.diagram_out.red_line_data.push_back({-3, y, z}); obj.diagram_out.red_line_data.push_back({-2.5f, y - 0.5f, z}); obj.diagram_out.red_line_data.push_back({-3, y, z}); obj.diagram_out.red_line_data.push_back({-2.5f, y + 0.5f, z}); obj.diagram_out.red_line_data.push_back({-3, y, z}); obj.resolver.resolved_op_callback = [&](const ResolvedTimelineOperation &op) { obj.do_resolved_operation(op); }; obj.resolver.start_repeat_callback = [&](const CircuitTimelineLoopData &loop_data) { obj.do_start_repeat(loop_data); }; obj.resolver.end_repeat_callback = [&](const CircuitTimelineLoopData &loop_data) { obj.do_end_repeat(loop_data); }; obj.resolver.do_circuit(circuit); if (obj.cur_moment_is_used) { obj.start_next_moment(); } std::set used; add_used_qubits(circuit, used); for (auto q : used) { auto p1 = obj.mq2xyz(0, q); p1.xyz[0] += 1; auto p2 = obj.mq2xyz(obj.cur_moment + 1, q); obj.diagram_out.line_data.push_back(p1); obj.diagram_out.line_data.push_back(p2); } return obj.diagram_out; } ================================================ FILE: src/stim/diagram/timeline/timeline_3d_drawer.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_TIMELINE_TIMELINE_3D_DRAWER_H #define _STIM_DIAGRAM_TIMELINE_TIMELINE_3D_DRAWER_H #include #include "stim/diagram/basic_3d_diagram.h" #include "stim/diagram/circuit_timeline_helper.h" namespace stim_draw_internal { struct DiagramTimeline3DDrawer { CircuitTimelineHelper resolver; std::vector loop_start_moment_stack; Basic3dDiagram diagram_out; size_t cur_moment = 0; size_t cur_moment_is_used = false; size_t tick_start_moment = 0; std::vector cur_moment_used_flags; size_t num_qubits = 0; bool has_ticks = false; std::vector> qubit_coords; std::pair, Coord<2>> coord_bounds; DiagramTimeline3DDrawer(size_t num_qubits, bool has_ticks); static Basic3dDiagram circuit_to_basic_3d_diagram(const stim::Circuit &circuit); void do_start_repeat(const CircuitTimelineLoopData &loop_data); void do_end_repeat(const CircuitTimelineLoopData &loop_data); void start_next_moment(); void reserve_drawing_room_for_targets(stim::SpanRef targets); Coord<3> mq2xyz(size_t m, size_t q) const; void draw_two_qubit_gate_end_point(Coord<3> center, std::string_view type); void draw_gate_connecting_line(Coord<3> a, Coord<3> b); void do_resolved_operation(const ResolvedTimelineOperation &op); void do_tick(); void do_two_qubit_gate_instance(const ResolvedTimelineOperation &op); void do_feedback( std::string_view gate, const stim::GateTarget &qubit_target, const stim::GateTarget &feedback_target); void do_single_qubit_gate_instance(const ResolvedTimelineOperation &op); void do_multi_qubit_gate_with_pauli_targets(const ResolvedTimelineOperation &op); void do_multi_qubit_gate_with_paired_pauli_targets(const ResolvedTimelineOperation &op); void do_mpp(const ResolvedTimelineOperation &op); void do_spp(const ResolvedTimelineOperation &op); void do_correlated_error(const ResolvedTimelineOperation &op); void do_qubit_coords(const ResolvedTimelineOperation &op); void do_else_correlated_error(const ResolvedTimelineOperation &op); void do_detector(const ResolvedTimelineOperation &op); void do_observable_include(const ResolvedTimelineOperation &op); }; } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/timeline/timeline_3d_drawer.test.cc ================================================ // 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. #include "stim/diagram/timeline/timeline_3d_drawer.h" #include #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/util_bot/test_util.test.h" using namespace stim; using namespace stim_draw_internal; void expect_diagram_is_identical_to_saved_file(const Circuit &circuit, std::string_view key) { auto diagram = DiagramTimeline3DDrawer::circuit_to_basic_3d_diagram(circuit); std::stringstream actual_ss; diagram.to_gltf_scene().to_json().write(actual_ss); auto actual = actual_ss.str(); auto path = resolve_test_file(key); FILE *f = fopen(path.c_str(), "rb"); auto expected = rewind_read_close(f); if (expected != actual) { auto dot = key.rfind('.'); std::string new_path; if (dot == std::string::npos) { new_path = path + ".new"; } else { dot += path.size() - key.size(); new_path = path.substr(0, dot) + ".new" + path.substr(dot); } std::ofstream out; out.open(new_path); out << actual; out.close(); EXPECT_TRUE(false) << "Diagram didn't agree. key=" << key; } } TEST(circuit_diagram_timeline_3d, single_qubit_gates) { Circuit circuit(R"CIRCUIT( I 0 X 1 Y 2 Z 3 C_XYZ 0 C_ZYX 1 H 2 H_XY 3 H_XZ 0 H_YZ 1 S 2 SQRT_X 3 SQRT_X_DAG 0 SQRT_Y 1 SQRT_Y_DAG 2 SQRT_Z 3 SQRT_Z_DAG 0 S_DAG 1 H 2 0 3 )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "single_qubits_gates.gltf"); } TEST(circuit_diagram_timeline_3d, two_qubits_gates) { Circuit circuit(R"CIRCUIT( CNOT 0 1 CX 2 3 CY 4 5 5 4 CZ 0 2 ISWAP 1 3 ISWAP_DAG 2 4 SQRT_XX 3 5 SQRT_XX_DAG 0 5 SQRT_YY 3 4 4 3 SQRT_YY_DAG 0 1 SQRT_ZZ 2 3 SQRT_ZZ_DAG 4 5 SWAP 0 1 XCX 2 3 XCY 3 4 XCZ 0 1 YCX 2 3 YCY 4 5 YCZ 0 1 ZCX 2 3 ZCY 4 5 ZCZ 0 5 2 3 1 4 CXSWAP 0 1 SWAPCX 2 3 )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "two_qubits_gates.gltf"); } TEST(circuit_diagram_timeline_3d, noise_gates) { Circuit circuit(R"CIRCUIT( DEPOLARIZE1(0.125) 0 1 DEPOLARIZE2(0.125) 0 2 4 5 X_ERROR(0.125) 0 1 2 Y_ERROR(0.125) 0 1 4 Z_ERROR(0.125) 2 3 5 )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "noise_gates_1.gltf"); circuit = Circuit(R"CIRCUIT( E(0.25) X1 X2 CORRELATED_ERROR(0.125) X1 Y2 Z3 ELSE_CORRELATED_ERROR(0.25) X2 Y4 Z3 ELSE_CORRELATED_ERROR(0.25) X5 )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "noise_gates_2.gltf"); circuit = Circuit(R"CIRCUIT( PAULI_CHANNEL_1(0.125,0.25,0.125) 0 1 2 3 PAULI_CHANNEL_2(0.01,0.01,0.01,0.02,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01) 0 1 2 4 )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "noise_gates_3.gltf"); } TEST(circuit_diagram_timeline_3d, collapsing) { Circuit circuit(R"CIRCUIT( R 0 RX 1 RY 2 RZ 3 M(0.001) 0 1 MR 1 0 MRX 1 2 MRY 0 3 1 MRZ 0 MX 1 MY 2 MZ 3 MPP X0*Y2 Z3 X1 Z2*Y3 )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "collapsing.gltf"); } TEST(circuit_diagram_timeline_3d, measurement_looping) { Circuit circuit(R"CIRCUIT( M 0 REPEAT 100 { M 1 REPEAT 5 { M 2 } REPEAT 7 { MPP X3*Y4 } } )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "measurement_looping.gltf"); } TEST(circuit_diagram_timeline_3d, repeat) { auto circuit = Circuit(R"CIRCUIT( H 0 1 2 REPEAT 5 { RX 2 REPEAT 100 { H 0 1 3 3 } } )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "repeat.gltf"); } TEST(circuit_diagram_timeline_3d, classical_feedback) { auto circuit = Circuit(R"CIRCUIT( M 0 CX rec[-1] 1 YCZ 2 sweep[5] )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "classical_feedback.gltf"); } TEST(circuit_diagram_timeline_3d, lattice_surgery_cnot) { auto circuit = Circuit(R"CIRCUIT( R 2 MPP X1*X2 MPP Z0*Z2 MX 2 CZ rec[-3] 0 CX rec[-2] 1 CZ rec[-1] 0 )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "lattice_surgery_cnot.gltf"); } TEST(circuit_diagram_timeline_3d, tick) { auto circuit = Circuit(R"CIRCUIT( H 0 0 TICK H 0 1 TICK H 0 REPEAT 1 { H 0 1 TICK H 0 S 0 } H 0 0 SQRT_X 0 TICK H 0 0 )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "tick.gltf"); } TEST(circuit_diagram_timeline_3d, detector_pseudo_targets) { auto circuit = Circuit(R"CIRCUIT( M 0 1 2 3 4 5 REPEAT 100 { M 1 2 } DETECTOR(1) rec[-1] DETECTOR(2) rec[-2] DETECTOR(3) rec[-3] DETECTOR(4) rec[-4] DETECTOR(5) rec[-1] rec[-2] OBSERVABLE_INCLUDE(100) rec[-201] rec[-203] )CIRCUIT"); expect_diagram_is_identical_to_saved_file(circuit, "detector_pseudo_targets.gltf"); } TEST(circuit_diagram_timeline_3d, repetition_code) { CircuitGenParameters params(10, 3, "memory"); auto circuit = generate_rep_code_circuit(params).circuit; expect_diagram_is_identical_to_saved_file(circuit, "repetition_code.gltf"); } TEST(circuit_diagram_timeline_3d, surface_code) { CircuitGenParameters params(10, 3, "unrotated_memory_z"); auto circuit = generate_surface_code_circuit(params).circuit; expect_diagram_is_identical_to_saved_file(circuit, "surface_code.gltf"); } TEST(circuit_diagram_timeline_3d, test_circuit_all_ops) { expect_diagram_is_identical_to_saved_file(generate_test_circuit_with_all_operations(), "circuit_all_ops_3d.gltf"); } ================================================ FILE: src/stim/diagram/timeline/timeline_ascii_drawer.cc ================================================ #include "stim/diagram/timeline/timeline_ascii_drawer.h" #include "stim/diagram/circuit_timeline_helper.h" #include "stim/diagram/diagram_util.h" #include "stim/util_bot/str_util.h" using namespace stim; using namespace stim_draw_internal; constexpr double GATE_ALIGNMENT_X = 0; // Left-justify gates when time moves right. constexpr double GATE_ALIGNMENT_Y = 0.5; // Center-justify gates when time moves down. size_t DiagramTimelineAsciiDrawer::m2x(size_t m) const { return m * (1 + moment_spacing) + 2; } size_t DiagramTimelineAsciiDrawer::q2y(size_t q) const { return q * 2 + 1; } void DiagramTimelineAsciiDrawer::do_feedback( std::string_view gate, const GateTarget &qubit_target, const GateTarget &feedback_target) { std::stringstream ss; ss << gate; ss << "^"; if (feedback_target.is_sweep_bit_target()) { ss << "sweep[" << feedback_target.value() << "]"; } else if (feedback_target.is_measurement_record_target()) { ss << "rec[" << (feedback_target.value() + resolver.measure_offset) << "]"; } diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(qubit_target.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, ss.str(), }); } void DiagramTimelineAsciiDrawer::do_two_qubit_gate_instance(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); const GateTarget &target1 = op.targets[0]; const GateTarget &target2 = op.targets[1]; const auto &gate_data = GATE_DATA[op.gate_type]; auto ends = two_qubit_gate_pieces(op.gate_type); if (target1.is_measurement_record_target() || target1.is_sweep_bit_target()) { do_feedback(ends.second, target2, target1); return; } if (target2.is_measurement_record_target() || target2.is_sweep_bit_target()) { do_feedback(ends.first, target1, target2); return; } std::stringstream first; std::stringstream second; first << (ends.first == "Z" ? "@" : ends.first); second << (ends.second == "Z" ? "@" : ends.second); if (!op.args.empty()) { if (op.gate_type == GateType::PAULI_CHANNEL_2) { first << "[0]"; second << "[1]"; } first << "(" << comma_sep(op.args, ",") << ")"; second << "(" << comma_sep(op.args, ",") << ")"; } if (gate_data.flags & GATE_PRODUCES_RESULTS) { first << ':'; write_rec_index(first); } diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(target1.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, first.str(), }); diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(target2.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, second.str(), }); } void DiagramTimelineAsciiDrawer::start_next_moment() { cur_moment++; cur_moment_is_used = false; cur_moment_used_flags.clear(); cur_moment_used_flags.resize(num_qubits); } void DiagramTimelineAsciiDrawer::do_tick() { if (has_ticks && cur_moment > tick_start_moment) { size_t x1 = m2x(tick_start_moment); size_t x2 = m2x(cur_moment); size_t y1 = 0; size_t y2 = q2y(num_qubits - 1) + 1; diagram.add_entry( AsciiDiagramEntry{ {x1, y1, 0, 0}, "/", }); diagram.add_entry( AsciiDiagramEntry{ {x2, y1, 1, 0}, "\\", }); diagram.add_entry( AsciiDiagramEntry{ {x1, y2, 0, 1}, "\\", }); diagram.add_entry( AsciiDiagramEntry{ {x2, y2, 1, 0}, "/", }); diagram.lines.push_back({{x1, y1, 0.0, 0.0}, {x2, y1, 1.0, 0.0}}); diagram.lines.push_back({{x1, y2, 0.0, 0.0}, {x2, y2, 1.0, 0.0}}); } start_next_moment(); tick_start_moment = cur_moment; } void DiagramTimelineAsciiDrawer::do_single_qubit_gate_instance(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); const auto &target = op.targets[0]; const auto &gate_data = GATE_DATA[op.gate_type]; std::stringstream ss; ss << gate_data.name; if (!op.args.empty()) { ss << "(" << comma_sep(op.args, ",") << ")"; } if (gate_data.flags & GATE_PRODUCES_RESULTS) { ss << ':'; write_rec_index(ss); } diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(target.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, ss.str(), }); } void DiagramTimelineAsciiDrawer::write_det_index(std::ostream &out) { out.put('D'); if (!resolver.cur_loop_nesting.empty()) { out.put('['); } out << resolver.detector_offset; for (size_t k = 0; k < resolver.cur_loop_nesting.size(); k++) { out << "+iter"; if (k > 0) { out << (k + 1); } auto p = resolver.cur_loop_nesting[k].detectors_per_iteration; if (p != 1) { out << '*' << p; } } if (!resolver.cur_loop_nesting.empty()) { out.put(']'); } } void DiagramTimelineAsciiDrawer::write_rec_index(std::ostream &out, int64_t lookback_shift) { out << "rec["; out << resolver.measure_offset + (decltype(resolver.measure_offset))lookback_shift; for (size_t k = 0; k < resolver.cur_loop_nesting.size(); k++) { auto n = resolver.cur_loop_nesting[k].measurements_per_iteration; if (n != 0) { out << "+iter"; if (k > 0) { out << (k + 1); } if (n != 1) { out << '*' << n; } } } out << ']'; } void DiagramTimelineAsciiDrawer::write_coords(std::ostream &out, SpanRef relative_coordinates) { out.put('('); for (size_t k = 0; k < relative_coordinates.size(); k++) { if (k) { out.put(','); } write_coord(out, k, relative_coordinates[k]); } out.put(')'); } void DiagramTimelineAsciiDrawer::write_coord(std::ostream &out, size_t coord_index, double absolute_coordinate) { out << absolute_coordinate; for (size_t k = 0; k < resolver.cur_loop_nesting.size(); k++) { const auto &p = resolver.cur_loop_nesting[k].shift_per_iteration; if (coord_index < p.size() && p[coord_index] != 0) { out << "+iter"; if (k > 0) { out << (k + 1); } if (p[coord_index] != 1) { out << '*' << p[coord_index]; } } } } void DiagramTimelineAsciiDrawer::reserve_drawing_room_for_targets(SpanRef targets) { size_t min_q = SIZE_MAX; size_t max_q = 0; for (const auto &t : targets) { if (t.is_combiner() || t.is_measurement_record_target() || t.is_sweep_bit_target()) { continue; } size_t q = t.qubit_value(); min_q = std::min(min_q, q); max_q = std::max(max_q, q); } if (min_q == SIZE_MAX) { return; } for (size_t q = min_q; q <= max_q; q++) { if (cur_moment_used_flags[q]) { start_next_moment(); break; } } for (size_t q = min_q; q <= max_q; q++) { cur_moment_used_flags[q] = true; } cur_moment_is_used = true; if (min_q < max_q) { diagram.lines.push_back( {{ m2x(cur_moment), q2y(min_q), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, { m2x(cur_moment), q2y(max_q), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }}); } } void DiagramTimelineAsciiDrawer::do_multi_qubit_gate_with_pauli_targets(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); for (const auto &t : op.targets) { if (t.is_combiner()) { continue; } std::stringstream ss; const auto &gate_data = GATE_DATA[op.gate_type]; ss << gate_data.name; if (t.is_x_target()) { ss << "[X]"; } else if (t.is_y_target()) { ss << "[Y]"; } else if (t.is_z_target()) { ss << "[Z]"; } if (!op.args.empty()) { ss << "(" << comma_sep(op.args, ",") << ")"; } if (gate_data.flags & GATE_PRODUCES_RESULTS) { ss << ':'; write_rec_index(ss); } diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(t.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, ss.str(), }); } } void DiagramTimelineAsciiDrawer::do_start_repeat(const CircuitTimelineLoopData &loop_data) { if (cur_moment_is_used) { do_tick(); } AsciiDiagramPos top{m2x(cur_moment), 0, 0.0, 0.0}; AsciiDiagramPos bot{m2x(cur_moment), q2y(num_qubits - 1) + 1, 0.0, 1.0}; diagram.add_entry( AsciiDiagramEntry{ top, "/REP " + std::to_string(loop_data.num_repetitions), }); diagram.add_entry( AsciiDiagramEntry{ bot, "\\", }); diagram.lines.push_back({bot, top}); start_next_moment(); tick_start_moment = cur_moment; } void DiagramTimelineAsciiDrawer::do_end_repeat(const CircuitTimelineLoopData &loop_data) { if (cur_moment_is_used) { do_tick(); } AsciiDiagramPos top{m2x(cur_moment), 0, 0.5, 0.0}; AsciiDiagramPos bot{m2x(cur_moment), q2y(num_qubits - 1) + 1, 0.5, 1.0}; diagram.lines.push_back({top, bot}); diagram.add_entry( AsciiDiagramEntry{ top, "\\", }); diagram.add_entry( AsciiDiagramEntry{ bot, "/", }); start_next_moment(); tick_start_moment = cur_moment; } void DiagramTimelineAsciiDrawer::do_correlated_error(const ResolvedTimelineOperation &op) { if (cur_moment_is_used) { start_next_moment(); } do_multi_qubit_gate_with_pauli_targets(op); } void DiagramTimelineAsciiDrawer::do_else_correlated_error(const ResolvedTimelineOperation &op) { do_correlated_error(op); } void DiagramTimelineAsciiDrawer::do_qubit_coords(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); assert(op.targets.size() == 1); const auto &target = op.targets[0]; std::stringstream ss; const auto &gate_data = GATE_DATA[op.gate_type]; ss << gate_data.name; write_coords(ss, op.args); diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(target.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, ss.str(), }); } void DiagramTimelineAsciiDrawer::do_detector(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); auto pseudo_target = op.targets[0]; SpanRef rec_targets = op.targets; rec_targets.ptr_start++; std::stringstream ss; ss << "DETECTOR"; if (!op.args.empty()) { write_coords(ss, op.args); } ss.put(':'); write_det_index(ss); ss.put('='); for (size_t k = 0; k < rec_targets.size(); k++) { if (k) { ss << "*"; } write_rec_index(ss, rec_targets[k].value()); } if (rec_targets.empty()) { ss.put('1'); } diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(pseudo_target.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, ss.str(), }); } void DiagramTimelineAsciiDrawer::do_observable_include(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); auto pseudo_target = op.targets[0]; SpanRef rec_targets = op.targets; rec_targets.ptr_start++; bool had_paulis = false; for (const auto &t : rec_targets) { if (t.is_pauli_target()) { had_paulis = true; std::stringstream ss; ss << "L" << (op.args.empty() ? 0 : op.args[0]) << "*="; ss << t.pauli_type(); diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(t.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, ss.str(), }); } } bool had_rec = false; std::stringstream ss; ss << "OBSERVABLE_INCLUDE:L" << (op.args.empty() ? 0 : op.args[0]); ss << "*="; for (const auto &t : rec_targets) { if (t.is_measurement_record_target()) { if (had_rec) { ss << "*"; } had_rec = true; write_rec_index(ss, t.value()); } } if (had_rec || !had_paulis) { if (rec_targets.empty()) { ss.put('1'); } diagram.add_entry( AsciiDiagramEntry{ { m2x(cur_moment), q2y(pseudo_target.qubit_value()), GATE_ALIGNMENT_X, GATE_ALIGNMENT_Y, }, ss.str(), }); } } void DiagramTimelineAsciiDrawer::do_resolved_operation(const ResolvedTimelineOperation &op) { if (op.gate_type == GateType::MPP || op.gate_type == GateType::SPP || op.gate_type == GateType::SPP_DAG) { do_multi_qubit_gate_with_pauli_targets(op); } else if (op.gate_type == GateType::DETECTOR) { do_detector(op); } else if (op.gate_type == GateType::OBSERVABLE_INCLUDE) { do_observable_include(op); } else if (op.gate_type == GateType::QUBIT_COORDS) { do_qubit_coords(op); } else if (op.gate_type == GateType::E) { do_correlated_error(op); } else if (op.gate_type == GateType::ELSE_CORRELATED_ERROR) { do_else_correlated_error(op); } else if (op.gate_type == GateType::TICK) { do_tick(); } else if (GATE_DATA[op.gate_type].flags & GATE_TARGETS_PAIRS) { do_two_qubit_gate_instance(op); } else { do_single_qubit_gate_instance(op); } } DiagramTimelineAsciiDrawer::DiagramTimelineAsciiDrawer(size_t num_qubits, bool has_ticks) : num_qubits(num_qubits), has_ticks(has_ticks) { cur_moment_used_flags.resize(num_qubits); } AsciiDiagram DiagramTimelineAsciiDrawer::make_diagram(const Circuit &circuit) { auto num_qubits = circuit.count_qubits(); auto has_ticks = circuit.count_ticks() > 0; DiagramTimelineAsciiDrawer obj(num_qubits, has_ticks); obj.resolver.resolved_op_callback = [&](const ResolvedTimelineOperation &op) { obj.do_resolved_operation(op); }; obj.resolver.start_repeat_callback = [&](const CircuitTimelineLoopData &loop_data) { obj.do_start_repeat(loop_data); }; obj.resolver.end_repeat_callback = [&](const CircuitTimelineLoopData &loop_data) { obj.do_end_repeat(loop_data); }; obj.resolver.do_circuit(circuit); if (obj.cur_moment_is_used) { obj.do_tick(); } // Make space for the qubit lines to be drawn before other things. obj.diagram.lines.insert(obj.diagram.lines.begin(), obj.num_qubits, {{0, 0, 0.0, 0.5}, {0, 0, 1.0, 0.5}}); // Overwrite the reserved space with the actual qubit lines. for (size_t q = 0; q < obj.num_qubits; q++) { obj.diagram.lines[q] = { {0, obj.q2y(q), 1.0, 0.5}, {obj.m2x(obj.cur_moment), obj.q2y(q), 0.0, 0.5}, }; std::stringstream qubit; qubit << 'q' << q << ": "; obj.diagram.add_entry( AsciiDiagramEntry{ {0, obj.q2y(q), 1.0, 0.5}, qubit.str(), }); } return obj.diagram; } ================================================ FILE: src/stim/diagram/timeline/timeline_ascii_drawer.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_TIMELINE_TIMELINE_ASCII_DRAWER_H #define _STIM_DIAGRAM_TIMELINE_TIMELINE_ASCII_DRAWER_H #include #include "stim/circuit/circuit.h" #include "stim/diagram/ascii_diagram.h" #include "stim/diagram/circuit_timeline_helper.h" #include "stim/diagram/lattice_map.h" namespace stim_draw_internal { struct DiagramTimelineAsciiDrawer { AsciiDiagram diagram; CircuitTimelineHelper resolver; size_t cur_moment = 0; size_t cur_moment_is_used = false; size_t tick_start_moment = 0; std::vector cur_moment_used_flags; size_t num_qubits = 0; bool has_ticks = false; size_t moment_spacing = 1; DiagramTimelineAsciiDrawer(size_t num_qubits, bool has_ticks); /// Converts a circuit into a cell diagram. static AsciiDiagram make_diagram(const stim::Circuit &circuit); void do_start_repeat(const CircuitTimelineLoopData &loop_data); void do_end_repeat(const CircuitTimelineLoopData &loop_data); void start_next_moment(); void reserve_drawing_room_for_targets(stim::SpanRef targets); void write_rec_index(std::ostream &out, int64_t lookback_shift = -1); void write_det_index(std::ostream &out); void write_coord(std::ostream &out, size_t coord_index, double relative_coordinate); void write_coords(std::ostream &out, stim::SpanRef relative_coordinates); size_t m2x(size_t m) const; size_t q2y(size_t q) const; void do_resolved_operation(const ResolvedTimelineOperation &op); void do_tick(); void do_two_qubit_gate_instance(const ResolvedTimelineOperation &op); void do_feedback( std::string_view gate, const stim::GateTarget &qubit_target, const stim::GateTarget &feedback_target); void do_single_qubit_gate_instance(const ResolvedTimelineOperation &op); void do_multi_qubit_gate_with_pauli_targets(const ResolvedTimelineOperation &op); void do_multi_qubit_gate_with_paired_pauli_targets(const ResolvedTimelineOperation &op); void do_correlated_error(const ResolvedTimelineOperation &op); void do_qubit_coords(const ResolvedTimelineOperation &op); void do_else_correlated_error(const ResolvedTimelineOperation &op); void do_detector(const ResolvedTimelineOperation &op); void do_observable_include(const ResolvedTimelineOperation &op); }; } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/timeline/timeline_ascii_drawer.test.cc ================================================ #include "stim/diagram/timeline/timeline_ascii_drawer.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" using namespace stim; using namespace stim_draw_internal; TEST(circuit_diagram_timeline_text, single_qubit_gates) { Circuit circuit(R"CIRCUIT( I 0 X 1 Y 2 Z 3 C_XYZ 0 C_ZYX 1 H 2 H_XY 3 H_XZ 0 H_YZ 1 S 2 SQRT_X 3 SQRT_X_DAG 0 SQRT_Y 1 SQRT_Y_DAG 2 SQRT_Z 3 SQRT_Z_DAG 0 S_DAG 1 H 2 0 3 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -I-C_XYZ-H------SQRT_X_DAG-S_DAG-H- q1: -X-C_ZYX-H_YZ---SQRT_Y-----S_DAG--- q2: -Y-H-----S------SQRT_Y_DAG-H------- q3: -Z-H_XY--SQRT_X-S----------------H- )DIAGRAM"); } TEST(circuit_diagram_timeline_text, two_qubits_gates) { Circuit circuit(R"CIRCUIT( CNOT 0 1 CX 2 3 CY 4 5 5 4 CZ 0 2 ISWAP 1 3 ISWAP_DAG 2 4 SQRT_XX 3 5 SQRT_XX_DAG 0 5 SQRT_YY 3 4 4 3 SQRT_YY_DAG 0 1 SQRT_ZZ 2 3 SQRT_ZZ_DAG 4 5 SWAP 0 1 XCX 2 3 XCY 3 4 XCZ 0 1 YCX 2 3 YCY 4 5 YCZ 0 1 ZCX 2 3 ZCY 4 5 ZCZ 0 5 2 3 1 4 CXSWAP 0 1 SWAPCX 0 1 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -@-@-------------------------SQRT_XX_DAG---------SQRT_YY_DAG-SWAP----------X-Y---@-----ZSWAP-XSWAP- | | | | | | | | | | q1: -X-|-ISWAP-------------------|-------------------SQRT_YY_DAG-SWAP----------@-@---|---@-XSWAP-ZSWAP- | | | | | q2: -@-@-|-----ISWAP_DAG---------|-------------------------------SQRT_ZZ-----X---Y-@-|-@-|------------- | | | | | | | | | | | q3: -X---ISWAP-|---------SQRT_XX-|-----------SQRT_YY-SQRT_YY-----SQRT_ZZ-----X-X-X-X-|-@-|------------- | | | | | | | | q4: -@-Y-------ISWAP_DAG-|-------|-----------SQRT_YY-SQRT_YY-----SQRT_ZZ_DAG---Y-Y-@-|---@------------- | | | | | | | | q5: -Y-@-----------------SQRT_XX-SQRT_XX_DAG---------------------SQRT_ZZ_DAG-----Y-Y-@----------------- )DIAGRAM"); } TEST(circuit_diagram_timeline_text, noise_gates) { Circuit circuit(R"CIRCUIT( DEPOLARIZE1(0.125) 0 1 DEPOLARIZE2(0.125) 0 2 4 5 X_ERROR(0.125) 0 1 2 Y_ERROR(0.125) 0 1 4 Z_ERROR(0.125) 2 3 5 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -DEPOLARIZE1(0.125)-DEPOLARIZE2(0.125)-X_ERROR(0.125)-Y_ERROR(0.125)- | q1: -DEPOLARIZE1(0.125)-|------------------X_ERROR(0.125)-Y_ERROR(0.125)- | q2: --------------------DEPOLARIZE2(0.125)-X_ERROR(0.125)-Z_ERROR(0.125)- q3: ------------------------------------------------------Z_ERROR(0.125)- q4: --------------------DEPOLARIZE2(0.125)----------------Y_ERROR(0.125)- | q5: --------------------DEPOLARIZE2(0.125)----------------Z_ERROR(0.125)- )DIAGRAM"); circuit = Circuit(R"CIRCUIT( E(0.25) X1 X2 CORRELATED_ERROR(0.125) X1 Y2 Z3 ELSE_CORRELATED_ERROR(0.25) X2 Y4 Z3 ELSE_CORRELATED_ERROR(0.25) X5 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -------------------------------------------------------------------------------------- q1: -E[X](0.25)-E[X](0.125)--------------------------------------------------------------- | | q2: -E[X](0.25)-E[Y](0.125)-ELSE_CORRELATED_ERROR[X](0.25)-------------------------------- | | q3: ------------E[Z](0.125)-ELSE_CORRELATED_ERROR[Z](0.25)-------------------------------- | q4: ------------------------ELSE_CORRELATED_ERROR[Y](0.25)-------------------------------- q5: -------------------------------------------------------ELSE_CORRELATED_ERROR[X](0.25)- )DIAGRAM"); circuit = Circuit(R"CIRCUIT( PAULI_CHANNEL_1(0.125,0.25,0.125) 0 1 2 3 PAULI_CHANNEL_2(0.01,0.01,0.01,0.02,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01) 0 1 2 4 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -PAULI_CHANNEL_1(0.125,0.25,0.125)-PAULI_CHANNEL_2[0](0.01,0.01,0.01,0.02,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01)- | q1: -PAULI_CHANNEL_1(0.125,0.25,0.125)-PAULI_CHANNEL_2[1](0.01,0.01,0.01,0.02,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01)- q2: -PAULI_CHANNEL_1(0.125,0.25,0.125)-PAULI_CHANNEL_2[0](0.01,0.01,0.01,0.02,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01)- | q3: -PAULI_CHANNEL_1(0.125,0.25,0.125)-|---------------------------------------------------------------------------------------------- | q4: -----------------------------------PAULI_CHANNEL_2[1](0.01,0.01,0.01,0.02,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01)- )DIAGRAM"); } TEST(circuit_diagram_timeline_text, collapsing) { Circuit circuit(R"CIRCUIT( R 0 RX 1 RY 2 RZ 3 M(0.001) 0 1 MR 1 0 MRX 1 2 MRY 0 3 1 MRZ 0 MX 1 MY 2 MZ 3 MPP X0*Y2 Z3 X1 Z2*Y3 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -R--M(0.001):rec[0]-MR:rec[3]-MRY:rec[6]-MR:rec[9]-------------MPP[X]:rec[13]---------------- | q1: -RX-M(0.001):rec[1]-MR:rec[2]-MRX:rec[4]-MRY:rec[8]-MX:rec[10]-|--------------MPP[X]:rec[15]- | q2: -RY---------------------------MRX:rec[5]------------MY:rec[11]-MPP[Y]:rec[13]-MPP[Z]:rec[16]- | q3: -R----------------------------MRY:rec[7]------------M:rec[12]--MPP[Z]:rec[14]-MPP[Y]:rec[16]- )DIAGRAM"); } TEST(circuit_diagram_timeline_text, unphysical_observable_include) { Circuit circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(0, 1) 2 QUBIT_COORDS(1, 1) 3 OBSERVABLE_INCLUDE(0) X0 X1 OBSERVABLE_INCLUDE(1) Z0 Z2 OBSERVABLE_INCLUDE(2) X2 X3 MPP X0*X1*X2*X3 Z0*Z1 Z2*Z3 DEPOLARIZE1(0.001) 0 1 2 3 MPP X0*X1*X2*X3 Z0*Z1 Z2*Z3 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] rec[-6] OBSERVABLE_INCLUDE(0) X0 X1 OBSERVABLE_INCLUDE(1) Z0 Z2 OBSERVABLE_INCLUDE(2) X2 X3 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -QUBIT_COORDS(0,0)-L0*=X-L1*=Z-------MPP[X]:rec[0]-MPP[Z]:rec[1]-DEPOLARIZE1(0.001)-MPP[X]:rec[3]-MPP[Z]:rec[4]-DETECTOR:D1=rec[4]*rec[1]-DETECTOR:D2=rec[3]*rec[0]-L0*=X-L1*=Z------- | | | | | | | | q1: -QUBIT_COORDS(1,0)-L0*=X-|-----------MPP[X]:rec[0]-MPP[Z]:rec[1]-DEPOLARIZE1(0.001)-MPP[X]:rec[3]-MPP[Z]:rec[4]-----------------------------------------------------L0*=X-|----------- | | | | q2: -QUBIT_COORDS(0,1)-------L1*=Z-L2*=X-MPP[X]:rec[0]-MPP[Z]:rec[2]-DEPOLARIZE1(0.001)-MPP[X]:rec[3]-MPP[Z]:rec[5]-DETECTOR:D0=rec[5]*rec[2]---------------------------------L1*=Z-L2*=X- | | | | | | q3: -QUBIT_COORDS(1,1)-------------L2*=X-MPP[X]:rec[0]-MPP[Z]:rec[2]-DEPOLARIZE1(0.001)-MPP[X]:rec[3]-MPP[Z]:rec[5]-----------------------------------------------------------------L2*=X- )DIAGRAM"); } TEST(circuit_diagram_timeline_text, measurement_looping) { Circuit circuit(R"CIRCUIT( M 0 REPEAT 100 { M 1 REPEAT 5 { M 2 } REPEAT 7 { MPP X3*Y4 } } )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( /REP 100 /REP 5 \ /REP 7 \ \ q0: -M:rec[0]-|-------------------------|-----------------------------|-|----------------------------------|-|- | | | | | | q1: ----------|--------M:rec[1+iter*13]-|-----------------------------|-|----------------------------------|-|- | | | | | | q2: ----------|-------------------------|------M:rec[2+iter*13+iter2]-|-|----------------------------------|-|- | | | | | | q3: ----------|-------------------------|-----------------------------|-|------MPP[X]:rec[7+iter*13+iter2]-|-|- | | | | | | | q4: ----------|-------------------------|-----------------------------|-|------MPP[Y]:rec[7+iter*13+iter2]-|-|- \ \ / \ / / )DIAGRAM"); } TEST(circuit_diagram_timeline_text, repeat) { auto circuit = Circuit(R"CIRCUIT( H 0 1 2 REPEAT 5 { RX 2 REPEAT 100 { H 0 1 3 3 } } )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( /REP 5 /REP 100 \ \ q0: -H-|---------|--------H---|-|- | | | | q1: -H-|---------|--------H---|-|- | | | | q2: -H-|------RX-|------------|-|- | | | | q3: ---|---------|--------H-H-|-|- \ \ / / )DIAGRAM"); } TEST(circuit_diagram_timeline_text, classical_feedback) { auto circuit = Circuit(R"CIRCUIT( M 0 CX rec[-1] 1 YCZ 2 sweep[5] )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -M:rec[0]--- q1: -X^rec[0]--- q2: -Y^sweep[5]- )DIAGRAM"); } TEST(circuit_diagram_timeline_text, lattice_surgery_cnot) { auto circuit = Circuit(R"CIRCUIT( R 2 MPP X1*X2 MPP Z0*Z2 MX 2 CZ rec[-3] 0 CX rec[-2] 1 CZ rec[-1] 0 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( q0: -----------------MPP[Z]:rec[1]-Z^rec[0]--Z^rec[2]- | q1: ---MPP[X]:rec[0]-|-------------X^rec[1]----------- | | q2: -R-MPP[X]:rec[0]-MPP[Z]:rec[1]-MX:rec[2]---------- )DIAGRAM"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).transposed().str() + "\n", R"DIAGRAM( q0: q1: q2: | | | | | R | | | | MPP[X]:rec[0]-MPP[X]:rec[0] | | | MPP[Z]:rec[1]---------------MPP[Z]:rec[1] | | | Z^rec[0] X^rec[1] MX:rec[2] | | | Z^rec[2] | | | | | )DIAGRAM"); } TEST(circuit_diagram_timeline_text, tick) { auto circuit = Circuit(R"CIRCUIT( H 0 0 TICK H 0 1 TICK H 0 REPEAT 1 { H 0 1 TICK H 0 S 0 } H 0 0 SQRT_X 0 TICK H 0 0 )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( /-\ /REP 1 /-\ \ /--------\ /-\ q0: -H-H-H-H-|------H-H-S-|-H-H-SQRT_X-H-H- | | q1: -----H---|------H-----|---------------- \-/ \ \-/ / \--------/ \-/ )DIAGRAM"); } TEST(circuit_diagram_timeline_text, shifted_coords) { auto circuit = Circuit(R"CIRCUIT( QUBIT_COORDS(1, 2) 1 DETECTOR(4, 5, 6) SHIFT_COORDS(10, 20, 30, 40) QUBIT_COORDS(1, 2) 2 DETECTOR(4, 5, 6) REPEAT 100 { QUBIT_COORDS(7, 8) 3 4 DETECTOR(9, 10, 11) SHIFT_COORDS(0, 200, 300, 400) } QUBIT_COORDS(1, 2) 5 DETECTOR(4, 5, 6) )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( /REP 100 \ q0: -DETECTOR(4,5,6):D0=1-DETECTOR(14,25,36):D1=1-|--------DETECTOR(19,30+iter*200,41+iter*300):D[2+iter]=1-|-DETECTOR(14,20025,30036):D102=1- | | q1: -QUBIT_COORDS(1,2)----------------------------|---------------------------------------------------------|--------------------------------- | | q2: -QUBIT_COORDS(11,22)--------------------------|---------------------------------------------------------|--------------------------------- | | q3: ----------------------------------------------|--------QUBIT_COORDS(17,28+iter*200)---------------------|--------------------------------- | | q4: ----------------------------------------------|--------QUBIT_COORDS(17,28+iter*200)---------------------|--------------------------------- | | q5: ----------------------------------------------|---------------------------------------------------------|-QUBIT_COORDS(11,20022)---------- \ / )DIAGRAM"); } TEST(circuit_diagram_timeline_text, detector_pseudo_targets) { auto circuit = Circuit(R"CIRCUIT( M 0 1 2 3 4 5 REPEAT 100 { M 1 2 } DETECTOR(1) rec[-1] DETECTOR(2) rec[-2] DETECTOR(3) rec[-3] DETECTOR(4) rec[-4] DETECTOR(5) rec[-1] rec[-2] OBSERVABLE_INCLUDE(100) rec[-201] rec[-203] )CIRCUIT"); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( /REP 100 \ q0: -M:rec[0]-|------------------------|---------------------------------------------------------------------------------------- | | q1: -M:rec[1]-|--------M:rec[6+iter*2]-|-DETECTOR(2):D1=rec[204]-DETECTOR(4):D3=rec[202]-DETECTOR(5):D4=rec[205]*rec[204]------- | | q2: -M:rec[2]-|--------M:rec[7+iter*2]-|-DETECTOR(1):D0=rec[205]-DETECTOR(3):D2=rec[203]---------------------------------------- | | q3: -M:rec[3]-|------------------------|-------------------------------------------------OBSERVABLE_INCLUDE:L100*=rec[5]*rec[3]- | | q4: -M:rec[4]-|------------------------|---------------------------------------------------------------------------------------- | | q5: -M:rec[5]-|------------------------|---------------------------------------------------------------------------------------- \ / )DIAGRAM"); } TEST(circuit_diagram_timeline_text, surface_code) { CircuitGenParameters params(10, 3, "unrotated_memory_z"); auto circuit = generate_surface_code_circuit(params).circuit; ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( /-----------------\ /-------\ /-------\ /----------------------------------\ /REP 9 /-------\ /-------\ /-----------------------------------------------------------------------------------\ \ /------------------------------------------------------------------------------------------------------------------\ q0: -QUBIT_COORDS(0,0)-R-----------------@-------X----------------------------------------|------------------------@-------X-----------------------------------------------------------------------------------------|-M:rec[120]---------------------------------------------------------OBSERVABLE_INCLUDE:L0*=rec[122]*rec[121]*rec[120]- | | | | | | q1: -QUBIT_COORDS(1,0)-R-H-@-@-----------|-------@-H-MR:rec[0]----------------------------|--------H-@-@-----------|-------@-H-MR:rec[12+iter*12]-DETECTOR(1,0,1+iter):D[6+iter*12]=rec[12+iter*12]*rec[0+iter*12]---|---------------------------------------------------------------------------------------------------------------------- | | | | | | | | q2: -QUBIT_COORDS(2,0)-R---X-|-----------|-@-----X----------------------------------------|----------X-|-----------|-@-----X-----------------------------------------------------------------------------------------|-M:rec[121]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | q3: -QUBIT_COORDS(3,0)-R-H-@-|-@---------|-|-----@-H-MR:rec[1]----------------------------|--------H-@-|-@---------|-|-----@-H-MR:rec[13+iter*12]-DETECTOR(3,0,1+iter):D[7+iter*12]=rec[13+iter*12]*rec[1+iter*12]---|---------------------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | q4: -QUBIT_COORDS(4,0)-R---X-|-|---------|-|-@--------------------------------------------|----------X-|-|---------|-|-@---------------------------------------------------------------------------------------------|-M:rec[122]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | q5: -QUBIT_COORDS(0,1)-R---X-|-|-X-------X-|-|-------MR:rec[2]--DETECTOR(0,1,0):D0=rec[2]-|----------X-|-|-X-------X-|-|-------MR:rec[14+iter*12]-DETECTOR(0,1,1+iter):D[8+iter*12]=rec[14+iter*12]*rec[2+iter*12]---|-DETECTOR(0,1,10):D114=rec[125]*rec[123]*rec[120]*rec[110]------------------------------------------------------------ | | | | | | | | | | | | | | q6: -QUBIT_COORDS(1,1)-R---@-X-|-|-----X---|-|---@----------------------------------------|----------@-X-|-|-----X---|-|---@-----------------------------------------------------------------------------------------|-M:rec[123]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | | | q7: -QUBIT_COORDS(2,1)-R---X---|-|-X---|---X-|---X---MR:rec[3]--DETECTOR(2,1,0):D2=rec[3]-|----------X---|-|-X---|---X-|---X---MR:rec[15+iter*12]-DETECTOR(2,1,1+iter):D[9+iter*12]=rec[15+iter*12]*rec[3+iter*12]---|-DETECTOR(2,1,10):D116=rec[126]*rec[124]*rec[123]*rec[121]*rec[111]--------------------------------------------------- | | | | | | | | | | | | | | q8: -QUBIT_COORDS(3,1)-R---@---X-|-|---|-X---|---@----------------------------------------|----------@---X-|-|---|-X---|---@-----------------------------------------------------------------------------------------|-M:rec[124]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | | | q9: -QUBIT_COORDS(4,1)-R---------|-|-X-|-|---X---X---MR:rec[4]--DETECTOR(4,1,0):D4=rec[4]-|----------------|-|-X-|-|---X---X---MR:rec[16+iter*12]-DETECTOR(4,1,1+iter):D[10+iter*12]=rec[16+iter*12]*rec[4+iter*12]--|-DETECTOR(4,1,10):D118=rec[127]*rec[124]*rec[122]*rec[112]------------------------------------------------------------ | | | | | | | | | | | | q10: -QUBIT_COORDS(0,2)-R---------@-|-|-|-|-@-----X----------------------------------------|----------------@-|-|-|-|-@-----X-----------------------------------------------------------------------------------------|-M:rec[125]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | | | q11: -QUBIT_COORDS(1,2)-R-H-@-@-----|-|-@-|-|-----@-H-MR:rec[5]----------------------------|--------H-@-@-----|-|-@-|-|-----@-H-MR:rec[17+iter*12]-DETECTOR(1,2,1+iter):D[11+iter*12]=rec[17+iter*12]*rec[5+iter*12]--|---------------------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | | | q12: -QUBIT_COORDS(2,2)-R---X-|-----@-|---|-|-@---X----------------------------------------|----------X-|-----@-|---|-|-@---X-----------------------------------------------------------------------------------------|-M:rec[126]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | | | q13: -QUBIT_COORDS(3,2)-R-H-@-|-@-----|---@-|-|---@-H-MR:rec[6]----------------------------|--------H-@-|-@-----|---@-|-|---@-H-MR:rec[18+iter*12]-DETECTOR(3,2,1+iter):D[12+iter*12]=rec[18+iter*12]*rec[6+iter*12]--|---------------------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | | | q14: -QUBIT_COORDS(4,2)-R---X-|-|-----@-----|-|-@------------------------------------------|----------X-|-|-----@-----|-|-@-------------------------------------------------------------------------------------------|-M:rec[127]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | q15: -QUBIT_COORDS(0,3)-R---X-|-|-X---------X-|-|-----MR:rec[7]--DETECTOR(0,3,0):D1=rec[7]-|----------X-|-|-X---------X-|-|-----MR:rec[19+iter*12]-DETECTOR(0,3,1+iter):D[13+iter*12]=rec[19+iter*12]*rec[7+iter*12]--|-DETECTOR(0,3,10):D115=rec[130]*rec[128]*rec[125]*rec[115]------------------------------------------------------------ | | | | | | | | | | | | | | q16: -QUBIT_COORDS(1,3)-R---@-X-|-|-----X-----|-|-@----------------------------------------|----------@-X-|-|-----X-----|-|-@-----------------------------------------------------------------------------------------|-M:rec[128]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | | | q17: -QUBIT_COORDS(2,3)-R---X---|-|-X---|-----X-|-X---MR:rec[8]--DETECTOR(2,3,0):D3=rec[8]-|----------X---|-|-X---|-----X-|-X---MR:rec[20+iter*12]-DETECTOR(2,3,1+iter):D[14+iter*12]=rec[20+iter*12]*rec[8+iter*12]--|-DETECTOR(2,3,10):D117=rec[131]*rec[129]*rec[128]*rec[126]*rec[116]--------------------------------------------------- | | | | | | | | | | | | | | q18: -QUBIT_COORDS(3,3)-R---@---X-|-|---|-X-----|-@----------------------------------------|----------@---X-|-|---|-X-----|-@-----------------------------------------------------------------------------------------|-M:rec[129]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | | | q19: -QUBIT_COORDS(4,3)-R---------|-|-X-|-|-----X-X---MR:rec[9]--DETECTOR(4,3,0):D5=rec[9]-|----------------|-|-X-|-|-----X-X---MR:rec[21+iter*12]-DETECTOR(4,3,1+iter):D[15+iter*12]=rec[21+iter*12]*rec[9+iter*12]--|-DETECTOR(4,3,10):D119=rec[132]*rec[129]*rec[127]*rec[117]------------------------------------------------------------ | | | | | | | | | | | | q20: -QUBIT_COORDS(0,4)-R---------@-|-|-|-|-------X----------------------------------------|----------------@-|-|-|-|-------X-----------------------------------------------------------------------------------------|-M:rec[130]----------------------------------------------------------------------------------------------------------- | | | | | | | | | | | | q21: -QUBIT_COORDS(1,4)-R-H-@-------|-|-@-|-------@-H-MR:rec[10]---------------------------|--------H-@-------|-|-@-|-------@-H-MR:rec[22+iter*12]-DETECTOR(1,4,1+iter):D[16+iter*12]=rec[22+iter*12]*rec[10+iter*12]-|---------------------------------------------------------------------------------------------------------------------- | | | | | | | | | | q22: -QUBIT_COORDS(2,4)-R---X-------@-|---|-------X----------------------------------------|----------X-------@-|---|-------X-----------------------------------------------------------------------------------------|-M:rec[131]----------------------------------------------------------------------------------------------------------- | | | | | | | | q23: -QUBIT_COORDS(3,4)-R-H-@---------|---@-------@-H-MR:rec[11]---------------------------|--------H-@---------|---@-------@-H-MR:rec[23+iter*12]-DETECTOR(3,4,1+iter):D[17+iter*12]=rec[23+iter*12]*rec[11+iter*12]-|---------------------------------------------------------------------------------------------------------------------- | | | | | | q24: -QUBIT_COORDS(4,4)-R---X---------@----------------------------------------------------|----------X---------@-----------------------------------------------------------------------------------------------------|-M:rec[132]----------------------------------------------------------------------------------------------------------- \-----------------/ \-------/ \-------/ \----------------------------------/ \ \-------/ \-------/ \-----------------------------------------------------------------------------------/ / \------------------------------------------------------------------------------------------------------------------/ )DIAGRAM"); } TEST(circuit_diagram_timeline_text, repetition_code) { CircuitGenParameters params(10, 9, "memory"); auto circuit = generate_rep_code_circuit(params).circuit; ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( /--------------------------------\ /REP 9 /-----------------------------------------------------------------------------\ \ /---------------------------------------------------\ q0: -R-@--------------------------------------|--------@-----------------------------------------------------------------------------------|-M:rec[80]-DETECTOR(1,10):D80=rec[81]*rec[80]*rec[72]-- | | | | q1: -R-X-X-MR:rec[0]-DETECTOR(1,0):D0=rec[0]--|--------X-X-MR:rec[8+iter*8]--DETECTOR(1,1+iter):D[8+iter*8]=rec[8+iter*8]*rec[0+iter*8]----|------------------------------------------------------- | | | | q2: -R-@-@------------------------------------|--------@-@---------------------------------------------------------------------------------|-M:rec[81]-DETECTOR(3,10):D81=rec[82]*rec[81]*rec[73]-- | | | | q3: -R-X-X-MR:rec[1]-DETECTOR(3,0):D1=rec[1]--|--------X-X-MR:rec[9+iter*8]--DETECTOR(3,1+iter):D[9+iter*8]=rec[9+iter*8]*rec[1+iter*8]----|------------------------------------------------------- | | | | q4: -R-@-@------------------------------------|--------@-@---------------------------------------------------------------------------------|-M:rec[82]-DETECTOR(5,10):D82=rec[83]*rec[82]*rec[74]-- | | | | q5: -R-X-X-MR:rec[2]-DETECTOR(5,0):D2=rec[2]--|--------X-X-MR:rec[10+iter*8]-DETECTOR(5,1+iter):D[10+iter*8]=rec[10+iter*8]*rec[2+iter*8]--|------------------------------------------------------- | | | | q6: -R-@-@------------------------------------|--------@-@---------------------------------------------------------------------------------|-M:rec[83]-DETECTOR(7,10):D83=rec[84]*rec[83]*rec[75]-- | | | | q7: -R-X-X-MR:rec[3]-DETECTOR(7,0):D3=rec[3]--|--------X-X-MR:rec[11+iter*8]-DETECTOR(7,1+iter):D[11+iter*8]=rec[11+iter*8]*rec[3+iter*8]--|------------------------------------------------------- | | | | q8: -R-@-@------------------------------------|--------@-@---------------------------------------------------------------------------------|-M:rec[84]-DETECTOR(9,10):D84=rec[85]*rec[84]*rec[76]-- | | | | q9: -R-X-X-MR:rec[4]-DETECTOR(9,0):D4=rec[4]--|--------X-X-MR:rec[12+iter*8]-DETECTOR(9,1+iter):D[12+iter*8]=rec[12+iter*8]*rec[4+iter*8]--|------------------------------------------------------- | | | | q10: -R-@-@------------------------------------|--------@-@---------------------------------------------------------------------------------|-M:rec[85]-DETECTOR(11,10):D85=rec[86]*rec[85]*rec[77]- | | | | q11: -R-X-X-MR:rec[5]-DETECTOR(11,0):D5=rec[5]-|--------X-X-MR:rec[13+iter*8]-DETECTOR(11,1+iter):D[13+iter*8]=rec[13+iter*8]*rec[5+iter*8]-|------------------------------------------------------- | | | | q12: -R-@-@------------------------------------|--------@-@---------------------------------------------------------------------------------|-M:rec[86]-DETECTOR(13,10):D86=rec[87]*rec[86]*rec[78]- | | | | q13: -R-X-X-MR:rec[6]-DETECTOR(13,0):D6=rec[6]-|--------X-X-MR:rec[14+iter*8]-DETECTOR(13,1+iter):D[14+iter*8]=rec[14+iter*8]*rec[6+iter*8]-|------------------------------------------------------- | | | | q14: -R-@-@------------------------------------|--------@-@---------------------------------------------------------------------------------|-M:rec[87]-DETECTOR(15,10):D87=rec[88]*rec[87]*rec[79]- | | | | q15: -R-X-X-MR:rec[7]-DETECTOR(15,0):D7=rec[7]-|--------X-X-MR:rec[15+iter*8]-DETECTOR(15,1+iter):D[15+iter*8]=rec[15+iter*8]*rec[7+iter*8]-|------------------------------------------------------- | | | | q16: -R---@------------------------------------|----------@---------------------------------------------------------------------------------|-M:rec[88]-OBSERVABLE_INCLUDE:L0*=rec[88]-------------- \--------------------------------/ \ \-----------------------------------------------------------------------------/ / \---------------------------------------------------/ )DIAGRAM"); } TEST(circuit_diagram_timeline_text, repetition_code_transposed) { CircuitGenParameters params(10, 3, "memory"); auto circuit = generate_rep_code_circuit(params).circuit; ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).transposed().str() + "\n", R"DIAGRAM( q0: q1: q2: q3: q4: | | | | | R R R R R | | | | | @--------------------------------------------------X @--------------------------------------------------X | | | | | | | X--------------------------------------------------@ X--------------------------------------------@ | | | | | / | MR:rec[0] | MR:rec[1] | \ | | | | | | | \ | DETECTOR(1,0):D0=rec[0] | DETECTOR(3,0):D1=rec[1] | / | | | | | /REP 9------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\ | | | | | | | | | | | | | | | @--------------------------------------------------X @--------------------------------------------------X | | | | | | | X--------------------------------------------------@ X--------------------------------------------@ | | | | | / | MR:rec[2+iter*2] | MR:rec[3+iter*2] | \ | | | | | | | \ | DETECTOR(1,1+iter):D[2+iter*2]=rec[2+iter*2]*rec[0+iter*2] | DETECTOR(3,1+iter):D[3+iter*2]=rec[3+iter*2]*rec[1+iter*2] | / | | | | | \-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/ | | | | | / M:rec[20] | M:rec[21] | M:rec[22] \ | | | | | | | \ DETECTOR(1,10):D20=rec[21]*rec[20]*rec[18] | DETECTOR(3,10):D21=rec[22]*rec[21]*rec[19] | OBSERVABLE_INCLUDE:L0*=rec[22]/ | | | | | )DIAGRAM"); } TEST(circuit_diagram_timeline_text, test_circuit_all_ops) { auto circuit = generate_test_circuit_with_all_operations(); ASSERT_EQ("\n" + DiagramTimelineAsciiDrawer::make_diagram(circuit).str() + "\n", R"DIAGRAM( /-------------------\ /---------------------\ /---------------------\ /-----------------------------------------------------------------------------------------------------------------------------------------------------\ /------------------------------------------------------\ /REP 3 /---\ \ /--------------------------------------------------------------------------------------------------------------------------\ /----------------------------------------\ q0: -QUBIT_COORDS(1,2,3)-I-C_XYZ--H_XY--SQRT_X-----ZSWAP-----SQRT_XX-----X------------DEPOLARIZE1(0.02)---------------X_ERROR(0.01)------------------------------------------------------------------------------------------------MPP[X]:rec[2]-MPP[Z]:rec[3]-SPP[X]-SPP_DAG[X]------------MRX:rec[4]-MXX:rec[11]-|------H-@---|---MR:rec[15]-X_ERROR(0.1)-MR(0.01):rec[16]-DETECTOR(2,4,6):D0=rec[16]-OBSERVABLE_INCLUDE:L0*=rec[16]-MPAD:rec[17]-MPAD:rec[19]-MRX:rec[20]--------------------------------X^rec[24]-- | | | | | | | | | | | q1: ---------------------X-C_NXYZ-H-----SQRT_X_DAG-XSWAP-----SQRT_XX-----X-E[X](0.01)-DEPOLARIZE2(0.03)---------------Y_ERROR(0.02)------------------------------------------------------------------------------------------------MPP[Y]:rec[2]-MPP[Z]:rec[3]-SPP[Y]-SPP_DAG[Y]------------MRY:rec[5]-MXX---------|--------X-S-|------------------------------------------------------------------------------------------------------MPAD:rec[18]--------------MY:rec[21]---------------------------------Y^sweep[0]- | | | | | | | q2: ---------------------Y-C_XNYZ-H_YZ--SQRT_Y-----ISWAP-----SQRT_XX_DAG-X-E[Y](0.01)-DEPOLARIZE2(0.03)---------------Z_ERROR(0.03)------------------------------------------------------------------------------------------------MPP[Z]:rec[2]---------------SPP[Z]-SPP_DAG[Z]-SPP_DAG[X]-MR:rec[6]--MXX:rec[12]-|------------|-------------------------------------------------------------------------------------------------------------------L1*=Z--------MZZ:rec[22]-OBSERVABLE_INCLUDE:L1*=rec[22]-Z^rec[24]-- | | | | | | | | | q3: ---------------------Z-C_XYNZ-H_NXY-SQRT_Y_DAG-ISWAP-----SQRT_XX_DAG-Y-E[Z](0.01)-PAULI_CHANNEL_1(0.01,0.02,0.03)-HERALDED_ERASE(0.04):rec[0]--------------------------------------------------------------------------------------------------------------SPP[X]-----------------------MX:rec[7]--MXX---------|------------|-------------------------------------------------------------------------------------------------------------------L1*=Z--------MZZ--------------------------------------------------- | | q4: -----------------------C_ZYX--H_NXZ-S----------ISWAP_DAG-SQRT_YY-----X------------ELSE_CORRELATED_ERROR[X](0.02)--PAULI_CHANNEL_2[0](0.001,0.002,0.003,0.004,0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015)----------------------------------------------------------MY:rec[8]--MYY:rec[13]-|------------|--------------------------------------------------------------------------------------------------------------------------------------------MYY:rec[23]------------------------------- | | | | | | | | | q5: -----------------------C_NZYX-H_NYZ-S_DAG------ISWAP_DAG-SQRT_YY-----@------------|-------------------------------PAULI_CHANNEL_2[1](0.001,0.002,0.003,0.004,0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015)----------------------------------------------------------M:rec[9]---MYY---------|------------|--------------------------------------------------------------------------------------------------------------------------------------------MYY--------------------------------------- | | | q6: -----------------------C_ZNYX------------------SWAP------SQRT_YY_DAG-Y------------ELSE_CORRELATED_ERROR[Z](0.02)--HERALDED_PAULI_CHANNEL_1(0.01,0.02,0.03,0.04):rec[1]------------------------------------------------------------------------------------------------------------------M:rec[10]--MZZ:rec[14]-|------------|--------------------------------------------------------------------------------------------------------------------------------------------MPP[X]:rec[24]---------------------------- | | | | | | | | q7: -----------------------C_ZYNX------------------SWAP------SQRT_YY_DAG-X------------ELSE_CORRELATED_ERROR[Y](0.02)--I_ERROR(0.06)---------------------------------------------------------------------------------------------------------------------------------------------------------RX---------MZZ---------|------------|--------------------------------------------------------------------------------------------------------------------------------------------MPP[Y]:rec[24]---------------------------- | | | q8: -----------------------------------------------XSWAP-----SQRT_ZZ-----Y--------------------------------------------II_ERROR(0.07)--------------------------------------------------------------------------------------------------------------------------------------------------------RY---------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------MPP[Z]:rec[24]---------------------------- | | | | | | q9: -----------------------------------------------ZSWAP-----SQRT_ZZ-----Y--------------------------------------------II_ERROR(0.07)--------------------------------------------------------------------------------------------------------------------------------------------------------R----------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | q10: -----------------------------------------------ZSWAP-----SQRT_ZZ_DAG-Y-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | | | q11: -----------------------------------------------ZSWAP-----SQRT_ZZ_DAG-@-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | q12: ---------------------------------------------------------II----------@-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | | q13: ---------------------------------------------------------II----------X-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | q14: ---------------------------------------------------------------------@-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | q15: ---------------------------------------------------------------------Y-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | q16: ---------------------------------------------------------------------@-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | q17: ---------------------------------------------------------------------@-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \-------------------/ \---------------------/ \---------------------/ \-----------------------------------------------------------------------------------------------------------------------------------------------------/ \------------------------------------------------------/ \ \---/ / \--------------------------------------------------------------------------------------------------------------------------/ \----------------------------------------/ )DIAGRAM"); } ================================================ FILE: src/stim/diagram/timeline/timeline_svg_drawer.cc ================================================ #include "stim/diagram/timeline/timeline_svg_drawer.h" #include "stim/circuit/gate_decomposition.h" #include "stim/diagram/circuit_timeline_helper.h" #include "stim/diagram/coord.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/diagram_util.h" #include "stim/stabilizers/pauli_string.h" using namespace stim; using namespace stim_draw_internal; constexpr uint16_t TIME_SLICE_PADDING = 64; constexpr uint16_t PADDING = 32; constexpr uint16_t CIRCUIT_START_X = 32; constexpr uint16_t CIRCUIT_START_Y = 32; constexpr uint16_t GATE_PITCH = 64; constexpr uint16_t GATE_RADIUS = 16; constexpr uint16_t CONTROL_RADIUS = 12; constexpr float SLICE_WINDOW_GAP = 1.1f; template inline void write_key_val(std::ostream &out, const char *key, const T &val) { out << ' ' << key << "=\"" << val << "\""; } size_t DiagramTimelineSvgDrawer::m2x(size_t m) const { return GATE_PITCH * m + GATE_RADIUS + GATE_RADIUS + CIRCUIT_START_X + PADDING; } Coord<2> DiagramTimelineSvgDrawer::qt2xy(uint64_t tick, uint64_t moment_delta, size_t q) const { if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { Coord<2> result = coord_sys.qubit_coords[q]; result.xyz[0] += moment_delta * 14; result.xyz[1] += moment_delta * 16; result.xyz[0] += TIME_SLICE_PADDING; result.xyz[1] += TIME_SLICE_PADDING; uint64_t s = tick - min_tick; uint64_t sx = s % num_cols; uint64_t sy = s / num_cols; result.xyz[0] += coord_sys.size.xyz[0] * sx * SLICE_WINDOW_GAP; result.xyz[1] += coord_sys.size.xyz[1] * sy * SLICE_WINDOW_GAP; return result; } return {(float)m2x(cur_moment), (float)q2y(q)}; } Coord<2> DiagramTimelineSvgDrawer::q2xy(size_t q) const { return qt2xy(resolver.num_ticks_seen, cur_moment - tick_start_moment, q); } size_t DiagramTimelineSvgDrawer::q2y(size_t q) const { return GATE_PITCH * q + CIRCUIT_START_Y + PADDING; } void DiagramTimelineSvgDrawer::do_feedback( std::string_view gate, const GateTarget &qubit_target, const GateTarget &feedback_target) { std::stringstream exponent; if (feedback_target.is_sweep_bit_target()) { exponent << "sweep"; if (mode == DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { exponent << "[" << feedback_target.value() << "]"; } } else if (feedback_target.is_measurement_record_target()) { exponent << "rec"; if (mode == DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { exponent << "[" << (feedback_target.value() + resolver.measure_offset) << "]"; } } auto c = q2xy(qubit_target.qubit_value()); draw_annotated_gate( c.xyz[0], c.xyz[1], SvgGateData{ (uint16_t)(mode == DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE ? 2 : 1), std::string(gate), "", exponent.str(), "lightgray", "black", 0, 10}, {}); } void DiagramTimelineSvgDrawer::draw_x_control(float cx, float cy) { svg_out << "\n"; svg_out << "\n"; } void DiagramTimelineSvgDrawer::draw_y_control(float cx, float cy) { float r = CONTROL_RADIUS * 1.4; float r_sin = r * sqrtf(3) * 0.5; float r_cos = r * 0.5; svg_out << "\n"; } void DiagramTimelineSvgDrawer::draw_z_control(float cx, float cy) { svg_out << "\n"; } void DiagramTimelineSvgDrawer::draw_xswap_control(float cx, float cy) { svg_out << "\n"; float r = CONTROL_RADIUS / 2.2f; svg_out << "\n"; } void DiagramTimelineSvgDrawer::draw_zswap_control(float cx, float cy) { svg_out << "\n"; float r = CONTROL_RADIUS / 2.2f; svg_out << "\n"; } void DiagramTimelineSvgDrawer::draw_swap_control(float cx, float cy) { float r = CONTROL_RADIUS / 2.0f; svg_out << "\n"; } void DiagramTimelineSvgDrawer::draw_iswap_control(float cx, float cy, bool inverse) { svg_out << "\n"; draw_swap_control(cx, cy); if (inverse) { svg_out << "\n"; } } void DiagramTimelineSvgDrawer::draw_generic_box( float cx, float cy, std::string_view text, SpanRef end_args) { auto f = gate_data_map.find(text); if (f == gate_data_map.end()) { throw std::invalid_argument( "DiagramTimelineSvgDrawer::draw_generic_box unhandled gate case: " + std::string(text)); } SvgGateData data = f->second; draw_annotated_gate(cx, cy, data, end_args); } void DiagramTimelineSvgDrawer::draw_annotated_gate( float cx, float cy, const SvgGateData &data, SpanRef end_args) { cx += (data.span - 1) * GATE_PITCH * 0.5f; float w = GATE_PITCH * (data.span - 1) + GATE_RADIUS * 2.0f; float h = GATE_RADIUS * 2.0f; size_t n = utf8_char_count(data.body) + utf8_char_count(data.subscript) + +utf8_char_count(data.superscript); auto font_size = data.font_size != 0 ? data.font_size : n == 1 ? 30 : n >= 4 && data.span == 1 ? 12 : 16; svg_out << "\n"; moment_width = std::max(moment_width, data.span); svg_out << ""; svg_out << data.body; if (data.superscript[0] != '\0') { svg_out << ""; svg_out << data.superscript; svg_out << ""; } if (data.subscript[0] != '\0') { svg_out << ""; svg_out << data.subscript; svg_out << ""; } svg_out << "\n"; if (!end_args.empty()) { svg_out << ""; svg_out << comma_sep(end_args, ","); svg_out << "\n"; } } void DiagramTimelineSvgDrawer::draw_two_qubit_gate_end_point( float cx, float cy, std::string_view type, SpanRef args) { if (type == "X") { draw_x_control(cx, cy); } else if (type == "Y") { draw_y_control(cx, cy); } else if (type == "Z") { draw_z_control(cx, cy); } else if (type == "SWAP") { draw_swap_control(cx, cy); } else if (type == "ISWAP") { draw_iswap_control(cx, cy, false); } else if (type == "ISWAP_DAG") { draw_iswap_control(cx, cy, true); } else if (type == "XSWAP") { draw_xswap_control(cx, cy); } else if (type == "ZSWAP") { draw_zswap_control(cx, cy); } else { draw_generic_box(cx, cy, type, args); } } void DiagramTimelineSvgDrawer::do_two_qubit_gate_instance(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); const GateTarget &target1 = op.targets[0]; const GateTarget &target2 = op.targets[1]; auto ends = two_qubit_gate_pieces(op.gate_type); if (target1.is_measurement_record_target() || target1.is_sweep_bit_target()) { do_feedback(ends.second, target2, target1); return; } if (target2.is_measurement_record_target() || target2.is_sweep_bit_target()) { do_feedback(ends.first, target1, target2); return; } auto pieces = two_qubit_gate_pieces(op.gate_type); std::string piece1 = std::string(pieces.first); std::string piece2 = std::string(pieces.second); if (op.gate_type == GateType::PAULI_CHANNEL_2) { piece1.append("[0]"); piece2.append("[1]"); } auto c1 = q2xy(target1.qubit_value()); auto c2 = q2xy(target2.qubit_value()); bool b = c1.xyz[1] > c2.xyz[1]; draw_two_qubit_gate_end_point(c1.xyz[0], c1.xyz[1], piece1, b ? op.args : SpanRef{}); draw_two_qubit_gate_end_point(c2.xyz[0], c2.xyz[1], piece2, !b ? op.args : SpanRef{}); } void DiagramTimelineSvgDrawer::start_next_moment() { cur_moment += moment_width; moment_width = 1; cur_moment_is_used = false; cur_moment_used_flags.clear(); cur_moment_used_flags.resize(num_qubits); } void DiagramTimelineSvgDrawer::do_tick() { if (has_ticks && cur_moment > tick_start_moment && mode == DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { float x1 = (float)m2x(tick_start_moment); float x2 = (float)m2x(cur_moment + moment_width - 1); float y1 = PADDING; float y2 = (float)q2y(num_qubits); x1 -= GATE_PITCH * 0.4375; x2 += GATE_PITCH * 0.4375; svg_out << "\n"; svg_out << "\n"; } start_next_moment(); tick_start_moment = cur_moment; } void DiagramTimelineSvgDrawer::do_single_qubit_gate_instance(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); const auto &target = op.targets[0]; std::stringstream ss; const auto &gate_data = GATE_DATA[op.gate_type]; ss << gate_data.name; auto c = q2xy(target.qubit_value()); draw_generic_box(c.xyz[0], c.xyz[1], ss.str(), op.args); if (gate_data.flags & GATE_PRODUCES_RESULTS) { draw_rec(c.xyz[0], c.xyz[1]); } } void DiagramTimelineSvgDrawer::write_det_index(std::ostream &out) { out.put('D'); if (!resolver.cur_loop_nesting.empty()) { out.put('['); } out << resolver.detector_offset; for (size_t k = 0; k < resolver.cur_loop_nesting.size(); k++) { out << "+iter"; if (k > 0) { out << (k + 1); } auto p = resolver.cur_loop_nesting[k].detectors_per_iteration; if (p != 1) { out << '*' << p; } } if (!resolver.cur_loop_nesting.empty()) { out.put(']'); } } void DiagramTimelineSvgDrawer::write_rec_index(std::ostream &out, int64_t lookback_shift) { out << "rec["; out << resolver.measure_offset + (decltype(resolver.measure_offset))lookback_shift; for (size_t k = 0; k < resolver.cur_loop_nesting.size(); k++) { auto n = resolver.cur_loop_nesting[k].measurements_per_iteration; if (n != 0) { out << "+iter"; if (k > 0) { out << (k + 1); } if (n != 1) { out << '*' << n; } } } out << ']'; } void DiagramTimelineSvgDrawer::write_coords(std::ostream &out, SpanRef relative_coordinates) { out.put('('); for (size_t k = 0; k < relative_coordinates.size(); k++) { if (k) { out.put(','); } write_coord(out, k, relative_coordinates[k]); } out.put(')'); } void DiagramTimelineSvgDrawer::write_coord(std::ostream &out, size_t coord_index, double absolute_coordinate) { out << absolute_coordinate; for (size_t k = 0; k < resolver.cur_loop_nesting.size(); k++) { const auto &p = resolver.cur_loop_nesting[k].shift_per_iteration; if (coord_index < p.size() && p[coord_index] != 0) { out << "+iter"; if (k > 0) { out << (k + 1); } if (p[coord_index] != 1) { out << '*' << p[coord_index]; } } } } std::pair compute_minmax_q(SpanRef targets) { size_t min_q = SIZE_MAX; size_t max_q = 0; for (const auto &t : targets) { if (t.is_combiner() || t.is_measurement_record_target() || t.is_sweep_bit_target()) { continue; } size_t q = t.qubit_value(); min_q = std::min(min_q, q); max_q = std::max(max_q, q); } return {min_q, max_q}; } void DiagramTimelineSvgDrawer::reserve_drawing_room_for_targets(SpanRef targets) { if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { for (const auto &t : targets) { if (t.has_qubit_value() && cur_moment_used_flags[t.qubit_value()]) { start_next_moment(); break; } } std::vector> coords; for (const auto &t : targets) { if (t.has_qubit_value()) { cur_moment_used_flags[t.qubit_value()] = true; coords.push_back(q2xy(t.qubit_value())); } } cur_moment_is_used = true; if (coords.size() > 1) { svg_out << "{-dp.xyz[1], dp.xyz[0]}; if (2 * dp2.xyz[0] + 3 * dp2.xyz[1] < 0) { dp2 *= -1; } auto p3 = p1 + dp * 0.2 + dp2 * 0.2; auto p4 = p2 + dp * -0.2 + dp2 * 0.2; svg_out << "C"; svg_out << p3.xyz[0] << " " << p3.xyz[1] << ","; svg_out << p4.xyz[0] << " " << p4.xyz[1] << ","; svg_out << p2.xyz[0] << " " << p2.xyz[1] << " "; } } svg_out << "\""; write_key_val(svg_out, "fill", "none"); write_key_val(svg_out, "stroke", "black"); write_key_val(svg_out, "stroke-width", "5"); svg_out << "/>\n"; } return; } auto minmax_q = compute_minmax_q(targets); auto min_q = minmax_q.first; auto max_q = minmax_q.second; if (min_q == SIZE_MAX) { return; } for (size_t q = min_q; q <= max_q; q++) { if (cur_moment_used_flags[q]) { start_next_moment(); break; } } for (size_t q = min_q; q <= max_q; q++) { cur_moment_used_flags[q] = true; } cur_moment_is_used = true; if (min_q < max_q) { auto x = m2x(cur_moment); auto y1 = q2y(min_q); auto y2 = q2y(max_q); svg_out << "\n"; } } void DiagramTimelineSvgDrawer::draw_rec(float cx, float cy) { if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { return; } svg_out << ""; write_rec_index(svg_out); svg_out << "\n"; } void DiagramTimelineSvgDrawer::do_multi_qubit_gate_with_pauli_targets(const ResolvedTimelineOperation &op) { reserve_drawing_room_for_targets(op.targets); auto minmax_q = compute_minmax_q(op.targets); for (const auto &t : op.targets) { if (t.is_combiner()) { continue; } std::stringstream ss; const auto &gate_data = GATE_DATA[op.gate_type]; ss << gate_data.name; if (t.is_x_target()) { ss << "[X]"; } else if (t.is_y_target()) { ss << "[Y]"; } else if (t.is_z_target()) { ss << "[Z]"; } auto c = q2xy(t.qubit_value()); draw_generic_box( c.xyz[0], c.xyz[1], ss.str(), t.qubit_value() == minmax_q.second ? op.args : SpanRef{}); if (gate_data.flags & GATE_PRODUCES_RESULTS && t.qubit_value() == minmax_q.first) { draw_rec(c.xyz[0], c.xyz[1]); } } } void DiagramTimelineSvgDrawer::do_start_repeat(const CircuitTimelineLoopData &loop_data) { if (resolver.num_ticks_seen < min_tick || resolver.num_ticks_seen > max_tick) { return; } if (cur_moment_is_used) { do_tick(); } if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { return; } auto x = m2x(cur_moment); auto y1 = PADDING; auto y2 = q2y(num_qubits); y1 += (resolver.cur_loop_nesting.size() - 1) * 4; y2 -= (resolver.cur_loop_nesting.size() - 1) * 4; svg_out << "\n"; svg_out << ""; svg_out << "REP" << loop_data.num_repetitions; svg_out << "\n"; start_next_moment(); tick_start_moment = cur_moment; } void DiagramTimelineSvgDrawer::do_end_repeat(const CircuitTimelineLoopData &loop_data) { if (resolver.num_ticks_seen < min_tick || resolver.num_ticks_seen > max_tick) { return; } if (cur_moment_is_used) { do_tick(); } if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { return; } auto x = m2x(cur_moment); auto y1 = PADDING; auto y2 = q2y(num_qubits); y1 += (resolver.cur_loop_nesting.size() - 1) * 4; y2 -= (resolver.cur_loop_nesting.size() - 1) * 4; svg_out << "\n"; start_next_moment(); tick_start_moment = cur_moment; } void DiagramTimelineSvgDrawer::do_mpp(const ResolvedTimelineOperation &op) { do_multi_qubit_gate_with_pauli_targets(op); } void DiagramTimelineSvgDrawer::do_spp(const ResolvedTimelineOperation &op) { do_multi_qubit_gate_with_pauli_targets(op); } void DiagramTimelineSvgDrawer::do_correlated_error(const ResolvedTimelineOperation &op) { if (cur_moment_is_used) { start_next_moment(); } do_multi_qubit_gate_with_pauli_targets(op); } void DiagramTimelineSvgDrawer::do_else_correlated_error(const ResolvedTimelineOperation &op) { do_correlated_error(op); } void DiagramTimelineSvgDrawer::do_qubit_coords(const ResolvedTimelineOperation &op) { if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { return; } reserve_drawing_room_for_targets(op.targets); assert(op.targets.size() == 1); const auto &target = op.targets[0]; std::stringstream ss; ss << "COORDS"; write_coords(ss, op.args); auto c = q2xy(target.qubit_value()); draw_annotated_gate( c.xyz[0], c.xyz[1], SvgGateData{(uint16_t)(2 + op.args.size()), ss.str(), "", "", "white", "black", 0, 10}, {}); } void DiagramTimelineSvgDrawer::do_detector(const ResolvedTimelineOperation &op) { if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { return; } reserve_drawing_room_for_targets(op.targets); auto pseudo_target = op.targets[0]; SpanRef rec_targets = op.targets; rec_targets.ptr_start++; auto c = q2xy(pseudo_target.qubit_value()); auto span = (uint16_t)(1 + std::max(std::max(op.targets.size(), op.args.size()), (size_t)2)); draw_annotated_gate(c.xyz[0], c.xyz[1], SvgGateData{span, "DETECTOR", "", "", "lightgray", "black", 0, 10}, {}); c.xyz[0] += (span - 1) * GATE_PITCH * 0.5f; if (!op.args.empty()) { svg_out << "coords="; write_coords(svg_out, op.args); svg_out << "\n"; } svg_out << ""; write_det_index(svg_out); svg_out << " = "; for (size_t k = 0; k < rec_targets.size(); k++) { if (k) { svg_out << "*"; } write_rec_index(svg_out, rec_targets[k].value()); } if (rec_targets.empty()) { svg_out << "1 (vacuous)"; } svg_out << "\n"; } void DiagramTimelineSvgDrawer::do_observable_include(const ResolvedTimelineOperation &op) { if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { return; } reserve_drawing_room_for_targets(op.targets); auto pseudo_target = op.targets[0]; SpanRef rec_targets = op.targets; rec_targets.ptr_start++; // Draw per-quit L#*=P boxes. bool had_paulis = false; bool had_rec = false; for (const auto &t : rec_targets) { if (t.is_measurement_record_target()) { had_rec = true; } if (t.is_pauli_target()) { had_paulis = true; std::stringstream ss; ss << "L" << (op.args.empty() ? 0 : op.args[0]) << "*="; ss << t.pauli_type(); auto c = q2xy(t.qubit_value()); draw_annotated_gate( c.xyz[0], c.xyz[1], SvgGateData{ .span = 2, .body = ss.str(), .subscript = "", .superscript = "", .fill = "lightgray", .text_color = "black", .font_size = 0, .sub_font_size = 10}, {}); } } // Draw OBS_INCLUDE(#) box with rec annotations above it, if there are measurement records. if (had_rec) { had_rec = false; auto span = (uint16_t)(1 + std::max(std::max(op.targets.size(), op.args.size()), (size_t)2)); auto c = q2xy(pseudo_target.qubit_value()); std::stringstream ss; ss << "OBS_INCLUDE(" << (op.args.empty() ? 0 : op.args[0]) << ")"; if (!had_paulis) { draw_annotated_gate( c.xyz[0], c.xyz[1], SvgGateData{span, ss.str(), "", "", "lightgray", "black", 0, 10}, {}); } c.xyz[0] += (span - 1) * GATE_PITCH * 0.5f; svg_out << ""; svg_out << "L" << op.args[0] << " *= "; ss << "*="; for (const auto &t : rec_targets) { if (t.is_measurement_record_target()) { if (had_rec) { svg_out << "*"; } had_rec = true; write_rec_index(svg_out, t.value()); } } if (!had_rec && !had_paulis) { svg_out << "1 (vacuous)"; } svg_out << "\n"; } } void DiagramTimelineSvgDrawer::do_resolved_operation(const ResolvedTimelineOperation &op) { if (resolver.num_ticks_seen < min_tick || resolver.num_ticks_seen > max_tick) { return; } if (op.gate_type == GateType::MPP) { do_mpp(op); } else if (op.gate_type == GateType::SPP || op.gate_type == GateType::SPP_DAG) { do_spp(op); } else if (op.gate_type == GateType::DETECTOR) { do_detector(op); } else if (op.gate_type == GateType::OBSERVABLE_INCLUDE) { do_observable_include(op); } else if (op.gate_type == GateType::QUBIT_COORDS) { do_qubit_coords(op); } else if (op.gate_type == GateType::E) { do_correlated_error(op); } else if (op.gate_type == GateType::ELSE_CORRELATED_ERROR) { do_else_correlated_error(op); } else if (op.gate_type == GateType::TICK) { do_tick(); } else if (GATE_DATA[op.gate_type].flags & GATE_TARGETS_PAIRS) { do_two_qubit_gate_instance(op); } else { do_single_qubit_gate_instance(op); } } DiagramTimelineSvgDrawer::DiagramTimelineSvgDrawer(std::ostream &svg_out, size_t num_qubits, bool has_ticks) : svg_out(svg_out), num_qubits(num_qubits), has_ticks(has_ticks), gate_data_map(SvgGateData::make_gate_data_map()) { cur_moment_used_flags.resize(num_qubits); } void DiagramTimelineSvgDrawer::make_diagram_write_to( const Circuit &circuit, std::ostream &svg_out, uint64_t tick_slice_start, uint64_t tick_slice_num, DiagramTimelineSvgDrawerMode mode, SpanRef filter, size_t num_rows) { uint64_t circuit_num_ticks = circuit.count_ticks(); auto circuit_has_ticks = circuit_num_ticks > 0; auto num_qubits = circuit.count_qubits(); std::stringstream buffer; DiagramTimelineSvgDrawer obj(buffer, num_qubits, circuit_has_ticks); tick_slice_num = std::min(tick_slice_num, circuit_num_ticks - tick_slice_start + 1); if (!circuit.operations.empty() && circuit.operations.back().gate_type == GateType::TICK) { tick_slice_num = std::min(tick_slice_num, circuit_num_ticks - tick_slice_start); } if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { // The +1 is because we're showing the detector slice at the end of each tick region. obj.detector_slice_set = DetectorSliceSet::from_circuit_ticks(circuit, tick_slice_start + 1, tick_slice_num, filter); obj.coord_sys = FlattenedCoords::from(obj.detector_slice_set, GATE_PITCH); obj.coord_sys.size.xyz[0] += TIME_SLICE_PADDING * 2; obj.coord_sys.size.xyz[1] += TIME_SLICE_PADDING * 2; if (num_rows == 0) { obj.num_cols = (uint64_t)ceil(sqrt((double)tick_slice_num)); obj.num_rows = tick_slice_num / obj.num_cols; } else { obj.num_rows = num_rows; obj.num_cols = (tick_slice_num + num_rows - 1) / num_rows; } while (obj.num_cols * obj.num_rows < tick_slice_num) { obj.num_rows++; } while (obj.num_cols * obj.num_rows >= tick_slice_num + obj.num_rows) { obj.num_cols--; } } obj.min_tick = tick_slice_start; obj.max_tick = tick_slice_start + tick_slice_num - 1; obj.mode = mode; obj.resolver.unroll_loops = mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE; obj.resolver.resolved_op_callback = [&](const ResolvedTimelineOperation &op) { obj.do_resolved_operation(op); }; obj.resolver.start_repeat_callback = [&](const CircuitTimelineLoopData &loop_data) { obj.do_start_repeat(loop_data); }; obj.resolver.end_repeat_callback = [&](const CircuitTimelineLoopData &loop_data) { obj.do_end_repeat(loop_data); }; obj.resolver.do_circuit(circuit); if (obj.cur_moment_is_used) { obj.do_tick(); } auto w = obj.m2x(obj.cur_moment) - GATE_PITCH * 0.5f; svg_out << R"SVG(\n"; if (mode == DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE) { obj.detector_slice_set.write_svg_contents_to( svg_out, [&](uint32_t qubit) { return obj.coord_sys.unscaled_qubit_coords[qubit]; }, [&](uint64_t tick, uint32_t qubit) { return obj.qt2xy(tick - 1, 0, qubit); }, obj.max_tick + 2, 24); } // Make sure qubit lines/points are drawn first, so they are in the background. if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE) { // Draw qubit points. auto tick = tick_slice_start; svg_out << "\n"; for (uint64_t col = 0; col < obj.num_cols; col++) { for (uint64_t row = 0; row < obj.num_rows && row * obj.num_cols + col < tick_slice_num; row++) { for (auto q : obj.detector_slice_set.used_qubits()) { std::stringstream id_ss; id_ss << "qubit_dot"; id_ss << ":" << q; add_coord_summary_to_ss( id_ss, obj.detector_slice_set.coordinates.at(q)); // the raw qubit coordinates, not projected to 2D id_ss << ":" << tick; // the absolute tick auto c = obj.coord_sys.qubit_coords[q]; // the flattened coordinates in 2D svg_out << "\n"; } } tick++; } svg_out << "\n"; } else { svg_out << "\n"; // Draw qubit lines. for (size_t q = 0; q < obj.num_qubits; q++) { std::stringstream id_ss; id_ss << "qubit_line"; id_ss << ":" << q; auto x1 = PADDING + CIRCUIT_START_X; auto x2 = w; auto y = obj.q2y(q); svg_out << "\n"; svg_out << ""; svg_out << "q" << q; svg_out << "\n"; } svg_out << "\n"; } svg_out << buffer.str(); // Border around different slices. if (mode != DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE && tick_slice_num > 1) { auto k = 0; svg_out << "\n"; for (uint64_t row = 0; row < obj.num_rows; row++) { for (uint64_t col = 0; col < obj.num_cols && row * obj.num_cols + col < tick_slice_num; col++) { auto sw = obj.coord_sys.size.xyz[0]; auto sh = obj.coord_sys.size.xyz[1]; std::stringstream id_ss; auto tick = k + tick_slice_start; // the absolute tick id_ss << "tick_border:" << k; id_ss << ":" << row << "_" << col; id_ss << ":" << tick; svg_out << ""; svg_out << "Tick " << tick; svg_out << "\n"; svg_out << "\n"; k++; } } svg_out << "\n"; } svg_out << ""; } ================================================ FILE: src/stim/diagram/timeline/timeline_svg_drawer.h ================================================ /* * 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. */ #ifndef _STIM_DIAGRAM_TIMELINE_TIMELINE_SVG_DRAWER_H #define _STIM_DIAGRAM_TIMELINE_TIMELINE_SVG_DRAWER_H #include #include "stim/circuit/circuit.h" #include "stim/diagram/ascii_diagram.h" #include "stim/diagram/circuit_timeline_helper.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/gate_data_svg.h" #include "stim/diagram/lattice_map.h" #include "stim/util_bot/str_util.h" namespace stim_draw_internal { enum class DiagramTimelineSvgDrawerMode { SVG_MODE_TIMELINE = 0, SVG_MODE_TIME_SLICE = 1, SVG_MODE_TIME_DETECTOR_SLICE = 2, }; struct DiagramTimelineSvgDrawer { std::ostream &svg_out; CircuitTimelineHelper resolver; size_t cur_moment = 0; uint16_t moment_width = 1; size_t cur_moment_is_used = false; size_t tick_start_moment = 0; std::vector cur_moment_used_flags; size_t num_qubits = 0; bool has_ticks = false; uint64_t min_tick = 0; uint64_t max_tick = UINT64_MAX; uint64_t num_cols = UINT64_MAX; uint64_t num_rows = 1; DiagramTimelineSvgDrawerMode mode; DetectorSliceSet detector_slice_set; FlattenedCoords coord_sys; std::map gate_data_map; DiagramTimelineSvgDrawer(std::ostream &out, size_t num_qubits, bool has_ticks); /// Converts a circuit into a cell diagram. static void make_diagram_write_to( const stim::Circuit &circuit, std::ostream &svg_out, uint64_t tick_slice_start, uint64_t tick_slice_num, DiagramTimelineSvgDrawerMode mode, stim::SpanRef det_coord_filter, size_t num_rows = 0); void do_start_repeat(const CircuitTimelineLoopData &loop_data); void do_end_repeat(const CircuitTimelineLoopData &loop_data); void start_next_moment(); void reserve_drawing_room_for_targets(stim::SpanRef targets); void write_rec_index(std::ostream &out, int64_t lookback_shift = -1); void write_det_index(std::ostream &out); void write_coord(std::ostream &out, size_t coord_index, double relative_coordinate); void write_coords(std::ostream &out, stim::SpanRef relative_coordinates); size_t m2x(size_t m) const; size_t q2y(size_t q) const; Coord<2> q2xy(size_t q) const; Coord<2> qt2xy(uint64_t tick, uint64_t moment_delta, size_t q) const; void draw_annotated_gate(float cx, float cy, const SvgGateData &data, stim::SpanRef end_args); void draw_xswap_control(float cx, float cy); void draw_zswap_control(float cx, float cy); void draw_x_control(float cx, float cy); void draw_y_control(float cx, float cy); void draw_z_control(float cx, float cy); void draw_swap_control(float cx, float cy); void draw_iswap_control(float cx, float cy, bool inverse); void draw_generic_box(float cx, float cy, std::string_view text, stim::SpanRef end_args); void draw_two_qubit_gate_end_point(float cx, float cy, std::string_view type, stim::SpanRef args); void draw_rec(float cx, float cy); void do_resolved_operation(const ResolvedTimelineOperation &op); void do_tick(); void do_two_qubit_gate_instance(const ResolvedTimelineOperation &op); void do_feedback( std::string_view gate, const stim::GateTarget &qubit_target, const stim::GateTarget &feedback_target); void do_single_qubit_gate_instance(const ResolvedTimelineOperation &op); void do_multi_qubit_gate_with_pauli_targets(const ResolvedTimelineOperation &op); void do_multi_qubit_gate_with_paired_pauli_targets(const ResolvedTimelineOperation &op); void do_mpp(const ResolvedTimelineOperation &op); void do_spp(const ResolvedTimelineOperation &op); void do_correlated_error(const ResolvedTimelineOperation &op); void do_qubit_coords(const ResolvedTimelineOperation &op); void do_else_correlated_error(const ResolvedTimelineOperation &op); void do_detector(const ResolvedTimelineOperation &op); void do_observable_include(const ResolvedTimelineOperation &op); }; } // namespace stim_draw_internal #endif ================================================ FILE: src/stim/diagram/timeline/timeline_svg_drawer.test.cc ================================================ // 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. #include "stim/diagram/timeline/timeline_svg_drawer.h" #include #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/util_bot/test_util.test.h" using namespace stim; using namespace stim_draw_internal; void expect_svg_diagram_is_identical_to_saved_file(const Circuit &circuit, std::string_view key) { std::stringstream ss; CoordFilter filter; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE, {&filter}); expect_string_is_identical_to_saved_file(ss.str(), key); } TEST(circuit_diagram_timeline_svg, single_qubit_gates) { Circuit circuit(R"CIRCUIT( I 0 X 1 Y 2 Z 3 C_XYZ 0 C_ZYX 1 H 2 H_XY 3 H_XZ 0 H_YZ 1 S 2 SQRT_X 3 SQRT_X_DAG 0 SQRT_Y 1 SQRT_Y_DAG 2 SQRT_Z 3 SQRT_Z_DAG 0 S_DAG 1 H 2 0 3 )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "single_qubits_gates.svg"); } TEST(circuit_diagram_timeline_svg, two_qubits_gates) { Circuit circuit(R"CIRCUIT( CNOT 0 1 CX 2 3 CY 4 5 5 4 CZ 0 2 ISWAP 1 3 ISWAP_DAG 2 4 SQRT_XX 3 5 SQRT_XX_DAG 0 5 SQRT_YY 3 4 4 3 SQRT_YY_DAG 0 1 SQRT_ZZ 2 3 SQRT_ZZ_DAG 4 5 SWAP 0 1 XCX 2 3 XCY 3 4 XCZ 0 1 YCX 2 3 YCY 4 5 YCZ 0 1 ZCX 2 3 ZCY 4 5 ZCZ 0 5 2 3 1 4 CXSWAP 0 1 SWAPCX 2 3 )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "two_qubits_gates.svg"); } TEST(circuit_diagram_timeline_svg, noise_gates) { Circuit circuit(R"CIRCUIT( DEPOLARIZE1(0.125) 0 1 DEPOLARIZE2(0.125) 0 2 4 5 X_ERROR(0.125) 0 1 2 Y_ERROR(0.125) 0 1 4 Z_ERROR(0.125) 2 3 5 )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "noise_gates_1.svg"); circuit = Circuit(R"CIRCUIT( E(0.25) X1 X2 CORRELATED_ERROR(0.125) X1 Y2 Z3 ELSE_CORRELATED_ERROR(0.25) X2 Y4 Z3 ELSE_CORRELATED_ERROR(0.25) X5 )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "noise_gates_2.svg"); circuit = Circuit(R"CIRCUIT( PAULI_CHANNEL_1(0.125,0.25,0.125) 0 1 2 3 PAULI_CHANNEL_2(0.01,0.01,0.01,0.02,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01) 0 1 2 4 )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "noise_gates_3.svg"); } TEST(circuit_diagram_timeline_svg, collapsing) { Circuit circuit(R"CIRCUIT( R 0 RX 1 RY 2 RZ 3 M(0.001) 0 1 MR 1 0 MRX 1 2 MRY 0 3 1 MRZ 0 MX 1 MY 2 MZ 3 MPP X0*Y2 Z3 X1 Z2*Y3 )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "collapsing.svg"); } TEST(circuit_diagram_timeline_svg, measurement_looping) { Circuit circuit(R"CIRCUIT( M 0 REPEAT 100 { M 1 REPEAT 5 { M 2 } REPEAT 7 { MPP X3*Y4 } } )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "measurement_looping.svg"); } TEST(circuit_diagram_timeline_svg, repeat) { auto circuit = Circuit(R"CIRCUIT( H 0 1 2 REPEAT 5 { RX 2 REPEAT 100 { H 0 1 3 3 } } )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "repeat.svg"); } TEST(circuit_diagram_timeline_svg, classical_feedback) { auto circuit = Circuit(R"CIRCUIT( M 0 CX rec[-1] 1 YCZ 2 sweep[5] )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "classical_feedback.svg"); } TEST(circuit_diagram_timeline_svg, lattice_surgery_cnot) { auto circuit = Circuit(R"CIRCUIT( R 2 MPP X1*X2 MPP Z0*Z2 MX 2 CZ rec[-3] 0 CX rec[-2] 1 CZ rec[-1] 0 )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "lattice_surgery_cnot.svg"); } TEST(circuit_diagram_timeline_svg, tick) { auto circuit = Circuit(R"CIRCUIT( H 0 0 TICK H 0 1 TICK H 0 REPEAT 1 { H 0 1 TICK H 0 S 0 } H 0 0 SQRT_X 0 TICK H 0 0 )CIRCUIT"); std::stringstream ss; CoordFilter filter; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE, {&filter}); expect_string_is_identical_to_saved_file(ss.str(), "tick.svg"); } TEST(circuit_diagram_timeline_svg, shifted_coords) { auto circuit = Circuit(R"CIRCUIT( QUBIT_COORDS(1, 2) 1 DETECTOR(4, 5, 6) SHIFT_COORDS(10, 20, 30, 40) QUBIT_COORDS(1, 2) 2 DETECTOR(4, 5, 6) REPEAT 100 { QUBIT_COORDS(7, 8) 3 4 DETECTOR(9, 10, 11) SHIFT_COORDS(0, 200, 300, 400) } QUBIT_COORDS(1, 2) 5 DETECTOR(4, 5, 6) )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "shifted_coords.svg"); } TEST(circuit_diagram_timeline_svg, detector_pseudo_targets) { auto circuit = Circuit(R"CIRCUIT( M 0 1 2 3 4 5 REPEAT 100 { M 1 2 } DETECTOR(1) rec[-1] DETECTOR(2) rec[-2] DETECTOR(3) rec[-3] DETECTOR(4) rec[-4] DETECTOR(5) rec[-1] rec[-2] OBSERVABLE_INCLUDE(100) rec[-201] rec[-203] )CIRCUIT"); expect_svg_diagram_is_identical_to_saved_file(circuit, "detector_pseudo_targets.svg"); } TEST(circuit_diagram_timeline_svg, repetition_code) { CircuitGenParameters params(10, 3, "memory"); auto circuit = generate_rep_code_circuit(params).circuit; expect_svg_diagram_is_identical_to_saved_file(circuit, "repetition_code.svg"); } TEST(circuit_diagram_timeline_svg, surface_code) { CircuitGenParameters params(10, 3, "unrotated_memory_z"); auto circuit = generate_surface_code_circuit(params).circuit; expect_svg_diagram_is_identical_to_saved_file(circuit, "surface_code.svg"); } TEST(circuit_diagram_time_detector_slice_svg, surface_code_partial) { CircuitGenParameters params(10, 3, "unrotated_memory_z"); auto circuit = generate_surface_code_circuit(params).circuit.flattened(); CoordFilter filter; filter.coordinates.push_back(2); std::stringstream ss; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 5, 11, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, {&filter}); expect_string_is_identical_to_saved_file(ss.str(), "surface_code_time_detector_slice.svg"); } TEST(circuit_diagram_time_detector_slice_svg, surface_code_full) { CircuitGenParameters params(5, 3, "unrotated_memory_z"); auto circuit = generate_surface_code_circuit(params).circuit; CoordFilter filter; filter.coordinates.push_back(1); std::stringstream ss; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, {&filter}); expect_string_is_identical_to_saved_file(ss.str(), "surface_code_full_time_detector_slice.svg"); } TEST(circuit_diagram_time_slice_svg, surface_code) { CircuitGenParameters params(10, 3, "rotated_memory_z"); auto circuit = generate_surface_code_circuit(params).circuit; CoordFilter filter; std::stringstream ss; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 5, 11, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_SLICE, {&filter}); expect_string_is_identical_to_saved_file(ss.str(), "surface_code_time_slice.svg"); } TEST(circuit_diagram_timeline_svg, chained_loops) { auto circuit = Circuit(R"CIRCUIT( REPEAT 2 { H 0 TICK } X 0 TICK Y 0 TICK Z 0 TICK REPEAT 3 { C_XYZ 0 TICK } X 1 TICK Y 1 TICK Z 1 TICK )CIRCUIT"); CoordFilter empty_filter; std::stringstream ss; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, {&empty_filter}); expect_string_is_identical_to_saved_file(ss.str(), "circuit_diagram_timeline_svg_chained_loops.svg"); } TEST(diagram_timeline_svg_drawer, make_diagram_write_to) { CircuitGenParameters params(2, 3, "rotated_memory_x"); auto circuit = generate_surface_code_circuit(params).circuit; std::vector coord_filter{CoordFilter{}}; std::stringstream ss; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, circuit.count_ticks(), DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, coord_filter); expect_string_is_identical_to_saved_file(ss.str(), "detslice-with-ops_surface_code.svg"); } TEST(diagram_timeline_svg_drawer, test_circuit_all_ops_time_slice) { auto circuit = generate_test_circuit_with_all_operations(); CoordFilter filter; std::stringstream ss; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_SLICE, {&filter}); expect_string_is_identical_to_saved_file(ss.str(), "circuit_all_ops_timeslice.svg"); } TEST(diagram_timeline_svg_drawer, test_circuit_all_ops_time_line) { auto circuit = generate_test_circuit_with_all_operations(); CoordFilter filter; std::stringstream ss; DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIMELINE, {&filter}); expect_string_is_identical_to_saved_file(ss.str(), "circuit_all_ops_timeline.svg"); } TEST(diagram_timeline_svg_drawer, test_circuit_all_ops_detslice) { CoordFilter empty_filter; std::stringstream ss; auto circuit = generate_test_circuit_with_all_operations(); DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, {&empty_filter}); expect_string_is_identical_to_saved_file(ss.str(), "circuit_all_ops_detslice.svg"); } TEST(diagram_timeline_svg_drawer, anticommuting_detector_circuit) { CoordFilter empty_filter; std::stringstream ss; auto circuit = Circuit(R"CIRCUIT( TICK R 0 TICK R 0 TICK MXX 0 1 M 2 DETECTOR rec[-1] DETECTOR rec[-2] )CIRCUIT"); DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, {&empty_filter}); expect_string_is_identical_to_saved_file(ss.str(), "anticommuting_detslice.svg"); } TEST(diagram_timeline_svg_drawer, bezier_curves) { CoordFilter empty_filter; std::stringstream ss; auto circuit = Circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 0) 3 CX 0 1 CX 2 3 TICK CX 0 2 CX 1 3 )CIRCUIT"); DiagramTimelineSvgDrawer::make_diagram_write_to( circuit, ss, 0, UINT64_MAX, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_SLICE, {&empty_filter}); expect_string_is_identical_to_saved_file(ss.str(), "bezier_time_slice.svg"); } ================================================ FILE: src/stim/gates/gate_data_annotations.cc ================================================ #include "stim/gates/gates.h" using namespace stim; void GateDataMap::add_gate_data_annotations(bool &failed) { add_gate( failed, Gate{ .name = "DETECTOR", .id = GateType::DETECTOR, .best_candidate_inverse_id = GateType::DETECTOR, .arg_count = ARG_COUNT_SYGIL_ANY, .flags = (GateFlags)(GATE_ONLY_TARGETS_MEASUREMENT_RECORD | GATE_IS_NOT_FUSABLE | GATE_HAS_NO_EFFECT_ON_QUBITS), .category = "Z_Annotations", .help = R"MARKDOWN( Annotates that a set of measurements can be used to detect errors, because the set's parity should be deterministic. Note that it is not necessary to say whether the measurement set's parity is even or odd; all that matters is that the parity should be *consistent* when running the circuit and omitting all noisy operations. Note that, for example, this means that even though `X` and `X_ERROR(1)` have equivalent effects on the measurements making up a detector, they have differing effects on the detector (because `X` is intended, determining the expected value, and `X_ERROR` is noise, causing deviations from the expected value). Detectors are ignored when sampling measurements, but produce results when sampling detection events. In detector sampling mode, each detector produces a result bit (where 0 means "measurement set had expected parity" and 1 means "measurement set had incorrect parity"). When converting a circuit into a detector error model, errors are grouped based on the detectors they flip (the "symptoms" of the error) and the observables they flip (the "frame changes" of the error). It is permitted, though not recommended, for the measurement set given to a `DETECTOR` instruction to have inconsistent parity. When a detector's measurement set is inconsistent, the detector is called a "gauge detector" and the expected parity of the measurement set is chosen arbitrarily (in an implementation-defined way). Some circuit analysis tools (such as the circuit-to-detector-error-model conversion) will by default refuse to process circuits containing gauge detectors. Gauge detectors produce random results when sampling detection events, though these results will be appropriately correlated with other gauge detectors. For example, if `DETECTOR rec[-1]` and `DETECTOR rec[-2]` are gauge detectors but `DETECTOR rec[-1] rec[-2]` is not, then under noiseless execution the two gauge detectors would either always produce the same result or always produce opposite results. Detectors can specify coordinates using their parens arguments. Coordinates have no effect on simulations, but can be useful to tools consuming the circuit. For example, a tool drawing how the detectors in a circuit relate to each other can use the coordinates as hints for where to place the detectors in the drawing. Parens Arguments: Optional. Coordinate metadata, relative to the current coordinate offset accumulated from `SHIFT_COORDS` instructions. Can be any number of coordinates from 1 to 16. There is no required convention for which coordinate is which. Targets: The measurement records to XOR together to get the deterministic-under-noiseless-execution parity. Example: R 0 X_ERROR(0.1) 0 M 0 # This measurement is always False under noiseless execution. # Annotate that most recent measurement should be deterministic. DETECTOR rec[-1] R 0 X 0 X_ERROR(0.1) 0 M 0 # This measurement is always True under noiseless execution. # Annotate that most recent measurement should be deterministic. DETECTOR rec[-1] R 0 1 H 0 CNOT 0 1 DEPOLARIZE2(0.001) 0 1 M 0 1 # These two measurements are always equal under noiseless execution. # Annotate that the parity of the previous two measurements should be consistent. DETECTOR rec[-1] rec[-2] # A series of trivial detectors with hinted coordinates along the diagonal line Y = 2X + 3. REPEAT 100 { R 0 M 0 SHIFT_COORDS(1, 2) DETECTOR(0, 3) rec[-1] } )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "OBSERVABLE_INCLUDE", .id = GateType::OBSERVABLE_INCLUDE, .best_candidate_inverse_id = GateType::OBSERVABLE_INCLUDE, .arg_count = 1, .flags = (GateFlags)(GATE_ONLY_TARGETS_MEASUREMENT_RECORD | GATE_TARGETS_PAULI_STRING | GATE_IS_NOT_FUSABLE | GATE_ARGS_ARE_UNSIGNED_INTEGERS | GATE_HAS_NO_EFFECT_ON_QUBITS), .category = "Z_Annotations", .help = R"MARKDOWN( Adds measurement records to a specified logical observable. A potential point of confusion here is that Stim's notion of a logical observable is nothing more than a set of measurements, potentially spanning across the entire circuit, that together produce a deterministic result. It's more akin to the "boundary of a parity sheet" in a topological spacetime diagram than it is to the notion of a qubit observable. For example, consider a surface code memory experiment that initializes a logical |0>, preserves the state noise, and eventually performs a logical Z basis measurement. The circuit representing this experiment would use `OBSERVABLE_INCLUDE` instructions to specifying which physical measurements within the logical Z basis measurement should be XOR'd together to get the logical measurement result. This effectively identifies the logical Z observable. But the circuit would *not* declare an X observable, because the X observable is not deterministic in a Z basis memory experiment; it has no corresponding deterministic measurement set. Logical observables are ignored when sampling measurements, but can produce results (if requested) when sampling detection events. In detector sampling mode, each observable can produce a result bit (where 0 means "measurement set had expected parity" and 1 means "measurement set had incorrect parity"). When converting a circuit into a detector error model, errors are grouped based on the detectors they flip (the "symptoms" of the error) and the observables they flip (the "frame changes" of the error). Another potential point of confusion is that when sampling logical measurement results, as part of sampling detection events in the circuit, the reported results are not measurements of the logical observable but rather whether those measurement results *were flipped*. This has significant simulation speed benefits, and also makes it so that it is not necessary to say whether the logical measurement result is supposed to be False or True. Note that, for example, this means that even though `X` and `X_ERROR(1)` have equivalent effects on the measurements making up an observable, they have differing effects on the reported value of an observable when sampling detection events (because `X` is intended, determining the expected value, and `X_ERROR` is noise, causing deviations from the expected value). It is not recommended for the measurement set of an observable to have inconsistent parity. For example, the circuit-to-detector-error-model conversion will refuse to operate on circuits containing such observables. In addition to targeting measurements, observables can target Pauli operators. This has no effect when running the quantum computation, but is used when configuring the decoder. For example, when performing a logical Z initialization, it allows a logical X operator to be introduced (by marking its Pauli terms) despite the fact that it anticommutes with the initialization. In practice, when physically sampling a circuit or simulating sampling its measurements and then computing the observables from the measurements, these Pauli terms are effectively ignored. However, they affect detection event simulations and affect whether the observable is included in errors in the detector error model. This makes it easier to benchmark all observables of a code, without having to introduce noiseless qubits entangled with the logical qubit to avoid the testing of the X observable anticommuting with the testing of the Z observable. Unlike a `DETECTOR` instruction which provides a complete description of a detector by listing all its constituent measurement records, an individual `OBSERVABLE_INCLUDE` instruction is not required to (and generally does not) fully describe a logical observable. Instead, measurement records or Pauli targets are added to it incrementally. A logical observable can be given both types of description: as a collection of Pauli targets and as a collection of measurement record targets. Parens Arguments: A non-negative integer specifying the index of the logical observable to add the measurement records to. Targets: The measurement records or Pauli terms to add to the specified observable. Example: R 0 1 H 0 CNOT 0 1 M 0 1 # Observable 0 is the parity of the previous two measurements. OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] R 0 1 H 0 CNOT 0 1 M 0 1 # Observable 1 is the parity of the previous measurement... OBSERVABLE_INCLUDE(1) rec[-1] # ...and the one before that. OBSERVABLE_INCLUDE(1) rec[-2] # Unphysically tracking two anticommuting observables of a 2x2 surface code. QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(0, 1) 2 QUBIT_COORDS(1, 1) 3 OBSERVABLE_INCLUDE(0) X0 X1 OBSERVABLE_INCLUDE(1) Z0 Z2 MPP X0*X1*X2*X3 Z0*Z1 Z2*Z3 DEPOLARIZE1(0.001) 0 1 2 3 MPP X0*X1*X2*X3 Z0*Z1 Z2*Z3 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] rec[-6] OBSERVABLE_INCLUDE(0) X0 X1 OBSERVABLE_INCLUDE(1) Z0 Z2 # Stim circuit may include a description of an observable in terms of Pauli targets # alongside a description in terms of measurement records. OBSERVABLE_INCLUDE(0) Z0 Z1 M 0 1 OBSERVABLE_INCLUDE(0) rec[-2] rec[-1] )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "TICK", .id = GateType::TICK, .best_candidate_inverse_id = GateType::TICK, .arg_count = 0, .flags = (GateFlags)(GATE_IS_NOT_FUSABLE | GATE_TAKES_NO_TARGETS | GATE_HAS_NO_EFFECT_ON_QUBITS), .category = "Z_Annotations", .help = R"MARKDOWN( Annotates the end of a layer of gates, or that time is advancing. This instruction is not necessary, it has no effect on simulations, but it can be used by tools that are transforming or visualizing the circuit. For example, a tool that adds noise to a circuit may include cross-talk terms that require knowing whether or not operations are happening in the same time step or not. TICK instructions are added, and checked for, by `stimcirq` in order to preserve the moment structure of cirq circuits converted between stim circuits and cirq circuits. Parens Arguments: This instruction takes no parens arguments. Targets: This instruction takes no targets. Example: # First time step. H 0 CZ 1 2 TICK # Second time step. H 1 TICK # Empty time step. TICK )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "QUBIT_COORDS", .id = GateType::QUBIT_COORDS, .best_candidate_inverse_id = GateType::QUBIT_COORDS, .arg_count = ARG_COUNT_SYGIL_ANY, .flags = (GateFlags)(GATE_IS_NOT_FUSABLE | GATE_HAS_NO_EFFECT_ON_QUBITS), .category = "Z_Annotations", .help = R"MARKDOWN( Annotates the location of a qubit. Coordinates are not required and have no effect on simulations, but can be useful to tools consuming the circuit. For example, a tool drawing the circuit can use the coordinates as hints for where to place the qubits in the drawing. `stimcirq` uses `QUBIT_COORDS` instructions to preserve `cirq.LineQubit` and `cirq.GridQubit` coordinates when converting between stim circuits and cirq circuits A qubit's coordinates can be specified multiple times, with the intended interpretation being that the qubit is at the location of the most recent assignment. For example, this could be used to indicate a simulated qubit is iteratively playing the role of many physical qubits. Parens Arguments: Optional. The latest coordinates of the qubit, relative to accumulated offsets from `SHIFT_COORDS` instructions. Can be any number of coordinates from 1 to 16. There is no required convention for which coordinate is which. Targets: The qubit or qubits the coordinates apply to. Example: # Annotate that qubits 0 to 3 are at the corners of a square. QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(0, 1) 1 QUBIT_COORDS(1, 0) 2 QUBIT_COORDS(1, 1) 3 )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "SHIFT_COORDS", .id = GateType::SHIFT_COORDS, .best_candidate_inverse_id = GateType::SHIFT_COORDS, .arg_count = ARG_COUNT_SYGIL_ANY, .flags = (GateFlags)(GATE_IS_NOT_FUSABLE | GATE_TAKES_NO_TARGETS | GATE_HAS_NO_EFFECT_ON_QUBITS), .category = "Z_Annotations", .help = R"MARKDOWN( Accumulates offsets that affect qubit coordinates and detector coordinates. Note: when qubit/detector coordinates use fewer dimensions than SHIFT_COORDS, the offsets from the additional dimensions are ignored (i.e. not specifying a dimension is different from specifying it to be 0). See also: `QUBIT_COORDS`, `DETECTOR`. Parens Arguments: Offsets to add into the current coordinate offset. Can be any number of coordinate offsets from 1 to 16. There is no required convention for which coordinate is which. Targets: This instruction takes no targets. Example: SHIFT_COORDS(500.5) QUBIT_COORDS(1510) 0 # Actually at 2010.5 SHIFT_COORDS(1500) QUBIT_COORDS(11) 1 # Actually at 2011.5 QUBIT_COORDS(10.5) 2 # Actually at 2011.0 # Declare some detectors with coordinates along a diagonal line. REPEAT 1000 { CNOT 0 2 CNOT 1 2 MR 2 DETECTOR(10.5, 0) rec[-1] rec[-2] # Actually at (2011.0, iteration_count). SHIFT_COORDS(0, 1) # Advance 2nd coordinate to track loop iterations. } )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "MPAD", .id = GateType::MPAD, .best_candidate_inverse_id = GateType::MPAD, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_PRODUCES_RESULTS | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "Z_Annotations", .help = R"MARKDOWN( Pads the measurement record with the listed measurement results. This can be useful for ensuring measurements are aligned to word boundaries, or that the number of measurement bits produced per circuit layer is always the same even if the number of measured qubits varies. Parens Arguments: If no parens argument is given, the padding bits are recorded perfectly. If one parens argument is given, the padding bits are recorded noisily. The argument is the probability of recording the wrong result. Targets: Each target is a measurement result to add. Targets should be the value 0 or the value 1. Examples: # Append a False result to the measurement record. MPAD 0 # Append a True result to the measurement record. MPAD 1 # Append a series of results to the measurement record. MPAD 0 0 1 0 1 )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); } ================================================ FILE: src/stim/gates/gate_data_blocks.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; void GateDataMap::add_gate_data_blocks(bool &failed) { add_gate( failed, Gate{ .name = "REPEAT", .id = GateType::REPEAT, .best_candidate_inverse_id = GateType::REPEAT, .arg_count = 0, .flags = (GateFlags)(GATE_IS_BLOCK | GATE_IS_NOT_FUSABLE), .category = "Y_Control Flow", .help = R"MARKDOWN( Repeats the instructions in its body N times. Currently, repetition counts of 0 are not allowed because they create corner cases with ambiguous resolutions. For example, if a logical observable is only given measurements inside a repeat block with a repetition count of 0, it's ambiguous whether the output of sampling the logical observables includes a bit for that logical observable. Parens Arguments: This instruction takes no parens arguments. Targets: A positive integer in [1, 10^18] specifying the number of repetitions. Example: REPEAT 2 { CNOT 0 1 CNOT 2 1 M 1 } REPEAT 10000000 { CNOT 0 1 CNOT 2 1 M 1 DETECTOR rec[-1] rec[-3] } )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); } ================================================ FILE: src/stim/gates/gate_data_collapsing.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; void GateDataMap::add_gate_data_collapsing(bool &failed) { // ===================== Measure Gates. ============================ add_gate( failed, Gate{ .name = "MX", .id = GateType::MX, .best_candidate_inverse_id = GateType::MX, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "L_Collapsing Gates", .help = R"MARKDOWN( X-basis measurement. Projects each target qubit into `|+>` or `|->` and reports its value (false=`|+>`, true=`|->`). Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The qubits to measure in the X basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the X basis, and append the result into the measurement record. MX 5 # Measure qubit 5 in the X basis, and append the INVERSE of its result into the measurement record. MX !5 # Do a noisy measurement where the result put into the measurement record is wrong 1% of the time. MX(0.01) 5 # Measure multiple qubits in the X basis, putting 3 bits into the measurement record. MX 2 3 5 # Perform multiple noisy measurements. Each measurement fails independently with 2% probability. MX(0.02) 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"X -> rec[-1]", "X -> +X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 M 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "MY", .id = GateType::MY, .best_candidate_inverse_id = GateType::MY, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "L_Collapsing Gates", .help = R"MARKDOWN( Y-basis measurement. Projects each target qubit into `|i>` or `|-i>` and reports its value (false=`|i>`, true=`|-i>`). Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The qubits to measure in the Y basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the Y basis, and append the result into the measurement record. MY 5 # Measure qubit 5 in the Y basis, and append the INVERSE of its result into the measurement record. MY !5 # Do a noisy measurement where the result put into the measurement record is wrong 1% of the time. MY(0.01) 5 # Measure multiple qubits in the X basis, putting 3 bits into the measurement record. MY 2 3 5 # Perform multiple noisy measurements. Each measurement fails independently with 2% probability. MY(0.02) 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"Y -> rec[-1]", "Y -> +Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 H 0 M 0 H 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "M", .id = GateType::M, .best_candidate_inverse_id = GateType::M, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "L_Collapsing Gates", .help = R"MARKDOWN( Z-basis measurement. Projects each target qubit into `|0>` or `|1>` and reports its value (false=`|0>`, true=`|1>`). Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The qubits to measure in the Z basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the Z basis, and append the result into the measurement record. M 5 # 'MZ' is the same as 'M'. This also measures qubit 5 in the Z basis. MZ 5 # Measure qubit 5 in the Z basis, and append the INVERSE of its result into the measurement record. MZ !5 # Do a noisy measurement where the result put into the measurement record is wrong 1% of the time. MZ(0.01) 5 # Measure multiple qubits in the Z basis, putting 3 bits into the measurement record. MZ 2 3 5 # Perform multiple noisy measurements. Each measurement fails independently with 2% probability. MZ(0.02) 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"Z -> rec[-1]", "Z -> +Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( M 0 )CIRCUIT", }); add_gate_alias(failed, "MZ", "M"); // ===================== Measure+Reset Gates. ============================ add_gate( failed, Gate{ .name = "MRX", .id = GateType::MRX, .best_candidate_inverse_id = GateType::MRX, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_IS_RESET), .category = "L_Collapsing Gates", .help = R"MARKDOWN( X-basis demolition measurement (optionally noisy). Projects each target qubit into `|+>` or `|->`, reports its value (false=`|+>`, true=`|->`), then resets to `|+>`. Parens Arguments: If no parens argument is given, the demolition measurement is perfect. If one parens argument is given, the demolition measurement's result is noisy. The argument is the probability of returning the wrong result. The argument does not affect the fidelity of the reset. Targets: The qubits to measure and reset in the X basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the X basis, reset it to the |+> state, append the measurement result into the measurement record. MRX 5 # Demolition measure qubit 5 in the X basis, but append the INVERSE of its result into the measurement record. MRX !5 # Do a noisy demolition measurement where the result put into the measurement record is wrong 1% of the time. MRX(0.01) 5 # Demolition measure multiple qubits in the X basis, putting 3 bits into the measurement record. MRX 2 3 5 # Perform multiple noisy demolition measurements. Each measurement result is flipped independently with 2% probability. MRX(0.02) 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"X -> rec[-1]", "1 -> +X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 M 0 R 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "MRY", .id = GateType::MRY, .best_candidate_inverse_id = GateType::MRY, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_IS_RESET), .category = "L_Collapsing Gates", .help = R"MARKDOWN( Y-basis demolition measurement (optionally noisy). Projects each target qubit into `|i>` or `|-i>`, reports its value (false=`|i>`, true=`|-i>`), then resets to `|i>`. Parens Arguments: If no parens argument is given, the demolition measurement is perfect. If one parens argument is given, the demolition measurement's result is noisy. The argument is the probability of returning the wrong result. The argument does not affect the fidelity of the reset. Targets: The qubits to measure and reset in the Y basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the Y basis, reset it to the |i> state, append the measurement result into the measurement record. MRY 5 # Demolition measure qubit 5 in the Y basis, but append the INVERSE of its result into the measurement record. MRY !5 # Do a noisy demolition measurement where the result put into the measurement record is wrong 1% of the time. MRY(0.01) 5 # Demolition measure multiple qubits in the Y basis, putting 3 bits into the measurement record. MRY 2 3 5 # Perform multiple noisy demolition measurements. Each measurement result is flipped independently with 2% probability. MRY(0.02) 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"Y -> rec[-1]", "1 -> +Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 H 0 M 0 R 0 H 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "MR", .id = GateType::MR, .best_candidate_inverse_id = GateType::MR, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_IS_RESET), .category = "L_Collapsing Gates", .help = R"MARKDOWN( Z-basis demolition measurement (optionally noisy). Projects each target qubit into `|0>` or `|1>`, reports its value (false=`|0>`, true=`|1>`), then resets to `|0>`. Parens Arguments: If no parens argument is given, the demolition measurement is perfect. If one parens argument is given, the demolition measurement's result is noisy. The argument is the probability of returning the wrong result. The argument does not affect the fidelity of the reset. Targets: The qubits to measure and reset in the Z basis. Prefixing a qubit target with `!` flips its reported measurement result. Examples: # Measure qubit 5 in the Z basis, reset it to the |0> state, append the measurement result into the measurement record. MRZ 5 # MR is also a Z-basis demolition measurement. MR 5 # Demolition measure qubit 5 in the Z basis, but append the INVERSE of its result into the measurement record. MRZ !5 # Do a noisy demolition measurement where the result put into the measurement record is wrong 1% of the time. MRZ(0.01) 5 # Demolition measure multiple qubits in the Z basis, putting 3 bits into the measurement record. MRZ 2 3 5 # Perform multiple noisy demolition measurements. Each measurement result is flipped independently with 2% probability. MRZ(0.02) 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"Z -> rec[-1]", "1 -> +Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( M 0 R 0 )CIRCUIT", }); add_gate_alias(failed, "MRZ", "MR"); // ===================== Reset Gates. ============================ add_gate( failed, Gate{ .name = "RX", .id = GateType::RX, .best_candidate_inverse_id = GateType::MX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_RESET), .category = "L_Collapsing Gates", .help = R"MARKDOWN( X-basis reset. Forces each target qubit into the `|+>` state by silently measuring it in the X basis and applying a `Z` gate if it ended up in the `|->` state. Parens Arguments: This instruction takes no parens arguments. Targets: The qubits to reset in the X basis. Examples: # Reset qubit 5 into the |+> state. RX 5 # Reset multiple qubits into the |+> state. RX 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"1 -> +X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( R 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "RY", .id = GateType::RY, .best_candidate_inverse_id = GateType::MY, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_RESET), .category = "L_Collapsing Gates", .help = R"MARKDOWN( Y-basis reset. Forces each target qubit into the `|i>` state by silently measuring it in the Y basis and applying an `X` gate if it ended up in the `|-i>` state. Parens Arguments: This instruction takes no parens arguments. Targets: The qubits to reset in the Y basis. Examples: # Reset qubit 5 into the |i> state. RY 5 # Reset multiple qubits into the |i> state. RY 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"1 -> +Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( R 0 H 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "R", .id = GateType::R, .best_candidate_inverse_id = GateType::M, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_RESET), .category = "L_Collapsing Gates", .help = R"MARKDOWN( Z-basis reset. Forces each target qubit into the `|0>` state by silently measuring it in the Z basis and applying an `X` gate if it ended up in the `|1>` state. Parens Arguments: This instruction takes no parens arguments. Targets: The qubits to reset in the Z basis. Examples: # Reset qubit 5 into the |0> state. RZ 5 # R means the same thing as RZ. R 5 # Reset multiple qubits into the |0> state. RZ 2 3 5 )MARKDOWN", .unitary_data = {}, .flow_data = {"1 -> +Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( R 0 )CIRCUIT", }); add_gate_alias(failed, "RZ", "R"); } ================================================ FILE: src/stim/gates/gate_data_controlled.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; static constexpr std::complex i = std::complex(0, 1); void GateDataMap::add_gate_data_controlled(bool &failed) { add_gate( failed, Gate{ .name = "XCX", .id = GateType::XCX, .best_candidate_inverse_id = GateType::XCX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The X-controlled X gate. First qubit is the control, second qubit is the target. Applies an X gate to the target if the control is in the |-> state. Negates the amplitude of the |->|-> state. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{0.5f, 0.5f, 0.5f, -0.5f}, {0.5f, 0.5f, -0.5f, 0.5f}, {0.5f, -0.5f, 0.5f, 0.5f}, {-0.5f, 0.5f, 0.5f, 0.5f}}, .flow_data = {"+XI", "+ZX", "+IX", "+XZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 CNOT 0 1 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "XCY", .id = GateType::XCY, .best_candidate_inverse_id = GateType::XCY, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The X-controlled Y gate. First qubit is the control, second qubit is the target. Applies a Y gate to the target if the control is in the |-> state. Negates the amplitude of the |->|-i> state. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{0.5f, 0.5f, -0.5f * i, 0.5f * i}, {0.5f, 0.5f, 0.5f * i, -0.5f * i}, {0.5f * i, -0.5f * i, 0.5f, 0.5f}, {-0.5f * i, 0.5f * i, 0.5f, 0.5f}}, .flow_data = {"+XI", "+ZY", "+XX", "+XZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 1 S 1 S 1 CNOT 0 1 H 0 S 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "XCZ", .id = GateType::XCZ, .best_candidate_inverse_id = GateType::XCZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS | GATE_CAN_TARGET_BITS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The X-controlled Z gate. Applies a Z gate to the target if the control is in the |-> state. Equivalently: negates the amplitude of the |->|1> state. Same as a CX gate, but with reversed qubit order. The first qubit is the control, and the second qubit is the target. To perform a classically controlled X, replace the Z target with a `rec` target like rec[-2]. To perform an I or X gate as configured by sweep data, replace the Z target with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Bit flip qubit 5 controlled by qubit 2. XCZ 5 2 # Perform CX 2 5 then CX 4 2. XCZ 5 2 2 4 # Bit flip qubit 6 if the most recent measurement result was TRUE. XCZ 6 rec[-1] # Bit flip qubits 7 and 8 conditioned on sweep configuration data. XCZ 7 sweep[5] 8 sweep[5] )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}}, .flow_data = {"+XI", "+ZZ", "+XX", "+IZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( CNOT 1 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "YCX", .id = GateType::YCX, .best_candidate_inverse_id = GateType::YCX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The Y-controlled X gate. First qubit is the control, second qubit is the target. Applies an X gate to the target if the control is in the |-i> state. Negates the amplitude of the |-i>|-> state. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{0.5f, -i * 0.5f, 0.5f, i * 0.5f}, {i * 0.5f, 0.5f, -i * 0.5f, 0.5f}, {0.5f, i * 0.5f, 0.5f, -i * 0.5f}, {-i * 0.5f, 0.5f, i * 0.5f, 0.5f}}, .flow_data = {"+XX", "+ZX", "+IX", "+YZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 H 1 CNOT 1 0 S 0 H 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "YCY", .id = GateType::YCY, .best_candidate_inverse_id = GateType::YCY, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The Y-controlled Y gate. First qubit is the control, second qubit is the target. Applies a Y gate to the target if the control is in the |-i> state. Negates the amplitude of the |-i>|-i> state. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{0.5f, -i * 0.5f, -i * 0.5f, 0.5f}, {i * 0.5f, 0.5f, -0.5f, -i * 0.5f}, {i * 0.5f, -0.5f, 0.5f, -i * 0.5f}, {0.5f, i * 0.5f, i * 0.5f, 0.5f}}, .flow_data = {"+XY", "+ZY", "+YX", "+YZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 S 1 S 1 S 1 H 0 CNOT 0 1 H 0 S 0 S 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "YCZ", .id = GateType::YCZ, .best_candidate_inverse_id = GateType::YCZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS | GATE_CAN_TARGET_BITS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The Y-controlled Z gate. Applies a Z gate to the target if the control is in the |-i> state. Equivalently: negates the amplitude of the |-i>|1> state. Same as a CY gate, but with reversed qubit order. The first qubit is called the control, and the second qubit is the target. To perform a classically controlled Y, replace the Z target with a `rec` target like rec[-2]. To perform an I or Y gate as configured by sweep data, replace the Z target with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Apply Y to qubit 5 controlled by qubit 2. YCZ 5 2 # Perform CY 2 5 then CY 4 2. YCZ 5 2 2 4 # Apply Y to qubit 6 if the most recent measurement result was TRUE. YCZ 6 rec[-1] # Apply Y to qubits 7 and 8 conditioned on sweep configuration data. YCZ 7 sweep[5] 8 sweep[5] )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, -i}, {0, 0, i, 0}}, .flow_data = {"+XZ", "+ZZ", "+YX", "+IZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 CNOT 1 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "CX", .id = GateType::CX, .best_candidate_inverse_id = GateType::CX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS | GATE_CAN_TARGET_BITS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The Z-controlled X gate. Applies an X gate to the target if the control is in the |1> state. Equivalently: negates the amplitude of the |1>|-> state. The first qubit is called the control, and the second qubit is the target. To perform a classically controlled X, replace the control with a `rec` target like rec[-2]. To perform an I or X gate as configured by sweep data, replace the control with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Bit flip qubit 5 controlled by qubit 2. CX 2 5 # Perform CX 2 5 then CX 4 2. CX 2 5 4 2 # Bit flip qubit 6 if the most recent measurement result was TRUE. CX rec[-1] 6 # Bit flip qubits 7 and 8 conditioned on sweep configuration data. CX sweep[5] 7 sweep[5] 8 )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}, {0, 1, 0, 0}}, .flow_data = {"+XX", "+ZI", "+IX", "+ZZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( CNOT 0 1 )CIRCUIT", }); add_gate_alias(failed, "ZCX", "CX"); add_gate_alias(failed, "CNOT", "CX"); add_gate( failed, Gate{ .name = "CY", .id = GateType::CY, .best_candidate_inverse_id = GateType::CY, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS | GATE_CAN_TARGET_BITS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The Z-controlled Y gate. Applies a Y gate to the target if the control is in the |1> state. Equivalently: negates the amplitude of the |1>|-i> state. The first qubit is the control, and the second qubit is the target. To perform a classically controlled Y, replace the control with a `rec` target like rec[-2]. To perform an I or Y gate as configured by sweep data, replace the control with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Apply Y to qubit 5 controlled by qubit 2. CY 2 5 # Perform CY 2 5 then CY 4 2. CY 2 5 4 2 # Apply Y to qubit 6 if the most recent measurement result was TRUE. CY rec[-1] 6 # Apply Y to qubits 7 and 8 conditioned on sweep configuration data. CY sweep[5] 7 sweep[5] 8 )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 0, 0, -i}, {0, 0, 1, 0}, {0, i, 0, 0}}, .flow_data = {"+XY", "+ZI", "+ZX", "+ZZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 1 S 1 S 1 CNOT 0 1 S 1 )CIRCUIT", }); add_gate_alias(failed, "ZCY", "CY"); add_gate( failed, Gate{ .name = "CZ", .id = GateType::CZ, .best_candidate_inverse_id = GateType::CZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS | GATE_CAN_TARGET_BITS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( The Z-controlled Z gate. Applies a Z gate to the target if the control is in the |1> state. Equivalently: negates the amplitude of the |1>|1> state. The first qubit is called the control, and the second qubit is the target. To perform a classically controlled Z, replace either qubit with a `rec` target like rec[-2]. To perform an I or Z gate as configured by sweep data, replace either qubit with a `sweep` target like sweep[3]. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Example: # Apply Z to qubit 5 controlled by qubit 2. CZ 2 5 # Perform CZ 2 5 then CZ 4 2. CZ 2 5 4 2 # Apply Z to qubit 6 if the most recent measurement result was TRUE. CZ rec[-1] 6 # Apply Z to qubit 7 if the 3rd most recent measurement result was TRUE. CZ 7 rec[-3] # Apply Z to qubits 7 and 8 conditioned on sweep configuration data. CZ sweep[5] 7 8 sweep[5] )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, -1}}, .flow_data = {"+XZ", "+ZI", "+ZX", "+IZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 1 CNOT 0 1 H 1 )CIRCUIT", }); add_gate_alias(failed, "ZCZ", "CZ"); } ================================================ FILE: src/stim/gates/gate_data_hada.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; static constexpr std::complex i = std::complex(0, 1); static constexpr std::complex s = 0.7071067811865475244f; void GateDataMap::add_gate_data_hada(bool &failed) { add_gate( failed, Gate{ .name = "H", .id = GateType::H, .best_candidate_inverse_id = GateType::H, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( The Hadamard gate. Swaps the X and Z axes. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{s, s}, {s, -s}}, .flow_data = {"+Z", "+X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 )CIRCUIT", }); add_gate_alias(failed, "H_XZ", "H"); add_gate( failed, Gate{ .name = "H_XY", .id = GateType::H_XY, .best_candidate_inverse_id = GateType::H_XY, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( A variant of the Hadamard gate that swaps the X and Y axes (instead of X and Z). Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0, s - i * s}, {s + i * s, 0}}, .flow_data = {"+Y", "-Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 S 0 H 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "H_YZ", .id = GateType::H_YZ, .best_candidate_inverse_id = GateType::H_YZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( A variant of the Hadamard gate that swaps the Y and Z axes (instead of X and Z). Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{s, -i * s}, {i * s, -s}}, .flow_data = {"-X", "+Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 H 0 S 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "H_NXY", .id = GateType::H_NXY, .best_candidate_inverse_id = GateType::H_NXY, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( A variant of the Hadamard gate that swaps the -X and +Y axes. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0, s + s * i}, {s - s * i, 0}}, .flow_data = {"-Y", "-Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 H 0 S 0 S 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "H_NXZ", .id = GateType::H_NXZ, .best_candidate_inverse_id = GateType::H_NXZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( A variant of the Hadamard gate that swaps the -X and +Z axes. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{-s, s}, {s, s}}, .flow_data = {"-Z", "-X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 H 0 S 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "H_NYZ", .id = GateType::H_NYZ, .best_candidate_inverse_id = GateType::H_NYZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( A variant of the Hadamard gate that swaps the -Y and +Z axes. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{-s, -i * s}, {i * s, s}}, .flow_data = {"-X", "-Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 H 0 S 0 H 0 )CIRCUIT", }); } ================================================ FILE: src/stim/gates/gate_data_heralded.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; void GateDataMap::add_gate_data_heralded(bool &failed) { add_gate( failed, Gate{ .name = "HERALDED_ERASE", .id = GateType::HERALDED_ERASE, .best_candidate_inverse_id = GateType::HERALDED_ERASE, .arg_count = 1, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_PRODUCES_RESULTS), .category = "F_Noise Channels", .help = R"MARKDOWN( The heralded erasure noise channel. Whether or not this noise channel fires is recorded into the measurement record. When it doesn't fire, nothing happens to the target qubit and a 0 is recorded. When it does fire, a 1 is recorded and the target qubit is erased to the maximally mixed state by applying X_ERROR(0.5) and Z_ERROR(0.5). CAUTION: when converting a circuit with this error into a detector error model, this channel is split into multiple potential effects. In the context of a DEM, these effects are considered independent. This is an approximation, because independent effects can be combined. The effect of this approximation, assuming a detector is declared on the herald, is that it appears this detector can be cancelled out by two of the (originally disjoint) heralded effects firing together. Sampling from the DEM instead of the circuit can thus produce unheralded errors, even if the circuit noise model only contains heralded errors. These issues occur with probability p^2, where p is the probability of a heralded error, since two effects that came from the same heralded error must occur together to cancel out the herald detector. This also means a decoder configured using the DEM will think there's a chance of unheralded errors even if the circuit the DEM came from only uses heralded errors. Parens Arguments: A single float (p) specifying the chance of the noise firing. Targets: Qubits to apply single-qubit depolarizing noise to. Each target is operated on independently. Pauli Mixture: 1-p: record 0, apply I p/4: record 1, apply I p/4: record 1, apply X p/4: record 1, apply Y p/4: record 1, apply Z Examples: # Erase qubit 0 with probability 1% HERALDED_ERASE(0.01) 0 # Declare a flag detector based on the erasure DETECTOR rec[-1] # Erase qubit 2 with 2% probability # Separately, erase qubit 3 with 2% probability HERALDED_ERASE(0.02) 2 3 # Do an XXXX measurement MPP X2*X3*X5*X7 # Apply partially-heralded noise to the two qubits HERALDED_ERASE(0.01) 2 3 5 7 DEPOLARIZE1(0.0001) 2 3 5 7 # Repeat the XXXX measurement MPP X2*X3*X5*X7 # Declare a detector comparing the two XXXX measurements DETECTOR rec[-1] rec[-6] # Declare flag detectors based on the erasures DETECTOR rec[-2] DETECTOR rec[-3] DETECTOR rec[-4] DETECTOR rec[-5] )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "HERALDED_PAULI_CHANNEL_1", .id = GateType::HERALDED_PAULI_CHANNEL_1, .best_candidate_inverse_id = GateType::HERALDED_PAULI_CHANNEL_1, .arg_count = 4, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_PRODUCES_RESULTS), .category = "F_Noise Channels", .help = R"MARKDOWN( A heralded error channel that applies biased noise. This error records a bit into the measurement record, indicating whether or not the herald fired. How likely it is that the herald fires, and the corresponding chance of each possible error effect (I, X, Y, or Z) are configured by the parens arguments of the instruction. CAUTION: when converting a circuit with this error into a detector error model, this channel is split into multiple potential effects. In the context of a DEM, these effects are considered independent. This is an approximation, because independent effects can be combined. The effect of this approximation, assuming a detector is declared on the herald, is that it appears this detector can be cancelled out by two of the (originally disjoint) heralded effects firing together. Sampling from the DEM instead of the circuit can thus produce unheralded errors, even if the circuit noise model only contains heralded errors. These issues occur with probability p^2, where p is the probability of a heralded error, since two effects that came from the same heralded error must occur together to cancel out the herald detector. This also means a decoder configured using the DEM will think there's a chance of unheralded errors even if the circuit the DEM came from only uses heralded errors. Parens Arguments: This instruction takes four arguments (pi, px, py, pz). The arguments are disjoint probabilities, specifying the chances of heralding with various effects. pi is the chance of heralding with no effect (a false positive). px is the chance of heralding with an X error. py is the chance of heralding with a Y error. pz is the chance of heralding with a Z error. Targets: Qubits to apply heralded biased noise to. Pauli Mixture: 1-pi-px-py-pz: record 0, apply I pi: record 1, apply I px: record 1, apply X py: record 1, apply Y pz: record 1, apply Z Examples: # With 10% probability perform a phase flip of qubit 0. HERALDED_PAULI_CHANNEL_1(0, 0, 0, 0.1) 0 DETECTOR rec[-1] # Include the herald in detectors available to the decoder # With 20% probability perform a heralded dephasing of qubit 0. HERALDED_PAULI_CHANNEL_1(0.1, 0, 0, 0.1) 0 DETECTOR rec[-1] # Subject a Bell Pair to heralded noise. MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.03, 0.04) 0 1 MXX 0 1 MZZ 0 1 DETECTOR rec[-1] rec[-5] # Did ZZ stabilizer change? DETECTOR rec[-2] rec[-6] # Did XX stabilizer change? DETECTOR rec[-3] # Did the herald on qubit 1 fire? DETECTOR rec[-4] # Did the herald on qubit 0 fire? )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); } ================================================ FILE: src/stim/gates/gate_data_noisy.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; void GateDataMap::add_gate_data_noisy(bool &failed) { add_gate( failed, Gate{ .name = "DEPOLARIZE1", .id = GateType::DEPOLARIZE1, .best_candidate_inverse_id = GateType::DEPOLARIZE1, .arg_count = 1, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "F_Noise Channels", .help = R"MARKDOWN( The single qubit depolarizing channel. Applies a single-qubit depolarizing error with the given probability. When a single-qubit depolarizing error is applied, a random Pauli error (except for I) is chosen and applied. Note that this means maximal mixing occurs when the probability parameter is set to 75%, rather than at 100%. Applies a randomly chosen Pauli with a given probability. Parens Arguments: A single float (p) specifying the depolarization strength. Targets: Qubits to apply single-qubit depolarizing noise to. Pauli Mixture: 1-p: I p/3: X p/3: Y p/3: Z Examples: # Apply 1-qubit depolarization to qubit 0 using p=1% DEPOLARIZE1(0.01) 0 # Apply 1-qubit depolarization to qubit 2 # Separately apply 1-qubit depolarization to qubits 3 and 5 DEPOLARIZE1(0.01) 2 3 5 # Maximally mix qubits 0 through 2 DEPOLARIZE1(0.75) 0 1 2 )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "DEPOLARIZE2", .id = GateType::DEPOLARIZE2, .best_candidate_inverse_id = GateType::DEPOLARIZE2, .arg_count = 1, .flags = (GateFlags)(GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_TARGETS_PAIRS), .category = "F_Noise Channels", .help = R"MARKDOWN( The two qubit depolarizing channel. Applies a two-qubit depolarizing error with the given probability. When a two-qubit depolarizing error is applied, a random pair of Pauli errors (except for II) is chosen and applied. Note that this means maximal mixing occurs when the probability parameter is set to 93.75%, rather than at 100%. Parens Arguments: A single float (p) specifying the depolarization strength. Targets: Qubit pairs to apply two-qubit depolarizing noise to. Pauli Mixture: 1-p: II p/15: IX p/15: IY p/15: IZ p/15: XI p/15: XX p/15: XY p/15: XZ p/15: YI p/15: YX p/15: YY p/15: YZ p/15: ZI p/15: ZX p/15: ZY p/15: ZZ Examples: # Apply 2-qubit depolarization to qubit 0 and qubit 1 using p=1% DEPOLARIZE2(0.01) 0 1 # Apply 2-qubit depolarization to qubit 2 and qubit 3 # Separately apply 2-qubit depolarization to qubit 5 and qubit 7 DEPOLARIZE2(0.01) 2 3 5 7 # Maximally mix qubits 0 through 3 DEPOLARIZE2(0.9375) 0 1 2 3 )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "I_ERROR", .id = GateType::I_ERROR, .best_candidate_inverse_id = GateType::I_ERROR, .arg_count = ARG_COUNT_SYGIL_ANY, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "F_Noise Channels", .help = R"MARKDOWN( Applies an identity with a given probability. This gate has no effect. It only exists because it can be useful as a communication mechanism for systems built on top of stim. Parens Arguments: A list of disjoint probabilities summing to at most 1. The probabilities have no effect on stim simulations or error analysis, but may be interpreted in arbitrary ways by external tools. Targets: Qubits to apply identity noise to. Pauli Mixture: *: I Examples: # does nothing I_ERROR 0 # does nothing with probability 0.1, else does nothing I_ERROR(0.1) 0 # doesn't require a probability argument I_ERROR[LEAKAGE_NOISE_FOR_AN_ADVANCED_SIMULATOR:0.1] 0 2 4 # checks for you that the disjoint probabilities in the arguments are legal I_ERROR[MULTIPLE_NOISE_MECHANISMS](0.1, 0.2) 0 2 4 )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "II_ERROR", .id = GateType::II_ERROR, .best_candidate_inverse_id = GateType::II_ERROR, .arg_count = ARG_COUNT_SYGIL_ANY, .flags = (GateFlags)(GATE_TARGETS_PAIRS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "F_Noise Channels", .help = R"MARKDOWN( Applies a two-qubit identity with a given probability. This gate has no effect. It only exists because it can be useful as a communication mechanism for systems built on top of stim. Parens Arguments: A list of disjoint probabilities summing to at most 1. The probabilities have no effect on stim simulations or error analysis, but may be interpreted in arbitrary ways by external tools. Targets: Qubits to apply identity noise to. Pauli Mixture: *: II Examples: # does nothing II_ERROR 0 1 # does nothing with probability 0.1, else does nothing II_ERROR(0.1) 0 1 # checks for you that the targets are two-qubit pairs II_ERROR[TWO_QUBIT_LEAKAGE_NOISE_FOR_AN_ADVANCED_SIMULATOR:0.1] 0 2 4 6 # checks for you that the disjoint probabilities in the arguments are legal II_ERROR[MULTIPLE_TWO_QUBIT_NOISE_MECHANISMS](0.1, 0.2) 0 2 4 6 )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "X_ERROR", .id = GateType::X_ERROR, .best_candidate_inverse_id = GateType::X_ERROR, .arg_count = 1, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "F_Noise Channels", .help = R"MARKDOWN( Applies a Pauli X with a given probability. Parens Arguments: A single float specifying the probability of applying an X operation. Targets: Qubits to apply bit flip noise to. Pauli Mixture: 1-p: I p : X )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "Y_ERROR", .id = GateType::Y_ERROR, .best_candidate_inverse_id = GateType::Y_ERROR, .arg_count = 1, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "F_Noise Channels", .help = R"MARKDOWN( Applies a Pauli Y with a given probability. Parens Arguments: A single float specifying the probability of applying a Y operation. Targets: Qubits to apply Y flip noise to. Pauli Mixture: 1-p: I p : Y )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "Z_ERROR", .id = GateType::Z_ERROR, .best_candidate_inverse_id = GateType::Z_ERROR, .arg_count = 1, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "F_Noise Channels", .help = R"MARKDOWN( Applies a Pauli Z with a given probability. Parens Arguments: A single float specifying the probability of applying a Z operation. Targets: Qubits to apply phase flip noise to. Pauli Mixture: 1-p: I p : Z )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "PAULI_CHANNEL_1", .id = GateType::PAULI_CHANNEL_1, .best_candidate_inverse_id = GateType::PAULI_CHANNEL_1, .arg_count = 3, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "F_Noise Channels", .help = R"MARKDOWN( A single qubit Pauli error channel with explicitly specified probabilities for each case. Parens Arguments: Three floats specifying disjoint Pauli case probabilities. px: Disjoint probability of applying an X error. py: Disjoint probability of applying a Y error. pz: Disjoint probability of applying a Z error. Targets: Qubits to apply the custom noise channel to. Example: # Sample errors from the distribution 10% X, 15% Y, 20% Z, 55% I. # Apply independently to qubits 1, 2, 4. PAULI_CHANNEL_1(0.1, 0.15, 0.2) 1 2 4 Pauli Mixture: 1-px-py-pz: I px: X py: Y pz: Z )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "PAULI_CHANNEL_2", .id = GateType::PAULI_CHANNEL_2, .best_candidate_inverse_id = GateType::PAULI_CHANNEL_2, .arg_count = 15, .flags = (GateFlags)(GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_TARGETS_PAIRS), .category = "F_Noise Channels", .help = R"MARKDOWN( A two qubit Pauli error channel with explicitly specified probabilities for each case. Parens Arguments: Fifteen floats specifying the disjoint probabilities of each possible Pauli pair that can occur (except for the non-error double identity case). The disjoint probability arguments are (in order): 1. pix: Probability of applying an IX operation. 2. piy: Probability of applying an IY operation. 3. piz: Probability of applying an IZ operation. 4. pxi: Probability of applying an XI operation. 5. pxx: Probability of applying an XX operation. 6. pxy: Probability of applying an XY operation. 7. pxz: Probability of applying an XZ operation. 8. pyi: Probability of applying a YI operation. 9. pyx: Probability of applying a YX operation. 10. pyy: Probability of applying a YY operation. 11. pyz: Probability of applying a YZ operation. 12. pzi: Probability of applying a ZI operation. 13. pzx: Probability of applying a ZX operation. 14. pzy: Probability of applying a ZY operation. 15. pzz: Probability of applying a ZZ operation. Targets: Pairs of qubits to apply the custom noise channel to. There must be an even number of targets. Example: # Sample errors from the distribution 10% XX, 20% YZ, 70% II. # Apply independently to qubit pairs (1,2), (5,6), and (8,3) PAULI_CHANNEL_2(0,0,0, 0,0.1,0,0, 0,0,0,0.2, 0,0,0,0) 1 2 5 6 8 3 Pauli Mixture: 1-pix-piy-piz-pxi-pxx-pxy-pxz-pyi-pyx-pyy-pyz-pzi-pzx-pzy-pzz: II pix: IX piy: IY piz: IZ pxi: XI pxx: XX pxy: XY pxz: XZ pyi: YI pyx: YX pyy: YY pyz: YZ pzi: ZI pzx: ZX pzy: ZY pzz: ZZ )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate( failed, Gate{ .name = "E", .id = GateType::E, .best_candidate_inverse_id = GateType::E, .arg_count = 1, .flags = (GateFlags)(GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_TARGETS_PAULI_STRING | GATE_IS_NOT_FUSABLE), .category = "F_Noise Channels", .help = R"MARKDOWN( Probabilistically applies a Pauli product error with a given probability. Sets the "correlated error occurred flag" to true if the error occurred. Otherwise sets the flag to false. See also: `ELSE_CORRELATED_ERROR`. Parens Arguments: A single float specifying the probability of applying the Paulis making up the error. Targets: Pauli targets specifying the Paulis to apply when the error occurs. Note that, for backwards compatibility reasons, the targets are not combined using combiners (`*`). They are implicitly all combined. Example: # With 60% probability, uniformly pick X1*Y2 or Z2*Z3 or X1*Y2*Z3. CORRELATED_ERROR(0.2) X1 Y2 ELSE_CORRELATED_ERROR(0.25) Z2 Z3 ELSE_CORRELATED_ERROR(0.33333333333) X1 Y2 Z3 )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); add_gate_alias(failed, "CORRELATED_ERROR", "E"); add_gate( failed, Gate{ .name = "ELSE_CORRELATED_ERROR", .id = GateType::ELSE_CORRELATED_ERROR, .best_candidate_inverse_id = GateType::ELSE_CORRELATED_ERROR, .arg_count = 1, .flags = (GateFlags)(GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES | GATE_TARGETS_PAULI_STRING | GATE_IS_NOT_FUSABLE), .category = "F_Noise Channels", .help = R"MARKDOWN( Probabilistically applies a Pauli product error with a given probability, unless the "correlated error occurred flag" is set. If the error occurs, sets the "correlated error occurred flag" to true. Otherwise leaves the flag alone. Note: when converting a circuit into a detector error model, every `ELSE_CORRELATED_ERROR` instruction must be preceded by an ELSE_CORRELATED_ERROR instruction or an E instruction. In other words, ELSE_CORRELATED_ERROR instructions should appear in contiguous chunks started by a CORRELATED_ERROR. See also: `CORRELATED_ERROR`. Parens Arguments: A single float specifying the probability of applying the Paulis making up the error, conditioned on the "correlated error occurred flag" being False. Targets: Pauli targets specifying the Paulis to apply when the error occurs. Note that, for backwards compatibility reasons, the targets are not combined using combiners (`*`). They are implicitly all combined. Example: # With 60% probability, uniformly pick X1*Y2 or Z2*Z3 or X1*Y2*Z3. CORRELATED_ERROR(0.2) X1 Y2 ELSE_CORRELATED_ERROR(0.25) Z2 Z3 ELSE_CORRELATED_ERROR(0.33333333333) X1 Y2 Z3 )MARKDOWN", .unitary_data = {}, .flow_data = {}, .h_s_cx_m_r_decomposition = nullptr, }); } ================================================ FILE: src/stim/gates/gate_data_pair_measure.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; void GateDataMap::add_gate_data_pair_measure(bool &failed) { add_gate( failed, Gate{ .name = "MXX", .id = GateType::MXX, .best_candidate_inverse_id = GateType::MXX, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_TARGETS_PAIRS | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "L_Pair Measurement Gates", .help = R"MARKDOWN( Two-qubit X basis parity measurement. This operation measures whether pairs of qubits are in the {|++>,|-->} subspace or in the {|+->,|-+>} subspace of the two qubit state space. |+> and |-> are the +1 and -1 eigenvectors of the X operator. If the qubits were in the {|++>,|-->} subspace, False is appended to the measurement record. If the qubits were in the {|+->,|-+>} subspace, True is appended to the measurement record. Inverting one of the qubit targets inverts the result. Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The pairs of qubits to measure in the X basis. This operation accepts inverted qubit targets (like `!5` instead of `5`). Inverted targets flip the measurement result. Examples: # Measure the +XX observable of qubit 1 vs qubit 2. MXX 1 2 # Measure the -XX observable of qubit 1 vs qubit 2. MXX !1 2 # Do a noisy measurement of the +XX observable of qubit 2 vs qubit 3. # The result recorded to the measurement record will be flipped 1% of the time. MXX(0.01) 2 3 # Measure the +XX observable qubit 1 vs qubit 2, and also qubit 8 vs qubit 9 MXX 1 2 8 9 # Perform multiple noisy measurements. # Each measurement has an independent 2% chance of being recorded wrong. MXX(0.02) 2 3 5 7 11 19 17 4 )MARKDOWN", .unitary_data = {}, .flow_data = { "X_ -> +X_", "_X -> +_X", "ZZ -> +ZZ", "XX -> rec[-1]", }, .h_s_cx_m_r_decomposition = R"CIRCUIT( CX 0 1 H 0 M 0 H 0 CX 0 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "MYY", .id = GateType::MYY, .best_candidate_inverse_id = GateType::MYY, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_TARGETS_PAIRS | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "L_Pair Measurement Gates", .help = R"MARKDOWN( Two-qubit Y basis parity measurement. This operation measures whether pairs of qubits are in the {|ii>,|jj>} subspace or in the {|ij>,|ji>} subspace of the two qubit state space. |i> and |j> are the +1 and -1 eigenvectors of the Y operator. If the qubits were in the {|ii>,|jj>} subspace, False is appended to the measurement record. If the qubits were in the {|ij>,|ji>} subspace, True is appended to the measurement record. Inverting one of the qubit targets inverts the result. Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The pairs of qubits to measure in the Y basis. This operation accepts inverted qubit targets (like `!5` instead of `5`). Inverted targets flip the measurement result. Examples: # Measure the +YY observable of qubit 1 vs qubit 2. MYY 1 2 # Measure the -YY observable of qubit 1 vs qubit 2. MYY !1 2 # Do a noisy measurement of the +YY observable of qubit 2 vs qubit 3. # The result recorded to the measurement record will be flipped 1% of the time. MYY(0.01) 2 3 # Measure the +YY observable qubit 1 vs qubit 2, and also qubit 8 vs qubit 9 MYY 1 2 8 9 # Perform multiple noisy measurements. # Each measurement has an independent 2% chance of being recorded wrong. MYY(0.02) 2 3 5 7 11 19 17 4 )MARKDOWN", .unitary_data = {}, .flow_data = { "XX -> +XX", "Y_ -> +Y_", "_Y -> +_Y", "YY -> rec[-1]", }, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 1 CX 0 1 H 0 M 0 S 1 1 H 0 CX 0 1 S 0 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "MZZ", .id = GateType::MZZ, .best_candidate_inverse_id = GateType::MZZ, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_TARGETS_PAIRS | GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "L_Pair Measurement Gates", .help = R"MARKDOWN( Two-qubit Z basis parity measurement. This operation measures whether pairs of qubits are in the {|00>,|11>} subspace or in the {|01>,|10>} subspace of the two qubit state space. |0> and |1> are the +1 and -1 eigenvectors of the Z operator. If the qubits were in the {|00>,|11>} subspace, False is appended to the measurement record. If the qubits were in the {|01>,|10>} subspace, True is appended to the measurement record. Inverting one of the qubit targets inverts the result. Parens Arguments: If no parens argument is given, the measurement is perfect. If one parens argument is given, the measurement result is noisy. The argument is the probability of returning the wrong result. Targets: The pairs of qubits to measure in the Z basis. This operation accepts inverted qubit targets (like `!5` instead of `5`). Inverted targets flip the measurement result. Examples: # Measure the +ZZ observable of qubit 1 vs qubit 2. MZZ 1 2 # Measure the -ZZ observable of qubit 1 vs qubit 2. MZZ !1 2 # Do a noisy measurement of the +ZZ observable of qubit 2 vs qubit 3. # The result recorded to the measurement record will be flipped 1% of the time. MZZ(0.01) 2 3 # Measure the +ZZ observable qubit 1 vs qubit 2, and also qubit 8 vs qubit 9 MZZ 1 2 8 9 # Perform multiple noisy measurements. # Each measurement has an independent 2% chance of being recorded wrong. MZZ(0.02) 2 3 5 7 11 19 17 4 )MARKDOWN", .unitary_data = {}, .flow_data = { "XX -> XX", "Z_ -> +Z_", "_Z -> +_Z", "ZZ -> rec[-1]", }, .h_s_cx_m_r_decomposition = R"CIRCUIT( CX 0 1 M 1 CX 0 1 )CIRCUIT", }); } ================================================ FILE: src/stim/gates/gate_data_pauli.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; static constexpr std::complex i = std::complex(0, 1); void GateDataMap::add_gate_data_pauli(bool &failed) { add_gate( failed, Gate{ .name = "I", .id = GateType::I, .best_candidate_inverse_id = GateType::I, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "A_Pauli Gates", .help = R"MARKDOWN( The identity gate. Does nothing to the target qubits. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to do nothing to. )MARKDOWN", .unitary_data = {{1, 0}, {0, 1}}, .flow_data = {"+X", "+Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( # (no operations) )CIRCUIT", }); add_gate( failed, Gate{ .name = "X", .id = GateType::X, .best_candidate_inverse_id = GateType::X, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "A_Pauli Gates", .help = R"MARKDOWN( The Pauli X gate. The bit flip gate. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0, 1}, {1, 0}}, .flow_data = {"+X", "-Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 S 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "Y", .id = GateType::Y, .best_candidate_inverse_id = GateType::Y, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "A_Pauli Gates", .help = R"MARKDOWN( The Pauli Y gate. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0, -i}, {i, 0}}, .flow_data = {"-X", "-Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 H 0 S 0 S 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "Z", .id = GateType::Z, .best_candidate_inverse_id = GateType::Z, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "A_Pauli Gates", .help = R"MARKDOWN( The Pauli Z gate. The phase flip gate. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{1, 0}, {0, -1}}, .flow_data = {"-X", "+Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 )CIRCUIT", }); } ================================================ FILE: src/stim/gates/gate_data_pauli_product.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; void GateDataMap::add_gate_data_pauli_product(bool &failed) { add_gate( failed, Gate{ .name = "MPP", .id = GateType::MPP, .best_candidate_inverse_id = GateType::MPP, .arg_count = ARG_COUNT_SYGIL_ZERO_OR_ONE, .flags = (GateFlags)(GATE_PRODUCES_RESULTS | GATE_IS_NOISY | GATE_TARGETS_PAULI_STRING | GATE_TARGETS_COMBINERS | GATE_ARGS_ARE_DISJOINT_PROBABILITIES), .category = "P_Generalized Pauli Product Gates", .help = R"MARKDOWN( Measures general pauli product operators, like X1*Y2*Z3. Parens Arguments: An optional failure probability. If no argument is given, all measurements are perfect. If one argument is given, it's the chance of reporting measurement results incorrectly. Targets: A series of Pauli products to measure. Each Pauli product is a series of Pauli targets (like `X1`, `Y2`, or `Z3`) separated by combiners (`*`). Each Pauli term can be inverted (like `!Y2` instead of `Y2`). A negated product will record the opposite measurement result. Note that, although you can write down instructions that measure anti-Hermitian products, like `MPP X1*Z1`, doing this will cause exceptions when you simulate or analyze the circuit since measuring an anti-Hermitian operator doesn't have well defined semantics. Using overly-complicated Hermitian products, like saying `MPP X1*Y1*Y2*Z2` instead of `MPP !Z1*X2`, is technically allowed. But probably not a great idea since tools consuming the circuit may have assumed that each qubit would appear at most once in each product. Examples: # Measure the two-body +X1*Y2 observable. MPP X1*Y2 # Measure the one-body -Z5 observable. MPP !Z5 # Measure the two-body +X1*Y2 observable and also the three-body -Z3*Z4*Z5 observable. MPP X1*Y2 !Z3*Z4*Z5 # Noisily measure +Z1+Z2 and +X1*X2 (independently flip each reported result 0.1% of the time). MPP(0.001) Z1*Z2 X1*X2 )MARKDOWN", .unitary_data = {}, .flow_data = { "XYZ__ -> rec[-2]", "___XX -> rec[-1]", "X____ -> X____", "_Y___ -> _Y___", "__Z__ -> __Z__", "___X_ -> ___X_", "____X -> ____X", "ZZ___ -> ZZ___", "_XX__ -> _XX__", "___ZZ -> ___ZZ", }, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 1 1 1 H 0 1 3 4 CX 2 0 1 0 4 3 M 0 3 CX 2 0 1 0 4 3 H 0 1 3 4 S 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SPP", .id = GateType::SPP, .best_candidate_inverse_id = GateType::SPP_DAG, .arg_count = 0, .flags = (GateFlags)(GATE_TARGETS_PAULI_STRING | GATE_TARGETS_COMBINERS | GATE_IS_UNITARY), .category = "P_Generalized Pauli Product Gates", .help = R"MARKDOWN( The generalized S gate. Phases the -1 eigenspace of Pauli product observables by i. Parens Arguments: This instruction takes no parens arguments. Targets: A series of Pauli products to phase. Each Pauli product is a series of Pauli targets (like `X1`, `Y2`, or `Z3`) separated by combiners (`*`). Each Pauli term can be inverted (like `!Y2` instead of `Y2`), to negate the product. Note that, although you can write down instructions that phase anti-Hermitian products, like `SPP X1*Z1`, doing this will cause exceptions when you simulate or analyze the circuit since phasing an anti-Hermitian operator doesn't have well defined semantics. Using overly-complicated Hermitian products, like saying `SPP X1*Y1*Y2*Z2` instead of `SPP !Z1*X2`, is technically allowed. But probably not a great idea since tools consuming the circuit may have assumed that each qubit would appear at most once in each product. Examples: # Perform an S gate on qubit 1. SPP Z1 # Perform a SQRT_X gate on qubit 1. SPP X1 # Perform a SQRT_X_DAG gate on qubit 1. SPP !X1 # Perform a SQRT_XX gate between qubit 1 and qubit 2. SPP X1*X2 # Perform a SQRT_YY gate between qubit 1 and 2, and a SQRT_ZZ_DAG between qubit 3 and 4. SPP Y1*Y2 !Z1*Z2 # Phase the -1 eigenspace of -X1*Y2*Z3 by i. SPP !X1*Y2*Z3 )MARKDOWN", .unitary_data = {}, .flow_data = { // For "SPP X0*Y1*Z2" "X__ -> X__", "Z__ -> -YYZ", "_X_ -> -XZZ", "_Z_ -> XXZ", "__X -> XYY", "__Z -> __Z", }, .h_s_cx_m_r_decomposition = R"CIRCUIT( CX 2 1 CX 1 0 S 1 S 1 H 1 CX 1 0 CX 2 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SPP_DAG", .id = GateType::SPP_DAG, .best_candidate_inverse_id = GateType::SPP, .arg_count = 0, .flags = (GateFlags)(GATE_TARGETS_PAULI_STRING | GATE_TARGETS_COMBINERS | GATE_IS_UNITARY), .category = "P_Generalized Pauli Product Gates", .help = R"MARKDOWN( The generalized S_DAG gate. Phases the -1 eigenspace of Pauli product observables by -i. Parens Arguments: This instruction takes no parens arguments. Targets: A series of Pauli products to phase. Each Pauli product is a series of Pauli targets (like `X1`, `Y2`, or `Z3`) separated by combiners (`*`). Each Pauli term can be inverted (like `!Y2` instead of `Y2`), to negate the product. Note that, although you can write down instructions that phase anti-Hermitian products, like `SPP X1*Z1`, doing this will cause exceptions when you simulate or analyze the circuit since phasing an anti-Hermitian operator doesn't have well defined semantics. Using overly-complicated Hermitian products, like saying `SPP X1*Y1*Y2*Z2` instead of `SPP !Z1*X2`, is technically allowed. But probably not a great idea since tools consuming the circuit may have assumed that each qubit would appear at most once in each product. Examples: # Perform an S_DAG gate on qubit 1. SPP_DAG Z1 # Perform a SQRT_X_DAG gate on qubit 1. SPP_DAG X1 # Perform a SQRT_X gate on qubit 1. SPP_DAG !X1 # Perform a SQRT_XX_DAG gate between qubit 1 and qubit 2. SPP_DAG X1*X2 # Perform a SQRT_YY_DAG gate between qubit 1 and 2, and a SQRT_ZZ between qubit 3 and 4. SPP_DAG Y1*Y2 !Z1*Z2 # Phase the -1 eigenspace of -X1*Y2*Z3 by -i. SPP_DAG !X1*Y2*Z3 )MARKDOWN", .unitary_data = {}, .flow_data = { // For "SPP_DAG X0*Y1*Z2" "X__ -> X__", "Z__ -> YYZ", "_X_ -> XZZ", "_Z_ -> -XXZ", "__X -> -XYY", "__Z -> __Z", }, .h_s_cx_m_r_decomposition = R"CIRCUIT( CX 2 1 CX 1 0 H 1 S 1 S 1 CX 1 0 CX 2 1 )CIRCUIT", }); } ================================================ FILE: src/stim/gates/gate_data_period_3.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; static constexpr std::complex i = std::complex(0, 1); void GateDataMap::add_gate_data_period_3(bool &failed) { add_gate( failed, Gate{ .name = "C_XYZ", .id = GateType::C_XYZ, .best_candidate_inverse_id = GateType::C_ZYX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Right handed period 3 axis cycling gate, sending X -> Y -> Z -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - i * 0.5f, -0.5f - 0.5f * i}, {0.5f - 0.5f * i, 0.5f + 0.5f * i}}, .flow_data = {"+Y", "+X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "C_NXYZ", .id = GateType::C_NXYZ, .best_candidate_inverse_id = GateType::C_ZYNX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Performs the period-3 cycle -X -> Y -> Z -> -X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f + i * 0.5f, 0.5f - 0.5f * i}, {-0.5f - 0.5f * i, 0.5f - 0.5f * i}}, .flow_data = {"-Y", "-X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 H 0 S 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "C_XNYZ", .id = GateType::C_XNYZ, .best_candidate_inverse_id = GateType::C_ZNYX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Performs the period-3 cycle X -> -Y -> Z -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f + i * 0.5f, -0.5f + 0.5f * i}, {0.5f + 0.5f * i, 0.5f - 0.5f * i}}, .flow_data = {"-Y", "+X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "C_XYNZ", .id = GateType::C_XYNZ, .best_candidate_inverse_id = GateType::C_NZYX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Performs the period-3 cycle X -> Y -> -Z -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - i * 0.5f, 0.5f + 0.5f * i}, {-0.5f + 0.5f * i, 0.5f + 0.5f * i}}, .flow_data = {"+Y", "-X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 H 0 S 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "C_ZYX", .id = GateType::C_ZYX, .best_candidate_inverse_id = GateType::C_XYZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Left handed period 3 axis cycling gate, sending Z -> Y -> X -> Z. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f + i * 0.5f, 0.5f + 0.5f * i}, {-0.5f + 0.5f * i, 0.5f - 0.5f * i}}, .flow_data = {"+Z", "+Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "C_ZYNX", .id = GateType::C_ZYNX, .best_candidate_inverse_id = GateType::C_NXYZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Performs the period-3 cycle -X -> Z -> Y -> -X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - i * 0.5f, -0.5f + 0.5f * i}, {0.5f + 0.5f * i, 0.5f + 0.5f * i}}, .flow_data = {"-Z", "+Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 H 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "C_ZNYX", .id = GateType::C_ZNYX, .best_candidate_inverse_id = GateType::C_XNYZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Performs the period-3 cycle X -> Z -> -Y -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - i * 0.5f, 0.5f - 0.5f * i}, {-0.5f - 0.5f * i, 0.5f + 0.5f * i}}, .flow_data = {"+Z", "-Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 S 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "C_NZYX", .id = GateType::C_NZYX, .best_candidate_inverse_id = GateType::C_XYNZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Performs the period-3 cycle X -> -Z -> Y -> X. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f + i * 0.5f, -0.5f + -0.5f * i}, {0.5f - 0.5f * i, 0.5f - 0.5f * i}}, .flow_data = {"-Z", "-Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 H 0 S 0 S 0 S 0 )CIRCUIT", }); } ================================================ FILE: src/stim/gates/gate_data_period_4.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; static constexpr std::complex i = std::complex(0, 1); void GateDataMap::add_gate_data_period_4(bool &failed) { add_gate( failed, Gate{ .name = "SQRT_X", .id = GateType::SQRT_X, .best_candidate_inverse_id = GateType::SQRT_X_DAG, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Principal square root of X gate. Phases the amplitude of |-> by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f + 0.5f * i, 0.5f - 0.5f * i}, {0.5f - 0.5f * i, 0.5f + 0.5f * i}}, .flow_data = {"+X", "-Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_X_DAG", .id = GateType::SQRT_X_DAG, .best_candidate_inverse_id = GateType::SQRT_X, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Adjoint of the principal square root of X gate. Phases the amplitude of |-> by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - 0.5f * i, 0.5f + 0.5f * i}, {0.5f + 0.5f * i, 0.5f - 0.5f * i}}, .flow_data = {"+X", "+Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 H 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_Y", .id = GateType::SQRT_Y, .best_candidate_inverse_id = GateType::SQRT_Y_DAG, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Principal square root of Y gate. Phases the amplitude of |-i> by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f + 0.5f * i, -0.5f - 0.5f * i}, {0.5f + 0.5f * i, 0.5f + 0.5f * i}}, .flow_data = {"-Z", "+X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_Y_DAG", .id = GateType::SQRT_Y_DAG, .best_candidate_inverse_id = GateType::SQRT_Y, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Adjoint of the principal square root of Y gate. Phases the amplitude of |-i> by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - 0.5f * i, 0.5f - 0.5f * i}, {-0.5f + 0.5f * i, 0.5f - 0.5f * i}}, .flow_data = {"+Z", "-X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "S", .id = GateType::S, .best_candidate_inverse_id = GateType::S_DAG, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Principal square root of Z gate. Phases the amplitude of |1> by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{1, 0}, {0, i}}, .flow_data = {"+Y", "+Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 )CIRCUIT", }); add_gate_alias(failed, "SQRT_Z", "S"); add_gate( failed, Gate{ .name = "S_DAG", .id = GateType::S_DAG, .best_candidate_inverse_id = GateType::S, .arg_count = 0, .flags = (GateFlags)(GATE_IS_SINGLE_QUBIT_GATE | GATE_IS_UNITARY), .category = "B_Single Qubit Clifford Gates", .help = R"MARKDOWN( Adjoint of the principal square root of Z gate. Phases the amplitude of |1> by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubits to operate on. )MARKDOWN", .unitary_data = {{1, 0}, {0, -i}}, .flow_data = {"-Y", "+Z"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 )CIRCUIT", }); add_gate_alias(failed, "SQRT_Z_DAG", "S_DAG"); } ================================================ FILE: src/stim/gates/gate_data_pp.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; static constexpr std::complex i = std::complex(0, 1); void GateDataMap::add_gate_data_pp(bool &failed) { add_gate( failed, Gate{ .name = "II", .id = GateType::II, .best_candidate_inverse_id = GateType::II, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( A two-qubit identity gate. Twice as much doing-nothing as the I gate! This gate only exists because it can be useful as a communication mechanism for systems built on top of stim. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. Examples: II 0 1 R 0 II[ACTUALLY_A_LEAKAGE_ISWAP] 0 1 R 0 CX 1 0 )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}, .flow_data = {"+XI", "+ZI", "+IX", "+IZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_XX", .id = GateType::SQRT_XX, .best_candidate_inverse_id = GateType::SQRT_XX_DAG, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Phases the -1 eigenspace of the XX observable by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{0.5f + 0.5f * i, 0, 0, 0.5f - 0.5f * i}, {0, 0.5f + 0.5f * i, 0.5f - 0.5f * i, 0}, {0, 0.5f - 0.5f * i, 0.5f + 0.5f * i, 0}, {0.5f - 0.5f * i, 0, 0, 0.5f + 0.5f * i}}, .flow_data = {"+XI", "-YX", "+IX", "-XY"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 CNOT 0 1 H 1 S 0 S 1 H 0 H 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_XX_DAG", .id = GateType::SQRT_XX_DAG, .best_candidate_inverse_id = GateType::SQRT_XX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Phases the -1 eigenspace of the XX observable by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{0.5f - 0.5f * i, 0, 0, 0.5f + 0.5f * i}, {0, 0.5f - 0.5f * i, 0.5f + 0.5f * i, 0}, {0, 0.5f + 0.5f * i, 0.5f - 0.5f * i, 0}, {0.5f + 0.5f * i, 0, 0, 0.5f - 0.5f * i}}, .flow_data = {"+XI", "+YX", "+IX", "+XY"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 CNOT 0 1 H 1 S 0 S 0 S 0 S 1 S 1 S 1 H 0 H 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_YY", .id = GateType::SQRT_YY, .best_candidate_inverse_id = GateType::SQRT_YY_DAG, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Phases the -1 eigenspace of the YY observable by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{0.5f + 0.5f * i, 0, 0, -0.5f + 0.5f * i}, {0, 0.5f + 0.5f * i, 0.5f - 0.5f * i, 0}, {0, 0.5f - 0.5f * i, 0.5f + 0.5f * i, 0}, {-0.5f + 0.5f * i, 0, 0, 0.5f + 0.5f * i}}, .flow_data = {"-ZY", "+XY", "-YZ", "+YX"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 S 1 S 1 S 1 H 0 CNOT 0 1 H 1 S 0 S 1 H 0 H 1 S 0 S 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_YY_DAG", .id = GateType::SQRT_YY_DAG, .best_candidate_inverse_id = GateType::SQRT_YY, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Phases the -1 eigenspace of the YY observable by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{0.5f - 0.5f * i, 0, 0, -0.5f - 0.5f * i}, {0, 0.5f - 0.5f * i, 0.5f + 0.5f * i, 0}, {0, 0.5f + 0.5f * i, 0.5f - 0.5f * i, 0}, {-0.5f - 0.5f * i, 0, 0, 0.5f - 0.5f * i}}, .flow_data = {"+ZY", "-XY", "+YZ", "-YX"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 S 1 H 0 CNOT 0 1 H 1 S 0 S 1 H 0 H 1 S 0 S 1 S 1 S 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_ZZ", .id = GateType::SQRT_ZZ, .best_candidate_inverse_id = GateType::SQRT_ZZ_DAG, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Phases the -1 eigenspace of the ZZ observable by i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, i, 0, 0}, {0, 0, i, 0}, {0, 0, 0, 1}}, .flow_data = {"+YZ", "+ZI", "+ZY", "+IZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 1 CNOT 0 1 H 1 S 0 S 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SQRT_ZZ_DAG", .id = GateType::SQRT_ZZ_DAG, .best_candidate_inverse_id = GateType::SQRT_ZZ, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Phases the -1 eigenspace of the ZZ observable by -i. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, -i, 0, 0}, {0, 0, -i, 0}, {0, 0, 0, 1}}, .flow_data = {"-YZ", "+ZI", "-ZY", "+IZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 1 CNOT 0 1 H 1 S 0 S 0 S 0 S 1 S 1 S 1 )CIRCUIT", }); } ================================================ FILE: src/stim/gates/gate_data_swaps.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; static constexpr std::complex i = std::complex(0, 1); void GateDataMap::add_gate_data_swaps(bool &failed) { add_gate( failed, Gate{ .name = "SWAP", .id = GateType::SWAP, .best_candidate_inverse_id = GateType::SWAP, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Swaps two qubits. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}}, .flow_data = {"+IX", "+IZ", "+XI", "+ZI"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( CNOT 0 1 CNOT 1 0 CNOT 0 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "ISWAP", .id = GateType::ISWAP, .best_candidate_inverse_id = GateType::ISWAP_DAG, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Swaps two qubits and phases the -1 eigenspace of the ZZ observable by i. Equivalent to `SWAP` then `CZ` then `S` on both targets. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 0, i, 0}, {0, i, 0, 0}, {0, 0, 0, 1}}, .flow_data = {"+ZY", "+IZ", "+YZ", "+ZI"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 CNOT 0 1 CNOT 1 0 H 1 S 1 S 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "ISWAP_DAG", .id = GateType::ISWAP_DAG, .best_candidate_inverse_id = GateType::ISWAP, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( Swaps two qubits and phases the -1 eigenspace of the ZZ observable by -i. Equivalent to `SWAP` then `CZ` then `S_DAG` on both targets. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 0, -i, 0}, {0, -i, 0, 0}, {0, 0, 0, 1}}, .flow_data = {"-ZY", "+IZ", "-YZ", "+ZI"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 S 0 S 1 S 1 S 1 H 1 CNOT 1 0 CNOT 0 1 H 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "CXSWAP", .id = GateType::CXSWAP, .best_candidate_inverse_id = GateType::SWAPCX, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( A combination CX-then-SWAP gate. This gate is kak-equivalent to the iswap gate, but preserves X/Z noise bias. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}, {0, 1, 0, 0}}, .flow_data = {"+XX", "+IZ", "+XI", "+ZZ"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( CNOT 1 0 CNOT 0 1 )CIRCUIT", }); add_gate( failed, Gate{ .name = "SWAPCX", .id = GateType::SWAPCX, .best_candidate_inverse_id = GateType::CXSWAP, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( A combination SWAP-then-CX gate. This gate is kak-equivalent to the iswap gate, but preserves X/Z noise bias. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 0, 0, 1}, {0, 1, 0, 0}, {0, 0, 1, 0}}, .flow_data = {"+IX", "+ZZ", "+XX", "+ZI"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( CNOT 0 1 CNOT 1 0 )CIRCUIT", }); add_gate( failed, Gate{ .name = "CZSWAP", .id = GateType::CZSWAP, .best_candidate_inverse_id = GateType::CZSWAP, .arg_count = 0, .flags = (GateFlags)(GATE_IS_UNITARY | GATE_TARGETS_PAIRS), .category = "C_Two Qubit Clifford Gates", .help = R"MARKDOWN( A combination CZ-and-SWAP gate. This gate is kak-equivalent to the iswap gate. Parens Arguments: This instruction takes no parens arguments. Targets: Qubit pairs to operate on. )MARKDOWN", .unitary_data = {{1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, -1}}, .flow_data = {"+ZX", "+IZ", "+XZ", "+ZI"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 CX 0 1 CX 1 0 H 1 )CIRCUIT", }); add_gate_alias(failed, "SWAPCZ", "CZSWAP"); } ================================================ FILE: src/stim/gates/gates.cc ================================================ // 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. #include "stim/gates/gates.h" using namespace stim; GateDataMap::GateDataMap() { bool failed = false; items[0].name = "NOT_A_GATE"; add_gate_data_annotations(failed); add_gate_data_blocks(failed); add_gate_data_collapsing(failed); add_gate_data_controlled(failed); add_gate_data_hada(failed); add_gate_data_heralded(failed); add_gate_data_noisy(failed); add_gate_data_pauli(failed); add_gate_data_period_3(failed); add_gate_data_period_4(failed); add_gate_data_pp(failed); add_gate_data_swaps(failed); add_gate_data_pair_measure(failed); add_gate_data_pauli_product(failed); for (size_t k = 1; k < NUM_DEFINED_GATES; k++) { if (items[k].name.empty()) { std::cerr << "Uninitialized gate id: " << k << ".\n"; failed = true; } } if (failed) { throw std::out_of_range("Failed to initialize gate data."); } } GateType Gate::hadamard_conjugated(bool ignoring_sign) const { switch (id) { case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::MPAD: case GateType::H: case GateType::H_NXZ: case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::Y_ERROR: case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: case GateType::Y: case GateType::SQRT_YY: case GateType::SQRT_YY_DAG: case GateType::MYY: case GateType::SWAP: return id; case GateType::MY: case GateType::MRY: case GateType::RY: case GateType::YCY: return ignoring_sign ? id : GateType::NOT_A_GATE; case GateType::ISWAP: case GateType::CZSWAP: case GateType::ISWAP_DAG: return GateType::NOT_A_GATE; case GateType::XCY: return ignoring_sign ? GateType::CY : GateType::NOT_A_GATE; case GateType::CY: return ignoring_sign ? GateType::XCY : GateType::NOT_A_GATE; case GateType::YCX: return ignoring_sign ? GateType::YCZ : GateType::NOT_A_GATE; case GateType::YCZ: return ignoring_sign ? GateType::YCX : GateType::NOT_A_GATE; case GateType::H_XY: return GateType::H_NYZ; case GateType::H_NYZ: return GateType::H_XY; case GateType::H_YZ: return GateType::H_NXY; case GateType::H_NXY: return GateType::H_YZ; case GateType::C_XYZ: return GateType::C_ZNYX; case GateType::C_ZNYX: return GateType::C_XYZ; case GateType::C_XNYZ: return GateType::C_ZYX; case GateType::C_ZYX: return GateType::C_XNYZ; case GateType::C_NXYZ: return GateType::C_ZYNX; case GateType::C_ZYNX: return GateType::C_NXYZ; case GateType::C_XYNZ: return GateType::C_NZYX; case GateType::C_NZYX: return GateType::C_XYNZ; case GateType::X: return GateType::Z; case GateType::Z: return GateType::X; case GateType::SQRT_Y: return GateType::SQRT_Y_DAG; case GateType::SQRT_Y_DAG: return GateType::SQRT_Y; case GateType::MX: return GateType::M; case GateType::M: return GateType::MX; case GateType::MRX: return GateType::MR; case GateType::MR: return GateType::MRX; case GateType::RX: return GateType::R; case GateType::R: return GateType::RX; case GateType::XCX: return GateType::CZ; case GateType::XCZ: return GateType::CX; case GateType::CX: return GateType::XCZ; case GateType::CZ: return GateType::XCX; case GateType::X_ERROR: return GateType::Z_ERROR; case GateType::Z_ERROR: return GateType::X_ERROR; case GateType::SQRT_X: return GateType::S; case GateType::SQRT_X_DAG: return GateType::S_DAG; case GateType::S: return GateType::SQRT_X; case GateType::S_DAG: return GateType::SQRT_X_DAG; case GateType::SQRT_XX: return GateType::SQRT_ZZ; case GateType::SQRT_XX_DAG: return GateType::SQRT_ZZ_DAG; case GateType::SQRT_ZZ: return GateType::SQRT_XX; case GateType::SQRT_ZZ_DAG: return GateType::SQRT_XX_DAG; case GateType::CXSWAP: return GateType::SWAPCX; case GateType::SWAPCX: return GateType::CXSWAP; case GateType::MXX: return GateType::MZZ; case GateType::MZZ: return GateType::MXX; default: return GateType::NOT_A_GATE; } } bool Gate::is_symmetric() const { if (flags & GATE_IS_SINGLE_QUBIT_GATE) { return true; } if (flags & GATE_TARGETS_PAIRS) { switch (id) { case GateType::II: case GateType::II_ERROR: case GateType::XCX: case GateType::YCY: case GateType::CZ: case GateType::DEPOLARIZE2: case GateType::SWAP: case GateType::ISWAP: case GateType::CZSWAP: case GateType::ISWAP_DAG: case GateType::MXX: case GateType::MYY: case GateType::MZZ: case GateType::SQRT_XX: case GateType::SQRT_YY: case GateType::SQRT_ZZ: case GateType::SQRT_XX_DAG: case GateType::SQRT_YY_DAG: case GateType::SQRT_ZZ_DAG: return true; default: return false; } } return false; } std::array Gate::to_euler_angles() const { if (unitary_data.size() != 2) { throw std::out_of_range(std::string(name) + " doesn't have 1q unitary data."); } auto a = unitary_data[0][0]; auto b = unitary_data[0][1]; auto c = unitary_data[1][0]; auto d = unitary_data[1][1]; std::array xyz; if (a == std::complex{0}) { xyz[0] = 3.14159265359f; xyz[1] = 0; xyz[2] = arg(-b / c); } else if (b == std::complex{0}) { xyz[0] = 0; xyz[1] = 0; xyz[2] = arg(d / a); } else { xyz[0] = 3.14159265359f / 2; xyz[1] = arg(c / a); xyz[2] = arg(-b / a); } return xyz; } std::array Gate::to_axis_angle() const { if (unitary_data.size() != 2) { throw std::out_of_range(std::string(name) + " doesn't have 1q unitary data."); } auto a = unitary_data[0][0]; auto b = unitary_data[0][1]; auto c = unitary_data[1][0]; auto d = unitary_data[1][1]; auto i = std::complex{0, 1}; auto x = b + c; auto y = b * i + c * -i; auto z = a - d; auto s = a + d; s *= -i; std::complex p = 1; if (s.imag() != 0) { p = s; } if (x.imag() != 0) { p = x; } if (y.imag() != 0) { p = y; } if (z.imag() != 0) { p = z; } p /= sqrt(p.imag() * p.imag() + p.real() * p.real()); p *= 2; x /= p; y /= p; z /= p; s /= p; assert(x.imag() == 0); assert(y.imag() == 0); assert(z.imag() == 0); assert(s.imag() == 0); auto rx = x.real(); auto ry = y.real(); auto rz = z.real(); auto rs = s.real(); // At this point it's more of a quaternion. Normalize the axis. auto r = sqrt(rx * rx + ry * ry + rz * rz); if (r == 0) { rx = 1; } else { rx /= r; ry /= r; rz /= r; } if ((rx < 0) + (ry < 0) + (rz < 0) > 1) { rx = -rx; ry = -ry; rz = -rz; rs = -rs; } return {rx, ry, rz, acosf(rs) * 2}; } bool Gate::has_known_unitary_matrix() const { return (flags & GateFlags::GATE_IS_UNITARY) && (flags & (GateFlags::GATE_IS_SINGLE_QUBIT_GATE | GateFlags::GATE_TARGETS_PAIRS)); } std::vector>> Gate::unitary() const { if (unitary_data.size() != 2 && unitary_data.size() != 4) { throw std::out_of_range(std::string(name) + " doesn't have 1q or 2q unitary data."); } std::vector>> result; for (size_t k = 0; k < unitary_data.size(); k++) { const auto &d = unitary_data[k]; result.emplace_back(); for (size_t j = 0; j < d.size(); j++) { result.back().push_back(d[j]); } } return result; } const Gate &Gate::inverse() const { if ((flags & GATE_IS_UNITARY) || id == GateType::TICK) { return GATE_DATA[best_candidate_inverse_id]; } throw std::out_of_range(std::string(name) + " has no inverse."); } void GateDataMap::add_gate(bool &failed, const Gate &gate) { assert((size_t)gate.id < NUM_DEFINED_GATES); auto h = gate_name_to_hash(gate.name); auto &hash_loc = hashed_name_to_gate_type_table[h]; if (!hash_loc.expected_name.empty()) { std::cerr << "GATE COLLISION " << gate.name << " vs " << items[(size_t)hash_loc.id].name << "\n"; failed = true; return; } items[(size_t)gate.id] = gate; hash_loc.id = gate.id; hash_loc.expected_name = gate.name; } void GateDataMap::add_gate_alias(bool &failed, const char *alt_name, const char *canon_name) { auto h_alt = gate_name_to_hash(alt_name); auto &hash_loc = hashed_name_to_gate_type_table[h_alt]; if (!hash_loc.expected_name.empty()) { std::cerr << "GATE COLLISION " << alt_name << " vs " << items[(size_t)hash_loc.id].name << "\n"; failed = true; return; } auto h_canon = gate_name_to_hash(canon_name); if (hashed_name_to_gate_type_table[h_canon].expected_name.empty()) { std::cerr << "MISSING CANONICAL GATE " << canon_name << "\n"; failed = true; return; } hash_loc.id = hashed_name_to_gate_type_table[h_canon].id; hash_loc.expected_name = alt_name; } extern const GateDataMap stim::GATE_DATA = GateDataMap(); ================================================ FILE: src/stim/gates/gates.h ================================================ /* * 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. */ #ifndef _STIM_GATES_GATE_DATA_H #define _STIM_GATES_GATE_DATA_H #include #include #include #include #include #include #include #include "stim/mem/fixed_cap_vector.h" namespace stim { template struct Tableau; template struct Flow; template struct PauliString; /// Used for gates' argument count to indicate that a gate takes a variable number of /// arguments. This is relevant to coordinate data on detectors and qubits, where there may /// be any number of coordinates. constexpr uint8_t ARG_COUNT_SYGIL_ANY = uint8_t{0xFF}; /// Used for gates' argument count to indicate that a gate takes 0 parens arguments or 1 /// parens argument. This is relevant to measurement gates, where 0 parens arguments means /// a noiseless result whereas 1 parens argument is a noisy result. constexpr uint8_t ARG_COUNT_SYGIL_ZERO_OR_ONE = uint8_t{0xFE}; constexpr inline uint16_t gate_name_to_hash(std::string_view text) { // HACK: A collision is considered to be an error. // Just do *anything* that makes all the defined gates have different values. constexpr uint16_t const1 = 2126; constexpr uint16_t const2 = 9883; constexpr uint16_t const3 = 8039; constexpr uint16_t const4 = 9042; constexpr uint16_t const5 = 4916; constexpr uint16_t const6 = 4048; constexpr uint16_t const7 = 7081; size_t n = text.size(); const char *v = text.data(); size_t result = n; if (n > 0) { auto c_first = v[0] | 0x20; auto c_last = v[n - 1] | 0x20; result ^= c_first * const1; result += c_last * const2; } if (n > 2) { auto c1 = v[1] | 0x20; auto c2 = v[2] | 0x20; result ^= c1 * const3; result += c2 * const4; } if (n > 4) { auto c3 = v[3] | 0x20; auto c4 = v[4] | 0x20; result ^= c3 * const5; result += c4 * const6; } if (n > 5) { auto c5 = v[5] | 0x20; result ^= c5 * const7; } return result & 0x1FF; } constexpr size_t NUM_DEFINED_GATES = 82; enum class GateType : uint8_t { NOT_A_GATE = 0, // Annotations DETECTOR, OBSERVABLE_INCLUDE, TICK, QUBIT_COORDS, SHIFT_COORDS, // Control flow REPEAT, // Collapsing gates MPAD, MX, MY, M, // alias when parsing: MZ MRX, MRY, MR, // alias when parsing: MRZ RX, RY, R, // alias when parsing: RZ // Controlled gates XCX, XCY, XCZ, YCX, YCY, YCZ, CX, // alias when parsing: CNOT, ZCX CY, // alias when parsing: ZCY CZ, // alias when parsing: ZCZ // Hadamard-like gates H, // alias when parsing: H_XZ H_XY, H_YZ, H_NXY, H_NXZ, H_NYZ, // Noise channels DEPOLARIZE1, DEPOLARIZE2, X_ERROR, Y_ERROR, Z_ERROR, I_ERROR, II_ERROR, PAULI_CHANNEL_1, PAULI_CHANNEL_2, E, // alias when parsing: CORRELATED_ERROR ELSE_CORRELATED_ERROR, // Heralded noise channels HERALDED_ERASE, HERALDED_PAULI_CHANNEL_1, // Pauli gates I, X, Y, Z, // Period 3 gates C_XYZ, C_ZYX, C_NXYZ, C_XNYZ, C_XYNZ, C_NZYX, C_ZNYX, C_ZYNX, // Period 4 gates SQRT_X, SQRT_X_DAG, SQRT_Y, SQRT_Y_DAG, S, // alias when parsing: SQRT_Z S_DAG, // alias when parsing: SQRT_Z_DAG // Parity phasing gates. II, SQRT_XX, SQRT_XX_DAG, SQRT_YY, SQRT_YY_DAG, SQRT_ZZ, SQRT_ZZ_DAG, // Pauli product gates MPP, SPP, SPP_DAG, // Swap gates SWAP, ISWAP, CXSWAP, SWAPCX, CZSWAP, ISWAP_DAG, // Pair measurement gates MXX, MYY, MZZ, }; enum GateFlags : uint16_t { // All gates must have at least one flag set. NO_GATE_FLAG = 0, // Indicates whether unitary and tableau data is available for the gate, so it can be tested more easily. GATE_IS_UNITARY = 1 << 0, // Determines whether or not the gate is omitted when computing a reference sample. GATE_IS_NOISY = 1 << 1, // Controls validation of probability arguments like X_ERROR(0.01). GATE_ARGS_ARE_DISJOINT_PROBABILITIES = 1 << 2, // Indicates whether the gate puts data into the measurement record or not. // Also determines whether or not inverted targets (like "!3") are permitted. GATE_PRODUCES_RESULTS = 1 << 3, // Prevents the same gate on adjacent lines from being combined into one longer invocation. GATE_IS_NOT_FUSABLE = 1 << 4, // Controls block functionality for instructions like REPEAT. GATE_IS_BLOCK = 1 << 5, // Controls validation code checking for arguments coming in pairs. GATE_TARGETS_PAIRS = 1 << 6, // Controls instructions like CORRELATED_ERROR taking Pauli product targets ("X1 Y2 Z3"). // Note that this enables the Pauli terms but not the combine terms like X1*Y2. GATE_TARGETS_PAULI_STRING = 1 << 7, // Controls instructions like DETECTOR taking measurement record targets ("rec[-1]"). // The "ONLY" refers to the fact that this flag switches the default behavior to not allowing qubit targets. // Further flags can then override that default. GATE_ONLY_TARGETS_MEASUREMENT_RECORD = 1 << 8, // Controls instructions like CX operating allowing measurement record targets and sweep bit targets. GATE_CAN_TARGET_BITS = 1 << 9, // Controls whether the gate takes qubit/record targets. GATE_TAKES_NO_TARGETS = 1 << 10, // Controls validation of index arguments like OBSERVABLE_INCLUDE(1). GATE_ARGS_ARE_UNSIGNED_INTEGERS = 1 << 11, // Controls instructions like MPP taking Pauli product combiners ("X1*Y2 Z3"). GATE_TARGETS_COMBINERS = 1 << 12, // Measurements and resets are dissipative operations. GATE_IS_RESET = 1 << 13, // Annotations like DETECTOR aren't strictly speaking identity operations, but they can be ignored by code that only // cares about effects that happen to qubits (as opposed to in the classical control system). GATE_HAS_NO_EFFECT_ON_QUBITS = 1 << 14, // Whether or not the gate trivially broadcasts over targets. GATE_IS_SINGLE_QUBIT_GATE = 1 << 15, }; struct Gate { /// The canonical name of the gate, used when printing it to a circuit file. std::string_view name; /// The gate's type, such as stim::GateType::X or stim::GateType::MRZ. GateType id; /// The id of the gate inverse to this one, or at least the closest thing to an inverse. /// Set to GateType::NOT_A_GATE for gates with no inverse. GateType best_candidate_inverse_id; /// The number of parens arguments the gate expects (e.g. X_ERROR takes 1, PAULI_CHANNEL_1 takes 3). /// Set to stim::ARG_COUNT_SYGIL_ANY to indicate any number is allowed (e.g. DETECTOR coordinate data). uint8_t arg_count; /// Bit-packed data describing details of the gate. GateFlags flags; /// A word describing what sort of gate this is. std::string_view category; /// Prose summary of what the gate is, how it fits into Stim, and how to use it. std::string_view help; /// A unitary matrix describing the gate. (Size 0 if the gate is not unitary.) FixedCapVector, 4>, 4> unitary_data; /// A shorthand description of the stabilizer flows of the gate. /// For single qubit Cliffords, this should be the output stabilizers for X then Z. /// For 2 qubit Cliffords, this should be the output stabilizers for X_, Z_, _X, _Z. /// For 2 qubit dissipative gates, this should be flows like "X_ -> XX xor rec[-1]". FixedCapVector flow_data; /// Stim circuit file contents of a decomposition into H+S+CX+M+R operations. (nullptr if not decomposable.) const char *h_s_cx_m_r_decomposition; inline bool operator==(const Gate &other) const { return id == other.id; } inline bool operator!=(const Gate &other) const { return id != other.id; } const Gate &inverse() const; template Tableau tableau() const { if (!(flags & GateFlags::GATE_IS_UNITARY)) { throw std::invalid_argument(std::string(name) + " isn't unitary so it doesn't have a tableau."); } const auto &d = flow_data; if (flow_data.size() == 2) { return Tableau::gate1(d[0], d[1]); } if (flow_data.size() == 4) { return Tableau::gate2(d[0], d[1], d[2], d[3]); } throw std::out_of_range(std::string(name) + " doesn't have 1q or 2q tableau data."); } template std::vector> flows() const { if (has_known_unitary_matrix()) { auto t = tableau(); if (flags & GateFlags::GATE_TARGETS_PAIRS) { return { Flow{stim::PauliString::from_str("X_"), t.xs[0], {}}, Flow{stim::PauliString::from_str("Z_"), t.zs[0], {}}, Flow{stim::PauliString::from_str("_X"), t.xs[1], {}}, Flow{stim::PauliString::from_str("_Z"), t.zs[1], {}}, }; } return { Flow{stim::PauliString::from_str("X"), t.xs[0], {}}, Flow{stim::PauliString::from_str("Z"), t.zs[0], {}}, }; } std::vector> out; for (const auto &c : flow_data) { out.push_back(Flow::from_str(c)); } return out; } std::vector>> unitary() const; bool is_symmetric() const; GateType hadamard_conjugated(bool ignoring_sign) const; /// Determines if the gate has a specified unitary matrix. /// /// Some unitary gates, such as SPP, don't have a specified matrix because the /// matrix depends crucially on the targets. bool has_known_unitary_matrix() const; /// Converts a single qubit unitary gate into an euler-angles rotation. /// /// Returns: /// {theta, phi, lambda} using the same convention as qiskit. /// Each angle is in radians. /// For stabilizer operations, every angle will be a multiple of pi/2. /// /// The unitary matrix U of the operation can be recovered (up to global phase) /// by computing U = RotZ(phi) * RotY(theta) * RotZ(lambda). std::array to_euler_angles() const; /// Converts a single qubit unitary gate into an axis-angle rotation. /// /// Returns: /// An array {x, y, z, a}. /// is a unit vector indicating the axis of rotation. /// is the angle of rotation in radians. std::array to_axis_angle() const; }; inline bool _case_insensitive_mismatch(std::string_view text1, std::string_view text2) { if (text1.size() != text2.size()) { return true; } bool failed = false; for (size_t k = 0; k < text1.size(); k++) { failed |= toupper(text1[k]) != text2[k]; } return failed; } struct GateDataMapHashEntry { GateType id = GateType::NOT_A_GATE; std::string_view expected_name; }; struct GateDataMap { private: void add_gate(bool &failed, const Gate &data); void add_gate_alias(bool &failed, const char *alt_name, const char *canon_name); void add_gate_data_annotations(bool &failed); void add_gate_data_blocks(bool &failed); void add_gate_data_heralded(bool &failed); void add_gate_data_collapsing(bool &failed); void add_gate_data_controlled(bool &failed); void add_gate_data_hada(bool &failed); void add_gate_data_noisy(bool &failed); void add_gate_data_pauli(bool &failed); void add_gate_data_period_3(bool &failed); void add_gate_data_period_4(bool &failed); void add_gate_data_pp(bool &failed); void add_gate_data_swaps(bool &failed); void add_gate_data_pair_measure(bool &failed); void add_gate_data_pauli_product(bool &failed); public: std::array hashed_name_to_gate_type_table; std::array items; GateDataMap(); inline const Gate &operator[](GateType g) const { return items[(uint64_t)g]; } inline const Gate &at(GateType g) const { if ((uint8_t)g >= items.size()) { throw std::out_of_range("Gate index out of range"); } return items[(uint8_t)g]; } inline const Gate &at(std::string_view text) const { auto h = gate_name_to_hash(text); const auto &entry = hashed_name_to_gate_type_table[h]; if (_case_insensitive_mismatch(text, entry.expected_name)) { throw std::out_of_range("Gate not found: '" + std::string(text) + "'"); } // Canonicalize. return (*this)[entry.id]; } inline bool has(std::string_view text) const { auto h = gate_name_to_hash(text); const auto &entry = hashed_name_to_gate_type_table[h]; return !_case_insensitive_mismatch(text, entry.expected_name); } }; extern const GateDataMap GATE_DATA; } // namespace stim #endif ================================================ FILE: src/stim/gates/gates.perf.cc ================================================ // 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. #include "stim/gates/gates.h" #include #include "stim/perf.perf.h" using namespace stim; BENCHMARK(gate_data_hash_all_gate_names) { std::vector names; for (const auto &gate : GATE_DATA.items) { names.emplace_back(gate.name); } size_t result = 0; benchmark_go([&]() { for (const auto &s : names) { result += gate_name_to_hash(s); } }) .goal_nanos(2.7 * names.size()) .show_rate("GateHashes", names.size()); if (result == 0) { std::cerr << "impossible"; } } ================================================ FILE: src/stim/gates/gates.pybind.cc ================================================ // 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. #include "stim/gates/gates.h" #include "stim/gates/gates.pybind.h" #include "stim/py/base.pybind.h" #include "stim/stabilizers/flow.h" #include "stim/util_bot/str_util.h" using namespace stim; using namespace stim_pybind; pybind11::object gate_num_parens_argument_range(const GateTypeWrapper &self_id) { const auto &self = GATE_DATA.at(self_id.type); auto r = pybind11::module::import("builtins").attr("range"); if (self.arg_count == ARG_COUNT_SYGIL_ZERO_OR_ONE) { return r(2); } if (self.arg_count == ARG_COUNT_SYGIL_ANY) { return r(256); } return r(self.arg_count, self.arg_count + 1); } std::vector gate_aliases(const GateTypeWrapper &self_id) { std::vector aliases; for (const auto &h : GATE_DATA.hashed_name_to_gate_type_table) { if (h.id == self_id.type) { aliases.push_back(h.expected_name); } } std::sort(aliases.begin(), aliases.end()); return aliases; } pybind11::object gate_tableau(const GateTypeWrapper &self_id) { const auto &self = GATE_DATA.at(self_id.type); if (self.flags & GATE_IS_UNITARY) { return pybind11::cast(self.tableau()); } return pybind11::none(); } pybind11::object gate_unitary_matrix(const GateTypeWrapper &self_id) { const Gate &self = GATE_DATA.at(self_id.type); if (self.has_known_unitary_matrix()) { auto r = self.unitary(); auto n = r.size(); std::complex *buffer = new std::complex[n * n]; for (size_t a = 0; a < n; a++) { for (size_t b = 0; b < n; b++) { buffer[b + a * n] = r[a][b]; } } pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast *>(f); }); return pybind11::array_t>( {(pybind11::ssize_t)n, (pybind11::ssize_t)n}, {(pybind11::ssize_t)(n * sizeof(std::complex)), (pybind11::ssize_t)sizeof(std::complex)}, buffer, free_when_done); } return pybind11::none(); } pybind11::class_ stim_pybind::pybind_gate_data(pybind11::module &m) { return pybind11::class_( m, "GateData", clean_doc_string(R"DOC( Details about a gate supported by stim. Examples: >>> import stim >>> stim.gate_data('h').name 'H' >>> stim.gate_data('h').is_unitary True >>> stim.gate_data('h').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) )DOC") .data()); } void stim_pybind::pybind_gate_data_methods(pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init([](const char *name) -> GateTypeWrapper { return {GATE_DATA.at(name).id}; }), pybind11::arg("name"), clean_doc_string(R"DOC( Finds gate data for the named gate. Examples: >>> import stim >>> stim.GateData('H').is_unitary True )DOC") .data()); m.def( "gate_data", [](const pybind11::object &name) -> pybind11::object { if (!name.is_none()) { return pybind11::cast(GateTypeWrapper{GATE_DATA.at(pybind11::cast(name)).id}); } std::map result; for (const auto &g : GATE_DATA.items) { if (g.id != GateType::NOT_A_GATE) { result.insert({g.name, GateTypeWrapper{g.id}}); } } return pybind11::cast(result); }, pybind11::arg("name") = pybind11::none(), clean_doc_string(R"DOC( @overload def gate_data(name: str) -> stim.GateData: @overload def gate_data() -> Dict[str, stim.GateData]: @signature def gate_data(name: Optional[str] = None) -> Union[str, Dict[str, stim.GateData]]: Returns gate data for the given named gate, or all gates. Examples: >>> import stim >>> stim.gate_data('cnot').aliases ['CNOT', 'CX', 'ZCX'] >>> stim.gate_data('cnot').is_two_qubit_gate True >>> gate_dict = stim.gate_data() >>> len(gate_dict) > 50 True >>> gate_dict['MX'].produces_measurements True )DOC") .data()); c.def_property_readonly( "name", [](const GateTypeWrapper &self_id) -> std::string_view { const Gate &self = GATE_DATA.at(self_id.type); return self.name; }, clean_doc_string(R"DOC( Returns the canonical name of the gate. Examples: >>> import stim >>> stim.gate_data('H').name 'H' >>> stim.gate_data('cnot').name 'CX' )DOC") .data()); c.def_property_readonly( "aliases", &gate_aliases, clean_doc_string(R"DOC( Returns all aliases that can be used to name the gate. Although gates can be referred to by lower case and mixed case named, the result only includes upper cased aliases. Examples: >>> import stim >>> stim.gate_data('H').aliases ['H', 'H_XZ'] >>> stim.gate_data('cnot').aliases ['CNOT', 'CX', 'ZCX'] )DOC") .data()); c.def( "__repr__", [](const GateTypeWrapper &self_id) -> std::string { const Gate &self = GATE_DATA.at(self_id.type); std::stringstream ss; ss << "stim.gate_data('" << self.name << "')"; return ss.str(); }, "Returns text that is a valid python expression evaluating to an equivalent `stim.GateData`."); c.def(pybind11::self == pybind11::self, "Determines if two GateData instances are identical."); c.def(pybind11::self != pybind11::self, "Determines if two GateData instances are not identical."); c.def( "__str__", [](const GateTypeWrapper &self_id) -> std::string { const Gate &self = GATE_DATA.at(self_id.type); std::stringstream ss; auto b = [](bool x) -> const char * { return x ? "True" : "False"; }; auto v = [](const pybind11::object &obj) { pybind11::object obj_repr = pybind11::repr(obj); std::string result; for (char c : pybind11::cast(obj_repr)) { result.push_back(c); if (c == '\n') { result.append(" "); } } return result; }; ss << "stim.GateData {\n"; ss << " .name = '" << self.name << "'\n"; ss << " .aliases = " << v(pybind11::cast(gate_aliases(self_id))) << "\n"; ss << " .is_noisy_gate = " << b(self.flags & GATE_IS_NOISY) << "\n"; ss << " .is_reset = " << b(self.flags & GATE_IS_RESET) << "\n"; ss << " .is_single_qubit_gate = " << b(self.flags & GATE_IS_SINGLE_QUBIT_GATE) << "\n"; ss << " .is_two_qubit_gate = " << b(self.flags & GATE_TARGETS_PAIRS) << "\n"; ss << " .is_unitary = " << b(self.flags & GATE_IS_UNITARY) << "\n"; ss << " .num_parens_arguments_range = " << v(gate_num_parens_argument_range(self_id)) << "\n"; ss << " .produces_measurements = " << b(self.flags & GATE_PRODUCES_RESULTS) << "\n"; ss << " .takes_measurement_record_targets = " << b(self.flags & (GATE_CAN_TARGET_BITS | GATE_ONLY_TARGETS_MEASUREMENT_RECORD)) << "\n"; ss << " .takes_pauli_targets = " << b(self.flags & GATE_TARGETS_PAULI_STRING) << "\n"; if (self.flags & GATE_IS_UNITARY) { ss << " .tableau = " << v(gate_tableau(self_id)) << "\n"; ss << " .unitary_matrix = np.array(" << v(pybind11::cast(self.unitary())) << ", dtype=np.complex64)\n"; } ss << "}"; return ss.str(); }, "Returns text describing the gate data."); c.def_property_readonly( "tableau", &gate_tableau, clean_doc_string(R"DOC( @signature def tableau(self) -> Optional[stim.Tableau]: Returns the gate's tableau, or None if the gate has no tableau. Examples: >>> import stim >>> print(stim.gate_data('M').tableau) None >>> stim.gate_data('H').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> stim.gate_data('ISWAP').tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZY"), stim.PauliString("+YZ"), ], zs=[ stim.PauliString("+_Z"), stim.PauliString("+Z_"), ], ) )DOC") .data()); c.def_property_readonly( "unitary_matrix", &gate_unitary_matrix, clean_doc_string(R"DOC( @signature def unitary_matrix(self) -> Optional[np.ndarray]: Returns the gate's unitary matrix, or None if the gate isn't unitary. Examples: >>> import stim >>> print(stim.gate_data('M').unitary_matrix) None >>> stim.gate_data('X').unitary_matrix array([[0.+0.j, 1.+0.j], [1.+0.j, 0.+0.j]], dtype=complex64) >>> stim.gate_data('ISWAP').unitary_matrix array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j], [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]], dtype=complex64) )DOC") .data()); c.def_property_readonly( "is_unitary", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.flags & GATE_IS_UNITARY; }, clean_doc_string(R"DOC( Returns whether or not the gate is a unitary gate. Examples: >>> import stim >>> stim.gate_data('H').is_unitary True >>> stim.gate_data('CX').is_unitary True >>> stim.gate_data('R').is_unitary False >>> stim.gate_data('M').is_unitary False >>> stim.gate_data('MXX').is_unitary False >>> stim.gate_data('X_ERROR').is_unitary False >>> stim.gate_data('CORRELATED_ERROR').is_unitary False >>> stim.gate_data('MPP').is_unitary False >>> stim.gate_data('DETECTOR').is_unitary False )DOC") .data()); c.def_property_readonly( "num_parens_arguments_range", &gate_num_parens_argument_range, clean_doc_string(R"DOC( @signature def num_parens_arguments_range(self) -> range: Returns the min/max parens arguments taken by the gate, as a python range. Examples: >>> import stim >>> stim.gate_data('M').num_parens_arguments_range range(0, 2) >>> list(stim.gate_data('M').num_parens_arguments_range) [0, 1] >>> list(stim.gate_data('R').num_parens_arguments_range) [0] >>> list(stim.gate_data('H').num_parens_arguments_range) [0] >>> list(stim.gate_data('X_ERROR').num_parens_arguments_range) [1] >>> list(stim.gate_data('PAULI_CHANNEL_1').num_parens_arguments_range) [3] >>> list(stim.gate_data('PAULI_CHANNEL_2').num_parens_arguments_range) [15] >>> stim.gate_data('DETECTOR').num_parens_arguments_range range(0, 256) >>> list(stim.gate_data('OBSERVABLE_INCLUDE').num_parens_arguments_range) [1] )DOC") .data()); c.def_property_readonly( "is_reset", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.flags & GATE_IS_RESET; }, clean_doc_string(R"DOC( Returns whether or not the gate resets qubits in any basis. Examples: >>> import stim >>> stim.gate_data('R').is_reset True >>> stim.gate_data('RX').is_reset True >>> stim.gate_data('MR').is_reset True >>> stim.gate_data('M').is_reset False >>> stim.gate_data('MXX').is_reset False >>> stim.gate_data('MPP').is_reset False >>> stim.gate_data('H').is_reset False >>> stim.gate_data('CX').is_reset False >>> stim.gate_data('HERALDED_ERASE').is_reset False >>> stim.gate_data('DEPOLARIZE2').is_reset False >>> stim.gate_data('X_ERROR').is_reset False >>> stim.gate_data('CORRELATED_ERROR').is_reset False >>> stim.gate_data('DETECTOR').is_reset False )DOC") .data()); c.def_property_readonly( "is_single_qubit_gate", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.flags & GATE_IS_SINGLE_QUBIT_GATE; }, clean_doc_string(R"DOC( Returns whether or not the gate is a single qubit gate. Single qubit gates apply separately to each of their targets. Variable-qubit gates like CORRELATED_ERROR and MPP are not considered single qubit gates. Examples: >>> import stim >>> stim.gate_data('H').is_single_qubit_gate True >>> stim.gate_data('R').is_single_qubit_gate True >>> stim.gate_data('M').is_single_qubit_gate True >>> stim.gate_data('X_ERROR').is_single_qubit_gate True >>> stim.gate_data('CX').is_single_qubit_gate False >>> stim.gate_data('MXX').is_single_qubit_gate False >>> stim.gate_data('CORRELATED_ERROR').is_single_qubit_gate False >>> stim.gate_data('MPP').is_single_qubit_gate False >>> stim.gate_data('DETECTOR').is_single_qubit_gate False >>> stim.gate_data('TICK').is_single_qubit_gate False >>> stim.gate_data('REPEAT').is_single_qubit_gate False )DOC") .data()); c.def_property_readonly( "flows", [](const GateTypeWrapper &self_id) -> pybind11::object { const Gate &self = GATE_DATA.at(self_id.type); auto f = self.flows(); if (f.empty()) { return pybind11::none(); } std::vector> results; for (const auto &e : f) { results.push_back(e); } return pybind11::cast(results); }, clean_doc_string(R"DOC( @signature def flows(self) -> Optional[List[stim.Flow]]: Returns stabilizer flow generators for the gate, or else None. A stabilizer flow describes an input-output relationship that the gate satisfies, where an input pauli string is transformed into an output pauli string mediated by certain measurement results. Caution: this method returns None for variable-target-count gates like MPP. Not because MPP has no stabilizer flows, but because its stabilizer flows depend on how many qubits it targets and what basis it targets them in. Returns: A list of stim.Flow instances representing the generators. Examples: >>> import stim >>> stim.gate_data('H').flows [stim.Flow("X -> Z"), stim.Flow("Z -> X")] >>> for e in stim.gate_data('ISWAP').flows: ... print(e) X_ -> ZY Z_ -> _Z _X -> YZ _Z -> Z_ >>> for e in stim.gate_data('MXX').flows: ... print(e) X_ -> X_ _X -> _X ZZ -> ZZ XX -> rec[-1] )DOC") .data()); c.def_property_readonly( "is_symmetric_gate", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.is_symmetric(); }, clean_doc_string(R"DOC( Returns whether or not the gate is the same when its targets are swapped. A two qubit gate is symmetric if it doesn't matter if you swap its targets. It is unaffected when conjugated by the SWAP gate. Single qubit gates are vacuously symmetric. A multi-qubit gate is symmetric if swapping any two of its targets has no effect. Note that this method is for symmetry *without broadcasting*. For example, SWAP is symmetric even though SWAP 1 2 3 4 isn't equal to SWAP 1 3 2 4. Returns: True if the gate is symmetric. False if the gate isn't symmetric. Examples: >>> import stim >>> stim.gate_data('CX').is_symmetric_gate False >>> stim.gate_data('CZ').is_symmetric_gate True >>> stim.gate_data('ISWAP').is_symmetric_gate True >>> stim.gate_data('CXSWAP').is_symmetric_gate False >>> stim.gate_data('MXX').is_symmetric_gate True >>> stim.gate_data('DEPOLARIZE2').is_symmetric_gate True >>> stim.gate_data('PAULI_CHANNEL_2').is_symmetric_gate False >>> stim.gate_data('H').is_symmetric_gate True >>> stim.gate_data('R').is_symmetric_gate True >>> stim.gate_data('X_ERROR').is_symmetric_gate True >>> stim.gate_data('CORRELATED_ERROR').is_symmetric_gate False >>> stim.gate_data('MPP').is_symmetric_gate False >>> stim.gate_data('DETECTOR').is_symmetric_gate False )DOC") .data()); c.def( "hadamard_conjugated", [](const GateTypeWrapper &self_id, bool ignoring_sign) -> pybind11::object { const Gate &self = GATE_DATA.at(self_id.type); GateType g = self.hadamard_conjugated(ignoring_sign); if (g == GateType::NOT_A_GATE) { return pybind11::none(); } return pybind11::cast(GateTypeWrapper{g}); }, pybind11::kw_only(), pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( @signature def hadamard_conjugated(self, *, unsigned: bool = False) -> Optional[stim.GateData]: Returns a stim gate equivalent to this gate conjugated by Hadamard gates. The Hadamard conjugate can be thought of as the XZ dual of the gate; the gate you get by exchanging the X and Z bases. For example, a SQRT_X will become a SQRT_Z and a CX gate will switch directions into an XCZ. If stim doesn't define a gate equivalent to conjugating this gate by Hadamards, the value `None` is returned. Args: unsigned: Defaults to False. When False, the returned gate must be *exactly* the Hadamard conjugation of this gate. When True, the returned gate must have the same flows but the sign of the flows can be different (i.e. the returned gate must be the Hadamard conjugate up to Pauli gate differences). Returns: A stim.GateData instance of the Hadamard conjugate, if it exists in stim. None, if stim doesn't define a gate equal to the Hadamard conjugate. Examples: >>> import stim >>> stim.gate_data('X').hadamard_conjugated() stim.gate_data('Z') >>> stim.gate_data('CX').hadamard_conjugated() stim.gate_data('XCZ') >>> stim.gate_data('RY').hadamard_conjugated() is None True >>> stim.gate_data('RY').hadamard_conjugated(unsigned=True) stim.gate_data('RY') >>> stim.gate_data('ISWAP').hadamard_conjugated(unsigned=True) is None True >>> stim.gate_data('SWAP').hadamard_conjugated() stim.gate_data('SWAP') >>> stim.gate_data('CXSWAP').hadamard_conjugated() stim.gate_data('SWAPCX') >>> stim.gate_data('MXX').hadamard_conjugated() stim.gate_data('MZZ') >>> stim.gate_data('DEPOLARIZE1').hadamard_conjugated() stim.gate_data('DEPOLARIZE1') >>> stim.gate_data('X_ERROR').hadamard_conjugated() stim.gate_data('Z_ERROR') >>> stim.gate_data('H_XY').hadamard_conjugated() stim.gate_data('H_NYZ') >>> stim.gate_data('DETECTOR').hadamard_conjugated(unsigned=True) stim.gate_data('DETECTOR') )DOC") .data()); c.def_property_readonly( "is_two_qubit_gate", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.flags & GATE_TARGETS_PAIRS; }, clean_doc_string(R"DOC( Returns whether or not the gate is a two qubit gate. Two qubit gates must be given an even number of targets. Variable-qubit gates like CORRELATED_ERROR and MPP are not considered two qubit gates. Returns: True if the gate is a two qubit gate. False if the gate isn't a two qubit gate. Examples: >>> import stim >>> stim.gate_data('CX').is_two_qubit_gate True >>> stim.gate_data('MXX').is_two_qubit_gate True >>> stim.gate_data('H').is_two_qubit_gate False >>> stim.gate_data('R').is_two_qubit_gate False >>> stim.gate_data('M').is_two_qubit_gate False >>> stim.gate_data('X_ERROR').is_two_qubit_gate False >>> stim.gate_data('CORRELATED_ERROR').is_two_qubit_gate False >>> stim.gate_data('MPP').is_two_qubit_gate False >>> stim.gate_data('DETECTOR').is_two_qubit_gate False )DOC") .data()); c.def_property_readonly( "is_noisy_gate", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.flags & GATE_IS_NOISY; }, clean_doc_string(R"DOC( Returns whether or not the gate can produce noise. Note that measurement operations are considered noisy, because for example `M(0.001) 2 3 5` will include noise that flips its result 0.1% of the time. Examples: >>> import stim >>> stim.gate_data('M').is_noisy_gate True >>> stim.gate_data('MXX').is_noisy_gate True >>> stim.gate_data('X_ERROR').is_noisy_gate True >>> stim.gate_data('CORRELATED_ERROR').is_noisy_gate True >>> stim.gate_data('MPP').is_noisy_gate True >>> stim.gate_data('H').is_noisy_gate False >>> stim.gate_data('CX').is_noisy_gate False >>> stim.gate_data('R').is_noisy_gate False >>> stim.gate_data('DETECTOR').is_noisy_gate False )DOC") .data()); c.def_property_readonly( "produces_measurements", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.flags & GATE_PRODUCES_RESULTS; }, clean_doc_string(R"DOC( Returns whether or not the gate produces measurement results. Examples: >>> import stim >>> stim.gate_data('M').produces_measurements True >>> stim.gate_data('MRY').produces_measurements True >>> stim.gate_data('MXX').produces_measurements True >>> stim.gate_data('MPP').produces_measurements True >>> stim.gate_data('HERALDED_ERASE').produces_measurements True >>> stim.gate_data('H').produces_measurements False >>> stim.gate_data('CX').produces_measurements False >>> stim.gate_data('R').produces_measurements False >>> stim.gate_data('X_ERROR').produces_measurements False >>> stim.gate_data('CORRELATED_ERROR').produces_measurements False >>> stim.gate_data('DETECTOR').produces_measurements False )DOC") .data()); c.def_property_readonly( "takes_pauli_targets", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.flags & GATE_TARGETS_PAULI_STRING; }, clean_doc_string(R"DOC( Returns whether or not the gate expects pauli targets. For example, `CORRELATED_ERROR` takes targets like `X0` and `Y1` instead of `0` or `1`. Examples: >>> import stim >>> stim.gate_data('CORRELATED_ERROR').takes_pauli_targets True >>> stim.gate_data('MPP').takes_pauli_targets True >>> stim.gate_data('H').takes_pauli_targets False >>> stim.gate_data('CX').takes_pauli_targets False >>> stim.gate_data('R').takes_pauli_targets False >>> stim.gate_data('M').takes_pauli_targets False >>> stim.gate_data('MRY').takes_pauli_targets False >>> stim.gate_data('MXX').takes_pauli_targets False >>> stim.gate_data('X_ERROR').takes_pauli_targets False >>> stim.gate_data('DETECTOR').takes_pauli_targets False )DOC") .data()); c.def_property_readonly( "inverse", [](const GateTypeWrapper &self_id) -> pybind11::object { const Gate &self = GATE_DATA.at(self_id.type); if (self.flags & GATE_IS_UNITARY) { return pybind11::cast(GateTypeWrapper{self.best_candidate_inverse_id}); } return pybind11::none(); }, clean_doc_string(R"DOC( @signature def inverse(self) -> Optional[stim.GateData]: The inverse of the gate, or None if it has no inverse. The inverse V of a gate U must have the property that V undoes the effects of U and that U undoes the effects of V. In particular, the circuit U 0 1 V 0 1 should be equivalent to doing nothing at all. Examples: >>> import stim >>> stim.gate_data('H').inverse stim.gate_data('H') >>> stim.gate_data('CX').inverse stim.gate_data('CX') >>> stim.gate_data('S').inverse stim.gate_data('S_DAG') >>> stim.gate_data('CXSWAP').inverse stim.gate_data('SWAPCX') >>> stim.gate_data('X_ERROR').inverse is None True >>> stim.gate_data('M').inverse is None True >>> stim.gate_data('R').inverse is None True >>> stim.gate_data('DETECTOR').inverse is None True >>> stim.gate_data('TICK').inverse is None True )DOC") .data()); c.def_property_readonly( "generalized_inverse", [](const GateTypeWrapper &self_id) -> GateTypeWrapper { const Gate &self = GATE_DATA.at(self_id.type); return GateTypeWrapper{self.best_candidate_inverse_id}; }, clean_doc_string(R"DOC( The closest-thing-to-an-inverse for the gate, if forced to pick something. The generalized inverse of a unitary gate U is its actual inverse U^-1. The generalized inverse of a reset or measurement gate U is a gate V such that, for every stabilizer flow that U has, V has the time reverse of that flow (up to Pauli feedback, with potentially more flows). For example, the time-reverse of R is MR because R has the single flow 1 -> Z and MR has the time reversed flow Z -> rec[-1]. The generalized inverse of noise like X_ERROR is just the same noise. The generalized inverse of an annotation like TICK is just the same annotation. Examples: >>> import stim >>> stim.gate_data('H').generalized_inverse stim.gate_data('H') >>> stim.gate_data('CXSWAP').generalized_inverse stim.gate_data('SWAPCX') >>> stim.gate_data('X_ERROR').generalized_inverse stim.gate_data('X_ERROR') >>> stim.gate_data('MX').generalized_inverse stim.gate_data('MX') >>> stim.gate_data('MRY').generalized_inverse stim.gate_data('MRY') >>> stim.gate_data('R').generalized_inverse stim.gate_data('M') >>> stim.gate_data('DETECTOR').generalized_inverse stim.gate_data('DETECTOR') >>> stim.gate_data('TICK').generalized_inverse stim.gate_data('TICK') )DOC") .data()); c.def_property_readonly( "takes_measurement_record_targets", [](const GateTypeWrapper &self_id) -> bool { const Gate &self = GATE_DATA.at(self_id.type); return self.flags & (GATE_CAN_TARGET_BITS | GATE_ONLY_TARGETS_MEASUREMENT_RECORD); }, clean_doc_string(R"DOC( Returns whether or not the gate can accept rec targets. For example, `CX` can take a measurement record target like `CX rec[-1] 1`. Examples: >>> import stim >>> stim.gate_data('CX').takes_measurement_record_targets True >>> stim.gate_data('DETECTOR').takes_measurement_record_targets True >>> stim.gate_data('H').takes_measurement_record_targets False >>> stim.gate_data('SWAP').takes_measurement_record_targets False >>> stim.gate_data('R').takes_measurement_record_targets False >>> stim.gate_data('M').takes_measurement_record_targets False >>> stim.gate_data('MRY').takes_measurement_record_targets False >>> stim.gate_data('MXX').takes_measurement_record_targets False >>> stim.gate_data('X_ERROR').takes_measurement_record_targets False >>> stim.gate_data('CORRELATED_ERROR').takes_measurement_record_targets False >>> stim.gate_data('MPP').takes_measurement_record_targets False )DOC") .data()); } ================================================ FILE: src/stim/gates/gates.pybind.h ================================================ // 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. #ifndef _STIM_GATES_GATE_DATA_PYBIND_H #define _STIM_GATES_GATE_DATA_PYBIND_H #include #include "stim/gates/gates.h" namespace stim_pybind { struct GateTypeWrapper { stim::GateType type; inline bool operator==(const GateTypeWrapper &other) const { return type == other.type; } }; pybind11::class_ pybind_gate_data(pybind11::module &m); void pybind_gate_data_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/gates/gates.test.cc ================================================ // 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. #include "stim/gates/gates.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.h" #include "stim/cmd/command_help.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/str_util.h" #include "stim/util_bot/test_util.test.h" #include "stim/util_top/circuit_flow_generators.h" #include "stim/util_top/has_flow.h" using namespace stim; TEST(gate_data, lookup) { ASSERT_TRUE(GATE_DATA.has("H")); ASSERT_FALSE(GATE_DATA.has("H2345")); ASSERT_EQ(GATE_DATA.at("H").id, GATE_DATA.at("H_XZ").id); ASSERT_NE(GATE_DATA.at("H").id, GATE_DATA.at("H_XY").id); ASSERT_THROW(GATE_DATA.at("MISSING"), std::out_of_range); ASSERT_TRUE(GATE_DATA.has("h")); ASSERT_TRUE(GATE_DATA.has("Cnot")); ASSERT_TRUE(GATE_DATA.at("h").id == GATE_DATA.at("H").id); ASSERT_TRUE(GATE_DATA.at("H_xz").id == GATE_DATA.at("H").id); } TEST(gate_data, zero_flag_means_not_a_gate) { ASSERT_EQ((GateType)0, GateType::NOT_A_GATE); ASSERT_EQ(GATE_DATA[(GateType)0].id, (GateType)0); ASSERT_EQ(GATE_DATA[(GateType)0].flags, GateFlags::NO_GATE_FLAG); for (size_t k = 0; k < GATE_DATA.items.size(); k++) { const auto &g = GATE_DATA[(GateType)k]; if (g.id != GateType::NOT_A_GATE) { EXPECT_NE(g.flags, GateFlags::NO_GATE_FLAG) << g.name; } } } TEST(gate_data, one_step_to_canonical_gate) { for (size_t k = 0; k < GATE_DATA.items.size(); k++) { const auto &g = GATE_DATA[(GateType)k]; if (g.id != GateType::NOT_A_GATE) { EXPECT_TRUE(g.id == (GateType)k || GATE_DATA[g.id].id == g.id) << g.name; } } } TEST(gate_data, hash_matches_storage_location) { ASSERT_EQ((GateType)0, GateType::NOT_A_GATE); ASSERT_EQ(GATE_DATA[(GateType)0].id, (GateType)0); ASSERT_EQ(GATE_DATA[(GateType)0].flags, GateFlags::NO_GATE_FLAG); for (size_t k = 0; k < GATE_DATA.items.size(); k++) { const auto &g = GATE_DATA[(GateType)k]; EXPECT_EQ(g.id, (GateType)k) << g.name; if (g.id != GateType::NOT_A_GATE) { EXPECT_EQ(GATE_DATA.hashed_name_to_gate_type_table[gate_name_to_hash(g.name)].id, g.id) << g.name; } } } template static std::pair>, std::vector>> circuit_output_eq_val( const Circuit &circuit) { // CAUTION: this is not 100% reliable when measurement count is larger than 1. TableauSimulator sim1(INDEPENDENT_TEST_RNG(), circuit.count_qubits(), -1); TableauSimulator sim2(INDEPENDENT_TEST_RNG(), circuit.count_qubits(), +1); sim1.safe_do_circuit(circuit); sim2.safe_do_circuit(circuit); return {sim1.canonical_stabilizers(), sim2.canonical_stabilizers()}; } template bool is_decomposition_correct(const Gate &gate) { const char *decomposition = gate.h_s_cx_m_r_decomposition; if (decomposition == nullptr) { return false; } Circuit original; original.safe_append(CircuitInstruction(gate.id, {}, gate_decomposition_help_targets_for_gate_type(gate.id), "")); uint32_t n = original.count_qubits(); Circuit epr; for (uint32_t q = 0; q < n; q++) { epr.safe_append_u("H", {q}); } for (uint32_t q = 0; q < n; q++) { epr.safe_append_u("CNOT", {q, q + n}); } Circuit circuit1 = epr + original; Circuit circuit2 = epr + Circuit(decomposition); // Reset gates make the ancillary qubits irrelevant because the final value is unrelated to the initial value. // So, for reset gates, discard the ancillary qubits. // CAUTION: this could give false positives if "partial reset" gates are added in the future. // (E.g. a two qubit gate that resets only one of the qubits.) if ((gate.flags & GATE_IS_RESET) && !(gate.flags & GATE_PRODUCES_RESULTS)) { for (uint32_t q = 0; q < n; q++) { circuit1.safe_append_u("R", {q + n}); circuit2.safe_append_u("R", {q + n}); } } for (const auto &op : circuit2.operations) { if (op.gate_type != GateType::CX && op.gate_type != GateType::H && op.gate_type != GateType::S && op.gate_type != GateType::M && op.gate_type != GateType::R) { return false; } } auto v1 = circuit_output_eq_val(circuit1); auto v2 = circuit_output_eq_val(circuit2); return v1 == v2; } TEST_EACH_WORD_SIZE_W(gate_data, decompositions_are_correct, { for (const auto &g : GATE_DATA.items) { if (g.flags & GATE_IS_UNITARY) { EXPECT_TRUE(g.h_s_cx_m_r_decomposition != nullptr) << g.name; } if (g.h_s_cx_m_r_decomposition != nullptr) { EXPECT_TRUE(is_decomposition_correct(g)) << g.name; } } }) TEST_EACH_WORD_SIZE_W(gate_data, unitary_inverses_are_correct, { for (const auto &g : GATE_DATA.items) { if (g.has_known_unitary_matrix()) { auto g_t_inv = g.tableau().inverse(false); auto g_inv_t = GATE_DATA[g.best_candidate_inverse_id].tableau(); EXPECT_EQ(g_t_inv, g_inv_t) << g.name; } } }) TEST_EACH_WORD_SIZE_W(gate_data, stabilizer_flows_are_correct, { for (const auto &g : GATE_DATA.items) { auto flows = g.flows(); if (flows.empty()) { continue; } std::vector targets = gate_decomposition_help_targets_for_gate_type(g.id); Circuit c; c.safe_append(CircuitInstruction(g.id, {}, targets, "")); auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_if_circuit_has_stabilizer_flows(256, rng, c, flows); for (uint32_t fk = 0; fk < (uint32_t)flows.size(); fk++) { EXPECT_TRUE(r[fk]) << "gate " << g.name << " has an unsatisfied flow: " << flows[fk]; } } }) TEST_EACH_WORD_SIZE_W(gate_data, stabilizer_flows_are_also_correct_for_decomposed_circuit, { auto rng = INDEPENDENT_TEST_RNG(); for (const auto &g : GATE_DATA.items) { auto flows = g.flows(); if (flows.empty()) { continue; } std::vector targets = gate_decomposition_help_targets_for_gate_type(g.id); Circuit c(g.h_s_cx_m_r_decomposition); auto r = sample_if_circuit_has_stabilizer_flows(256, rng, c, flows); for (uint32_t fk = 0; fk < (uint32_t)flows.size(); fk++) { EXPECT_TRUE(r[fk]) << "gate " << g.name << " has a decomposition with an unsatisfied flow: " << flows[fk]; } } }) std::array, 4> canonicalize_global_phase(std::array, 4> v) { for (std::complex pivot : v) { if (std::abs(pivot) > 1e-5) { for (auto &t : v) { t /= pivot; } return v; } } return v; } void expect_unitaries_close_up_global_phase( const Gate &g, std::array, 4> u1, std::array, 4> u2) { u1 = canonicalize_global_phase(u1); u2 = canonicalize_global_phase(u2); for (size_t k = 0; k < 4; k++) { if (std::abs(u1[k] - u2[k]) > 1e-5) { std::stringstream out; out << g.name << ":\n"; for (size_t k2 = 0; k2 < 4; k2++) { out << " " << u1[k2] << " vs " << u2[k2] << "\n"; } EXPECT_EQ(u1, u2) << out.str() << "\n" << comma_sep(g.to_euler_angles(), ","); return; } } EXPECT_TRUE(true); } std::array, 4> reconstruct_unitary_from_euler_angles(const Gate &g) { auto xyz = g.to_euler_angles(); auto c = cosf(xyz[0] / 2); auto s = sinf(xyz[0] / 2); return { c, -s * std::exp(std::complex{0, xyz[2]}), s * std::exp(std::complex{0, xyz[1]}), c * std::exp(std::complex{0, xyz[1] + xyz[2]}), }; } std::array, 4> reconstruct_unitary_from_data(Gate g) { return { g.unitary_data[0][0], g.unitary_data[0][1], g.unitary_data[1][0], g.unitary_data[1][1], }; } std::array, 4> reconstruct_unitary_from_axis_angle(const Gate &g) { auto xyz_a = g.to_axis_angle(); auto x = xyz_a[0]; auto y = xyz_a[1]; auto z = xyz_a[2]; auto a = xyz_a[3]; auto c = cosf(a / 2); auto s = -sinf(a / 2); return { std::complex{c, s * z}, std::complex{s * y, s * x}, std::complex{-s * y, s * x}, std::complex{c, -s * z}, }; } std::array, 4> reconstruct_unitary_from_euler_angles_via_vector_sim_for_axis_reference( const Gate &g) { auto xyz = g.to_euler_angles(); std::array half_turns; for (size_t k = 0; k < 3; k++) { half_turns[k] = (int)(roundf(xyz[k] / 3.14159265359f * 2)) & 3; } std::array y_rots{"I", "SQRT_Y", "Y", "SQRT_Y_DAG"}; std::array z_rots{"I", "S", "Z", "S_DAG"}; // Recover the unitary matrix via the state channel duality. Circuit c; c.safe_append_u("H", {0}); c.safe_append_u("CX", {0, 1}); c.safe_append_u(z_rots[half_turns[2]], {1}); c.safe_append_u(y_rots[half_turns[0]], {1}); c.safe_append_u(z_rots[half_turns[1]], {1}); VectorSimulator v(2); v.do_unitary_circuit(c); return {v.state[0], v.state[1], v.state[2], v.state[3]}; } TEST(gate_data, to_euler_angles) { for (const auto &g : GATE_DATA.items) { if ((g.flags & GATE_IS_UNITARY) && (g.flags & GATE_IS_SINGLE_QUBIT_GATE)) { auto u1 = reconstruct_unitary_from_data(g); auto u2 = reconstruct_unitary_from_euler_angles(g); expect_unitaries_close_up_global_phase(g, u1, u2); } } } TEST(gate_data, to_axis_angle) { for (const auto &g : GATE_DATA.items) { if ((g.flags & GATE_IS_UNITARY) && (g.flags & GATE_IS_SINGLE_QUBIT_GATE)) { auto u1 = reconstruct_unitary_from_data(g); auto u2 = reconstruct_unitary_from_axis_angle(g); expect_unitaries_close_up_global_phase(g, u1, u2); } } } TEST(gate_data, to_euler_angles_axis_reference) { for (const auto &g : GATE_DATA.items) { if ((g.flags & GATE_IS_UNITARY) && (g.flags & GATE_IS_SINGLE_QUBIT_GATE)) { auto u1 = reconstruct_unitary_from_data(g); auto u2 = reconstruct_unitary_from_euler_angles_via_vector_sim_for_axis_reference(g); expect_unitaries_close_up_global_phase(g, u1, u2); } } } TEST(gate_data, is_symmetric_vs_flow_generators_of_two_qubit_gates) { for (const auto &g : GATE_DATA.items) { if ((g.flags & stim::GATE_IS_NOISY) && !(g.flags & stim::GATE_PRODUCES_RESULTS)) { continue; } if (g.flags & GATE_TARGETS_PAIRS) { Circuit c1; Circuit c2; c1.safe_append_u(g.name, {0, 1}, {}); c2.safe_append_u(g.name, {1, 0}, {}); auto f1 = circuit_flow_generators<64>(c1); auto f2 = circuit_flow_generators<64>(c2); EXPECT_EQ(g.is_symmetric(), f1 == f2) << g.name; } } } TEST(gate_data, hadamard_conjugated_vs_flow_generators_of_two_qubit_gates) { auto flow_key = [](const Circuit &circuit, bool ignore_sign) { auto f = circuit_flow_generators<64>(circuit); if (ignore_sign) { for (auto &e : f) { e.input.sign = false; e.output.sign = false; } } std::stringstream ss; ss << comma_sep(f); return ss.str(); }; std::map known_flows_s; std::map> known_flows_u; for (const auto &g : GATE_DATA.items) { if (g.id == GateType::II || g.id == GateType::II_ERROR || g.id == GateType::I_ERROR) { ASSERT_EQ(g.hadamard_conjugated(false), g.id); ASSERT_EQ(g.hadamard_conjugated(true), g.id); continue; } if (g.arg_count != 0 && g.arg_count != ARG_COUNT_SYGIL_ZERO_OR_ONE && g.arg_count != ARG_COUNT_SYGIL_ANY) { continue; } if ((g.flags & GATE_TARGETS_PAIRS) || (g.flags & GATE_IS_SINGLE_QUBIT_GATE)) { Circuit c; c.safe_append_u(g.name, {0, 1}, {}); auto key_s = flow_key(c, false); auto key_u = flow_key(c, true); ASSERT_EQ(known_flows_s.find(key_s), known_flows_s.end()) << "collision between " << g.name << " and " << GATE_DATA[known_flows_s[key_s]].name; known_flows_s[key_s] = g.id; known_flows_u[key_u].push_back(g.id); } } for (const auto &g : GATE_DATA.items) { if (g.id == GateType::II || g.id == GateType::II_ERROR || g.id == GateType::I_ERROR) { continue; } if (g.arg_count != 0 && g.arg_count != ARG_COUNT_SYGIL_ZERO_OR_ONE && g.arg_count != ARG_COUNT_SYGIL_ANY) { continue; } if ((g.flags & GATE_TARGETS_PAIRS) || (g.flags & GATE_IS_SINGLE_QUBIT_GATE)) { Circuit c; c.safe_append_u("H", {0, 1}, {}); c.safe_append_u(g.name, {0, 1}, {}); c.safe_append_u("H", {0, 1}, {}); auto key_s = flow_key(c, false); auto key_u = flow_key(c, true); auto other_s = known_flows_s.find(key_s); auto &other_us = known_flows_u[key_u]; if (other_us.empty()) { other_us.push_back(GateType::NOT_A_GATE); } GateType expected_s = other_s == known_flows_s.end() ? GateType::NOT_A_GATE : other_s->second; GateType actual_s = g.hadamard_conjugated(false); GateType actual_u = g.hadamard_conjugated(true); bool found = std::find(other_us.begin(), other_us.end(), actual_u) != other_us.end(); EXPECT_EQ(actual_s, expected_s) << "signed " << g.name << " -> " << GATE_DATA[actual_s].name << " != " << GATE_DATA[expected_s].name; EXPECT_TRUE(found) << "unsigned " << g.name << " -> " << GATE_DATA[actual_u].name << " not in " << GATE_DATA[other_us[0]].name; } } } ================================================ FILE: src/stim/gates/gates_test.py ================================================ import numpy as np import stim def test_gate_data_eq(): assert stim.gate_data('H') == stim.GateData('H') assert stim.gate_data('H') == stim.gate_data('H_XZ') assert not (stim.gate_data('H') == stim.GateData('X_ERROR')) assert stim.gate_data('X') != stim.GateData('H') def test_gate_data_str(): assert str(stim.GateData('MXX')) == ''' stim.GateData { .name = 'MXX' .aliases = ['MXX'] .is_noisy_gate = True .is_reset = False .is_single_qubit_gate = False .is_two_qubit_gate = True .is_unitary = False .num_parens_arguments_range = range(0, 2) .produces_measurements = True .takes_measurement_record_targets = False .takes_pauli_targets = False } '''.strip() assert str(stim.GateData('H')) == ''' stim.GateData { .name = 'H' .aliases = ['H', 'H_XZ'] .is_noisy_gate = False .is_reset = False .is_single_qubit_gate = True .is_two_qubit_gate = False .is_unitary = True .num_parens_arguments_range = range(0, 1) .produces_measurements = False .takes_measurement_record_targets = False .takes_pauli_targets = False .tableau = stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) .unitary_matrix = np.array([[(0.7071067690849304+0j), (0.7071067690849304+0j)], [(0.7071067690849304+0j), (-0.7071067690849304-0j)]], dtype=np.complex64) } '''.strip() def test_num_parens_arguments_range(): assert stim.gate_data('H').num_parens_arguments_range == range(0, 1) assert stim.gate_data('M').num_parens_arguments_range == range(0, 2) def test_is_reset(): assert not stim.gate_data('H').is_reset assert stim.gate_data('R').is_reset assert stim.gate_data('MR').is_reset def test_is_two_qubit_gate(): assert not stim.gate_data('H').is_two_qubit_gate assert stim.gate_data('CX').is_two_qubit_gate def test_is_single_qubit_gate(): assert stim.gate_data('H').is_single_qubit_gate assert not stim.gate_data('CX').is_single_qubit_gate def test_is_noisy_gate(): assert stim.gate_data('X_ERROR').is_noisy_gate assert not stim.gate_data('X').is_noisy_gate def test_produces_measurements(): assert stim.gate_data('MR').produces_measurements assert not stim.gate_data('R').produces_measurements def test_takes_pauli_targets(): assert stim.gate_data('MPP').takes_pauli_targets assert not stim.gate_data('MXX').takes_pauli_targets def test_aliases(): assert stim.gate_data('H').aliases == ['H', 'H_XZ'] assert stim.gate_data('CX').aliases == ['CNOT', 'CX', 'ZCX'] def test_tableau(): assert stim.gate_data('H').tableau == stim.Tableau.from_named_gate('H') def test_name(): assert stim.gate_data('H').name == 'H' def test_gate_data_repr(): val = stim.GateData('MPP') assert eval(repr(val), {"stim": stim}) == val def test_takes_measurement_record_targets(): assert not stim.gate_data('H').takes_measurement_record_targets assert stim.gate_data('DETECTOR').takes_measurement_record_targets def test_gate_data_inverse(): for v in stim.gate_data().values(): assert v.is_unitary == (v.inverse is not None) matrix = v.unitary_matrix if matrix is not None: assert v.is_unitary assert np.allclose(matrix.conj().T, v.inverse.unitary_matrix, atol=1e-6), (v.name, v.inverse.name) assert v.inverse == v.generalized_inverse assert stim.gate_data('H').inverse == stim.gate_data('H') assert stim.gate_data('S').inverse == stim.gate_data('S_DAG') assert stim.gate_data('M').inverse is None assert stim.gate_data('CXSWAP').inverse == stim.gate_data('SWAPCX') assert stim.gate_data('SPP').inverse == stim.gate_data('SPP_DAG') assert stim.gate_data('S').generalized_inverse == stim.gate_data('S_DAG') assert stim.gate_data('M').generalized_inverse == stim.gate_data('M') assert stim.gate_data('R').generalized_inverse == stim.gate_data('M') assert stim.gate_data('MR').generalized_inverse == stim.gate_data('MR') assert stim.gate_data('MPP').generalized_inverse == stim.gate_data('MPP') assert stim.gate_data('ELSE_CORRELATED_ERROR').generalized_inverse == stim.gate_data('ELSE_CORRELATED_ERROR') def test_gate_data_flows(): assert stim.GateData('H').flows == [ stim.Flow("X -> Z"), stim.Flow("Z -> X"), ] def test_gate_is_symmetric(): assert stim.GateData('SWAP').is_symmetric_gate assert stim.GateData('H').is_symmetric_gate assert stim.GateData('MYY').is_symmetric_gate assert stim.GateData('DEPOLARIZE2').is_symmetric_gate assert not stim.GateData('PAULI_CHANNEL_2').is_symmetric_gate assert not stim.GateData('DETECTOR').is_symmetric_gate assert not stim.GateData('TICK').is_symmetric_gate def test_gate_hadamard_conjugated(): assert stim.GateData('CZSWAP').hadamard_conjugated(unsigned=True) is None assert stim.GateData('TICK').hadamard_conjugated() == stim.GateData('TICK') assert stim.GateData('MYY').hadamard_conjugated() == stim.GateData('MYY') assert stim.GateData('XCZ').hadamard_conjugated() == stim.GateData('CX') assert stim.GateData('X_ERROR').hadamard_conjugated() == stim.GateData('Z_ERROR') assert stim.GateData('Y_ERROR').hadamard_conjugated() == stim.GateData('Y_ERROR') assert stim.GateData('Z_ERROR').hadamard_conjugated() == stim.GateData('X_ERROR') assert stim.GateData('I_ERROR').hadamard_conjugated() == stim.GateData('I_ERROR') assert stim.GateData('II_ERROR').hadamard_conjugated() == stim.GateData('II_ERROR') ================================================ FILE: src/stim/gen/circuit_gen_params.cc ================================================ #include "stim/gen/circuit_gen_params.h" #include "stim/util_bot/arg_parse.h" using namespace stim; void append_anti_basis_error(Circuit &circuit, const std::vector &targets, double p, char basis) { if (p > 0) { if (basis == 'X') { circuit.safe_append_ua("Z_ERROR", targets, p); } else { circuit.safe_append_ua("X_ERROR", targets, p); } } } void CircuitGenParameters::validate_params() const { if (before_measure_flip_probability < 0 || before_measure_flip_probability > 1) { throw std::invalid_argument("not 0 <= before_measure_flip_probability <= 1"); } if (before_round_data_depolarization < 0 || before_round_data_depolarization > 1) { throw std::invalid_argument("not 0 <= before_round_data_depolarization <= 1"); } if (after_clifford_depolarization < 0 || after_clifford_depolarization > 1) { throw std::invalid_argument("not 0 <= after_clifford_depolarization <= 1"); } if (after_reset_flip_probability < 0 || after_reset_flip_probability > 1) { throw std::invalid_argument("not 0 <= after_reset_flip_probability <= 1"); } } CircuitGenParameters::CircuitGenParameters(uint64_t rounds, uint32_t distance, std::string task) : rounds(rounds), distance(distance), task(task) { } void CircuitGenParameters::append_begin_round_tick(Circuit &circuit, const std::vector &data_qubits) const { circuit.safe_append_u("TICK", {}); if (before_round_data_depolarization > 0) { circuit.safe_append_ua("DEPOLARIZE1", data_qubits, before_round_data_depolarization); } } void CircuitGenParameters::append_unitary_1( Circuit &circuit, std::string_view name, const std::vector targets) const { circuit.safe_append_u(name, targets); if (after_clifford_depolarization > 0) { circuit.safe_append_ua("DEPOLARIZE1", targets, after_clifford_depolarization); } } void CircuitGenParameters::append_unitary_2( Circuit &circuit, std::string_view name, const std::vector targets) const { circuit.safe_append_u(name, targets); if (after_clifford_depolarization > 0) { circuit.safe_append_ua("DEPOLARIZE2", targets, after_clifford_depolarization); } } void CircuitGenParameters::append_reset(Circuit &circuit, const std::vector targets, char basis) const { std::string gate("R"); gate.push_back(basis); circuit.safe_append_u(gate, targets); append_anti_basis_error(circuit, targets, after_reset_flip_probability, basis); } void CircuitGenParameters::append_measure(Circuit &circuit, const std::vector targets, char basis) const { std::string gate("M"); gate.push_back(basis); append_anti_basis_error(circuit, targets, before_measure_flip_probability, basis); circuit.safe_append_u(gate, targets); } void CircuitGenParameters::append_measure_reset( Circuit &circuit, const std::vector targets, char basis) const { std::string gate("MR"); gate.push_back(basis); append_anti_basis_error(circuit, targets, before_measure_flip_probability, basis); circuit.safe_append_u(gate, targets); append_anti_basis_error(circuit, targets, after_reset_flip_probability, basis); } std::string GeneratedCircuit::layout_str() const { std::stringstream ss; std::vector> lines; for (const auto &kv : layout) { auto x = kv.first.first; auto y = kv.first.second; while (lines.size() <= y) { lines.push_back({}); } while (lines[y].size() <= x) { lines[y].push_back(""); } lines[y][x] = kv.second.first + std::to_string(kv.second.second); } size_t max_len = 0; for (const auto &line : lines) { for (const auto &entry : line) { max_len = std::max(max_len, entry.size()); } } for (auto p = lines.crbegin(); p != lines.crend(); p++) { const auto &line = *p; ss << "#"; for (const auto &entry : line) { ss << ' ' << entry; ss << std::string(max_len - entry.size(), ' '); } ss << "\n"; } return ss.str(); } ================================================ FILE: src/stim/gen/circuit_gen_params.h ================================================ #ifndef _STIM_GEN_CIRCUIT_GEN_PARAMS_H #define _STIM_GEN_CIRCUIT_GEN_PARAMS_H #include #include #include #include "stim/circuit/circuit.h" namespace stim { struct CircuitGenParameters { uint64_t rounds; uint32_t distance; std::string task; double after_clifford_depolarization = 0; double before_round_data_depolarization = 0; double before_measure_flip_probability = 0; double after_reset_flip_probability = 0; void validate_params() const; CircuitGenParameters(uint64_t rounds, uint32_t distance, std::string task); void append_begin_round_tick(Circuit &circuit, const std::vector &data_qubits) const; void append_unitary_1(Circuit &circuit, std::string_view name, const std::vector targets) const; void append_unitary_2(Circuit &circuit, std::string_view name, const std::vector targets) const; void append_reset(Circuit &circuit, const std::vector targets, char basis = 'Z') const; void append_measure(Circuit &circuit, const std::vector targets, char basis = 'Z') const; void append_measure_reset(Circuit &circuit, const std::vector targets, char basis = 'Z') const; }; struct GeneratedCircuit { Circuit circuit; std::map, std::pair> layout; std::string hint_str; std::string layout_str() const; }; } // namespace stim #endif ================================================ FILE: src/stim/gen/circuit_gen_params.test.cc ================================================ // 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. #include "stim/gen/circuit_gen_params.h" #include "gtest/gtest.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(circuit_gen_params, append_begin_round_tick) { CircuitGenParameters params(3, 5, "test"); Circuit circuit; circuit.clear(); params.append_begin_round_tick(circuit, {1, 2, 3}); ASSERT_EQ(circuit, Circuit("TICK")); params.before_round_data_depolarization = 0.125; circuit.clear(); params.append_begin_round_tick(circuit, {1, 2, 3}); ASSERT_EQ(circuit, Circuit("TICK\nDEPOLARIZE1(0.125) 1 2 3")); } TEST(circuit_gen_params, append_unitary_1) { CircuitGenParameters params(3, 5, "test"); Circuit circuit; circuit.clear(); params.append_unitary_1(circuit, "H", {2, 3, 5}); ASSERT_EQ(circuit, Circuit("H 2 3 5")); params.after_clifford_depolarization = 0.125; circuit.clear(); params.append_unitary_1(circuit, "H", {2, 3, 5}); ASSERT_EQ(circuit, Circuit("H 2 3 5\nDEPOLARIZE1(0.125) 2 3 5")); } TEST(circuit_gen_params, append_unitary_2) { CircuitGenParameters params(3, 5, "test"); Circuit circuit; circuit.clear(); params.append_unitary_2(circuit, "CNOT", {2, 3, 5, 7}); ASSERT_EQ(circuit, Circuit("CX 2 3 5 7")); params.after_clifford_depolarization = 0.125; circuit.clear(); params.append_unitary_2(circuit, "CNOT", {2, 3, 5, 7}); ASSERT_EQ(circuit, Circuit("CX 2 3 5 7\nDEPOLARIZE2(0.125) 2 3 5 7")); } TEST(circuit_gen_params, append_reset) { CircuitGenParameters params(3, 5, "test"); Circuit circuit; circuit.clear(); params.append_reset(circuit, {2, 3, 5}); params.append_reset(circuit, {2, 3, 5}, 'Z'); ASSERT_EQ(circuit, Circuit("R 2 3 5\nR 2 3 5")); params.append_reset(circuit, {1}, 'X'); params.append_reset(circuit, {4}, 'Y'); ASSERT_EQ(circuit, Circuit("R 2 3 5\nR 2 3 5\nRX 1\nRY 4")); params.after_reset_flip_probability = 0.125; circuit.clear(); params.append_reset(circuit, {2, 3, 5}); ASSERT_EQ(circuit, Circuit("R 2 3 5\nX_ERROR(0.125) 2 3 5")); params.append_reset(circuit, {1}, 'X'); params.append_reset(circuit, {4}, 'Y'); ASSERT_EQ(circuit, Circuit("R 2 3 5\nX_ERROR(0.125) 2 3 5\nRX 1\nZ_ERROR(0.125) 1\nRY 4\nX_ERROR(0.125) 4")); } TEST(circuit_gen_params, append_measure) { CircuitGenParameters params(3, 5, "test"); Circuit circuit; circuit.clear(); params.append_measure(circuit, {2, 3, 5}); params.append_measure(circuit, {2, 3, 5}, 'Z'); ASSERT_EQ(circuit, Circuit("M 2 3 5\nM 2 3 5")); params.append_measure(circuit, {1}, 'X'); params.append_measure(circuit, {4}, 'Y'); ASSERT_EQ(circuit, Circuit("M 2 3 5\nM 2 3 5\nMX 1\nMY 4")); params.before_measure_flip_probability = 0.125; circuit.clear(); params.append_measure(circuit, {2, 3, 5}); ASSERT_EQ(circuit, Circuit("X_ERROR(0.125) 2 3 5\nM 2 3 5")); params.append_measure(circuit, {1}, 'X'); params.append_measure(circuit, {4}, 'Y'); ASSERT_EQ(circuit, Circuit("X_ERROR(0.125) 2 3 5\nM 2 3 5\nZ_ERROR(0.125) 1\nMX 1\nX_ERROR(0.125) 4\nMY 4")); } TEST(circuit_gen_params, append_measure_reset) { CircuitGenParameters params(3, 5, "test"); Circuit circuit; circuit.clear(); params.append_measure_reset(circuit, {2, 3, 5}); params.append_measure_reset(circuit, {2, 3, 5}, 'Z'); ASSERT_EQ(circuit, Circuit("MR 2 3 5\nMR 2 3 5")); params.append_measure_reset(circuit, {1}, 'X'); params.append_measure_reset(circuit, {4}, 'Y'); ASSERT_EQ(circuit, Circuit("MR 2 3 5\nMR 2 3 5\nMRX 1\nMRY 4")); params.before_measure_flip_probability = 0.125; params.after_reset_flip_probability = 0.25; circuit.clear(); params.append_measure_reset(circuit, {2, 3, 5}); params.append_measure_reset(circuit, {1}, 'X'); params.append_measure_reset(circuit, {4}, 'Y'); ASSERT_EQ(circuit, Circuit(R"CIRCUIT( X_ERROR(0.125) 2 3 5 MR 2 3 5 X_ERROR(0.25) 2 3 5 Z_ERROR(0.125) 1 MRX 1 Z_ERROR(0.25) 1 X_ERROR(0.125) 4 MRY 4 X_ERROR(0.25) 4 )CIRCUIT")); } ================================================ FILE: src/stim/gen/gen_color_code.cc ================================================ #include "stim/gen/gen_color_code.h" #include #include #include #include #include using namespace stim; struct color_coord { float x; float y; color_coord operator+(color_coord other) const { return {x + other.x, y + other.y}; } color_coord operator-(color_coord other) const { return {x - other.x, y - other.y}; } bool operator==(color_coord other) const { return x == other.x && y == other.y; } bool operator<(color_coord other) const { if (x != other.x) { return x < other.x; } return y < other.y; } }; GeneratedCircuit stim::generate_color_code_circuit(const CircuitGenParameters ¶ms) { if (params.task != "memory_xyz") { throw std::invalid_argument( "Unrecognized task '" + params.task + "'. Known color_code tasks:\n" " 'memory_xyz': Initialize logical |0>, protect by cycling X then Y then Z stabilizer measurements, " "measure logical Z.\n"); } if (params.rounds < 2) { throw std::invalid_argument("Need rounds >= 2."); } if (params.distance < 2 || params.distance % 2 == 0) { throw std::invalid_argument("Need an odd distance >= 3."); } uint32_t d = params.distance; uint32_t w = d + (d - 1) / 2; // Lay out and index qubits. std::set data_coords; std::set measure_coords; std::map p2q; std::vector data_qubits; std::vector measurement_qubits; for (size_t y = 0; y < w; y++) { for (size_t x = 0; x < w - y; x++) { color_coord q{x + y / 2.0f, (float)y}; auto i = (uint32_t)p2q.size(); p2q[q] = i; if ((x + 2 * y) % 3 == 2) { measure_coords.insert(q); measurement_qubits.push_back(p2q[q]); } else { data_coords.insert(q); data_qubits.push_back(p2q[q]); } } } // Keep targets sorted to make the output look a bit cleaner. std::vector all_qubits; all_qubits.insert(all_qubits.end(), data_qubits.begin(), data_qubits.end()); all_qubits.insert(all_qubits.end(), measurement_qubits.begin(), measurement_qubits.end()); std::sort(all_qubits.begin(), all_qubits.end()); std::sort(data_qubits.begin(), data_qubits.end()); std::sort(measurement_qubits.begin(), measurement_qubits.end()); // Reverse indices. std::map data_coord_to_order; std::map measure_coord_to_order; std::map q2p; for (const auto &kv : p2q) { q2p[kv.second] = kv.first; } for (auto q : data_qubits) { auto i = (uint32_t)data_coord_to_order.size(); data_coord_to_order[q2p[q]] = i; } for (auto q : measurement_qubits) { auto i = (uint32_t)measure_coord_to_order.size(); measure_coord_to_order[q2p[q]] = i; } // Precompute targets for each tick of CNOT gates. std::array, 6> cnot_targets; std::vector deltas{ {1, 0}, {0.5, 1}, {0.5, -1}, {-1, 0}, {-0.5, 1}, {-0.5, -1}, }; for (size_t k = 0; k < deltas.size(); k++) { for (auto measure : measure_coords) { auto data = measure + deltas[k]; if (p2q.find(data) != p2q.end()) { cnot_targets[k].push_back(p2q[data]); cnot_targets[k].push_back(p2q[measure]); } } } // Build the repeated actions that make up the color code cycle. Circuit cycle_actions; params.append_begin_round_tick(cycle_actions, data_qubits); params.append_unitary_1(cycle_actions, "C_XYZ", data_qubits); for (const auto &targets : cnot_targets) { cycle_actions.safe_append_u("TICK", {}); params.append_unitary_2(cycle_actions, "CNOT", targets); } cycle_actions.safe_append_u("TICK", {}); params.append_measure_reset(cycle_actions, measurement_qubits); // Build the start of the circuit, getting a state that's ready to cycle. // In particular, the first cycle has different detectors and so has to be handled special. auto m = (uint32_t)measurement_qubits.size(); Circuit head; for (auto q : all_qubits) { color_coord c = q2p[q]; head.safe_append_u("QUBIT_COORDS", {q}, {c.x, c.y}); } params.append_reset(head, all_qubits); head += cycle_actions * 2; for (uint32_t k = m; k-- > 0;) { color_coord c = q2p[measurement_qubits[m - k - 1]]; head.safe_append_u("DETECTOR", {(k + 1) | TARGET_RECORD_BIT, (k + 1 + m) | TARGET_RECORD_BIT}, {c.x, c.y, 0}); } // Build the repeated body of the circuit, including the detectors comparing to previous cycles. Circuit body = cycle_actions; body.safe_append_u("SHIFT_COORDS", {}, {0, 0, 1}); for (uint32_t k = m; k-- > 0;) { color_coord c = q2p[measurement_qubits[m - k - 1]]; body.safe_append_u( "DETECTOR", {(k + 1) | TARGET_RECORD_BIT, (k + 1 + m) | TARGET_RECORD_BIT, (k + 1 + 2 * m) | TARGET_RECORD_BIT}, {c.x, c.y, 0}); } // Build the end of the circuit, getting out of the cycle state and terminating. // In particular, the data measurements create detectors that have to be handled special. // Also, the tail is responsible for identifying the logical observable. Circuit tail; params.append_measure(tail, data_qubits, "ZXY"[params.rounds % 3]); for (auto m_q : measurement_qubits) { auto measure = q2p[m_q]; std::vector detectors; for (auto delta : deltas) { auto data = measure + delta; if (p2q.find(data) != p2q.end()) { detectors.push_back((uint32_t)(data_qubits.size() - data_coord_to_order[data]) | TARGET_RECORD_BIT); } } uint32_t p = (data_qubits.size() + measurement_qubits.size() - measure_coord_to_order[measure]) | TARGET_RECORD_BIT; // Depending on if the last two rounds were XY, YZ, or ZX, different combinations are equal the data // measurements. if (params.rounds % 3 == 0) { detectors.push_back(p); } else if (params.rounds % 3 == 1) { detectors.push_back(p + m); } else { detectors.push_back(p); detectors.push_back(p + m); } std::sort(detectors.begin(), detectors.end()); tail.safe_append_u("DETECTOR", detectors, {measure.x, measure.y, 1}); } // Logical observable. std::vector obs_inc; for (auto q : data_coords) { if (q.y == 0) { obs_inc.push_back((data_qubits.size() - data_coord_to_order[q]) | TARGET_RECORD_BIT); } } std::sort(obs_inc.begin(), obs_inc.end()); tail.safe_append_ua("OBSERVABLE_INCLUDE", obs_inc, 0); // Put it all together. auto full_circuit = head + body * (params.rounds - 2) + tail; // Make 2d layout. std::map, std::pair> layout; for (auto q : data_coords) { layout[{(uint32_t)(q.x * 2), (uint32_t)q.y}] = {q.y == 0 ? 'L' : 'd', p2q[q]}; } std::array rgb{'R', 'G', 'B'}; for (auto q : measure_coords) { auto x = (uint32_t)(q.x * 2); auto y = (uint32_t)q.y; layout[{x, y}] = {rgb[(x + y) % 3], p2q[q]}; } return { full_circuit, layout, "# Legend:\n" "# d# = data qubit\n" "# L# = data qubit with logical observable crossing\n" "# R# = measurement qubit (red hex)\n" "# G# = measurement qubit (green hex)\n" "# B# = measurement qubit (blue hex)\n"}; } ================================================ FILE: src/stim/gen/gen_color_code.h ================================================ #ifndef _STIM_GEN_GEN_COLOR_CODE_H #define _STIM_GEN_GEN_COLOR_CODE_H #include "stim/circuit/circuit.h" #include "stim/gen/circuit_gen_params.h" namespace stim { GeneratedCircuit generate_color_code_circuit(const CircuitGenParameters ¶ms); } #endif ================================================ FILE: src/stim/gen/gen_color_code.test.cc ================================================ // 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. #include "stim/gen/gen_color_code.h" #include "gtest/gtest.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(gen_color_code, color_code_hard_coded_comparison) { CircuitGenParameters params(100, 5, "memory_xyz"); params.after_clifford_depolarization = 0.125; params.after_reset_flip_probability = 0.25; params.before_measure_flip_probability = 0.375; params.before_round_data_depolarization = 0.0625; auto out = generate_color_code_circuit(params); ASSERT_EQ( out.layout_str(), "" "# d27\n" "# d25 R26\n" "# B22 d23 d24\n" "# d18 d19 G20 d21\n" "# d13 R14 d15 d16 R17\n" "# B7 d8 d9 B10 d11 d12\n" "# L0 L1 G2 L3 L4 G5 L6 \n"); params.distance = 3; out = generate_color_code_circuit(params); ASSERT_EQ( out.layout_str(), "" "# d9\n" "# d7 R8\n" "# B4 d5 d6\n" "# L0 L1 G2 L3\n"); ASSERT_EQ( out.circuit.str(), Circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 0) 3 QUBIT_COORDS(0.5, 1) 4 QUBIT_COORDS(1.5, 1) 5 QUBIT_COORDS(2.5, 1) 6 QUBIT_COORDS(1, 2) 7 QUBIT_COORDS(2, 2) 8 QUBIT_COORDS(1.5, 3) 9 R 0 1 2 3 4 5 6 7 8 9 X_ERROR(0.25) 0 1 2 3 4 5 6 7 8 9 REPEAT 2 { TICK DEPOLARIZE1(0.0625) 0 1 3 5 6 7 9 C_XYZ 0 1 3 5 6 7 9 DEPOLARIZE1(0.125) 0 1 3 5 6 7 9 TICK CX 5 4 3 2 DEPOLARIZE2(0.125) 5 4 3 2 TICK CX 7 4 6 2 DEPOLARIZE2(0.125) 7 4 6 2 TICK CX 1 4 6 8 DEPOLARIZE2(0.125) 1 4 6 8 TICK CX 1 2 7 8 DEPOLARIZE2(0.125) 1 2 7 8 TICK CX 5 2 9 8 DEPOLARIZE2(0.125) 5 2 9 8 TICK CX 0 4 5 8 DEPOLARIZE2(0.125) 0 4 5 8 TICK X_ERROR(0.375) 2 4 8 MR 2 4 8 X_ERROR(0.25) 2 4 8 } DETECTOR(2, 0, 0) rec[-3] rec[-6] DETECTOR(0.5, 1, 0) rec[-2] rec[-5] DETECTOR(2, 2, 0) rec[-1] rec[-4] REPEAT 98 { TICK DEPOLARIZE1(0.0625) 0 1 3 5 6 7 9 C_XYZ 0 1 3 5 6 7 9 DEPOLARIZE1(0.125) 0 1 3 5 6 7 9 TICK CX 5 4 3 2 DEPOLARIZE2(0.125) 5 4 3 2 TICK CX 7 4 6 2 DEPOLARIZE2(0.125) 7 4 6 2 TICK CX 1 4 6 8 DEPOLARIZE2(0.125) 1 4 6 8 TICK CX 1 2 7 8 DEPOLARIZE2(0.125) 1 2 7 8 TICK CX 5 2 9 8 DEPOLARIZE2(0.125) 5 2 9 8 TICK CX 0 4 5 8 DEPOLARIZE2(0.125) 0 4 5 8 TICK X_ERROR(0.375) 2 4 8 MR 2 4 8 X_ERROR(0.25) 2 4 8 SHIFT_COORDS(0, 0, 1) DETECTOR(2, 0, 0) rec[-3] rec[-6] rec[-9] DETECTOR(0.5, 1, 0) rec[-2] rec[-5] rec[-8] DETECTOR(2, 2, 0) rec[-1] rec[-4] rec[-7] } Z_ERROR(0.375) 0 1 3 5 6 7 9 MX 0 1 3 5 6 7 9 DETECTOR(2, 0, 1) rec[-3] rec[-4] rec[-5] rec[-6] rec[-13] DETECTOR(0.5, 1, 1) rec[-2] rec[-4] rec[-6] rec[-7] rec[-12] DETECTOR(2, 2, 1) rec[-1] rec[-2] rec[-3] rec[-4] rec[-11] OBSERVABLE_INCLUDE(0) rec[-5] rec[-6] rec[-7] )CIRCUIT") .str()); } ================================================ FILE: src/stim/gen/gen_rep_code.cc ================================================ #include "stim/gen/gen_rep_code.h" using namespace stim; GeneratedCircuit stim::generate_rep_code_circuit(const CircuitGenParameters ¶ms) { if (params.task != "memory") { throw std::invalid_argument( "Unrecognized task '" + params.task + "'. Known repetition_code tasks:\n" " 'memory': Initialize |0>, protect with parity measurements, measure.\n"); } if (params.rounds < 1) { throw std::invalid_argument("Need rounds >= 1."); } if (params.distance < 2) { throw std::invalid_argument("Need a distance >= 2."); } uint32_t m = params.distance - 1; uint32_t n = m * 2 + 1; // Lay out qubits and determine interaction targets. std::vector all_qubits; std::vector data_qubits; std::vector cnot_targets_1; std::vector cnot_targets_2; std::vector measurement_qubits; for (uint32_t q = 0; q < n; q++) { all_qubits.push_back(q); if (q % 2 == 0) { data_qubits.push_back(q); } else { measurement_qubits.push_back(q); cnot_targets_1.push_back(q - 1); cnot_targets_1.push_back(q); cnot_targets_2.push_back(q + 1); cnot_targets_2.push_back(q); } } // Build the repeated actions that make up the repetition code cycle. Circuit cycle_actions; params.append_begin_round_tick(cycle_actions, data_qubits); params.append_unitary_2(cycle_actions, "CNOT", cnot_targets_1); cycle_actions.safe_append_u("TICK", {}); params.append_unitary_2(cycle_actions, "CNOT", cnot_targets_2); cycle_actions.safe_append_u("TICK", {}); params.append_measure_reset(cycle_actions, measurement_qubits); // Build the start of the circuit, getting a state that's ready to cycle. // In particular, the first cycle has different detectors and so has to be handled special. Circuit head; params.append_reset(head, all_qubits); head += cycle_actions; for (uint32_t k = 0; k < m; k++) { head.safe_append_u("DETECTOR", {(m - k) | TARGET_RECORD_BIT}, {(double)2 * k + 1, 0}); } // Build the repeated body of the circuit, including the detectors comparing to previous cycles. Circuit body = cycle_actions; body.safe_append_u("SHIFT_COORDS", {}, {0, 1}); for (uint32_t k = 0; k < m; k++) { body.safe_append_u( "DETECTOR", {(m - k) | TARGET_RECORD_BIT, (2 * m - k) | TARGET_RECORD_BIT}, {(double)2 * k + 1, 0}); } // Build the end of the circuit, getting out of the cycle state and terminating. // In particular, the data measurements create detectors that have to be handled special. // Also, the tail is responsible for identifying the logical observable. Circuit tail; params.append_measure(tail, data_qubits); for (uint32_t k = 0; k < m; k++) { tail.safe_append_u( "DETECTOR", {(m - k) | TARGET_RECORD_BIT, (m - k + 1) | TARGET_RECORD_BIT, (2 * m - k + 1) | TARGET_RECORD_BIT}, {(double)2 * k + 1, 1}); } tail.safe_append_ua("OBSERVABLE_INCLUDE", {1 | TARGET_RECORD_BIT}, 0); // Combine to form final circuit. Circuit full_circuit = head + body * (params.rounds - 1) + tail; // Produce a 2d layout. std::map, std::pair> layout; for (uint32_t k = 0; k < n; k++) { layout[{k, 0}] = {"dZ"[k & 1], k}; } layout[{0, 0}].first = 'L'; return { full_circuit, layout, "# Legend:\n" "# d# = data qubit\n" "# L# = data qubit with logical observable crossing\n" "# Z# = measurement qubit\n"}; } ================================================ FILE: src/stim/gen/gen_rep_code.h ================================================ #ifndef _STIM_GEN_GEN_REP_CODE_H #define _STIM_GEN_GEN_REP_CODE_H #include "stim/circuit/circuit.h" #include "stim/gen/circuit_gen_params.h" namespace stim { GeneratedCircuit generate_rep_code_circuit(const CircuitGenParameters ¶ms); } #endif ================================================ FILE: src/stim/gen/gen_rep_code.test.cc ================================================ // 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. #include "stim/gen/gen_rep_code.h" #include "gtest/gtest.h" using namespace stim; TEST(gen_rep_code, rep_code) { CircuitGenParameters params(5000, 4, "memory"); params.after_clifford_depolarization = 0.125; params.after_reset_flip_probability = 0.25; params.before_measure_flip_probability = 0.375; params.before_round_data_depolarization = 0.0625; auto out = generate_rep_code_circuit(params); ASSERT_EQ(out.layout_str(), R"LAYOUT(# L0 Z1 d2 Z3 d4 Z5 d6 )LAYOUT"); ASSERT_EQ( out.circuit.str(), Circuit(R"CIRCUIT( R 0 1 2 3 4 5 6 X_ERROR(0.25) 0 1 2 3 4 5 6 TICK DEPOLARIZE1(0.0625) 0 2 4 6 CX 0 1 2 3 4 5 DEPOLARIZE2(0.125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.125) 2 1 4 3 6 5 TICK X_ERROR(0.375) 1 3 5 MR 1 3 5 X_ERROR(0.25) 1 3 5 DETECTOR(1, 0) rec[-3] DETECTOR(3, 0) rec[-2] DETECTOR(5, 0) rec[-1] REPEAT 4999 { TICK DEPOLARIZE1(0.0625) 0 2 4 6 CX 0 1 2 3 4 5 DEPOLARIZE2(0.125) 0 1 2 3 4 5 TICK CX 2 1 4 3 6 5 DEPOLARIZE2(0.125) 2 1 4 3 6 5 TICK X_ERROR(0.375) 1 3 5 MR 1 3 5 X_ERROR(0.25) 1 3 5 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-3] rec[-6] DETECTOR(3, 0) rec[-2] rec[-5] DETECTOR(5, 0) rec[-1] rec[-4] } X_ERROR(0.375) 0 2 4 6 M 0 2 4 6 DETECTOR(1, 1) rec[-3] rec[-4] rec[-7] DETECTOR(3, 1) rec[-2] rec[-3] rec[-6] DETECTOR(5, 1) rec[-1] rec[-2] rec[-5] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT") .str()); } ================================================ FILE: src/stim/gen/gen_surface_code.cc ================================================ #include "stim/gen/gen_surface_code.h" #include #include #include #include #include #include using namespace stim; struct surface_coord { float x; float y; surface_coord operator+(surface_coord other) const { return {x + other.x, y + other.y}; } surface_coord operator-(surface_coord other) const { return {x - other.x, y - other.y}; } bool operator==(surface_coord other) const { return x == other.x && y == other.y; } bool operator<(surface_coord other) const { if (x != other.x) { return x < other.x; } return y < other.y; } }; GeneratedCircuit _finish_surface_code_circuit( std::function coord_to_index, const std::set &data_coords, const std::set &x_measure_coords, const std::set &z_measure_coords, const CircuitGenParameters ¶ms, const std::vector &x_order, const std::vector &z_order, const std::vector x_observable, const std::vector z_observable, bool is_memory_x) { if (params.rounds < 1) { throw std::invalid_argument("Need rounds >= 1."); } if (params.distance < 2) { throw std::invalid_argument("Need a distance >= 2."); } const auto &chosen_basis_observable = is_memory_x ? x_observable : z_observable; const auto &chosen_basis_measure_coords = is_memory_x ? x_measure_coords : z_measure_coords; // Index the measurement qubits and data qubits. std::map p2q; for (auto q : data_coords) { p2q[q] = coord_to_index(q); } for (auto q : x_measure_coords) { p2q[q] = coord_to_index(q); } for (auto q : z_measure_coords) { p2q[q] = coord_to_index(q); } // Reverse index. std::map q2p; for (const auto &kv : p2q) { q2p[kv.second] = kv.first; } // Make target lists for various types of qubits. std::vector data_qubits; std::vector measurement_qubits; std::vector x_measurement_qubits; std::vector all_qubits; for (auto q : data_coords) { data_qubits.push_back(p2q[q]); } for (auto q : x_measure_coords) { measurement_qubits.push_back(p2q[q]); x_measurement_qubits.push_back(p2q[q]); } for (auto q : z_measure_coords) { measurement_qubits.push_back(p2q[q]); } all_qubits.insert(all_qubits.end(), data_qubits.begin(), data_qubits.end()); all_qubits.insert(all_qubits.end(), measurement_qubits.begin(), measurement_qubits.end()); std::sort(all_qubits.begin(), all_qubits.end()); std::sort(data_qubits.begin(), data_qubits.end()); std::sort(measurement_qubits.begin(), measurement_qubits.end()); std::sort(x_measurement_qubits.begin(), x_measurement_qubits.end()); // Reverse index the measurement order used for defining detectors. std::map data_coord_to_order; std::map measure_coord_to_order; for (auto q : data_qubits) { auto i = data_coord_to_order.size(); data_coord_to_order[q2p[q]] = i; } for (auto q : measurement_qubits) { auto i = measure_coord_to_order.size(); measure_coord_to_order[q2p[q]] = i; } // List out CNOT gate targets using given interaction orders. std::array, 4> cnot_targets; for (size_t k = 0; k < 4; k++) { for (auto measure : x_measure_coords) { auto data = measure + x_order[k]; if (p2q.find(data) != p2q.end()) { cnot_targets[k].push_back(p2q[measure]); cnot_targets[k].push_back(p2q[data]); } } for (auto measure : z_measure_coords) { auto data = measure + z_order[k]; if (p2q.find(data) != p2q.end()) { cnot_targets[k].push_back(p2q[data]); cnot_targets[k].push_back(p2q[measure]); } } } // Build the repeated actions that make up the surface code cycle. Circuit cycle_actions; params.append_begin_round_tick(cycle_actions, data_qubits); params.append_unitary_1(cycle_actions, "H", x_measurement_qubits); for (const auto &targets : cnot_targets) { cycle_actions.safe_append_u("TICK", {}); params.append_unitary_2(cycle_actions, "CNOT", targets); } cycle_actions.safe_append_u("TICK", {}); params.append_unitary_1(cycle_actions, "H", x_measurement_qubits); cycle_actions.safe_append_u("TICK", {}); params.append_measure_reset(cycle_actions, measurement_qubits); // Build the start of the circuit, getting a state that's ready to cycle. // In particular, the first cycle has different detectors and so has to be handled special. Circuit head; for (const auto &kv : q2p) { head.safe_append_u("QUBIT_COORDS", {kv.first}, {kv.second.x, kv.second.y}); } params.append_reset(head, data_qubits, "ZX"[is_memory_x]); params.append_reset(head, measurement_qubits); head += cycle_actions; for (auto measure : chosen_basis_measure_coords) { head.safe_append_u( "DETECTOR", {(uint32_t)(measurement_qubits.size() - measure_coord_to_order[measure]) | TARGET_RECORD_BIT}, {measure.x, measure.y, 0}); } // Build the repeated body of the circuit, including the detectors comparing to previous cycles. Circuit body = cycle_actions; uint32_t m = measurement_qubits.size(); body.safe_append_u("SHIFT_COORDS", {}, {0, 0, 1}); for (auto m_index : measurement_qubits) { auto m_coord = q2p[m_index]; auto k = (uint32_t)measurement_qubits.size() - measure_coord_to_order[m_coord] - 1; body.safe_append_u( "DETECTOR", {(k + 1) | TARGET_RECORD_BIT, (k + 1 + m) | TARGET_RECORD_BIT}, {m_coord.x, m_coord.y, 0}); } // Build the end of the circuit, getting out of the cycle state and terminating. // In particular, the data measurements create detectors that have to be handled special. // Also, the tail is responsible for identifying the logical observable. Circuit tail; params.append_measure(tail, data_qubits, "ZX"[is_memory_x]); // Detectors. for (auto measure : chosen_basis_measure_coords) { std::vector detectors; for (auto delta : z_order) { auto data = measure + delta; if (p2q.find(data) != p2q.end()) { detectors.push_back((data_qubits.size() - data_coord_to_order[data]) | TARGET_RECORD_BIT); } } detectors.push_back( (data_qubits.size() + measurement_qubits.size() - measure_coord_to_order[measure]) | TARGET_RECORD_BIT); std::sort(detectors.begin(), detectors.end()); tail.safe_append_u("DETECTOR", detectors, {measure.x, measure.y, 1}); } // Logical observable. std::vector obs_inc; for (auto q : chosen_basis_observable) { obs_inc.push_back((data_qubits.size() - data_coord_to_order[q]) | TARGET_RECORD_BIT); } std::sort(obs_inc.begin(), obs_inc.end()); tail.safe_append_ua("OBSERVABLE_INCLUDE", obs_inc, 0); // Combine to form final circuit. Circuit full_circuit = head + body * (params.rounds - 1) + tail; // Produce a 2d layout. std::map, std::pair> layout; float scale = x_order[0].x == 0.5 ? 2 : 1; for (auto q : data_coords) { layout[{(uint32_t)(q.x * scale), (uint32_t)(q.y * scale)}] = {'d', p2q[q]}; } for (auto q : x_measure_coords) { layout[{(uint32_t)(q.x * scale), (uint32_t)(q.y * scale)}] = {'X', p2q[q]}; } for (auto q : z_measure_coords) { layout[{(uint32_t)(q.x * scale), (uint32_t)(q.y * scale)}] = {'Z', p2q[q]}; } for (auto q : chosen_basis_observable) { layout[{(uint32_t)(q.x * scale), (uint32_t)(q.y * scale)}].first = 'L'; } return { full_circuit, layout, "# Legend:\n" "# d# = data qubit\n" "# L# = data qubit with logical observable crossing\n" "# X# = measurement qubit (X stabilizer)\n" "# Z# = measurement qubit (Z stabilizer)\n"}; } GeneratedCircuit _generate_rotated_surface_code_circuit(const CircuitGenParameters ¶ms, bool is_memory_x) { uint32_t d = params.distance; // Place data qubits. std::set data_coords; std::vector x_observable; std::vector z_observable; for (float x = 0.5; x <= d; x++) { for (float y = 0.5; y <= d; y++) { surface_coord q{x * 2, y * 2}; data_coords.insert(q); if (y == 0.5) { z_observable.push_back(q); } if (x == 0.5) { x_observable.push_back(q); } } } // Place measurement qubits. std::set x_measure_coords; std::set z_measure_coords; for (size_t x = 0; x <= d; x++) { for (size_t y = 0; y <= d; y++) { surface_coord q{(float)x * 2, (float)y * 2}; bool on_boundary_1 = x == 0 || x == d; bool on_boundary_2 = y == 0 || y == d; bool parity = x % 2 != y % 2; if (on_boundary_1 && parity) { continue; } if (on_boundary_2 && !parity) { continue; } if (parity) { x_measure_coords.insert(q); } else { z_measure_coords.insert(q); } } } // Define interaction orders so that hook errors run against the error grain instead of with it. std::vector z_order{ {1, 1}, {1, -1}, {-1, 1}, {-1, -1}, }; std::vector x_order{ {1, 1}, {-1, 1}, {1, -1}, {-1, -1}, }; // Delegate. return _finish_surface_code_circuit( [&](surface_coord q) { q = q - surface_coord{0, fmodf(q.x, 2)}; return (uint32_t)(q.x + q.y * (d + 0.5)); }, data_coords, x_measure_coords, z_measure_coords, params, x_order, z_order, x_observable, z_observable, is_memory_x); } GeneratedCircuit _generate_unrotated_surface_code_circuit(const CircuitGenParameters ¶ms, bool is_memory_x) { uint32_t d = params.distance; assert(params.rounds > 0); // Place qubits. std::set data_coords; std::set x_measure_coords; std::set z_measure_coords; std::vector x_observable; std::vector z_observable; for (size_t x = 0; x < 2 * d - 1; x++) { for (size_t y = 0; y < 2 * d - 1; y++) { surface_coord q{(float)x, (float)y}; bool parity = x % 2 != y % 2; if (parity) { if (x % 2 == 0) { z_measure_coords.insert(q); } else { x_measure_coords.insert(q); } } else { data_coords.insert(q); if (x == 0) { x_observable.push_back(q); } if (y == 0) { z_observable.push_back(q); } } } } // Define interaction order. Doesn't matter so much for unrotated. std::vector order{ {1, 0}, {0, 1}, {0, -1}, {-1, 0}, }; // Delegate. return _finish_surface_code_circuit( [&](surface_coord q) { return (uint32_t)(q.x + q.y * (2 * d - 1)); }, data_coords, x_measure_coords, z_measure_coords, params, order, order, x_observable, z_observable, is_memory_x); } GeneratedCircuit stim::generate_surface_code_circuit(const CircuitGenParameters ¶ms) { if (params.task == "rotated_memory_x") { return _generate_rotated_surface_code_circuit(params, true); } else if (params.task == "rotated_memory_z") { return _generate_rotated_surface_code_circuit(params, false); } else if (params.task == "unrotated_memory_x") { return _generate_unrotated_surface_code_circuit(params, true); } else if (params.task == "unrotated_memory_z") { return _generate_unrotated_surface_code_circuit(params, false); } else { throw std::invalid_argument( "Unrecognized task '" + params.task + "'. Known surface_code tasks:\n" " 'rotated_memory_x': Initialize logical |+> in rotated code, protect with parity measurements, measure " "logical X.\n" " 'rotated_memory_z': Initialize logical |0> in rotated code, protect with parity measurements, measure " "logical Z.\n" " 'unrotated_memory_x': Initialize logical |+> in unrotated code, protect with parity measurements, " "measure logical X.\n" " 'unrotated_memory_z': Initialize logical |0> in unrotated code, protect with parity measurements, " "measure logical Z.\n" ""); } } ================================================ FILE: src/stim/gen/gen_surface_code.h ================================================ #ifndef _STIM_GEN_GEN_SURFACE_CODE_H #define _STIM_GEN_GEN_SURFACE_CODE_H #include "stim/circuit/circuit.h" #include "stim/gen/circuit_gen_params.h" namespace stim { GeneratedCircuit generate_surface_code_circuit(const CircuitGenParameters ¶ms); } #endif ================================================ FILE: src/stim/gen/gen_surface_code.test.cc ================================================ // 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. #include "stim/gen/gen_surface_code.h" #include "gtest/gtest.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(gen_surface_code, unrotated_surface_code_hard_coded_comparison) { CircuitGenParameters params(5, 2, "unrotated_memory_z"); params.after_clifford_depolarization = 0.125; params.after_reset_flip_probability = 0.25; params.before_measure_flip_probability = 0.375; params.before_round_data_depolarization = 0.0625; auto out = generate_surface_code_circuit(params); ASSERT_EQ( out.layout_str(), "" "# d6 X7 d8\n" "# Z3 d4 Z5\n" "# L0 X1 L2\n"); ASSERT_EQ( out.circuit.str(), Circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(0, 1) 3 QUBIT_COORDS(1, 1) 4 QUBIT_COORDS(2, 1) 5 QUBIT_COORDS(0, 2) 6 QUBIT_COORDS(1, 2) 7 QUBIT_COORDS(2, 2) 8 R 0 2 4 6 8 X_ERROR(0.25) 0 2 4 6 8 R 1 3 5 7 X_ERROR(0.25) 1 3 5 7 TICK DEPOLARIZE1(0.0625) 0 2 4 6 8 H 1 7 DEPOLARIZE1(0.125) 1 7 TICK CX 1 2 7 8 4 3 DEPOLARIZE2(0.125) 1 2 7 8 4 3 TICK CX 1 4 6 3 8 5 DEPOLARIZE2(0.125) 1 4 6 3 8 5 TICK CX 7 4 0 3 2 5 DEPOLARIZE2(0.125) 7 4 0 3 2 5 TICK CX 1 0 7 6 4 5 DEPOLARIZE2(0.125) 1 0 7 6 4 5 TICK H 1 7 DEPOLARIZE1(0.125) 1 7 TICK X_ERROR(0.375) 1 3 5 7 MR 1 3 5 7 X_ERROR(0.25) 1 3 5 7 DETECTOR(0, 1, 0) rec[-3] DETECTOR(2, 1, 0) rec[-2] REPEAT 4 { TICK DEPOLARIZE1(0.0625) 0 2 4 6 8 H 1 7 DEPOLARIZE1(0.125) 1 7 TICK CX 1 2 7 8 4 3 DEPOLARIZE2(0.125) 1 2 7 8 4 3 TICK CX 1 4 6 3 8 5 DEPOLARIZE2(0.125) 1 4 6 3 8 5 TICK CX 7 4 0 3 2 5 DEPOLARIZE2(0.125) 7 4 0 3 2 5 TICK CX 1 0 7 6 4 5 DEPOLARIZE2(0.125) 1 0 7 6 4 5 TICK H 1 7 DEPOLARIZE1(0.125) 1 7 TICK X_ERROR(0.375) 1 3 5 7 MR 1 3 5 7 X_ERROR(0.25) 1 3 5 7 SHIFT_COORDS(0, 0, 1) DETECTOR(1, 0, 0) rec[-4] rec[-8] DETECTOR(0, 1, 0) rec[-3] rec[-7] DETECTOR(2, 1, 0) rec[-2] rec[-6] DETECTOR(1, 2, 0) rec[-1] rec[-5] } X_ERROR(0.375) 0 2 4 6 8 M 0 2 4 6 8 DETECTOR(0, 1, 1) rec[-2] rec[-3] rec[-5] rec[-8] DETECTOR(2, 1, 1) rec[-1] rec[-3] rec[-4] rec[-7] OBSERVABLE_INCLUDE(0) rec[-4] rec[-5] )CIRCUIT") .str()); params.rounds = 1; params.task = "unrotated_memory_x"; auto out2 = generate_surface_code_circuit(params); ASSERT_EQ( out2.layout_str(), "" "# L6 X7 d8\n" "# Z3 d4 Z5\n" "# L0 X1 d2\n"); ASSERT_EQ( out2.circuit.str(), Circuit(R"CIRCUIT( QUBIT_COORDS(0, 0) 0 QUBIT_COORDS(1, 0) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(0, 1) 3 QUBIT_COORDS(1, 1) 4 QUBIT_COORDS(2, 1) 5 QUBIT_COORDS(0, 2) 6 QUBIT_COORDS(1, 2) 7 QUBIT_COORDS(2, 2) 8 RX 0 2 4 6 8 Z_ERROR(0.25) 0 2 4 6 8 R 1 3 5 7 X_ERROR(0.25) 1 3 5 7 TICK DEPOLARIZE1(0.0625) 0 2 4 6 8 H 1 7 DEPOLARIZE1(0.125) 1 7 TICK CX 1 2 7 8 4 3 DEPOLARIZE2(0.125) 1 2 7 8 4 3 TICK CX 1 4 6 3 8 5 DEPOLARIZE2(0.125) 1 4 6 3 8 5 TICK CX 7 4 0 3 2 5 DEPOLARIZE2(0.125) 7 4 0 3 2 5 TICK CX 1 0 7 6 4 5 DEPOLARIZE2(0.125) 1 0 7 6 4 5 TICK H 1 7 DEPOLARIZE1(0.125) 1 7 TICK X_ERROR(0.375) 1 3 5 7 MR 1 3 5 7 X_ERROR(0.25) 1 3 5 7 DETECTOR(1, 0, 0) rec[-4] DETECTOR(1, 2, 0) rec[-1] Z_ERROR(0.375) 0 2 4 6 8 MX 0 2 4 6 8 DETECTOR(1, 0, 1) rec[-3] rec[-4] rec[-5] rec[-9] DETECTOR(1, 2, 1) rec[-1] rec[-2] rec[-3] rec[-6] OBSERVABLE_INCLUDE(0) rec[-2] rec[-5] )CIRCUIT") .str()); } TEST(gen_surface_code, rotated_surface_code_hard_coded_comparison) { CircuitGenParameters params(5, 2, "rotated_memory_z"); params.after_clifford_depolarization = 0.125; params.after_reset_flip_probability = 0.25; params.before_measure_flip_probability = 0.375; params.before_round_data_depolarization = 0.0625; auto out = generate_surface_code_circuit(params); ASSERT_EQ( out.layout_str(), "" "# X12\n" "# d6 d8 \n" "# Z7 \n" "# L1 L3 \n" "# X2 \n"); ASSERT_EQ( out.circuit.str(), Circuit(R"CIRCUIT( QUBIT_COORDS(1, 1) 1 QUBIT_COORDS(2, 0) 2 QUBIT_COORDS(3, 1) 3 QUBIT_COORDS(1, 3) 6 QUBIT_COORDS(2, 2) 7 QUBIT_COORDS(3, 3) 8 QUBIT_COORDS(2, 4) 12 R 1 3 6 8 X_ERROR(0.25) 1 3 6 8 R 2 7 12 X_ERROR(0.25) 2 7 12 TICK DEPOLARIZE1(0.0625) 1 3 6 8 H 2 12 DEPOLARIZE1(0.125) 2 12 TICK CX 2 3 8 7 DEPOLARIZE2(0.125) 2 3 8 7 TICK CX 2 1 3 7 DEPOLARIZE2(0.125) 2 1 3 7 TICK CX 12 8 6 7 DEPOLARIZE2(0.125) 12 8 6 7 TICK CX 12 6 1 7 DEPOLARIZE2(0.125) 12 6 1 7 TICK H 2 12 DEPOLARIZE1(0.125) 2 12 TICK X_ERROR(0.375) 2 7 12 MR 2 7 12 X_ERROR(0.25) 2 7 12 DETECTOR(2, 2, 0) rec[-2] REPEAT 4 { TICK DEPOLARIZE1(0.0625) 1 3 6 8 H 2 12 DEPOLARIZE1(0.125) 2 12 TICK CX 2 3 8 7 DEPOLARIZE2(0.125) 2 3 8 7 TICK CX 2 1 3 7 DEPOLARIZE2(0.125) 2 1 3 7 TICK CX 12 8 6 7 DEPOLARIZE2(0.125) 12 8 6 7 TICK CX 12 6 1 7 DEPOLARIZE2(0.125) 12 6 1 7 TICK H 2 12 DEPOLARIZE1(0.125) 2 12 TICK X_ERROR(0.375) 2 7 12 MR 2 7 12 X_ERROR(0.25) 2 7 12 SHIFT_COORDS(0, 0, 1) DETECTOR(2, 0, 0) rec[-3] rec[-6] DETECTOR(2, 2, 0) rec[-2] rec[-5] DETECTOR(2, 4, 0) rec[-1] rec[-4] } X_ERROR(0.375) 1 3 6 8 M 1 3 6 8 DETECTOR(2, 2, 1) rec[-1] rec[-2] rec[-3] rec[-4] rec[-6] OBSERVABLE_INCLUDE(0) rec[-3] rec[-4] )CIRCUIT") .str()); params.distance = 4; out = generate_surface_code_circuit(params); ASSERT_EQ( out.layout_str(), "" "# X38 X42\n" "# d28 d30 d32 d34\n" "# Z29 X31 Z33\n" "# d19 d21 d23 d25\n" "# Z18 X20 Z22 X24 Z26\n" "# d10 d12 d14 d16\n" "# Z11 X13 Z15\n" "# L1 L3 L5 L7 \n" "# X2 X6 \n"); params.distance = 5; out = generate_surface_code_circuit(params); ASSERT_EQ( out.layout_str(), "" "# X59 X63\n" "# d45 d47 d49 d51 d53\n" "# Z44 X46 Z48 X50 Z52\n" "# d34 d36 d38 d40 d42\n" "# Z35 X37 Z39 X41 Z43\n" "# d23 d25 d27 d29 d31\n" "# Z22 X24 Z26 X28 Z30\n" "# d12 d14 d16 d18 d20\n" "# Z13 X15 Z17 X19 Z21\n" "# L1 L3 L5 L7 L9 \n" "# X2 X6 \n"); } ================================================ FILE: src/stim/io/README.md ================================================ # `io` directory This directory contains types and functions responsible for reading and writing data in a variety of file formats supported by stim. ================================================ FILE: src/stim/io/measure_record.cc ================================================ /* * 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. */ #include "stim/io/measure_record.h" #include #include "stim/io/measure_record_writer.h" using namespace stim; MeasureRecord::MeasureRecord(size_t max_lookback) : max_lookback(max_lookback), unwritten(0) { } void MeasureRecord::write_unwritten_results_to(MeasureRecordWriter &writer) { size_t n = storage.size(); for (size_t k = n - unwritten; k < n; k++) { writer.write_bit(storage[k]); } unwritten = 0; if ((storage.size() >> 1) > max_lookback) { storage.erase(storage.begin(), storage.end() - max_lookback); } } bool MeasureRecord::lookback(size_t lookback) const { if (lookback > storage.size()) { throw std::out_of_range("Referred to a measurement record before the beginning of time."); } if (lookback == 0) { throw std::out_of_range("Lookback must be non-zero."); } if (lookback > max_lookback) { throw std::out_of_range("Referred to a measurement record past the lookback limit."); } return *(storage.end() - lookback); } void MeasureRecord::record_result(bool result) { storage.push_back(result); unwritten++; } void MeasureRecord::record_results(const std::vector &results) { storage.insert(storage.end(), results.begin(), results.end()); unwritten += results.size(); } void MeasureRecord::clear() { unwritten = 0; storage.clear(); } void MeasureRecord::discard_results_past_max_lookback() { if (storage.size() > max_lookback) { storage.erase(storage.begin(), storage.end() - max_lookback); } if (unwritten > max_lookback) { unwritten = max_lookback; } } ================================================ FILE: src/stim/io/measure_record.h ================================================ /* * 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. */ #ifndef _STIM_IO_MEASURE_RECORD_H #define _STIM_IO_MEASURE_RECORD_H #include #include #include "stim/io/measure_record_writer.h" namespace stim { /// Stores a historical record of measurement results that can be looked up and written to the external world. /// /// Results that have been written and are further back than `max_lookback` may be discarded from memory. struct MeasureRecord { /// How far back into the measurement record a circuit being simulated may look. /// Results younger than this cannot be discarded. size_t max_lookback; /// How many results have been recorded but not yet written to the external world. /// Results younger than this cannot be discarded. size_t unwritten; /// The actual recorded results. std::vector storage; /// Creates an empty measurement record. MeasureRecord(size_t max_lookback = SIZE_MAX); /// Forces all unwritten results to be written via the given writer. /// /// After the results are written, older measurements now eligible to be discarded may be removed from memory. void write_unwritten_results_to(MeasureRecordWriter &writer); /// Returns a measurement result from the record. /// /// Args: /// lookback: How far back the measurement is. lookback=1 is the latest measurement, 2 the second latest, etc. bool lookback(size_t lookback) const; /// Batch record. void record_results(const std::vector &results); /// Appends a measurement to the record. void record_result(bool result); /// Clear the record. void clear(); /// Truncates the record to only include bits within the lookback limit. void discard_results_past_max_lookback(); }; } // namespace stim #endif ================================================ FILE: src/stim/io/measure_record.test.cc ================================================ /* * 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. */ #include "stim/io/measure_record.h" #include "gtest/gtest.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(MeasureRecord, basic_usage) { MeasureRecord r(20); r.record_result(true); ASSERT_EQ(r.lookback(1), true); r.record_result(false); ASSERT_EQ(r.lookback(1), false); ASSERT_EQ(r.lookback(2), true); for (size_t k = 0; k < 50; k++) { r.record_result(true); r.record_result(false); } ASSERT_EQ(r.storage.size(), 102); FILE *tmp = tmpfile(); r.write_unwritten_results_to(*MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_01)); rewind(tmp); for (size_t k = 0; k < 102; k++) { ASSERT_EQ(getc(tmp), '0' + (~k & 1)); } ASSERT_EQ(getc(tmp), EOF); ASSERT_LE(r.storage.size(), 40); } ================================================ FILE: src/stim/io/measure_record_batch.h ================================================ /* * 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. */ #ifndef _STIM_IO_MEASURE_RECORD_BATCH_H #define _STIM_IO_MEASURE_RECORD_BATCH_H #include "stim/circuit/circuit_instruction.h" #include "stim/io/measure_record_batch_writer.h" namespace stim { /// Stores a record of multiple measurement streams that can be looked up and written to the external world. /// /// Results that have been written and are further back than `max_lookback` may be discarded from memory. /// /// The template parameter, W, represents the SIMD width. template struct MeasureRecordBatch { size_t num_shots; /// How far back into the measurement record a circuit being simulated may look. /// Results younger than this cannot be discarded. size_t max_lookback; /// How many results have been recorded but not yet written to the external world. /// Results younger than this cannot be discarded. size_t unwritten; /// How many results are currently stored (from each separate stream). size_t stored; /// How many results have been written to the external world. size_t written; /// For performance reasons, measurement data given to store may include non-zero values past the data corresponding /// to the number of expected shots. AND-ing the data with this mask fixes the problem. simd_bits shot_mask; /// The 2-dimensional block of bits storing the measurement results from each separate measurement stream. /// Major index is measurement index, minor index is shot index. simd_bit_table storage; /// Constructs an empty MeasureRecordBatch configured for the given max_lookback and number of shots. MeasureRecordBatch(size_t num_shots, size_t max_lookback); /// Allows measurements older than max_lookback to be discarded, even though they weren't written out. /// /// E.g. this is used during detection event sampling, when what is written is derived detection events. void mark_all_as_written(); /// Hints that measurements can be written to the given writer. /// /// For performance reasons, they may not be written until a large enough block has been accumulated. void intermediate_write_unwritten_results_to(MeasureRecordBatchWriter &writer, simd_bits_range_ref ref_sample); /// Forces measurements to be written to the given writer, and to tell the writer the measurements are ending. void final_write_unwritten_results_to(MeasureRecordBatchWriter &writer, simd_bits_range_ref ref_sample); /// Looks up a historical batch measurement. /// /// Returns: /// A reference into the storage table, with the bit at offset k corresponding to the measurement from stream k. simd_bits_range_ref lookback(size_t lookback) const; /// Writes a zero'd result into the record and returns a reference to it to edit. simd_bits_range_ref record_zero_result_to_edit(); /// Xors a batch measurement result into pre-reserved noisy storage. void xor_record_reserved_result(simd_bits_range_ref result); /// Appends a batch measurement result into storage. void record_result(simd_bits_range_ref result); /// Reserves space for storing measurement results. Initializes bits to be noisy with the given probability. void reserve_noisy_space_for_results(const CircuitInstruction &inst, std::mt19937_64 &rng); /// Ensures there is enough space for storing a number of measurement results, without moving memory. void reserve_space_for_results(size_t count); /// Resets the record to an empty state. void clear(); void destructive_resize(size_t new_num_shots, size_t new_max_lookback); }; } // namespace stim #include "stim/io/measure_record_batch.inl" #endif ================================================ FILE: src/stim/io/measure_record_batch.inl ================================================ /* * 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. */ #include #include "stim/io/measure_record_batch.h" #include "stim/io/measure_record_batch_writer.h" #include "stim/util_bot/probability_util.h" namespace stim { template MeasureRecordBatch::MeasureRecordBatch(size_t num_shots, size_t max_lookback) : num_shots(num_shots), max_lookback(max_lookback), unwritten(0), stored(0), written(0), shot_mask(num_shots), storage(1, num_shots) { for (size_t k = 0; k < num_shots; k++) { shot_mask[k] = true; } } template void MeasureRecordBatch::reserve_space_for_results(size_t count) { if (stored + count > storage.num_major_bits_padded()) { simd_bit_table new_storage((stored + count) * 2, storage.num_minor_bits_padded()); new_storage.data.word_range_ref(0, storage.data.num_simd_words) = storage.data; storage = std::move(new_storage); } } template void MeasureRecordBatch::reserve_noisy_space_for_results(const CircuitInstruction &inst, std::mt19937_64 &rng) { size_t count = inst.targets.size(); reserve_space_for_results(count); float p = inst.args.empty() ? 0 : inst.args[0]; biased_randomize_bits(p, storage[stored].u64, storage[stored + count].u64, rng); } template void MeasureRecordBatch::xor_record_reserved_result(simd_bits_range_ref result) { storage[stored] ^= result; storage[stored] &= shot_mask; stored++; unwritten++; } template void MeasureRecordBatch::record_result(simd_bits_range_ref result) { reserve_space_for_results(1); storage[stored] = result; storage[stored] &= shot_mask; stored++; unwritten++; } template simd_bits_range_ref MeasureRecordBatch::record_zero_result_to_edit() { reserve_space_for_results(1); storage[stored].clear(); stored++; unwritten++; return storage[stored - 1]; } template simd_bits_range_ref MeasureRecordBatch::lookback(size_t lookback) const { if (lookback > stored) { throw std::out_of_range("Referred to a measurement record before the beginning of time."); } if (lookback == 0) { throw std::out_of_range("Lookback must be non-zero."); } if (lookback > max_lookback) { throw std::out_of_range("Referred to a measurement record past the lookback limit."); } return storage[stored - lookback]; } template void MeasureRecordBatch::mark_all_as_written() { unwritten = 0; size_t m = max_lookback; if ((stored >> 1) > m) { memcpy(storage.data.u8, storage[stored - m].u8, m * storage.num_minor_u8_padded()); stored = m; } } template void MeasureRecordBatch::intermediate_write_unwritten_results_to( MeasureRecordBatchWriter &writer, simd_bits_range_ref ref_sample) { constexpr size_t WRITE_SIZE = 256; while (unwritten >= WRITE_SIZE) { auto slice = storage.slice_maj(stored - unwritten, stored - unwritten + WRITE_SIZE); for (size_t k = 0; k < WRITE_SIZE; k++) { size_t j = written + k; if (j < ref_sample.num_bits_padded() && ref_sample[j]) { slice[k] ^= shot_mask; } } writer.batch_write_bytes(slice, WRITE_SIZE >> 6); unwritten -= WRITE_SIZE; written += WRITE_SIZE; } size_t m = std::max(max_lookback, unwritten); if ((stored >> 1) > m) { memcpy(storage.data.u8, storage[stored - m].u8, m * storage.num_minor_u8_padded()); stored = m; } } template void MeasureRecordBatch::final_write_unwritten_results_to( MeasureRecordBatchWriter &writer, simd_bits_range_ref ref_sample) { size_t n = stored; for (size_t k = n - unwritten; k < n; k++) { bool invert = written < ref_sample.num_bits_padded() && ref_sample[written]; if (invert) { storage[k] ^= shot_mask; } writer.batch_write_bit(storage[k]); if (invert) { storage[k] ^= shot_mask; } written++; } unwritten = 0; writer.write_end(); } template void MeasureRecordBatch::clear() { stored = 0; unwritten = 0; } template void MeasureRecordBatch::destructive_resize(size_t new_num_shots, size_t new_max_lookback) { unwritten = 0; stored = 0; written = 0; max_lookback = new_max_lookback; if (new_num_shots != num_shots) { num_shots = new_num_shots; shot_mask = simd_bits(num_shots); for (size_t k = 0; k < num_shots; k++) { shot_mask[k] = true; } storage.destructive_resize(1, num_shots); } } } // namespace stim ================================================ FILE: src/stim/io/measure_record_batch.test.cc ================================================ /* * 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. */ #include "stim/io/measure_record_batch.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(MeasureRecordBatch, basic_usage, { simd_bits s0(5); simd_bits s1(5); s0[0] = true; s1[1] = true; s0[2] = true; s1[3] = true; s0[4] = true; MeasureRecordBatch r(5, 20); ASSERT_EQ(r.stored, 0); r.record_result(s0); ASSERT_EQ(r.stored, 1); ASSERT_EQ(r.lookback(1), s0); r.record_result(s1); ASSERT_EQ(r.stored, 2); ASSERT_EQ(r.lookback(1), s1); ASSERT_EQ(r.lookback(2), s0); for (size_t k = 0; k < 50; k++) { r.record_result(s0); r.record_result(s1); } ASSERT_EQ(r.unwritten, 102); ASSERT_EQ(r.stored, 102); FILE *tmp = tmpfile(); MeasureRecordBatchWriter w(tmp, 5, SampleFormat::SAMPLE_FORMAT_01); r.intermediate_write_unwritten_results_to(w, simd_bits(0)); ASSERT_EQ(r.unwritten, 102); for (size_t k = 0; k < 500; k++) { r.record_result(s0); r.record_result(s1); } ASSERT_EQ(r.unwritten, 1102); ASSERT_EQ(r.stored, 1102); r.intermediate_write_unwritten_results_to(w, simd_bits(0)); ASSERT_LT(r.unwritten, 100); ASSERT_LT(r.stored, 100); r.final_write_unwritten_results_to(w, simd_bits(0)); ASSERT_EQ(r.unwritten, 0); ASSERT_LT(r.stored, 100); rewind(tmp); for (size_t s = 0; s < 5; s++) { simd_bits sk = (s & 1) ? s1 : s0; for (size_t k = 0; k < 1102; k++) { ASSERT_EQ(getc(tmp), '0' + ((s + k + 1) & 1)); } ASSERT_EQ(getc(tmp), '\n'); } ASSERT_EQ(getc(tmp), EOF); }) TEST_EACH_WORD_SIZE_W(MeasureRecordBatch, record_zero_result, { MeasureRecordBatch r(5, 2); ASSERT_EQ(r.stored, 0); auto v = r.record_zero_result_to_edit(); v[2] = 1; ASSERT_EQ(r.stored, 1); ASSERT_EQ(r.storage[0][1], 0); ASSERT_EQ(r.storage[0][2], 1); ASSERT_EQ(r.storage[0][3], 0); ASSERT_EQ(r.lookback(1)[1], 0); ASSERT_EQ(r.lookback(1)[2], 1); r.record_zero_result_to_edit()[3] = 4; ASSERT_EQ(r.storage[0][1], 0); ASSERT_EQ(r.storage[0][2], 1); ASSERT_EQ(r.storage[0][3], 0); ASSERT_EQ(r.storage[1][1], 0); ASSERT_EQ(r.storage[1][2], 0); ASSERT_EQ(r.storage[1][3], 1); }) ================================================ FILE: src/stim/io/measure_record_batch_writer.cc ================================================ /* * 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. */ #include "stim/io/measure_record_batch_writer.h" #include using namespace stim; MeasureRecordBatchWriter::MeasureRecordBatchWriter(FILE *out, size_t num_shots, SampleFormat output_format) : output_format(output_format), out(out) { if (num_shots > 768) { throw std::out_of_range("num_shots > 768 (safety check to ensure staying away from linux file handle limit)"); } if (output_format == SampleFormat::SAMPLE_FORMAT_PTB64 && num_shots % 64 != 0) { throw std::out_of_range("Number of shots must be a multiple of 64 to use output format ptb64."); } auto f = output_format; auto s = num_shots; if (output_format == SampleFormat::SAMPLE_FORMAT_PTB64) { f = SampleFormat::SAMPLE_FORMAT_B8; s += 63; s /= 64; } if (s) { writers.push_back(MeasureRecordWriter::make(out, f)); } for (size_t k = 1; k < s; k++) { FILE *file = tmpfile(); if (file == nullptr) { throw std::out_of_range("Failed to open a temp file."); } writers.push_back(MeasureRecordWriter::make(file, f)); temporary_files.push_back(file); } } MeasureRecordBatchWriter::~MeasureRecordBatchWriter() { for (auto &e : temporary_files) { fclose(e); } temporary_files.clear(); } void MeasureRecordBatchWriter::begin_result_type(char result_type) { for (auto &e : writers) { e->begin_result_type(result_type); } } void MeasureRecordBatchWriter::write_end() { for (auto &writer : writers) { writer->write_end(); } for (FILE *file : temporary_files) { rewind(file); while (true) { int c = getc(file); if (c == EOF) { break; } putc(c, out); } fclose(file); } temporary_files.clear(); } ================================================ FILE: src/stim/io/measure_record_batch_writer.h ================================================ /* * 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. */ #ifndef _STIM_IO_MEASURE_RECORD_BATCH_WRITER_H #define _STIM_IO_MEASURE_RECORD_BATCH_WRITER_H #include "stim/io/measure_record_writer.h" #include "stim/mem/simd_bit_table.h" namespace stim { /// Handles buffering and writing multiple measurement data streams that ultimately need to be concatenated. struct MeasureRecordBatchWriter { SampleFormat output_format; FILE *out; /// Temporary files used to hold data that will eventually be concatenated onto the main stream. std::vector temporary_files; /// The individual writers for each incoming stream of measurement results. /// The first writer will go directly to `out`, whereas the others go into temporary files. std::vector> writers; MeasureRecordBatchWriter(FILE *out, size_t num_shots, SampleFormat output_format); /// Cleans up temporary files. ~MeasureRecordBatchWriter(); /// See MeasureRecordWriter::begin_result_type. void begin_result_type(char result_type); /// Writes a separate measurement result to each MeasureRecordWriter. /// /// Args: /// bits: The measurement results. The bit at offset k is the bit for the writer at offset k. template void batch_write_bit(simd_bits_range_ref bits) { if (output_format == SampleFormat::SAMPLE_FORMAT_PTB64) { uint8_t *p = bits.u8; for (auto &writer : writers) { uint8_t *n = p + 8; writer->write_bytes({p, n}); p = n; } } else { for (size_t k = 0; k < writers.size(); k++) { writers[k]->write_bit(bits[k]); } } } /// Writes multiple separate measurement results to each MeasureRecordWriter. /// /// This method can be called after calling `batch_write_bit`, but for performance reasons it is recommended to /// not do this since it can result in the individual writers doing extra work due to not being on byte boundaries. /// /// Args: /// table: The measurement results. /// The bits at minor offset k, from major offset 0 to major offset 64*num_major_u64, are the bits for the /// writer at offset k. /// num_major_u64: The number of measurement results (divided by 64) for each writer. The actual number of /// results is required to be a multiple of 64 for performance reasons. template void batch_write_bytes(const simd_bit_table &table, size_t num_major_u64) { if (output_format == SampleFormat::SAMPLE_FORMAT_PTB64) { for (size_t k = 0; k < writers.size(); k++) { for (size_t w = 0; w < num_major_u64; w++) { uint8_t *p = table.data.u8 + (k * 8) + table.num_minor_u8_padded() * w; writers[k]->write_bytes({p, p + 8}); } } } else { auto transposed = table.transposed(); for (size_t k = 0; k < writers.size(); k++) { uint8_t *p = transposed[k].u8; writers[k]->write_bytes({p, p + num_major_u64 * 8}); } } } /// Tells each writer to finish up, then concatenates all of their data into the `out` stream and cleans up. void write_end(); }; } // namespace stim #endif ================================================ FILE: src/stim/io/measure_record_batch_writer.test.cc ================================================ /* * 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. */ #include "stim/io/measure_record_batch_writer.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(MeasureRecordBatchWriter, basic_usage, { FILE *tmp = tmpfile(); MeasureRecordBatchWriter w(tmp, 5, SampleFormat::SAMPLE_FORMAT_01); simd_bits v(5); v[1] = true; w.batch_write_bit(v); w.write_end(); rewind(tmp); ASSERT_EQ(getc(tmp), '0'); ASSERT_EQ(getc(tmp), '\n'); ASSERT_EQ(getc(tmp), '1'); ASSERT_EQ(getc(tmp), '\n'); ASSERT_EQ(getc(tmp), '0'); ASSERT_EQ(getc(tmp), '\n'); ASSERT_EQ(getc(tmp), '0'); ASSERT_EQ(getc(tmp), '\n'); ASSERT_EQ(getc(tmp), '0'); ASSERT_EQ(getc(tmp), '\n'); }) ================================================ FILE: src/stim/io/measure_record_reader.h ================================================ /* * 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. */ #ifndef _STIM_IO_MEASURE_RECORD_READER_H #define _STIM_IO_MEASURE_RECORD_READER_H #include #include "stim/io/sparse_shot.h" #include "stim/io/stim_data_formats.h" #include "stim/mem/simd_bit_table.h" #include "stim/mem/span_ref.h" namespace stim { // Returns true if an integer value is found at current position. Returns false otherwise. // Uses two output variables: value to return the integer value read and next for the next // character or EOF. inline bool read_uint64(FILE *in, uint64_t &value, int &next, bool include_next = false) { if (!include_next) { next = getc(in); } if (!isdigit(next)) { return false; } value = 0; while (isdigit(next)) { uint64_t prev_value = value; value *= 10; value += next - '0'; if (value < prev_value) { throw std::runtime_error("Integer value read from file was too big"); } next = getc(in); } return true; } /// Handles reading measurement data from the outside world. /// /// Child classes implement the various input formats. Each file format encodes a certain number of records. /// Each record is a sequence of 0s and 1s. File formats B8 and R8 encode a single record. File formats 01, /// HITS and DETS encode any number of records. Record size in bits is fixed for each file and the client /// must specify it upfront. /// /// The template parameter, W, represents the SIMD width. template struct MeasureRecordReader { size_t num_measurements; size_t num_detectors; size_t num_observables; size_t bits_per_record() const; MeasureRecordReader(size_t num_measurements, size_t num_detectors, size_t num_observables); /// Creates a MeasureRecordReader that reads measurement records in the given format from the given FILE*. /// Record size must be specified upfront. The DETS format supports three different types of records /// and size of each is specified independently. All other formats support one type of record. It is /// an error to specify non-zero size of detection event records or logical observable records unless /// the input format is DETS. static std::unique_ptr> make( FILE *in, SampleFormat input_format, size_t num_measurements, size_t num_detectors = 0, size_t num_observables = 0); virtual ~MeasureRecordReader() = default; /// Determines whether or not there is no actual data written for each shot. /// /// For example, sampling a circuit with no measurements produces no bytes of data /// when using the 'b8' format. /// /// This is important to check for sometimes. For example, instead of getting stuck /// in an infinite loop repeatedly reading zero bytes of data and not reaching the /// end of the file, code can check for this degenerate code. virtual bool expects_empty_serialized_data_for_each_shot() const = 0; /// Reads entire records into the given bit table. /// /// This method must only be called when the reader is at the start of a record. /// /// Args: /// out: The bit table to write the records into. /// The major axis indexes shots. /// The minor axis indexes results within a shot. /// major_index_is_shot_index: Whether or not the data should be transposed. /// max_shots: Maximum number of shots to read. Automatically clamped down based on the size of `out`. /// /// Returns: /// The number of records that were read. /// Cannot be larger than the capacity of the output table. /// If this value is 0, there are no more records to read. /// /// Throws: /// std::invalid_argument: /// The minor axis of the table has a length that's too short to hold an entire record. /// The major axis of the table has length zero. /// The reader is not at the start of a record. size_t read_records_into(simd_bit_table &out, bool major_index_is_shot_index, size_t max_shots = UINT32_MAX); /// Reads an entire record from start to finish, returning False if there are no more records. /// The data from the record is bit packed into a simd_bits. /// /// Args: /// dirty_out_buffer: The simd-compatible buffer to write the data to. The buffer is not required to be zero'd. /// /// Returns: /// True: The record was read successfully. /// False: End of file. There were no more records. No record was read. /// /// Throws: /// std::invalid_argument: A record was only partially read. virtual bool start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) = 0; /// Reads an entire record from start to finish, returning False if there are no more records. /// The data from the record is stored as sparse indices-of-ones data. /// /// When reading detection event data with observables appended, the observable data goes into the `mask` field /// of the output. Note that this method requires that there be at most 32 observables. /// /// Args: /// cleared_out: A cleared SparseShot struct to write data into. /// /// Returns: /// True: The record was read successfully. /// False: End of file. There were no more records. No record was read. /// /// Throws: /// std::invalid_argument: A record was only partially read. virtual bool start_and_read_entire_record(SparseShot &cleared_out) = 0; /// Reads many records into a shot table. /// /// Args: /// out_table: The table to write shots into. /// Must have num_minor_bits >= bits_per_shot. /// num_major_bits is max read shots. /// max_shots: Don't read more than this many shots. /// Must be at most the number of shots that can be stored in the table. /// /// Returns: /// The number of shots that were read. virtual size_t read_into_table_with_major_shot_index(simd_bit_table &out_table, size_t max_shots); /// Reads many records into a shot table. /// /// Args: /// out_table: The table to write shots into. /// Must have num_major_bits >= bits_per_shot. /// num_minor_bits is max read shots. /// max_shots: Don't read more than this many shots. /// Must be at most the number of shots that can be stored in the table. /// /// Returns: /// The number of shots that were read. virtual size_t read_into_table_with_minor_shot_index(simd_bit_table &out_table, size_t max_shots) = 0; protected: void move_obs_in_shots_to_mask_assuming_sorted(SparseShot &shot); }; template struct MeasureRecordReaderFormatPTB64 : MeasureRecordReader { FILE *in; // This buffer stores partially transposed shots. // The uint64_t for index k of shot s is stored in the buffer at offset k*64 + s. simd_bits buf; size_t num_unread_shots_in_buf; MeasureRecordReaderFormatPTB64(FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables); bool start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) override; bool start_and_read_entire_record(SparseShot &cleared_out) override; bool expects_empty_serialized_data_for_each_shot() const override; size_t read_into_table_with_major_shot_index(simd_bit_table &out_table, size_t max_shots) override; size_t read_into_table_with_minor_shot_index(simd_bit_table &out_table, size_t max_shots) override; private: bool load_cache(); }; template struct MeasureRecordReaderFormat01 : MeasureRecordReader { FILE *in; MeasureRecordReaderFormat01(FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables); bool start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) override; bool start_and_read_entire_record(SparseShot &cleared_out) override; bool expects_empty_serialized_data_for_each_shot() const override; size_t read_into_table_with_minor_shot_index(simd_bit_table &out_table, size_t max_shots) override; private: template bool start_and_read_entire_record_helper(SAW0 saw0, SAW1 saw1); }; template struct MeasureRecordReaderFormatB8 : MeasureRecordReader { FILE *in; MeasureRecordReaderFormatB8(FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables); bool start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) override; bool start_and_read_entire_record(SparseShot &cleared_out) override; bool expects_empty_serialized_data_for_each_shot() const override; size_t read_into_table_with_minor_shot_index(simd_bit_table &out_table, size_t max_shots) override; }; template struct MeasureRecordReaderFormatHits : MeasureRecordReader { FILE *in; MeasureRecordReaderFormatHits(FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables); bool start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) override; bool start_and_read_entire_record(SparseShot &cleared_out) override; bool expects_empty_serialized_data_for_each_shot() const override; size_t read_into_table_with_minor_shot_index(simd_bit_table &out_table, size_t max_shots) override; private: template bool start_and_read_entire_record_helper(HANDLE_HIT handle_hit); }; template struct MeasureRecordReaderFormatR8 : MeasureRecordReader { FILE *in; MeasureRecordReaderFormatR8(FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables); bool start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) override; bool start_and_read_entire_record(SparseShot &cleared_out) override; bool expects_empty_serialized_data_for_each_shot() const override; size_t read_into_table_with_minor_shot_index(simd_bit_table &out_table, size_t max_shots) override; private: template bool start_and_read_entire_record_helper(HANDLE_HIT handle_hit); }; template struct MeasureRecordReaderFormatDets : MeasureRecordReader { FILE *in; MeasureRecordReaderFormatDets( FILE *in, size_t num_measurements, size_t num_detectors = 0, size_t num_observables = 0); bool start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) override; bool start_and_read_entire_record(SparseShot &cleared_out) override; bool expects_empty_serialized_data_for_each_shot() const override; size_t read_into_table_with_minor_shot_index(simd_bit_table &out_table, size_t max_shots) override; private: template bool start_and_read_entire_record_helper(HANDLE_HIT handle_hit); }; template size_t read_file_data_into_shot_table( FILE *in, size_t max_shots, size_t num_bits_per_shot, SampleFormat format, char dets_char, simd_bit_table &out_table, bool shots_is_major_index_of_out_table); } // namespace stim #include "stim/io/measure_record_reader.inl" #endif ================================================ FILE: src/stim/io/measure_record_reader.inl ================================================ /* * 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. */ #include #include "stim/io/measure_record_reader.h" namespace stim { template MeasureRecordReader::MeasureRecordReader(size_t num_measurements, size_t num_detectors, size_t num_observables) : num_measurements(num_measurements), num_detectors(num_detectors), num_observables(num_observables) { } template size_t MeasureRecordReader::read_records_into( simd_bit_table &out, bool major_index_is_shot_index, size_t max_shots) { if (!major_index_is_shot_index) { simd_bit_table buf(out.num_minor_bits_padded(), out.num_major_bits_padded()); size_t r = read_records_into(buf, true, max_shots); buf.transpose_into(out); return r; } size_t num_read = 0; max_shots = std::min(max_shots, out.num_major_bits_padded()); while (num_read < max_shots && start_and_read_entire_record(out[num_read])) { num_read++; } return num_read; } template std::unique_ptr> MeasureRecordReader::make( FILE *in, SampleFormat input_format, size_t num_measurements, size_t num_detectors, size_t num_observables) { switch (input_format) { case SampleFormat::SAMPLE_FORMAT_01: return std::make_unique>( in, num_measurements, num_detectors, num_observables); case SampleFormat::SAMPLE_FORMAT_B8: return std::make_unique>( in, num_measurements, num_detectors, num_observables); case SampleFormat::SAMPLE_FORMAT_DETS: return std::make_unique>( in, num_measurements, num_detectors, num_observables); case SampleFormat::SAMPLE_FORMAT_HITS: return std::make_unique>( in, num_measurements, num_detectors, num_observables); case SampleFormat::SAMPLE_FORMAT_PTB64: return std::make_unique>( in, num_measurements, num_detectors, num_observables); case SampleFormat::SAMPLE_FORMAT_R8: return std::make_unique>( in, num_measurements, num_detectors, num_observables); default: throw std::invalid_argument("Sample format not recognized by MeasurementRecordReader"); } } template size_t MeasureRecordReader::bits_per_record() const { return num_measurements + num_detectors + num_observables; } template void MeasureRecordReader::move_obs_in_shots_to_mask_assuming_sorted(SparseShot &shot) { if (num_observables > 32) { throw std::invalid_argument("More than 32 observables. Can't read into SparseShot struct."); } size_t nd = num_measurements + num_detectors; size_t n = nd + num_observables; shot.obs_mask.clear(); while (!shot.hits.empty()) { auto top = shot.hits.back(); if (top < nd) { break; } if (top >= n) { throw std::invalid_argument("Hit index from data is too large."); } shot.hits.pop_back(); shot.obs_mask[top - nd] ^= true; } } template size_t MeasureRecordReader::read_into_table_with_major_shot_index(simd_bit_table &out_table, size_t max_shots) { size_t read_shots = 0; while (read_shots < max_shots && start_and_read_entire_record(out_table[read_shots])) { read_shots++; } return read_shots; } /// 01 format template MeasureRecordReaderFormat01::MeasureRecordReaderFormat01( FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables) : MeasureRecordReader(num_measurements, num_detectors, num_observables), in(in) { } template bool MeasureRecordReaderFormat01::start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) { return start_and_read_entire_record_helper( [&](size_t k) { dirty_out_buffer[k] = false; }, [&](size_t k) { dirty_out_buffer[k] = true; }); } template bool MeasureRecordReaderFormat01::start_and_read_entire_record(SparseShot &cleared_out) { if (cleared_out.obs_mask.num_bits_padded() < this->num_observables) { cleared_out.obs_mask = simd_bits<64>(this->num_observables); } bool result = start_and_read_entire_record_helper( [&](size_t k) { }, [&](size_t k) { cleared_out.hits.push_back((uint64_t)k); }); this->move_obs_in_shots_to_mask_assuming_sorted(cleared_out); return result; } template bool MeasureRecordReaderFormat01::expects_empty_serialized_data_for_each_shot() const { return false; } template size_t MeasureRecordReaderFormat01::read_into_table_with_minor_shot_index( simd_bit_table &out_table, size_t max_shots) { size_t read_shots = 0; while (read_shots < max_shots) { bool more = start_and_read_entire_record_helper( [&](size_t k) { out_table[k][read_shots] &= 0; }, [&](size_t k) { out_table[k][read_shots] |= 1; }); if (!more) { break; } read_shots++; } return read_shots; } template template bool MeasureRecordReaderFormat01::start_and_read_entire_record_helper(SAW0 saw0, SAW1 saw1) { size_t n = this->bits_per_record(); for (size_t k = 0; k < n; k++) { int b = getc(in); switch (b) { case '0': saw0(k); break; case '1': saw1(k); break; case EOF: if (k == 0) { return false; } [[fallthrough]]; case '\r': [[fallthrough]]; case '\n': throw std::invalid_argument( "01 data ended in middle of record at byte position " + std::to_string(k) + ".\nExpected bits per record was " + std::to_string(n) + "."); default: throw std::invalid_argument("Unexpected character in 01 format data: '" + std::to_string(b) + "'."); } } int last = getc(in); if (n == 0 && last == EOF) { return false; } if (last == '\r') { last = getc(in); } if (last != '\n') { throw std::invalid_argument( "01 data didn't end with a newline after the expected data length of '" + std::to_string(n) + "'."); } return true; } /// B8 format template MeasureRecordReaderFormatB8::MeasureRecordReaderFormatB8( FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables) : MeasureRecordReader(num_measurements, num_detectors, num_observables), in(in) { } template bool MeasureRecordReaderFormatB8::start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) { size_t n = this->bits_per_record(); size_t nb = (n + 7) >> 3; size_t nr = fread(dirty_out_buffer.u8, 1, nb, in); if (nr == 0) { return false; } if (nr != nb) { throw std::invalid_argument( "b8 data ended in middle of record at byte position " + std::to_string(nr) + ".\n" "Expected bytes per record was " + std::to_string(nb) + " (" + std::to_string(n) + " bits padded)."); } return true; } template size_t MeasureRecordReaderFormatB8::read_into_table_with_minor_shot_index( simd_bit_table &out_table, size_t max_shots) { size_t n = this->bits_per_record(); if (n == 0) { return 0; // Ambiguous when the data ends. Stop as early as possible. } for (size_t read_shots = 0; read_shots < max_shots; read_shots++) { for (size_t bit = 0; bit < n; bit += 8) { int c = getc(in); if (c == EOF) { if (bit == 0) { return read_shots; } throw std::invalid_argument("b8 data ended in middle of record."); } for (size_t b = 0; b < 8 && bit + b < n; b++) { out_table[bit + b][read_shots] = ((c >> b) & 1) != 0; } } } return max_shots; } template bool MeasureRecordReaderFormatB8::start_and_read_entire_record(SparseShot &cleared_out) { if (cleared_out.obs_mask.num_bits_padded() < this->num_observables) { cleared_out.obs_mask = simd_bits<64>(this->num_observables); } size_t n = this->bits_per_record(); size_t nb = (n + 7) >> 3; if (n == 0) { return 0; // Ambiguous when the data ends. Stop as early as possible. } for (size_t k = 0; k < nb; k++) { int b = getc(in); if (b == EOF) { if (k == 0) { return false; } throw std::invalid_argument( "b8 data ended in middle of record at byte position " + std::to_string(k) + ".\n" "Expected bytes per record was " + std::to_string(nb) + " (" + std::to_string(n) + " bits padded)."); } size_t bit_offset = k << 3; for (size_t r = 0; r < 8; r++) { if (b & (1 << r)) { cleared_out.hits.push_back(bit_offset + r); } } } this->move_obs_in_shots_to_mask_assuming_sorted(cleared_out); return true; } template bool MeasureRecordReaderFormatB8::expects_empty_serialized_data_for_each_shot() const { return this->bits_per_record() == 0; } /// Hits format template MeasureRecordReaderFormatHits::MeasureRecordReaderFormatHits( FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables) : MeasureRecordReader(num_measurements, num_detectors, num_observables), in(in) { } template bool MeasureRecordReaderFormatHits::start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) { size_t m = this->bits_per_record(); dirty_out_buffer.prefix_ref(m).clear(); return start_and_read_entire_record_helper([&](size_t bit_index) { if (bit_index >= m) { throw std::invalid_argument("hit index is too large."); } dirty_out_buffer[bit_index] ^= true; }); } template bool MeasureRecordReaderFormatHits::start_and_read_entire_record(SparseShot &cleared_out) { if (cleared_out.obs_mask.num_bits_padded() < this->num_observables) { cleared_out.obs_mask = simd_bits<64>(this->num_observables); } size_t m = this->bits_per_record(); size_t nmd = this->num_measurements + this->num_detectors; return start_and_read_entire_record_helper([&](size_t bit_index) { if (bit_index >= m) { throw std::invalid_argument("hit index is too large."); } if (bit_index < nmd) { cleared_out.hits.push_back(bit_index); } else { cleared_out.obs_mask[bit_index - nmd] ^= true; } }); } template bool MeasureRecordReaderFormatHits::expects_empty_serialized_data_for_each_shot() const { return false; } template size_t MeasureRecordReaderFormatHits::read_into_table_with_minor_shot_index( simd_bit_table &out_table, size_t max_shots) { size_t read_shots = 0; out_table.clear(); while (read_shots < max_shots) { bool more = start_and_read_entire_record_helper([&](size_t bit_index) { out_table[bit_index][read_shots] |= 1; }); if (!more) { break; } read_shots++; } return read_shots; } template template bool MeasureRecordReaderFormatHits::start_and_read_entire_record_helper(HANDLE_HIT handle_hit) { bool first = true; while (true) { int next_char; uint64_t value; if (!read_uint64(in, value, next_char, false)) { if (first && next_char == EOF) { return false; } if (first && next_char == '\r') { next_char = getc(in); } if (first && next_char == '\n') { return true; } throw std::invalid_argument("HITS data wasn't comma-separated integers terminated by a newline."); } handle_hit((size_t)value); first = false; if (next_char == '\r') { next_char = getc(in); if (next_char == '\n') { return true; } } else if (next_char == '\n') { return true; } if (next_char != ',') { throw std::invalid_argument("HITS data wasn't comma-separated integers terminated by a newline."); } } } /// R8 format template MeasureRecordReaderFormatR8::MeasureRecordReaderFormatR8( FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables) : MeasureRecordReader(num_measurements, num_detectors, num_observables), in(in) { } template bool MeasureRecordReaderFormatR8::start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) { dirty_out_buffer.prefix_ref(this->bits_per_record()).clear(); return start_and_read_entire_record_helper([&](size_t bit_index) { dirty_out_buffer[bit_index] = 1; }); } template bool MeasureRecordReaderFormatR8::start_and_read_entire_record(SparseShot &cleared_out) { if (cleared_out.obs_mask.num_bits_padded() < this->num_observables) { cleared_out.obs_mask = simd_bits<64>(this->num_observables); } bool result = start_and_read_entire_record_helper([&](size_t bit_index) { cleared_out.hits.push_back(bit_index); }); this->move_obs_in_shots_to_mask_assuming_sorted(cleared_out); return result; } template bool MeasureRecordReaderFormatR8::expects_empty_serialized_data_for_each_shot() const { return false; } template size_t MeasureRecordReaderFormatR8::read_into_table_with_minor_shot_index( simd_bit_table &out_table, size_t max_shots) { size_t read_shots = 0; out_table.clear(); while (read_shots < max_shots) { bool more = start_and_read_entire_record_helper([&](size_t bit_index) { out_table[bit_index][read_shots] |= 1; }); if (!more) { break; } read_shots++; } return read_shots; } template template bool MeasureRecordReaderFormatR8::start_and_read_entire_record_helper(HANDLE_HIT handle_hit) { int next_char = getc(in); if (next_char == EOF) { return false; } size_t n = this->bits_per_record(); size_t pos = 0; while (true) { pos += next_char; if (next_char != 255) { if (pos < n) { handle_hit(pos); pos++; } else if (pos == n) { return true; } else { throw std::invalid_argument( "r8 data jumped past expected end of encoded data. Expected to decode " + std::to_string(this->bits_per_record()) + " bits."); } } next_char = getc(in); if (next_char == EOF) { throw std::invalid_argument( "End of file before end of r8 data. Expected to decode " + std::to_string(this->bits_per_record()) + " bits."); } } } /// DETS format template bool MeasureRecordReaderFormatDets::start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) { dirty_out_buffer.prefix_ref(this->bits_per_record()).clear(); return start_and_read_entire_record_helper([&](size_t bit_index) { dirty_out_buffer[bit_index] = true; }); } template bool MeasureRecordReaderFormatDets::start_and_read_entire_record(SparseShot &cleared_out) { if (cleared_out.obs_mask.num_bits_padded() < this->num_observables) { cleared_out.obs_mask = simd_bits<64>(this->num_observables); } size_t obs_start = this->num_measurements + this->num_detectors; return start_and_read_entire_record_helper([&](size_t bit_index) { if (bit_index < obs_start) { cleared_out.hits.push_back(bit_index); } else { cleared_out.obs_mask[bit_index - obs_start] ^= true; } }); } template MeasureRecordReaderFormatDets::MeasureRecordReaderFormatDets( FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables) : MeasureRecordReader(num_measurements, num_detectors, num_observables), in(in) { } template bool MeasureRecordReaderFormatDets::expects_empty_serialized_data_for_each_shot() const { return false; } template size_t MeasureRecordReaderFormatDets::read_into_table_with_minor_shot_index( simd_bit_table &out_table, size_t max_shots) { size_t read_shots = 0; out_table.clear(); while (read_shots < max_shots) { bool more = start_and_read_entire_record_helper([&](size_t bit_index) { out_table[bit_index][read_shots] |= 1; }); if (!more) { break; } read_shots++; } return read_shots; } template template bool MeasureRecordReaderFormatDets::start_and_read_entire_record_helper(HANDLE_HIT handle_hit) { // Read "shot" prefix, or notice end of data. Ignore indentation and spacing. while (true) { int next_char = getc(in); if (next_char == ' ' || next_char == '\n' || next_char == '\r' || next_char == '\t') { continue; } if (next_char == EOF) { return false; } if (next_char != 's' || getc(in) != 'h' || getc(in) != 'o' || getc(in) != 't') { throw std::invalid_argument("DETS data didn't start with 'shot'"); } break; } // Read prefixed integers until end of line. int next_char = getc(in); while (true) { if (next_char == '\r') { next_char = getc(in); } if (next_char == '\n' || next_char == EOF) { return true; } if (next_char != ' ') { throw std::invalid_argument("DETS data wasn't single-space-separated with no trailing spaces."); } next_char = getc(in); uint64_t offset; uint64_t length; if (next_char == 'M') { offset = 0; length = this->num_measurements; } else if (next_char == 'D') { offset = this->num_measurements; length = this->num_detectors; } else if (next_char == 'L') { offset = this->num_measurements + this->num_detectors; length = this->num_observables; } else { throw std::invalid_argument( "Unrecognized DETS prefix. Expected M or D or L not '" + std::to_string(next_char) + "'"); } char prefix = next_char; uint64_t value; if (!read_uint64(in, value, next_char, false)) { throw std::invalid_argument("DETS data had a value prefix (M or D or L) not followed by an integer."); } if (value >= length) { std::stringstream msg; msg << "DETS data had a value larger than expected. "; msg << "Got " << prefix << value << " but expected length of " << prefix << " space to be " << length << "."; throw std::invalid_argument(msg.str()); } handle_hit((size_t)(offset + value)); } } /// PTB64 format template MeasureRecordReaderFormatPTB64::MeasureRecordReaderFormatPTB64( FILE *in, size_t num_measurements, size_t num_detectors, size_t num_observables) : MeasureRecordReader(num_measurements, num_detectors, num_observables), in(in), buf(0), num_unread_shots_in_buf(0) { } template bool MeasureRecordReaderFormatPTB64::load_cache() { size_t n = this->bits_per_record(); size_t expected_buf_bits = (n + 63) / 64 * 64 * 64; if (buf.num_bits_padded() < expected_buf_bits) { buf = simd_bits(expected_buf_bits); } size_t nb = this->bits_per_record() * (64 / 8); size_t nr = fread(buf.u8, 1, nb, in); if (nr == 0) { num_unread_shots_in_buf = 0; return false; } if (nr != nb) { throw std::invalid_argument( "ptb64 data ended in middle of 64 record group at byte position " + std::to_string(nr) + ".\n" "Expected bytes per 64 records was " + std::to_string(nb) + " (" + std::to_string(n) + " bits padded)."); } // Convert from bit interleaving to uint64_t interleaving. for (size_t k = 0; k + 63 < n; k += 64) { inplace_transpose_64x64(buf.u64 + k, 1); } num_unread_shots_in_buf = 64; return true; } template bool MeasureRecordReaderFormatPTB64::start_and_read_entire_record(simd_bits_range_ref dirty_out_buffer) { if (num_unread_shots_in_buf == 0) { load_cache(); } if (num_unread_shots_in_buf == 0) { return false; } size_t offset = 64 - num_unread_shots_in_buf; size_t n = this->bits_per_record(); size_t n64 = n / 64; for (size_t k = 0; k < n64; k++) { dirty_out_buffer.u64[k] = buf.u64[k * 64 + offset]; } for (size_t k = n64 * 64; k < n; k++) { dirty_out_buffer[k] = buf[k * 64 + offset]; } num_unread_shots_in_buf -= 1; return true; } template bool MeasureRecordReaderFormatPTB64::start_and_read_entire_record(SparseShot &cleared_out) { if (cleared_out.obs_mask.num_bits_padded() < this->num_observables) { cleared_out.obs_mask = simd_bits<64>(this->num_observables); } if (num_unread_shots_in_buf == 0) { load_cache(); } if (num_unread_shots_in_buf == 0) { return false; } size_t offset = 64 - num_unread_shots_in_buf; size_t n = this->bits_per_record(); size_t n64 = n / 64; for (size_t k = 0; k < n64; k++) { uint64_t v = buf.u64[k * 64 + offset]; if (v) { for (size_t k2 = 0; k2 < 64; k2++) { if ((v >> k2) & 1) { cleared_out.hits.push_back(k * 64 + k2); } } } } for (size_t k = n64 * 64; k < n; k++) { if (buf[k * 64 + offset]) { cleared_out.hits.push_back(k); } } num_unread_shots_in_buf -= 1; this->move_obs_in_shots_to_mask_assuming_sorted(cleared_out); return true; } template bool MeasureRecordReaderFormatPTB64::expects_empty_serialized_data_for_each_shot() const { return this->bits_per_record() == 0; } template size_t MeasureRecordReaderFormatPTB64::read_into_table_with_minor_shot_index( simd_bit_table &out_table, size_t max_shots) { size_t n = this->bits_per_record(); if (n == 0) { return 0; // Ambiguous when the data ends. Stop as early as possible. } if (max_shots % 64 != 0) { throw std::invalid_argument("max_shots must be a multiple of 64 when using PTB64 format"); } for (size_t shots_read = 0; shots_read < max_shots; shots_read += 64) { for (size_t bit = 0; bit < n; bit++) { size_t read = fread(&out_table[bit].u64[shots_read >> 6], 1, sizeof(uint64_t), in); if (read != sizeof(uint64_t)) { if (read == 0 && bit == 0) { // End of file at a shot boundary. return shots_read; } else { // Fragmented file. throw std::invalid_argument("File ended in the middle of a ptb64 record."); } } } } return max_shots; } template size_t MeasureRecordReaderFormatPTB64::read_into_table_with_major_shot_index( simd_bit_table &out_table, size_t max_shots) { size_t n = this->bits_per_record(); if (n == 0) { return 0; // Ambiguous when the data ends. Stop as early as possible. } uint64_t buffer[64]; assert(max_shots % 64 == 0); for (size_t shot = 0; shot < max_shots; shot += 64) { for (size_t bit = 0; bit < n; bit += 64) { for (size_t b = 0; b < 64; b++) { if (bit + b >= n) { buffer[b] = 0; } else { size_t read = fread(&buffer[b], 1, sizeof(uint64_t), in); if (read != sizeof(uint64_t)) { if (read == 0 && bit == 0 && b == 0) { // End of file at a shot boundary. return shot; } else { // Fragmented file. throw std::invalid_argument("File ended in the middle of a ptb64 record."); } } } } inplace_transpose_64x64(buffer, 1); for (size_t s = 0; s < 64; s++) { out_table[shot + s].u64[bit >> 6] = buffer[s]; } } } return max_shots; } template size_t read_file_data_into_shot_table( FILE *in, size_t max_shots, size_t num_bits_per_shot, SampleFormat format, char dets_char, simd_bit_table &out_table, bool shots_is_major_index_of_out_table) { auto reader = MeasureRecordReader::make( in, format, dets_char == 'M' ? num_bits_per_shot : 0, dets_char == 'D' ? num_bits_per_shot : 0, dets_char == 'L' ? num_bits_per_shot : 0); if (shots_is_major_index_of_out_table) { return reader->read_into_table_with_major_shot_index(out_table, max_shots); } else { return reader->read_into_table_with_minor_shot_index(out_table, max_shots); } } } // namespace stim ================================================ FILE: src/stim/io/measure_record_reader.perf.cc ================================================ // 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. #include "stim/io/measure_record_reader.h" #include "stim/io/measure_record_writer.h" #include "stim/perf.perf.h" #include "stim/util_bot/probability_util.h" using namespace stim; template void dense_reader_benchmark(double goal_micros) { double p = 1 / (double)denom; FILE *f = tmpfile(); { auto writer = MeasureRecordWriter::make(f, format); simd_bits data(n); std::mt19937_64 rng(0); biased_randomize_bits(p, data.u64, data.u64 + (n >> 6), rng); writer->write_bytes({data.u8, data.u8 + (n >> 3)}); writer->write_end(); } auto reader = MeasureRecordReader::make(f, format, n, 0, 0); simd_bits buffer(n); benchmark_go([&]() { rewind(f); reader->start_and_read_entire_record(buffer); }) .goal_micros(goal_micros) .show_rate("Bits", n); if (!buffer.not_zero()) { std::cerr << "data dependence!\n"; } fclose(f); } template void sparse_reader_benchmark(double goal_micros) { double p = 1 / (double)denom; FILE *f = tmpfile(); { auto writer = MeasureRecordWriter::make(f, format); simd_bits data(n); std::mt19937_64 rng(0); biased_randomize_bits(p, data.u64, data.u64 + (n >> 6), rng); writer->write_bytes({data.u8, data.u8 + (n >> 3)}); writer->write_end(); } auto reader = MeasureRecordReader::make(f, format, n, 0, 0); SparseShot buffer; buffer.hits.reserve((size_t)ceil(n * p * 1.1)); benchmark_go([&]() { rewind(f); buffer.clear(); reader->start_and_read_entire_record(buffer); }) .goal_micros(goal_micros) .show_rate("Pops", n * p); if (buffer.hits.empty()) { std::cerr << "data dependence!\n"; } fclose(f); } BENCHMARK(read_01_dense_per10) { dense_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_01>(60); } BENCHMARK(read_01_sparse_per10) { sparse_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_01>(45); } BENCHMARK(read_b8_dense_per10) { dense_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_B8>(0.65); } BENCHMARK(read_b8_sparse_per10) { sparse_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_B8>(6); } BENCHMARK(read_hits_dense_per10) { dense_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_HITS>(16); } BENCHMARK(read_hits_dense_per100) { dense_reader_benchmark<10000, 100, SampleFormat::SAMPLE_FORMAT_HITS>(2.1); } BENCHMARK(read_hits_sparse_per10) { sparse_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_HITS>(15); } BENCHMARK(read_hits_sparse_per100) { sparse_reader_benchmark<10000, 100, SampleFormat::SAMPLE_FORMAT_HITS>(2.2); } BENCHMARK(read_dets_dense_per10) { dense_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_DETS>(23); } BENCHMARK(read_dets_dense_per100) { dense_reader_benchmark<10000, 100, SampleFormat::SAMPLE_FORMAT_DETS>(3.0); } BENCHMARK(read_dets_sparse_per10) { sparse_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_DETS>(23); } BENCHMARK(read_dets_sparse_per100) { sparse_reader_benchmark<10000, 100, SampleFormat::SAMPLE_FORMAT_DETS>(3.0); } BENCHMARK(read_r8_dense_per10) { dense_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_R8>(5); } BENCHMARK(read_r8_dense_per100) { dense_reader_benchmark<10000, 100, SampleFormat::SAMPLE_FORMAT_R8>(1.3); } BENCHMARK(read_r8_sparse_per10) { sparse_reader_benchmark<10000, 10, SampleFormat::SAMPLE_FORMAT_R8>(3.5); } BENCHMARK(read_r8_sparse_per100) { sparse_reader_benchmark<10000, 100, SampleFormat::SAMPLE_FORMAT_R8>(1.0); } ================================================ FILE: src/stim/io/measure_record_reader.test.cc ================================================ /* * 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. */ #include "stim/io/measure_record_reader.h" #include "gtest/gtest.h" #include "stim/io/measure_record_batch_writer.h" #include "stim/io/measure_record_writer.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/probability_util.h" #include "stim/util_bot/test_util.test.h" using namespace stim; FILE *tmpfile_with_contents(std::string_view contents) { FILE *tmp = tmpfile(); size_t written = fwrite(contents.data(), 1, contents.size(), tmp); if (written != contents.size()) { int en = errno; std::cerr << "Failed to write to tmpfile: " << strerror(en) << std::endl; throw std::invalid_argument("Failed to write to tmpfile"); } rewind(tmp); return tmp; } TEST(read_unsigned_int, BasicUsage) { int next = 0; uint64_t value = 0; FILE *tmp = tmpfile_with_contents("105\n"); ASSERT_NE(tmp, nullptr); ASSERT_TRUE(read_uint64(tmp, value, next)); ASSERT_EQ(next, '\n'); ASSERT_EQ(value, 105); } TEST(read_unsigned_int, ValueTooBig) { int next = 0; uint64_t value = 0; FILE *tmp = tmpfile_with_contents("18446744073709551616\n"); ASSERT_NE(tmp, nullptr); ASSERT_THROW({ read_uint64(tmp, value, next); }, std::runtime_error); } template void assert_contents_load_correctly(SampleFormat format, std::string_view contents) { FILE *tmp = tmpfile_with_contents(contents); auto reader = MeasureRecordReader::make(tmp, format, 18); simd_bits buf(18); reader->start_and_read_entire_record(buf); ASSERT_EQ(buf[0], 0); ASSERT_EQ(buf[1], 0); ASSERT_EQ(buf[2], 0); ASSERT_EQ(buf[3], 1); ASSERT_EQ(buf[4], 1); ASSERT_EQ(buf[5], 1); ASSERT_EQ(buf[6], 1); ASSERT_EQ(buf[7], 1); ASSERT_EQ(buf[8], 0); ASSERT_EQ(buf[9], 0); ASSERT_EQ(buf[10], 0); ASSERT_EQ(buf[11], 0); ASSERT_EQ(buf[12], 1); ASSERT_EQ(buf[13], 1); ASSERT_EQ(buf[14], 1); ASSERT_EQ(buf[15], 1); ASSERT_EQ(buf[16], 1); ASSERT_EQ(buf[17], 1); rewind(tmp); SparseShot sparse; reader->start_and_read_entire_record(sparse); ASSERT_EQ(sparse.hits, (std::vector{3, 4, 5, 6, 7, 12, 13, 14, 15, 16, 17})); } TEST_EACH_WORD_SIZE_W(MeasureRecordReader, Format01, { assert_contents_load_correctly(SampleFormat::SAMPLE_FORMAT_01, "000111110000111111\n"); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatB8, { assert_contents_load_correctly(SampleFormat::SAMPLE_FORMAT_B8, "\xF8\xF0\x03"); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatHits, { assert_contents_load_correctly(SampleFormat::SAMPLE_FORMAT_HITS, "3,4,5,6,7,12,13,14,15,16,17\n"); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatR8, { char tmp_data[]{3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0}; assert_contents_load_correctly( SampleFormat::SAMPLE_FORMAT_R8, std::string(std::begin(tmp_data), std::end(tmp_data))); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatR8_LongGap, { FILE *tmp = tmpfile_with_contents("\xFF\xFF\x02\x20"); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_R8, 8 * 64 + 32 + 1); SparseShot sparse; ASSERT_TRUE(reader->start_and_read_entire_record(sparse)); ASSERT_EQ(sparse.hits, (std::vector{255 + 255 + 2})); ASSERT_FALSE(reader->start_and_read_entire_record(sparse)); }) FILE *write_records(SpanRef data, SampleFormat format) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, format); writer->write_bytes(data); writer->write_end(); return tmp; } template size_t read_records_as_bytes(FILE *in, SpanRef buf, SampleFormat format, size_t bits_per_record) { auto reader = MeasureRecordReader::make(in, format, bits_per_record); if (buf.size() * 8 < reader->bits_per_record()) { throw std::invalid_argument("buf too small"); } simd_bits buf2(reader->bits_per_record()); bool success = reader->start_and_read_entire_record(buf2); EXPECT_TRUE(success); memcpy(buf.ptr_start, buf2.u8, (reader->bits_per_record() + 7) / 8); return reader->bits_per_record(); } TEST_EACH_WORD_SIZE_W(MeasureRecordReader, Format01_WriteRead, { uint8_t src[]{0, 1, 2, 3, 4, 0xFF, 0xBF, 0xFE, 80, 0, 0, 1, 20}; constexpr size_t num_bytes = sizeof(src) / sizeof(uint8_t); uint8_t dst[num_bytes]; memset(dst, 0, num_bytes); FILE *tmp = write_records({src, src + num_bytes}, SampleFormat::SAMPLE_FORMAT_01); rewind(tmp); ASSERT_EQ( num_bytes * 8, read_records_as_bytes(tmp, {dst, dst + num_bytes}, SampleFormat::SAMPLE_FORMAT_01, 8 * num_bytes)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(src[i], dst[i]); } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatB8_WriteRead, { uint8_t src[]{0, 1, 2, 3, 4, 0xFF, 0xBF, 0xFE, 80, 0, 0, 1, 20}; constexpr size_t num_bytes = sizeof(src) / sizeof(uint8_t); uint8_t dst[num_bytes]; memset(dst, 0, num_bytes); FILE *tmp = write_records({src, src + num_bytes}, SampleFormat::SAMPLE_FORMAT_B8); rewind(tmp); ASSERT_EQ( num_bytes * 8, read_records_as_bytes(tmp, {dst, dst + num_bytes}, SampleFormat::SAMPLE_FORMAT_B8, 8 * num_bytes)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(src[i], dst[i]); } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatR8_WriteRead, { uint8_t src[]{0, 1, 2, 3, 4, 0xFF, 0xBF, 0xFE, 80, 0, 0, 1, 20}; constexpr size_t num_bytes = sizeof(src) / sizeof(uint8_t); uint8_t dst[num_bytes]{}; FILE *tmp = write_records({src, src + num_bytes}, SampleFormat::SAMPLE_FORMAT_R8); rewind(tmp); ASSERT_EQ( num_bytes * 8, read_records_as_bytes(tmp, {dst, dst + num_bytes}, SampleFormat::SAMPLE_FORMAT_R8, 8 * num_bytes)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(src[i], dst[i]); } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatHits_WriteRead, { uint8_t src[]{0, 1, 2, 3, 4, 0xFF, 0xBF, 0xFE, 80, 0, 0, 1, 20}; constexpr size_t num_bytes = sizeof(src) / sizeof(uint8_t); uint8_t dst[num_bytes]; memset(dst, 0, num_bytes); FILE *tmp = write_records({src, src + num_bytes}, SampleFormat::SAMPLE_FORMAT_HITS); rewind(tmp); ASSERT_EQ( num_bytes * 8 - 1, read_records_as_bytes(tmp, {dst, dst + num_bytes}, SampleFormat::SAMPLE_FORMAT_HITS, 8 * num_bytes - 1)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(src[i], dst[i]); } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatDets_WriteRead, { uint8_t src[]{0, 1, 2, 3, 4, 0xFF, 0xBF, 0xFE, 80, 0, 0, 1, 20}; constexpr size_t num_bytes = sizeof(src) / sizeof(uint8_t); uint8_t dst[num_bytes]; memset(dst, 0, num_bytes); FILE *tmp = write_records({src, src + num_bytes}, SampleFormat::SAMPLE_FORMAT_DETS); rewind(tmp); ASSERT_EQ( num_bytes * 8 - 1, read_records_as_bytes(tmp, {dst, dst + num_bytes}, SampleFormat::SAMPLE_FORMAT_DETS, 8 * num_bytes - 1)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(src[i], dst[i]); } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, Format01_WriteRead_MultipleRecords, { uint8_t record1[]{0x12, 0xAB, 0x00, 0xFF, 0x75}; uint8_t record2[]{0x80, 0xFF, 0x01, 0x56, 0x57}; uint8_t record3[]{0x2F, 0x08, 0xF0, 0x1C, 0x60}; constexpr size_t num_bytes = sizeof(record1) / sizeof(uint8_t); constexpr size_t bits_per_record = 8 * num_bytes; FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_01); writer->write_bytes({record1, record1 + num_bytes}); writer->write_end(); writer->write_bytes({record2, record2 + num_bytes}); writer->write_end(); writer->write_bytes({record3, record3 + num_bytes}); writer->write_end(); rewind(tmp); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_01, bits_per_record); simd_bits buf(num_bytes * 8); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record1[i]); } ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record2[i]); } ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record3[i]); } ASSERT_FALSE(reader->start_and_read_entire_record(buf)); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatHits_WriteRead_MultipleRecords, { uint8_t record1[]{0x12, 0xAB, 0x00, 0xFF, 0x75}; uint8_t record2[]{0x80, 0xFF, 0x01, 0x56, 0x57}; uint8_t record3[]{0x2F, 0x08, 0xF0, 0x1C, 0x60}; constexpr size_t num_bytes = sizeof(record1) / sizeof(uint8_t); constexpr size_t bits_per_record = 8 * num_bytes - 1; FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_HITS); writer->write_bytes({record1, record1 + num_bytes}); writer->write_end(); writer->write_bytes({record2, record2 + num_bytes}); writer->write_end(); writer->write_bytes({record3, record3 + num_bytes}); writer->write_end(); rewind(tmp); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_HITS, bits_per_record); simd_bits buf(num_bytes * 8); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record1[i]); } ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record2[i]); } ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record3[i]); } ASSERT_FALSE(reader->start_and_read_entire_record(buf)); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatDets_WriteRead_MultipleRecords, { uint8_t record1[]{0x12, 0xAB, 0x00, 0xFF, 0x75}; uint8_t record2[]{0x80, 0xFF, 0x01, 0x56, 0x57}; uint8_t record3[]{0x2F, 0x08, 0xF0, 0x1C, 0x60}; constexpr size_t num_bytes = sizeof(record1) / sizeof(uint8_t); constexpr size_t bits_per_record = 8 * num_bytes - 1; FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS); writer->write_bytes({record1, record1 + num_bytes}); writer->write_end(); writer->write_bytes({record2, record2 + num_bytes}); writer->write_end(); writer->write_bytes({record3, record3 + num_bytes}); writer->write_end(); rewind(tmp); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS, bits_per_record); simd_bits buf(num_bytes * 8); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record1[i]); } ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record2[i]); } ASSERT_TRUE(reader->start_and_read_entire_record(buf)); for (size_t i = 0; i < num_bytes; ++i) { ASSERT_EQ(buf.u8[i], record3[i]); } ASSERT_FALSE(reader->start_and_read_entire_record(buf)); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, Format01_MultipleRecords, { FILE *tmp = tmpfile_with_contents("111011001\n010000000\n101100011\n"); ASSERT_NE(tmp, nullptr); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_01, 9); simd_bits buf(9); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_FALSE(reader->start_and_read_entire_record(buf)); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatDets_MultipleShortRecords, { FILE *tmp = tmpfile_with_contents("shot M0\nshot M1\nshot M0\nshot\n"); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS, 2); simd_bits buf(2); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_EQ(buf[0], 1); ASSERT_EQ(buf[1], 0); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_EQ(buf[0], 0); ASSERT_EQ(buf[1], 1); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_EQ(buf[0], 1); ASSERT_EQ(buf[1], 0); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_EQ(buf[0], 0); ASSERT_EQ(buf[1], 0); ASSERT_FALSE(reader->start_and_read_entire_record(buf)); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatDets_HugeObservables, { FILE *tmp = tmpfile_with_contents("shot L1000\nshot D2 L999\n"); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS, 0, 10, 1500); simd_bits<64> expected(1500); simd_bits buf(2); SparseShot s; bool b = reader->start_and_read_entire_record(s); ASSERT_TRUE(b); ASSERT_EQ(s.hits, (std::vector{})); expected[1000] = true; ASSERT_EQ(s.obs_mask, expected); s.clear(); b = reader->start_and_read_entire_record(s); ASSERT_TRUE(b); ASSERT_EQ(s.hits, (std::vector{2})); expected.clear(); expected[999] = true; ASSERT_EQ(s.obs_mask, expected); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatDets_MultipleResultTypes_D0L0, { FILE *tmp = tmpfile_with_contents("shot D0 D3 D5 L1 L2\n"); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS, 0, 7, 4); simd_bits buf(11); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_EQ(buf[0], 1); ASSERT_EQ(buf[1], 0); ASSERT_EQ(buf[2], 0); ASSERT_EQ(buf[3], 1); ASSERT_EQ(buf[4], 0); ASSERT_EQ(buf[5], 1); ASSERT_EQ(buf[6], 0); ASSERT_EQ(buf[7], 0); ASSERT_EQ(buf[8], 1); ASSERT_EQ(buf[9], 1); ASSERT_EQ(buf[10], 0); rewind(tmp); reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS, 0, 7, 4); SparseShot sparse; reader->start_and_read_entire_record(sparse); ASSERT_EQ(sparse.hits, (std::vector{0, 3, 5})); ASSERT_EQ(sparse.obs_mask.ptr_simd[0], 6); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, Format01_InvalidInput, { FILE *tmp = tmpfile_with_contents("012\n"); ASSERT_NE(tmp, nullptr); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_01, 3); simd_bits buf(3); ASSERT_THROW({ reader->start_and_read_entire_record(buf); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatHits_InvalidInput, { FILE *tmp = tmpfile_with_contents("100,1\n"); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_HITS, 3); simd_bits buf(3); ASSERT_THROW({ reader->start_and_read_entire_record(buf); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatHits_InvalidInput_sparse, { FILE *tmp = tmpfile_with_contents("100,1\n"); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_HITS, 3); SparseShot sparse; ASSERT_THROW({ reader->start_and_read_entire_record(sparse); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatHits_Repeated_Dense, { FILE *tmp = tmpfile_with_contents("1,1\n"); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_HITS, 3); simd_bits buf(3); ASSERT_TRUE(reader->start_and_read_entire_record(buf)); ASSERT_EQ(buf[0], 0); ASSERT_EQ(buf[1], 0); ASSERT_EQ(buf[2], 0); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatHits_Repeated_Sparse, { FILE *tmp = tmpfile_with_contents("1,1\n"); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_HITS, 3); SparseShot sparse; ASSERT_TRUE(reader->start_and_read_entire_record(sparse)); ASSERT_EQ(sparse.hits, (std::vector{1, 1})); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatDets_InvalidInput_Sparse, { FILE *tmp = tmpfile_with_contents("D2\n"); auto r = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS, 3); SparseShot sparse; ASSERT_THROW({ r->start_and_read_entire_record(sparse); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, FormatDets_InvalidInput_Dense, { FILE *tmp = tmpfile_with_contents("D2\n"); auto r = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS, 3); simd_bits buf(3); ASSERT_THROW({ r->start_and_read_entire_record(buf); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, read_records_into_RoundTrip, { size_t n_shots = 100; size_t n_results = 512 - 8; auto rng = INDEPENDENT_TEST_RNG(); auto shot_maj_data = simd_bit_table::random(n_shots, n_results, rng); auto shot_min_data = shot_maj_data.transposed(); for (const auto &kv : format_name_to_enum_map()) { SampleFormat format = kv.second.id; if (format == SampleFormat::SAMPLE_FORMAT_PTB64) { // TODO: support this format. continue; } // Write data to file. FILE *f = tmpfile(); { auto writer = MeasureRecordWriter::make(f, format); for (size_t k = 0; k < n_shots; k++) { writer->write_bytes({shot_maj_data[k].u8, shot_maj_data[k].u8 + n_results / 8}); writer->write_end(); } } // Check that read shot-min data matches written data. rewind(f); { auto reader = MeasureRecordReader::make(f, format, n_results, 0, 0); simd_bit_table read_shot_min_data(n_results, n_shots); size_t n = reader->read_records_into(read_shot_min_data, false); EXPECT_EQ(n, n_shots) << kv.second.name << " (not striped)"; EXPECT_EQ(read_shot_min_data, shot_min_data) << kv.second.name << " (not striped)"; } // Check that read shot-maj data matches written data when transposing. rewind(f); { auto reader = MeasureRecordReader::make(f, format, n_results, 0, 0); simd_bit_table read_shot_maj_data(n_shots, n_results); size_t n = reader->read_records_into(read_shot_maj_data, true); EXPECT_EQ(n, n_shots) << kv.second.name << " (striped)"; EXPECT_EQ(read_shot_maj_data, shot_maj_data) << kv.second.name << " (striped)"; } fclose(f); } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, read_bits_into_bytes_entire_record_across_result_type, { FILE *f = tmpfile_with_contents("shot D1 L1"); auto reader = MeasureRecordReader::make(f, SampleFormat::SAMPLE_FORMAT_DETS, 0, 3, 3); simd_bit_table read(6, 1); size_t n = reader->read_records_into(read, false); fclose(f); ASSERT_EQ(n, 1); ASSERT_EQ(read[0][0], false); ASSERT_EQ(read[1][0], true); ASSERT_EQ(read[2][0], false); ASSERT_EQ(read[3][0], false); ASSERT_EQ(read[4][0], true); ASSERT_EQ(read[5][0], false); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, read_r8_detection_event_data, { FILE *f = tmpfile(); putc(6, f); putc(1, f); putc(4, f); rewind(f); auto reader = MeasureRecordReader::make(f, SampleFormat::SAMPLE_FORMAT_R8, 0, 3, 3); simd_bit_table read(6, 2); size_t n = reader->read_records_into(read, false); fclose(f); ASSERT_EQ(n, 2); ASSERT_EQ(read[0][0], false); ASSERT_EQ(read[1][0], false); ASSERT_EQ(read[2][0], false); ASSERT_EQ(read[3][0], false); ASSERT_EQ(read[4][0], false); ASSERT_EQ(read[5][0], false); ASSERT_EQ(read[0][1], false); ASSERT_EQ(read[1][1], true); ASSERT_EQ(read[2][1], false); ASSERT_EQ(read[3][1], false); ASSERT_EQ(read[4][1], false); ASSERT_EQ(read[5][1], false); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, read_b8_detection_event_data_full_run_together, { FILE *f = tmpfile(); putc(0, f); putc(1, f); putc(2, f); putc(3, f); rewind(f); auto reader = MeasureRecordReader::make(f, SampleFormat::SAMPLE_FORMAT_B8, 0, 27, 0); simd_bit_table read(27, 1); size_t n = reader->read_records_into(read, false); fclose(f); ASSERT_EQ(n, 1); auto t = read.transposed(); ASSERT_EQ(t[0].u8[0], 0); ASSERT_EQ(t[0].u8[1], 1); ASSERT_EQ(t[0].u8[2], 2); ASSERT_EQ(t[0].u8[3], 3); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, start_and_read_entire_record, { auto rng = INDEPENDENT_TEST_RNG(); size_t n = 512 - 8; size_t no = 5; size_t nd = n - no; // Compute expected data. simd_bits test_data(n); biased_randomize_bits(0.1, test_data.u64, test_data.u64 + test_data.num_u64_padded(), rng); SparseShot sparse_test_data; sparse_test_data.obs_mask = simd_bits<64>(no); for (size_t k = 0; k < nd; k++) { if (test_data[k]) { sparse_test_data.hits.push_back(k); } } for (size_t k = 0; k < no; k++) { if (test_data[k + nd]) { sparse_test_data.obs_mask[k] = true; } } for (const auto &kv : format_name_to_enum_map()) { SampleFormat format = kv.second.id; if (format == SampleFormat::SAMPLE_FORMAT_PTB64) { continue; } // Write data to file. FILE *f = tmpfile(); { auto writer = MeasureRecordWriter::make(f, format); writer->begin_result_type('D'); for (size_t k = 0; k < nd; k++) { writer->write_bit(test_data[k]); } writer->begin_result_type('L'); for (size_t k = 0; k < no; k++) { writer->write_bit(test_data[k + nd]); } writer->write_end(); } { auto reader = MeasureRecordReader::make(f, format, 0, nd, no); // Check sparse record read. SparseShot sparse_out; rewind(f); ASSERT_TRUE(reader->start_and_read_entire_record(sparse_out)); ASSERT_EQ(sparse_out, sparse_test_data); ASSERT_FALSE(reader->start_and_read_entire_record(sparse_out)); rewind(f); simd_bits dense_out(n); ASSERT_TRUE(reader->start_and_read_entire_record(dense_out)); for (size_t k = 0; k < n; k++) { ASSERT_EQ(dense_out[k], test_data[k]); } ASSERT_FALSE(reader->start_and_read_entire_record(dense_out)); } fclose(f); } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, start_and_read_entire_record_all_zero, { simd_bits test_data(256); SparseShot sparse_test_data; for (const auto &kv : format_name_to_enum_map()) { SampleFormat format = kv.second.id; if (format == SampleFormat::SAMPLE_FORMAT_PTB64) { continue; } // Write data to file. FILE *f = tmpfile(); { auto writer = MeasureRecordWriter::make(f, format); writer->write_bytes({test_data.u8, test_data.u8 + 32}); writer->write_end(); } { auto reader = MeasureRecordReader::make(f, format, 256, 0, 0); // Check sparse record read. SparseShot sparse_out; rewind(f); ASSERT_TRUE(reader->start_and_read_entire_record(sparse_out)); ASSERT_EQ(sparse_out, sparse_test_data); ASSERT_FALSE(reader->start_and_read_entire_record(sparse_out)); rewind(f); simd_bits dense_out(256); ASSERT_TRUE(reader->start_and_read_entire_record(dense_out)); ASSERT_EQ(dense_out, test_data); ASSERT_FALSE(reader->start_and_read_entire_record(dense_out)); } fclose(f); } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, start_and_read_entire_record_ptb64_dense, { auto rng = INDEPENDENT_TEST_RNG(); FILE *f = tmpfile(); auto saved1 = simd_bits::random(64 * 71, rng); auto saved2 = simd_bits::random(64 * 71, rng); for (size_t k = 0; k < 64 * 71 / 8; k++) { putc(saved1.u8[k], f); } for (size_t k = 0; k < 64 * 71 / 8; k++) { putc(saved2.u8[k], f); } rewind(f); simd_bits loaded(71); auto reader = MeasureRecordReader::make(f, SampleFormat::SAMPLE_FORMAT_PTB64, 71, 0, 0); for (size_t shot = 0; shot < 64; shot++) { ASSERT_TRUE(reader->start_and_read_entire_record(loaded)); for (size_t m = 0; m < 71; m++) { ASSERT_EQ(saved1[m * 64 + shot], loaded[m]); } } for (size_t shot = 0; shot < 64; shot++) { ASSERT_TRUE(reader->start_and_read_entire_record(loaded)); for (size_t m = 0; m < 71; m++) { ASSERT_EQ(saved2[m * 64 + shot], loaded[m]); } } ASSERT_FALSE(reader->start_and_read_entire_record(loaded)); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, start_and_read_entire_record_ptb64_sparse, { auto rng = INDEPENDENT_TEST_RNG(); FILE *tmp = tmpfile(); simd_bit_table ground_truth(71, 64 * 5); { MeasureRecordBatchWriter writer(tmp, 64 * 5, SampleFormat::SAMPLE_FORMAT_PTB64); for (size_t k = 0; k < 71; k++) { ground_truth[k].randomize(64 * 5, rng); writer.batch_write_bit(ground_truth[k]); } writer.write_end(); } rewind(tmp); auto reader = MeasureRecordReader::make(tmp, SampleFormat::SAMPLE_FORMAT_PTB64, 71, 0, 0); for (size_t shot = 0; shot < 64 * 5; shot++) { SparseShot loaded; ASSERT_TRUE(reader->start_and_read_entire_record(loaded)); std::vector expected; for (size_t m = 0; m < 71; m++) { if (ground_truth[m][shot]) { expected.push_back(m); } } ASSERT_EQ(loaded.hits, expected); } SparseShot discard; ASSERT_FALSE(reader->start_and_read_entire_record(discard)); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, read_file_data_into_shot_table_vs_write_table, { auto rng = INDEPENDENT_TEST_RNG(); for (const auto &format_data : format_name_to_enum_map()) { SampleFormat format = format_data.second.id; size_t num_shots = 500; if (format == SampleFormat::SAMPLE_FORMAT_PTB64) { num_shots = 512 + 64; } size_t bits_per_shot = 1000; simd_bit_table expected(num_shots, bits_per_shot); for (size_t shot = 0; shot < num_shots; shot++) { expected[shot].randomize(bits_per_shot, rng); } simd_bit_table expected_transposed = expected.transposed(); RaiiTempNamedFile tmp; FILE *f = fopen(tmp.path.c_str(), "wb"); write_table_data(f, num_shots, bits_per_shot, simd_bits(0), expected_transposed, format, 'M', 'M', 0); fclose(f); f = fopen(tmp.path.c_str(), "rb"); simd_bit_table output(num_shots, bits_per_shot); read_file_data_into_shot_table(f, num_shots, bits_per_shot, format, 'M', output, true); ASSERT_EQ(getc(f), EOF) << format_data.second.name << ", not transposed"; fclose(f); ASSERT_EQ(output, expected) << format_data.second.name << ", not transposed"; f = fopen(tmp.path.c_str(), "rb"); simd_bit_table output_transposed(bits_per_shot, num_shots); read_file_data_into_shot_table(f, num_shots, bits_per_shot, format, 'M', output_transposed, false); ASSERT_EQ(getc(f), EOF) << format_data.second.name << ", yes transposed"; fclose(f); ASSERT_EQ(output_transposed, expected_transposed) << format_data.second.name << ", yes transposed"; } }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, read_windows_newlines_01, { FILE *f = tmpfile(); fprintf(f, "01\r\n01\r\n"); rewind(f); auto reader = MeasureRecordReader::make(f, SampleFormat::SAMPLE_FORMAT_01, 2, 0, 0); simd_bit_table read(2, 2); size_t n = reader->read_records_into(read, false); ASSERT_EQ(n, 2); ASSERT_EQ(read[0][0], false); ASSERT_EQ(read[1][0], true); ASSERT_EQ(read[0][1], false); ASSERT_EQ(read[1][1], true); fclose(f); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, read_windows_newlines_hits, { FILE *f = tmpfile(); fprintf(f, "3\r\n1\r\n"); rewind(f); auto reader = MeasureRecordReader::make(f, SampleFormat::SAMPLE_FORMAT_HITS, 4, 0, 0); simd_bit_table read(4, 2); size_t n = reader->read_records_into(read, false); ASSERT_EQ(n, 2); ASSERT_EQ(read[0][0], false); ASSERT_EQ(read[1][0], false); ASSERT_EQ(read[2][0], false); ASSERT_EQ(read[3][0], true); ASSERT_EQ(read[0][1], false); ASSERT_EQ(read[1][1], true); ASSERT_EQ(read[2][1], false); ASSERT_EQ(read[3][1], false); fclose(f); }) TEST_EACH_WORD_SIZE_W(MeasureRecordReader, read_windows_newlines_dets, { FILE *f = tmpfile(); fprintf(f, "shot M3\r\n\r\n\n shot M1\r\n\n"); rewind(f); auto reader = MeasureRecordReader::make(f, SampleFormat::SAMPLE_FORMAT_DETS, 4, 0, 0); simd_bit_table read(4, 2); size_t n = reader->read_records_into(read, false); ASSERT_EQ(n, 2); ASSERT_EQ(read[0][0], false); ASSERT_EQ(read[1][0], false); ASSERT_EQ(read[2][0], false); ASSERT_EQ(read[3][0], true); ASSERT_EQ(read[0][1], false); ASSERT_EQ(read[1][1], true); ASSERT_EQ(read[2][1], false); ASSERT_EQ(read[3][1], false); fclose(f); }) ================================================ FILE: src/stim/io/measure_record_writer.cc ================================================ /* * 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. */ #include "stim/io/measure_record_writer.h" #include using namespace stim; std::unique_ptr MeasureRecordWriter::make(FILE *out, SampleFormat output_format) { switch (output_format) { case SampleFormat::SAMPLE_FORMAT_01: return std::make_unique(out); case SampleFormat::SAMPLE_FORMAT_B8: return std::make_unique(out); case SampleFormat::SAMPLE_FORMAT_DETS: return std::make_unique(out); case SampleFormat::SAMPLE_FORMAT_HITS: return std::make_unique(out); case SampleFormat::SAMPLE_FORMAT_PTB64: throw std::invalid_argument("SAMPLE_FORMAT_PTB64 incompatible with SingleMeasurementRecord"); case SampleFormat::SAMPLE_FORMAT_R8: return std::make_unique(out); default: throw std::invalid_argument("Sample format not recognized by SingleMeasurementRecord"); } } void MeasureRecordWriter::begin_result_type(char result_type) { } void MeasureRecordWriter::write_bits(uint8_t *data, size_t num_bits) { size_t num_bytes = num_bits >> 3; write_bytes({data, data + num_bytes}); for (size_t b = 0; b < (num_bits & 7); b++) { write_bit((data[num_bytes] >> b) & 1); } } void MeasureRecordWriter::write_bytes(SpanRef data) { for (uint8_t b : data) { for (size_t k = 0; k < 8; k++) { write_bit((b >> k) & 1); } } } MeasureRecordWriterFormat01::MeasureRecordWriterFormat01(FILE *out) : out(out) { } void MeasureRecordWriterFormat01::write_bit(bool b) { putc('0' + b, out); } void MeasureRecordWriterFormat01::write_end() { putc('\n', out); } MeasureRecordWriterFormatB8::MeasureRecordWriterFormatB8(FILE *out) : out(out) { } void MeasureRecordWriterFormatB8::write_bytes(SpanRef data) { if (count == 0) { fwrite(data.ptr_start, sizeof(uint8_t), data.ptr_end - data.ptr_start, out); } else { MeasureRecordWriter::write_bytes(data); } } void MeasureRecordWriterFormatB8::write_bit(bool b) { payload |= uint8_t{b} << count; count++; if (count == 8) { putc(payload, out); count = 0; payload = 0; } } void MeasureRecordWriterFormatB8::write_end() { if (count > 0) { putc(payload, out); count = 0; payload = 0; } } MeasureRecordWriterFormatHits::MeasureRecordWriterFormatHits(FILE *out) : out(out) { } void MeasureRecordWriterFormatHits::write_bytes(SpanRef data) { for (uint8_t b : data) { if (!b) { position += 8; } else { for (size_t k = 0; k < 8; k++) { write_bit((b >> k) & 1); } } } } void MeasureRecordWriterFormatHits::write_bit(bool b) { if (b) { if (first) { first = false; } else { putc(',', out); } fprintf(out, "%lld", (unsigned long long)(position)); } position++; } void MeasureRecordWriterFormatHits::write_end() { putc('\n', out); position = 0; first = true; } MeasureRecordWriterFormatR8::MeasureRecordWriterFormatR8(FILE *out) : out(out) { } void MeasureRecordWriterFormatR8::write_bytes(SpanRef data) { for (uint8_t b : data) { if (!b) { run_length += 8; if (run_length >= 0xFF) { putc(0xFF, out); run_length -= 0xFF; } } else { for (size_t k = 0; k < 8; k++) { write_bit((b >> k) & 1); } } } } void MeasureRecordWriterFormatR8::write_bit(bool b) { if (b) { putc(run_length, out); run_length = 0; } else { run_length++; if (run_length == 255) { putc(run_length, out); run_length = 0; } } } void MeasureRecordWriterFormatR8::write_end() { putc(run_length, out); run_length = 0; } MeasureRecordWriterFormatDets::MeasureRecordWriterFormatDets(FILE *out) : out(out) { } void MeasureRecordWriterFormatDets::begin_result_type(char new_result_type) { result_type = new_result_type; position = 0; } void MeasureRecordWriterFormatDets::write_bytes(SpanRef data) { for (uint8_t b : data) { if (!b) { position += 8; } else { for (size_t k = 0; k < 8; k++) { write_bit((b >> k) & 1); } } } } void MeasureRecordWriterFormatDets::write_bit(bool b) { if (b) { if (first) { fprintf(out, "shot"); first = false; } putc(' ', out); putc(result_type, out); fprintf(out, "%lld", (unsigned long long)(position)); } position++; } void MeasureRecordWriterFormatDets::write_end() { if (first) { fprintf(out, "shot"); } putc('\n', out); position = 0; first = true; } ================================================ FILE: src/stim/io/measure_record_writer.h ================================================ /* * 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. */ #ifndef _STIM_IO_MEASURE_RECORD_WRITER_H #define _STIM_IO_MEASURE_RECORD_WRITER_H #include #include "stim/io/stim_data_formats.h" #include "stim/mem/simd_bit_table.h" #include "stim/mem/span_ref.h" namespace stim { /// Handles writing measurement data to the outside world. /// /// Child classes implement the various output formats. struct MeasureRecordWriter { /// Creates a MeasureRecordWriter that writes the given format into the given FILE*. static std::unique_ptr make(FILE *out, SampleFormat output_format); virtual ~MeasureRecordWriter() = default; /// Writes (or buffers) one measurement result. virtual void write_bit(bool b) = 0; /// Writes (or buffers) multiple measurement results. virtual void write_bytes(SpanRef data); /// Flushes all buffered measurement results and writes any end-of-record markers that are needed (e.g. a newline). virtual void write_end() = 0; /// Writes (or buffers) multiple measurement results. virtual void write_bits(uint8_t *data, size_t num_bits); /// Used to control the DETS format prefix character (M for measurement, D for detector, L for logical observable). /// /// Setting this is understood to reset the "result index" back to 0 so that e.g. listing logical observables after /// detectors results in the first logical observable being L0 instead of L[number-of-detectors]. virtual void begin_result_type(char result_type); }; struct MeasureRecordWriterFormat01 : MeasureRecordWriter { FILE *out; MeasureRecordWriterFormat01(FILE *out); void write_bit(bool b) override; void write_end() override; }; struct MeasureRecordWriterFormatB8 : MeasureRecordWriter { FILE *out; uint8_t payload = 0; uint8_t count = 0; MeasureRecordWriterFormatB8(FILE *out); void write_bytes(SpanRef data) override; void write_bit(bool b) override; void write_end() override; }; struct MeasureRecordWriterFormatHits : MeasureRecordWriter { FILE *out; uint64_t position = 0; bool first = true; MeasureRecordWriterFormatHits(FILE *out); void write_bytes(SpanRef data) override; void write_bit(bool b) override; void write_end() override; }; struct MeasureRecordWriterFormatR8 : MeasureRecordWriter { FILE *out; uint16_t run_length = 0; MeasureRecordWriterFormatR8(FILE *out); void write_bytes(SpanRef data) override; void write_bit(bool b) override; void write_end() override; }; struct MeasureRecordWriterFormatDets : MeasureRecordWriter { FILE *out; uint64_t position = 0; char result_type = 'M'; bool first = true; MeasureRecordWriterFormatDets(FILE *out); void begin_result_type(char result_type) override; void write_bytes(SpanRef data) override; void write_bit(bool b) override; void write_end() override; }; template simd_bit_table transposed_vs_ref( size_t num_samples_raw, const simd_bit_table &table, const simd_bits &reference_sample) { auto result = table.transposed(); for (size_t s = 0; s < num_samples_raw; s++) { result[s].word_range_ref(0, reference_sample.num_simd_words) ^= reference_sample; } return result; } template void write_table_data( FILE *out, size_t num_shots, size_t num_measurements, const simd_bits &reference_sample, const simd_bit_table &table, SampleFormat format, char dets_prefix_1, char dets_prefix_2, size_t dets_prefix_transition) { if (format == SampleFormat::SAMPLE_FORMAT_PTB64) { if (num_shots % 64 != 0) { throw std::invalid_argument("shots must be a multiple of 64 to use ptb64 format."); } auto f64 = num_shots >> 6; for (size_t s = 0; s < f64; s++) { for (size_t m = 0; m < num_measurements; m++) { uint64_t v = table[m].u64[s]; if (m < reference_sample.num_bits_padded() && reference_sample[m]) { v = ~v; } fwrite(&v, 1, 64 >> 3, out); } } } else { auto result = transposed_vs_ref(num_shots, table, reference_sample); if (dets_prefix_transition == 0) { dets_prefix_transition = num_measurements; dets_prefix_1 = dets_prefix_2; } else if (dets_prefix_1 == dets_prefix_2 || dets_prefix_transition >= num_measurements) { dets_prefix_transition = num_measurements; } for (size_t shot = 0; shot < num_shots; shot++) { auto w = MeasureRecordWriter::make(out, format); w->begin_result_type(dets_prefix_1); size_t n8 = dets_prefix_transition >> 3; uint8_t *p = result[shot].u8; w->write_bytes({p, p + n8}); size_t m = n8 << 3; while (m < dets_prefix_transition) { w->write_bit(result[shot][m]); m++; } w->begin_result_type(dets_prefix_2); while (m < num_measurements) { w->write_bit(result[shot][m]); m++; } w->write_end(); } } } } // namespace stim #endif ================================================ FILE: src/stim/io/measure_record_writer.test.cc ================================================ /* * 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. */ #include "stim/io/measure_record_writer.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(MeasureRecordWriter, Format01) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_01); uint8_t bytes[]{0xF8}; writer->write_bytes({bytes}); writer->write_bit(false); writer->write_bytes({bytes}); writer->write_bit(true); writer->write_end(); ASSERT_EQ(rewind_read_close(tmp), "000111110000111111\n"); } TEST(MeasureRecordWriter, FormatB8) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_B8); uint8_t bytes[]{0xF8}; writer->write_bytes({bytes}); writer->write_bit(false); writer->write_bytes({bytes}); writer->write_bit(true); writer->write_end(); auto s = rewind_read_close(tmp); ASSERT_EQ(s.size(), 3); ASSERT_EQ(s[0], (char)0xF8); ASSERT_EQ(s[1], (char)0xF0); ASSERT_EQ(s[2], (char)0x03); } TEST(MeasureRecordWriter, FormatHits) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_HITS); uint8_t bytes[]{0xF8}; writer->write_bytes({bytes}); writer->write_bit(false); writer->write_bytes({bytes}); writer->write_bit(true); writer->write_end(); ASSERT_EQ(rewind_read_close(tmp), "3,4,5,6,7,12,13,14,15,16,17\n"); } TEST(MeasureRecordWriter, FormatDets) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS); uint8_t bytes[]{0xF8}; writer->begin_result_type('D'); writer->write_bytes({bytes}); writer->write_bit(false); writer->write_bytes({bytes}); writer->begin_result_type('L'); writer->write_bit(false); writer->write_bit(true); writer->write_end(); ASSERT_EQ(rewind_read_close(tmp), "shot D3 D4 D5 D6 D7 D12 D13 D14 D15 D16 L1\n"); } TEST(MeasureRecordWriter, FormatDets_EmptyRecords) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS); writer->write_end(); writer->write_end(); writer->write_end(); ASSERT_EQ(rewind_read_close(tmp), "shot\nshot\nshot\n"); } TEST(MeasureRecordWriter, FormatDets_MultipleRecords) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_DETS); // First record writer->begin_result_type('D'); writer->write_bit(false); writer->write_bit(false); writer->write_bit(true); writer->begin_result_type('L'); writer->write_bit(false); writer->write_bit(true); writer->write_end(); // Second record writer->begin_result_type('D'); writer->write_bit(true); writer->write_bit(false); writer->write_bit(false); writer->write_bit(true); writer->begin_result_type('L'); writer->write_bit(true); writer->write_bit(false); writer->write_bit(true); writer->write_end(); ASSERT_EQ(rewind_read_close(tmp), "shot D2 L1\nshot D0 D3 L0 L2\n"); } TEST(MeasureRecordWriter, FormatR8) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_R8); uint8_t bytes[]{0xF8}; writer->write_bytes({bytes}); writer->write_bit(false); writer->write_bytes({bytes}); writer->write_bit(true); writer->write_end(); auto s = rewind_read_close(tmp); ASSERT_EQ(s.size(), 12); ASSERT_EQ(s[0], (char)3); ASSERT_EQ(s[1], (char)0); ASSERT_EQ(s[2], (char)0); ASSERT_EQ(s[3], (char)0); ASSERT_EQ(s[4], (char)0); ASSERT_EQ(s[5], (char)4); ASSERT_EQ(s[6], (char)0); ASSERT_EQ(s[7], (char)0); ASSERT_EQ(s[8], (char)0); ASSERT_EQ(s[9], (char)0); ASSERT_EQ(s[10], (char)0); ASSERT_EQ(s[11], (char)0); } TEST(MeasureRecordWriter, FormatR8_LongGap) { FILE *tmp = tmpfile(); auto writer = MeasureRecordWriter::make(tmp, SampleFormat::SAMPLE_FORMAT_R8); uint8_t bytes[]{0, 0, 0, 0, 0, 0, 0, 0}; writer->write_bytes({bytes, bytes + 8}); writer->write_bytes({bytes, bytes + 8}); writer->write_bytes({bytes, bytes + 8}); writer->write_bytes({bytes, bytes + 8}); writer->write_bytes({bytes, bytes + 8}); writer->write_bytes({bytes, bytes + 8}); writer->write_bytes({bytes, bytes + 8}); writer->write_bytes({bytes, bytes + 8}); writer->write_bit(true); writer->write_bytes({bytes, bytes + 4}); writer->write_end(); auto s = rewind_read_close(tmp); ASSERT_EQ(s.size(), 4); ASSERT_EQ(s[0], (char)255); ASSERT_EQ(s[1], (char)255); ASSERT_EQ(s[2], (char)2); ASSERT_EQ(s[3], (char)32); } TEST_EACH_WORD_SIZE_W(MeasureRecordWriter, write_table_data_small, { simd_bit_table results(4, 5); simd_bits ref_sample(0); results[1][0] ^= 1; results[1][1] ^= 1; results[1][2] ^= 1; results[1][3] ^= 1; results[1][4] ^= 1; FILE *tmp; tmp = tmpfile(); write_table_data(tmp, 5, 4, ref_sample, results, SampleFormat::SAMPLE_FORMAT_01, 'M', 'M', 0); ASSERT_EQ(rewind_read_close(tmp), "0100\n0100\n0100\n0100\n0100\n"); tmp = tmpfile(); write_table_data(tmp, 5, 4, ref_sample, results, SampleFormat::SAMPLE_FORMAT_HITS, 'M', 'M', 0); ASSERT_EQ(rewind_read_close(tmp), "1\n1\n1\n1\n1\n"); tmp = tmpfile(); write_table_data(tmp, 5, 4, ref_sample, results, SampleFormat::SAMPLE_FORMAT_DETS, 'M', 'M', 0); ASSERT_EQ(rewind_read_close(tmp), "shot M1\nshot M1\nshot M1\nshot M1\nshot M1\n"); tmp = tmpfile(); write_table_data(tmp, 5, 4, ref_sample, results, SampleFormat::SAMPLE_FORMAT_DETS, 'D', 'L', 1); ASSERT_EQ(rewind_read_close(tmp), "shot L0\nshot L0\nshot L0\nshot L0\nshot L0\n"); tmp = tmpfile(); write_table_data(tmp, 5, 4, ref_sample, results, SampleFormat::SAMPLE_FORMAT_R8, 'M', 'M', 0); ASSERT_EQ(rewind_read_close(tmp), "\x01\x02\x01\x02\x01\x02\x01\x02\x01\x02"); tmp = tmpfile(); write_table_data(tmp, 5, 4, ref_sample, results, SampleFormat::SAMPLE_FORMAT_B8, 'M', 'M', 0); ASSERT_EQ(rewind_read_close(tmp), "\x02\x02\x02\x02\x02"); tmp = tmpfile(); write_table_data(tmp, 64, 4, ref_sample, results, SampleFormat::SAMPLE_FORMAT_PTB64, 'M', 'M', 0); ASSERT_EQ( rewind_read_close(tmp), std::string( "\0\0\0\0\0\0\0\0" "\x1F\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0", 8 * 4)); }) TEST_EACH_WORD_SIZE_W(MeasureRecordWriter, write_table_data_large, { simd_bit_table results(100, 2); simd_bits ref_sample(100); ref_sample[2] ^= true; ref_sample[3] ^= true; ref_sample[5] ^= true; ref_sample[7] ^= true; ref_sample[11] ^= true; results[7][1] ^= true; FILE *tmp; tmp = tmpfile(); write_table_data(tmp, 2, 100, ref_sample, results, SampleFormat::SAMPLE_FORMAT_01, 'M', 'M', 0); ASSERT_EQ( rewind_read_close(tmp), "0011010100" "0100000000" "0000000000" "0000000000" "0000000000" "0000000000" "0000000000" "0000000000" "0000000000" "0000000000\n" "0011010000" "0100000000" "0000000000" "0000000000" "0000000000" "0000000000" "0000000000" "0000000000" "0000000000" "0000000000\n"); tmp = tmpfile(); write_table_data(tmp, 2, 100, ref_sample, results, SampleFormat::SAMPLE_FORMAT_HITS, 'M', 'M', 0); ASSERT_EQ(rewind_read_close(tmp), "2,3,5,7,11\n2,3,5,11\n"); tmp = tmpfile(); write_table_data(tmp, 2, 100, ref_sample, results, SampleFormat::SAMPLE_FORMAT_DETS, 'D', 'L', 5); ASSERT_EQ(rewind_read_close(tmp), "shot D2 D3 L0 L2 L6\nshot D2 D3 L0 L6\n"); tmp = tmpfile(); write_table_data(tmp, 2, 100, ref_sample, results, SampleFormat::SAMPLE_FORMAT_DETS, 'D', 'L', 90); ASSERT_EQ(rewind_read_close(tmp), "shot D2 D3 D5 D7 D11\nshot D2 D3 D5 D11\n"); tmp = tmpfile(); write_table_data(tmp, 2, 100, ref_sample, results, SampleFormat::SAMPLE_FORMAT_R8, 'M', 'M', 0); ASSERT_EQ( rewind_read_close(tmp), std::string( "\x02\x00\x01\x01\x03\x58" "\x02\x00\x01\x05\x58", 11)); tmp = tmpfile(); write_table_data(tmp, 2, 100, ref_sample, results, SampleFormat::SAMPLE_FORMAT_B8, 'M', 'M', 0); ASSERT_EQ( rewind_read_close(tmp), std::string( "\xAC\x08\0\0\0\0\0\0\0\0\0\0\0" "\x2C\x08\0\0\0\0\0\0\0\0\0\0\0", 26)); tmp = tmpfile(); write_table_data(tmp, 64, 100, ref_sample, results, SampleFormat::SAMPLE_FORMAT_PTB64, 'M', 'M', 0); auto actual = rewind_read_close(tmp); ASSERT_EQ( actual, std::string( "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" "\0\0\0\0\0\0\0\0" "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" "\0\0\0\0\0\0\0\0" "\xFD\xFF\xFF\xFF\xFF\xFF\xFF\xFF" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0", 8 * 100)); }) TEST(MeasureRecordWriter, write_bits_01_a) { FILE *f = tmpfile(); uint8_t data[]{0x0, 0xFF}; auto writer = MeasureRecordWriter::make(f, SampleFormat::SAMPLE_FORMAT_01); writer->write_bits(&data[0], 11); writer->write_end(); ASSERT_EQ(rewind_read_close(f), "00000000111\n"); } TEST(MeasureRecordWriter, write_bits_01_b) { FILE *f = tmpfile(); uint8_t data[]{0xFF, 0x0}; auto writer = MeasureRecordWriter::make(f, SampleFormat::SAMPLE_FORMAT_01); writer->write_bits(&data[0], 11); writer->write_end(); ASSERT_EQ(rewind_read_close(f), "11111111000\n"); } TEST(MeasureRecordWriter, write_bits_b8_a) { FILE *f = tmpfile(); uint8_t data[]{0x0, 0xFF}; auto writer = MeasureRecordWriter::make(f, SampleFormat::SAMPLE_FORMAT_B8); writer->write_bits(&data[0], 11); writer->write_end(); ASSERT_EQ(rewind_read_close(f), std::string("\x00\x07", 2)); } TEST(MeasureRecordWriter, write_bits_b8_b) { FILE *f = tmpfile(); uint8_t data[]{0xFF, 0x0}; auto writer = MeasureRecordWriter::make(f, SampleFormat::SAMPLE_FORMAT_B8); writer->write_bits(&data[0], 11); writer->write_end(); ASSERT_EQ(rewind_read_close(f), std::string("\xFF\x00", 2)); } TEST(MeasureRecordWriter, write_bits_r8_a) { FILE *f = tmpfile(); uint8_t data[]{0x0, 0xFF}; auto writer = MeasureRecordWriter::make(f, SampleFormat::SAMPLE_FORMAT_R8); writer->write_bits(&data[0], 11); writer->write_end(); ASSERT_EQ(rewind_read_close(f), std::string("\x08\x00\x00\x00", 4)); } TEST(MeasureRecordWriter, write_bits_r8_b) { FILE *f = tmpfile(); uint8_t data[]{0xFF, 0x0}; auto writer = MeasureRecordWriter::make(f, SampleFormat::SAMPLE_FORMAT_R8); writer->write_bits(&data[0], 11); writer->write_end(); ASSERT_EQ(rewind_read_close(f), std::string("\x00\x00\x00\x00\x00\x00\x00\x00\x03", 9)); } ================================================ FILE: src/stim/io/raii_file.cc ================================================ // 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. #include "stim/io/raii_file.h" #include using namespace stim; RaiiFile::RaiiFile(FILE *claim_ownership) : f(claim_ownership), responsible_for_closing(true) { } RaiiFile::RaiiFile(RaiiFile &&other) noexcept : f(other.f), responsible_for_closing(other.responsible_for_closing) { other.responsible_for_closing = false; other.f = nullptr; } RaiiFile::RaiiFile(const char *optional_path, const char *mode) : f(nullptr), responsible_for_closing(true) { open(optional_path, mode); } RaiiFile::RaiiFile(std::string_view optional_path, const char *mode) : f(nullptr), responsible_for_closing(true) { open(optional_path, mode); } void RaiiFile::open(const char *optional_path, const char *mode) { done(); if (optional_path == nullptr) { return; } open(std::string_view(optional_path), mode); } void RaiiFile::open(std::string_view optional_path, const char *mode) { done(); if (optional_path.empty()) { return; } // TODO: avoid needing the string copy (for null termination) to safely open the file. f = fopen(std::string(optional_path).c_str(), mode); if (f == nullptr) { std::stringstream ss; ss << "Failed to open '"; ss << optional_path; ss << "' for "; if (*mode == 'r') { ss << "reading."; } else { ss << "writing."; } throw std::invalid_argument(ss.str()); } } RaiiFile::~RaiiFile() { done(); } void RaiiFile::done() { if (f != nullptr && responsible_for_closing) { fclose(f); f = nullptr; responsible_for_closing = false; } } ================================================ FILE: src/stim/io/raii_file.h ================================================ // 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. #ifndef _STIM_IO_RAII_FILE #define _STIM_IO_RAII_FILE #include #include namespace stim { struct RaiiFile { FILE *f; bool responsible_for_closing; RaiiFile(const char *optional_path, const char *mode); RaiiFile(std::string_view optional_path, const char *mode); RaiiFile(FILE *claim_ownership); RaiiFile(const RaiiFile &other) = delete; RaiiFile(RaiiFile &&other) noexcept; ~RaiiFile(); void open(std::string_view optional_path, const char *mode); void open(const char *optional_path, const char *mode); void done(); }; } // namespace stim #endif ================================================ FILE: src/stim/io/read_write.pybind.cc ================================================ // 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. #include "stim/io/read_write.pybind.h" #include "stim/circuit/circuit.h" #include "stim/io/measure_record_reader.h" #include "stim/io/measure_record_writer.h" #include "stim/io/raii_file.h" #include "stim/mem/simd_bits.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" using namespace stim; using namespace stim_pybind; std::string path_to_string(const pybind11::object &path_obj) { try { return pybind11::cast(path_obj); } catch (pybind11::cast_error &ex) { } auto py_path = pybind11::module::import("pathlib").attr("Path"); if (pybind11::isinstance(path_obj, py_path)) { return pybind11::cast(pybind11::str(path_obj)); } throw std::invalid_argument("Not a str or pathlib.Path: " + pybind11::cast(pybind11::str(path_obj))); } pybind11::object buffer_slice_to_numpy( size_t num_shots, size_t shot_num_stride_bytes, size_t shot_bit_copy_offset, size_t shot_bit_copy_length, bool bit_packed, SpanRef immovable_buffer) { size_t num_bytes_copied_per_shot = (shot_bit_copy_length + 7) / 8; if (bit_packed) { uint8_t *buffer = new uint8_t[num_bytes_copied_per_shot * num_shots]; memset(buffer, 0, num_bytes_copied_per_shot * num_shots); for (size_t s = 0; s < num_shots; s++) { size_t t = s * num_bytes_copied_per_shot * 8; for (size_t k = 0; k < shot_bit_copy_length; k++) { auto k2 = k + shot_bit_copy_offset; auto bi = s * shot_num_stride_bytes + k2 / 8; bool bit = (immovable_buffer[bi] >> (k2 % 8)) & 1; buffer[t / 8] |= bit << (t & 7); t++; } } pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast(f); }); return pybind11::array_t( {(pybind11::ssize_t)num_shots, (pybind11::ssize_t)num_bytes_copied_per_shot}, {(pybind11::ssize_t)num_bytes_copied_per_shot, (pybind11::ssize_t)1}, buffer, free_when_done); } else { bool *buffer = new bool[shot_bit_copy_length * num_shots]; size_t t = 0; for (size_t s = 0; s < num_shots; s++) { for (size_t k = 0; k < shot_bit_copy_length; k++) { auto k2 = k + shot_bit_copy_offset; auto bi = s * shot_num_stride_bytes + k2 / 8; bool bit = (immovable_buffer[bi] >> (k2 % 8)) & 1; buffer[t++] = bit; } } pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast(f); }); return pybind11::array_t( {(pybind11::ssize_t)num_shots, (pybind11::ssize_t)shot_bit_copy_length}, {(pybind11::ssize_t)shot_bit_copy_length, (pybind11::ssize_t)1}, buffer, free_when_done); } } pybind11::object read_shot_data_file( const pybind11::object &path_obj, const char *format, const pybind11::handle &num_measurements, const pybind11::handle &num_detectors, const pybind11::handle &num_observables, bool separate_observables, bool bit_packed, bool _legacy_bit_pack) { auto path = path_to_string(path_obj); auto parsed_format = format_to_enum(format); bit_packed |= _legacy_bit_pack; if (num_measurements.is_none() && num_detectors.is_none() && num_observables.is_none()) { throw std::invalid_argument("Must specify num_measurements, num_detectors, num_observables."); } size_t nm = num_measurements.is_none() ? 0 : pybind11::cast(num_measurements); size_t nd = num_detectors.is_none() ? 0 : pybind11::cast(num_detectors); size_t no = num_observables.is_none() ? 0 : pybind11::cast(num_observables); std::vector full_buffer; size_t num_bits_per_shot = nm + nd + no; size_t num_bytes_per_shot = (num_bits_per_shot + 7) / 8; size_t num_shots = 0; { RaiiFile f(path.c_str(), "rb"); auto reader = MeasureRecordReader::make(f.f, parsed_format, nm, nd, no); simd_bits buffer(num_bits_per_shot); while (true) { if (!reader->start_and_read_entire_record(buffer)) { break; } full_buffer.insert(full_buffer.end(), buffer.u8, buffer.u8 + num_bytes_per_shot); num_shots += 1; } } if (separate_observables) { pybind11::object dets = buffer_slice_to_numpy(num_shots, num_bytes_per_shot, 0, num_bits_per_shot - no, bit_packed, full_buffer); pybind11::object obs = buffer_slice_to_numpy(num_shots, num_bytes_per_shot, num_bits_per_shot - no, no, bit_packed, full_buffer); return pybind11::make_tuple(dets, obs); } return buffer_slice_to_numpy(num_shots, num_bytes_per_shot, 0, num_bits_per_shot, bit_packed, full_buffer); } void write_shot_data_file( const pybind11::object &data, const pybind11::object &path_obj, const char *format, const pybind11::handle &num_measurements, const pybind11::handle &num_detectors, const pybind11::handle &num_observables) { auto parsed_format = format_to_enum(format); auto path = path_to_string(path_obj); if (num_measurements.is_none() && num_detectors.is_none() && num_observables.is_none()) { throw std::invalid_argument("Must specify num_measurements, num_detectors, num_observables."); } size_t nm = num_measurements.is_none() ? 0 : pybind11::cast(num_measurements); size_t nd = num_detectors.is_none() ? 0 : pybind11::cast(num_detectors); size_t no = num_observables.is_none() ? 0 : pybind11::cast(num_observables); if (nm != 0 && (nd != 0 || no != 0)) { throw std::invalid_argument("num_measurements and (num_detectors or num_observables)"); } size_t num_bits_per_shot = nm + nd + no; size_t num_shots; simd_bit_table buffer = numpy_array_to_transposed_simd_table(data, num_bits_per_shot, &num_shots); RaiiFile f(path.c_str(), "wb"); simd_bits unused(0); write_table_data( f.f, num_shots, num_bits_per_shot, unused, buffer, parsed_format, nm == 0 ? 'D' : 'M', nm == 0 ? 'L' : 'M', nm + nd); } void stim_pybind::pybind_read_write(pybind11::module &m) { m.def( "read_shot_data_file", &read_shot_data_file, pybind11::kw_only(), pybind11::arg("path"), pybind11::arg("format"), pybind11::arg("num_measurements") = pybind11::none(), pybind11::arg("num_detectors") = pybind11::none(), pybind11::arg("num_observables") = pybind11::none(), pybind11::arg("separate_observables") = false, pybind11::arg("bit_packed") = false, pybind11::arg("bit_pack") = false, // Legacy argument for backwards compat. clean_doc_string(R"DOC( Reads shot data, such as measurement samples, from a file. @overload def read_shot_data_file(*, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0) -> np.ndarray: @overload def read_shot_data_file(*, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, separate_observables: Literal[True]) -> Tuple[np.ndarray, np.ndarray]: @signature def read_shot_data_file(*, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], bit_packed: bool = False, num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0, separate_observables: bool = False) -> Union[Tuple[np.ndarray, np.ndarray], np.ndarray]: Args: path: The path to the file to read the data from. format: The format that the data is stored in, such as 'b8'. See https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md bit_packed: Defaults to false. Determines whether the result is a bool_ numpy array with one bit per byte, or a uint8 numpy array with 8 bits per byte. num_measurements: How many measurements there are per shot. num_detectors: How many detectors there are per shot. num_observables: How many observables there are per shot. Note that this only refers to observables *stored in the file*, not to observables from the original circuit that was sampled. separate_observables: When set to True, the result is a tuple of two arrays, one containing the detection event data and the other containing the observable data, instead of a single array. Returns: If separate_observables=True: A tuple (dets, obs) of numpy arrays containing the loaded data. If bit_packed=False: dets.dtype = np.bool_ dets.shape = (num_shots, num_measurements + num_detectors) det bit b from shot s is at dets[s, b] obs.dtype = np.bool_ obs.shape = (num_shots, num_observables) obs bit b from shot s is at dets[s, b] If bit_packed=True: dets.dtype = np.uint8 dets.shape = (num_shots, math.ceil( (num_measurements + num_detectors) / 8)) obs.dtype = np.uint8 obs.shape = (num_shots, math.ceil(num_observables / 8)) det bit b from shot s is at dets[s, b // 8] & (1 << (b % 8)) obs bit b from shot s is at obs[s, b // 8] & (1 << (b % 8)) If separate_observables=False: A numpy array containing the loaded data. If bit_packed=False: dtype = np.bool_ shape = (num_shots, num_measurements + num_detectors + num_observables) bit b from shot s is at result[s, b] If bit_packed=True: dtype = np.uint8 shape = (num_shots, math.ceil( (num_measurements + num_detectors + num_observables) / 8)) bit b from shot s is at result[s, b // 8] & (1 << (b % 8)) Examples: >>> import stim >>> import pathlib >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = pathlib.Path(d) / 'shots' ... with open(path, 'w') as f: ... print("0000", file=f) ... print("0101", file=f) ... ... read = stim.read_shot_data_file( ... path=str(path), ... format='01', ... num_measurements=4) >>> read array([[False, False, False, False], [False, True, False, True]]) )DOC") .data()); m.def( "write_shot_data_file", &write_shot_data_file, pybind11::kw_only(), pybind11::arg("data"), pybind11::arg("path"), pybind11::arg("format"), pybind11::arg("num_measurements") = pybind11::none(), pybind11::arg("num_detectors") = pybind11::none(), pybind11::arg("num_observables") = pybind11::none(), clean_doc_string(R"DOC( Writes shot data, such as measurement samples, to a file. @signature def write_shot_data_file(*, data: np.ndarray, path: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"], num_measurements: int = 0, num_detectors: int = 0, num_observables: int = 0) -> None: Args: data: The data to write to the file. This must be a numpy array. The dtype of the array determines whether or not the data is bit packed, and the shape must match the bits per shot. dtype=np.bool_: Not bit packed. Shape must be (num_shots, num_measurements + num_detectors + num_observables). dtype=np.uint8: Yes bit packed. Shape must be (num_shots, math.ceil( (num_measurements + num_detectors + num_observables) / 8)). path: The path to the file to write the data to. format: The format that the data is stored in, such as 'b8'. See https://github.com/quantumlib/Stim/blob/main/doc/result_formats.md num_measurements: How many measurements there are per shot. num_detectors: How many detectors there are per shot. num_observables: How many observables there are per shot. Note that this only refers to observables *in the given shot data*, not to observables from the original circuit that was sampled. Examples: >>> import stim >>> import pathlib >>> import tempfile >>> import numpy as np >>> with tempfile.TemporaryDirectory() as d: ... path = pathlib.Path(d) / 'shots' ... shot_data = np.array([ ... [0, 1, 0], ... [0, 1, 1], ... ], dtype=np.bool_) ... ... stim.write_shot_data_file( ... path=str(path), ... data=shot_data, ... format='01', ... num_measurements=3) ... ... with open(path) as f: ... written = f.read() >>> written '010\n011\n' )DOC") .data()); } ================================================ FILE: src/stim/io/read_write.pybind.h ================================================ // 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. #ifndef _STIM_IO_READ_WRITE_PYBIND_H #define _STIM_IO_READ_WRITE_PYBIND_H #include #include #include "stim/mem/simd_bit_table.h" namespace stim_pybind { void pybind_read_write(pybind11::module &m); } // namespace stim_pybind #endif ================================================ FILE: src/stim/io/read_write_pytest.py ================================================ # 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. import itertools import pathlib import pytest import tempfile import numpy as np import stim def test_read_shot_data_file_01(): with tempfile.TemporaryDirectory() as d: path = str(pathlib.Path(d) / 'file.01') with open(path, 'w') as f: print('0001110011', file=f) print('0000000000', file=f) with pytest.raises(ValueError, match='expected data length'): stim.read_shot_data_file( path=path, format='01', num_measurements=9, ) with pytest.raises(ValueError, match='ended in middle'): stim.read_shot_data_file( path=path, format='01', num_measurements=11, ) result = stim.read_shot_data_file( path=path, format='01', num_measurements=10, ) assert result.dtype == np.bool_ assert result.shape == (2, 10) np.testing.assert_array_equal( result, [ [0, 0, 0, 1, 1, 1, 0, 0, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ]) result = stim.read_shot_data_file( path=path, format='01', num_measurements=10, bit_pack=True, ) assert result.dtype == np.uint8 assert result.shape == (2, 2) np.testing.assert_array_equal( result, [ [0x38, 0x03], [0x00, 0x00], ]) def test_read_shot_data_file_dets(): with tempfile.TemporaryDirectory() as d: path = str(pathlib.Path(d) / 'file.01') with open(path, 'w') as f: print('shot', file=f) print('shot D0 D5 L0', file=f) with pytest.raises(ValueError, match='D0'): stim.read_shot_data_file( path=path, format='dets', num_measurements=9, ) with pytest.raises(ValueError, match='D5'): stim.read_shot_data_file( path=path, format='dets', num_measurements=0, num_detectors=5, num_observables=2, ) result = stim.read_shot_data_file( path=path, format='dets', num_measurements=0, num_detectors=10, num_observables=2, ) assert result.dtype == np.bool_ assert result.shape == (2, 12) np.testing.assert_array_equal( result, [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0], ]) # Separate observables result_dets, result_obs = stim.read_shot_data_file( path=path, format='dets', num_measurements=0, num_detectors=10, num_observables=2, separate_observables=True, ) assert result_dets.dtype == np.bool_ assert result_dets.shape == (2, 10) np.testing.assert_array_equal( result_dets, [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 1, 0, 0, 0, 0], ]) assert result_obs.dtype == np.bool_ assert result_obs.shape == (2, 2) np.testing.assert_array_equal( result_obs, [ [0, 0], [1, 0], ]) # Separate observables bit packed. result_dets, result_obs = stim.read_shot_data_file( path=path, format='dets', num_measurements=0, num_detectors=10, num_observables=2, separate_observables=True, bit_packed=True, ) assert result_dets.dtype == np.uint8 assert result_dets.shape == (2, 2) np.testing.assert_array_equal( result_dets, [ [0, 0], [0b00100001, 0], ]) assert result_obs.dtype == np.uint8 assert result_obs.shape == (2, 1) np.testing.assert_array_equal( result_obs, [ [0], [1], ]) def test_write_shot_data_file_01(): with tempfile.TemporaryDirectory() as d: path = str(pathlib.Path(d) / 'file.01') with pytest.raises(ValueError, match='4 bits per shot.+bool_'): stim.write_shot_data_file( data=np.array([ [0, 1, 0], [0, 1, 1], ], dtype=np.bool_), path=path, format='01', num_measurements=4, ) with pytest.raises(ValueError, match='4 bits per shot.+uint8'): stim.write_shot_data_file( data=np.array([ [0, 1, 0], [0, 1, 1], ], dtype=np.uint8), path=path, format='01', num_measurements=4, ) with pytest.raises(ValueError, match='num_measurements and'): stim.write_shot_data_file( data=np.array([ [0, 1, 0], [0, 1, 1], ], dtype=np.bool_), path=path, format='01', num_measurements=1, num_detectors=2, ) stim.write_shot_data_file( data=np.array([ [0, 1, 0], [0, 1, 1], ], dtype=np.bool_), path=path, format='01', num_measurements=3, ) with open(path) as f: assert f.read() == '010\n011\n' stim.write_shot_data_file( data=np.array([ [0x38, 0x03], [0x00, 0x00], ], dtype=np.uint8), path=path, format='01', num_measurements=13, ) with open(path) as f: assert f.read() == '0001110011000\n0000000000000\n' def test_read_data_file_partial_b8(): with tempfile.TemporaryDirectory() as d: path = pathlib.Path(d) / 'tmp.b8' with open(path, 'wb') as f: f.write(b'\0' * 273) with pytest.raises(ValueError, match="middle of record"): stim.read_shot_data_file( path=str(path), format="b8", num_detectors=2185, num_observables=0, ) def test_read_data_file_big_b8(): with tempfile.TemporaryDirectory() as d: path = pathlib.Path(d) / 'tmp.b8' with open(path, 'wb') as f: f.write(b'\0' * 274000) stim.read_shot_data_file( path=str(path), format="b8", num_detectors=2185, num_observables=0, ) def test_read_01_shots(): with tempfile.TemporaryDirectory() as d: path = pathlib.Path(d) / 'shots' with open(path, 'w') as f: print("0000", file=f) print("0101", file=f) read = stim.read_shot_data_file( path=str(path), format='01', num_measurements=4) np.testing.assert_array_equal( read, [[0, 0, 0, 0], [0, 1, 0, 1]] ) @pytest.mark.parametrize("data_format,bit_packed,path_type", itertools.product( ["01", "b8", "r8", "ptb64", "hits", "dets"], [False, True], ["str", "path"])) def test_read_write_shots_fuzzing(data_format: str, bit_packed: bool, path_type: str): with tempfile.TemporaryDirectory() as d: file_path = pathlib.Path(d) / 'shots' num_shots = 320 num_measurements = 500 data = np.random.randint(2, size=(num_shots, num_measurements), dtype=np.bool_) if bit_packed: packed_data = np.packbits(data, axis=1, bitorder='little') else: packed_data = data if path_type == "path": path_arg = file_path elif path_type == "str": path_arg = str(file_path) else: raise NotImplementedError(f'{path_type=}') stim.write_shot_data_file( data=packed_data, path=path_arg, format=data_format, num_measurements=num_measurements, ) round_trip_data = stim.read_shot_data_file( path=path_arg, format=data_format, num_measurements=num_measurements, bit_pack=bit_packed, ) np.testing.assert_array_equal(packed_data, round_trip_data) @pytest.mark.parametrize("data_format,num_bits_per_shot", itertools.product( ["01", "b8", "r8", "ptb64", "hits", "dets"], [11, 511, 512, 513], )) def test_read_write_shots_fuzzing_vs_python_references(data_format: str, num_bits_per_shot: int): with tempfile.TemporaryDirectory() as d: path = pathlib.Path(d) / 'shots' num_shots = 320 data = np.random.randint(2, size=(num_shots, num_bits_per_shot), dtype=np.bool_) stim.write_shot_data_file( data=data, path=path, format=data_format, num_detectors=num_bits_per_shot, ) g = {} exec(stim._UNSTABLE_raw_format_data()[data_format]['save_example'], g) save_method = g[f'save_{data_format}'] if data_format == 'dets': reference_written_data = save_method(data, num_detectors=num_bits_per_shot, num_observables=0) else: reference_written_data = save_method(data) with open(path, 'rb' if isinstance(reference_written_data, bytes) else 'r') as f: actual_written_data = f.read() assert actual_written_data == reference_written_data g = {} exec(stim._UNSTABLE_raw_format_data()[data_format]['parse_example'], g) read_method = g[f'parse_{data_format}'] if data_format == "01": reference_read_data = read_method(actual_written_data) elif data_format == "dets": reference_read_data = read_method(actual_written_data, num_detectors=num_bits_per_shot, num_observables=0) else: reference_read_data = read_method(actual_written_data, bits_per_shot=num_bits_per_shot) actual_read_data = stim.read_shot_data_file( path=path, format=data_format, num_detectors=num_bits_per_shot, ) np.testing.assert_array_equal(actual_read_data, reference_read_data) ================================================ FILE: src/stim/io/sparse_shot.cc ================================================ /* * 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. */ #include "stim/io/sparse_shot.h" #include #include "stim/util_bot/str_util.h" using namespace stim; SparseShot::SparseShot() : hits(), obs_mask(0) { } SparseShot::SparseShot(std::vector hits, simd_bits<64> obs_mask) : hits(std::move(hits)), obs_mask(std::move(obs_mask)) { } void SparseShot::clear() { hits.clear(); obs_mask.clear(); } bool SparseShot::operator==(const SparseShot &other) const { return hits == other.hits && obs_mask == other.obs_mask; } bool SparseShot::operator!=(const SparseShot &other) const { return !(*this == other); } uint64_t SparseShot::obs_mask_as_u64() const { if (obs_mask.num_u64_padded() == 0) { return 0; } return obs_mask.u64[0]; } std::string SparseShot::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::ostream &stim::operator<<(std::ostream &out, const SparseShot &v) { return out << "SparseShot{{" << comma_sep(v.hits) << "}, " << v.obs_mask << "}"; } ================================================ FILE: src/stim/io/sparse_shot.h ================================================ /* * 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. */ #ifndef _STIM_IO_SPARSE_SHOT_H #define _STIM_IO_SPARSE_SHOT_H #include #include #include "stim/mem/simd_bits.h" namespace stim { /// Stores shot data sparsely, as a list of locations of 1 bits in the shot. /// /// For contrast, dense storage would involve storing one bit for each bit in the shot. /// /// If this shot is detection event data, the observable data is still stored densely in /// the obs_mask field. struct SparseShot { /// Indices of non-zero bits. std::vector hits; /// When reading detection event data with observables appended, the observables go into this mask. /// The observable with index k goes into the 1< obs_mask; SparseShot(); SparseShot(std::vector hits, simd_bits<64> obs_mask); /// Returns a uint64_t containing the first 64 observable bit flips. /// Defaults to 0 if there are no observables. uint64_t obs_mask_as_u64() const; void clear(); bool operator==(const SparseShot &other) const; bool operator!=(const SparseShot &other) const; std::string str() const; }; std::ostream &operator<<(std::ostream &out, const SparseShot &v); } // namespace stim #endif ================================================ FILE: src/stim/io/sparse_shot.test.cc ================================================ /* * 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. */ #include "stim/io/sparse_shot.h" #include "gtest/gtest.h" using namespace stim; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(sparse_shot, equality) { ASSERT_TRUE((SparseShot{}) == (SparseShot{})); ASSERT_FALSE((SparseShot{}) != (SparseShot{})); ASSERT_TRUE((SparseShot{}) != (SparseShot{{2}, obs_mask(0)})); ASSERT_FALSE((SparseShot{}) == (SparseShot{{2}, obs_mask(0)})); ASSERT_EQ((SparseShot{{1, 2, 3}, obs_mask(4)}), (SparseShot{{1, 2, 3}, obs_mask(4)})); ASSERT_NE((SparseShot{{1, 2, 3}, obs_mask(4)}), (SparseShot{{1, 2, 3}, obs_mask(5)})); ASSERT_NE((SparseShot{{1, 2, 3}, obs_mask(4)}), (SparseShot{{1, 2, 4}, obs_mask(4)})); ASSERT_NE((SparseShot{{1, 2, 3}, obs_mask(4)}), (SparseShot{{1, 2}, obs_mask(4)})); } TEST(sparse_shot, str) { ASSERT_EQ( (SparseShot{{1, 2, 3}, obs_mask(4)}.str()), "SparseShot{{1, 2, 3}, __1_____________________________________________________________}"); } TEST(spares_shot, obs_mask_as_u64) { SparseShot s{}; ASSERT_EQ(s.obs_mask_as_u64(), 0); s.obs_mask = simd_bits<64>(5); ASSERT_EQ(s.obs_mask_as_u64(), 0); s.obs_mask[1] = true; ASSERT_EQ(s.obs_mask_as_u64(), 2); s.obs_mask = simd_bits<64>(125); ASSERT_EQ(s.obs_mask_as_u64(), 0); s.obs_mask[1] = true; ASSERT_EQ(s.obs_mask_as_u64(), 2); s.obs_mask[64] = true; ASSERT_EQ(s.obs_mask_as_u64(), 2); } ================================================ FILE: src/stim/io/stim_data_formats.cc ================================================ #include "stim/io/stim_data_formats.h" using namespace stim; const std::map &stim::format_name_to_enum_map() { static const std::map mapping{ { "01", FileFormatData{ "01", SampleFormat::SAMPLE_FORMAT_01, R"HELP( The 01 format is a dense human readable format that stores shots as lines of '0' and '1' characters. The data from each shot is terminated by a newline character '\n'. Each character in the line is a '0' (indicating False) or a '1' (indicating True) corresponding to a measurement result (or a detector result) from a circuit. This is the default format used by Stim, because it's the easiest to understand. *Example of producing 01 format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="01") ... with open(path) as f: ... print(f.read().strip()) 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 00001111001101 )HELP", R"PYTHON( from typing import List def save_01(shots: List[List[bool]]) -> str: output = "" for shot in shots: for sample in shot: output += '1' if sample else '0' output += "\n" return output )PYTHON", R"PYTHON( from typing import List def parse_01(data: str) -> List[List[bool]]: shots = [] for line in data.split('\n'): if not line: continue shot = [] for c in line: assert c in '01' shot.append(c == '1') shots.append(shot) return shots )PYTHON", }, }, { "b8", FileFormatData{ "b8", SampleFormat::SAMPLE_FORMAT_B8, R"HELP( The b8 format is a dense binary format that stores shots as bit-packed bytes. Each shot is stored into ceil(n / 8) bytes, where n is the number of bits in the shot. Effectively, each shot is padded up to a multiple of 8 by appending False bits, so that shots always start on a byte boundary. Bits are packed into bytes in significance order (the 1s bit is the first bit, the 2s bit is the second, the 4s bit is the third, and so forth until the 128s bit which is the eighth bit). This format requires the reader to know the number of bits in each shot. *Example of producing b8 format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="b8") ... with open(path, 'rb') as f: ... print(' '.join(hex(e)[2:] for e in f.read())) f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c f0 2c )HELP", R"PYTHON( from typing import List def save_b8(shots: List[List[bool]]) -> bytes: output = b"" for shot in shots: bytes_per_shot = (len(shot) + 7) // 8 v = 0 for b in reversed(shot): v <<= 1 v += int(b) output += v.to_bytes(bytes_per_shot, 'little') return output )PYTHON", R"PYTHON( from typing import List def parse_b8(data: bytes, bits_per_shot: int) -> List[List[bool]]: shots = [] bytes_per_shot = (bits_per_shot + 7) // 8 for offset in range(0, len(data), bytes_per_shot): shot = [] for k in range(bits_per_shot): byte = data[offset + k // 8] bit = (byte >> (k % 8)) % 2 == 1 shot.append(bit) shots.append(shot) return shots )PYTHON", }, }, { "ptb64", FileFormatData{ "ptb64", SampleFormat::SAMPLE_FORMAT_PTB64, R"HELP( The ptb64 format is a dense SIMD-focused binary format that stores shots as partially transposed bit-packed data. Each 64 bit word (8 bytes) of the data contains bits from the same measurement result across 64 separate shots. The next 8 bytes contain bits for the next measurement result from the 64 separate shots. This continues until the 8 bytes containing the bits from the last measurement result, and then starts over again with data from the next 64 shots (if there are more). The shots are stored by byte order then significance order. The first shot's data goes into the least significant bit of the first byte of each 8 byte group. This format requires the number of shots to be a multiple of 64. This format requires the reader to know the number of shots that were taken. This format is generally more tedious to work with, but useful for achieving good performance on data processing tasks where it is possible to parallelize across shots using SIMD instructions. *Example of producing ptb64 format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 1 ... """).compile_sampler().sample_write(shots=64, filepath=path, format="ptb64") ... with open(path, 'rb') as f: ... print(' '.join(hex(e)[2:] for e in f.read())) 0 0 0 0 0 0 0 0 ff ff ff ff ff ff ff ff )HELP", R"PYTHON( from typing import List def save_ptb64(shots: List[List[bool]]): if len(shots) % 64 != 0: raise ValueError("Number of shots must be a multiple of 64.") output = [] for shot_offset in range(0, len(shots), 64): bits_per_shot = len(shots[0]) for measure_index in range(bits_per_shot): v = 0 for k in range(64)[::-1]: v <<= 1 v += int(shots[shot_offset + k][measure_index]) output.append(v.to_bytes(8, 'little')) return b''.join(output) )PYTHON", R"PYTHON( from typing import List def parse_ptb64(data: bytes, bits_per_shot: int) -> List[List[bool]]: num_shot_groups = int(len(data) * 8 / bits_per_shot / 64) if len(data) * 8 != num_shot_groups * 64 * bits_per_shot: raise ValueError("Number of shots must be a multiple of 64.") result = [[False] * bits_per_shot for _ in range(num_shot_groups * 64)] for group_index in range(num_shot_groups): group_bit_offset = 64 * bits_per_shot * group_index for m in range(bits_per_shot): m_bit_offset = m * 64 for shot in range(64): bit_offset = group_bit_offset + m_bit_offset + shot bit = data[bit_offset // 8] & (1 << (bit_offset % 8)) != 0 s = group_index * 64 + shot result[s][m] = bit return result )PYTHON", }, }, { "hits", FileFormatData{ "hits", SampleFormat::SAMPLE_FORMAT_HITS, R"HELP( The hits format is a dense human readable format that stores shots as a comma-separated list of integers. Each integer indicates the position of a bit that was True. Positions that aren't mentioned had bits that were False. The data from each shot is terminated by a newline character '\n'. The line is a series of non-negative integers separated by commas, with each integer indicating a bit from the shot that was true. This format requires the reader to know the number of bits in each shot (if they want to get a list instead of a set). This format requires the reader to know how many trailing newlines, that don't correspond to shots with no hit, are in the text data. This format is useful in contexts where the number of set bits is expected to be low, e.g. when sampling detection events. *Example of producing hits format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="hits") ... with open(path) as f: ... print(f.read().strip()) 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 4,5,6,7,10,11,13 )HELP", R"PYTHON( from typing import List def save_hits(shots: List[List[bool]]) -> str: output = "" for shot in shots: output += ",".join(str(index) for index, bit in enumerate(shot) if bit) + "\n" return output )PYTHON", R"PYTHON( from typing import List def parse_hits(data: str, bits_per_shot: int) -> List[List[bool]]: shots = [] if data.endswith('\n'): data = data[:-1] for line in data.split('\n'): shot = [False] * bits_per_shot if line: for term in line.split(','): shot[int(term)] = True shots.append(shot) return shots )PYTHON", }, }, { "r8", FileFormatData{ "r8", SampleFormat::SAMPLE_FORMAT_R8, R"HELP( The r8 format is a sparse binary format that stores shots as a series of lengths of runs between 1s. Each byte in the data indicates how many False bits there are before the next True bit. The maximum byte value (255) is special; it indicates to include 255 False bits but not follow them by a True bit. A shot always has a terminating True bit appended to it before encoding. Decoding of the shot ends (and the next shot begin) when this True bit just past the end of the shot data is reached. This format requires the reader to know the number of bits in each shot. This format is useful in contexts where the number of set bits is expected to be low, e.g. when sampling detection events. *Example of producing r8 format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="r8") ... with open(path, 'rb') as f: ... print(' '.join(hex(e)[2:] for e in f.read())) 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 4 0 0 0 2 0 1 0 >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... """).compile_sampler().sample_write(shots=10, filepath=path, format="r8") ... with open(path, 'rb') as f: ... print(' '.join(hex(e)[2:] for e in f.read())) 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f 9 1f )HELP", R"PYTHON( from typing import List def save_r8(shots: List[List[bool]]) -> bytes: output = [] for shot in shots: gap = 0 for b in list(shot) + [True]: if b: while gap >= 255: gap -= 255 output.append((255).to_bytes(1, 'big')) output.append(gap.to_bytes(1, 'big')) gap = 0 else: gap += 1 return b''.join(output) )PYTHON", R"PYTHON( from typing import List def parse_r8(data: bytes, bits_per_shot: int) -> List[List[bool]]: shots = [] shot = [] for byte in data: shot += [False] * byte if byte != 255: shot.append(True) if len(shot) > bits_per_shot: assert len(shot) == bits_per_shot + 1 and shot[-1] shot.pop() shots.append(shot) shot = [] assert len(shot) == 0 return shots )PYTHON", }, }, { "dets", FileFormatData{ "dets", SampleFormat::SAMPLE_FORMAT_DETS, R"HELP( The dets format is a sparse human readable format that stores shots as lines starting with the word 'shot' and then containing space-separated prefixed values like 'D5' and 'L2'. Each value's prefix indicates whether it is a measurement (M), a detector (D), or observable frame change (L) and its integer indicates that the corresponding measurement/detection-event/frame-change was True instead of False. The data from each shot is started with the text 'shot' and terminated by a newline character '\n'. The rest of the line is a series of integers, separated by spaces and prefixed by a single letter. The prefix letter indicates the type of value ('M' for measurement, 'D' for detector, and 'L' for logical observable). The integer indicates the index of the value. For example, "D1 D3 L0" indicates detectors 1 and 3 fired, and logical observable 0 was flipped. This format requires the reader to know the number of measurements/detectors/observables in each shot, if the reader wants to produce vectors of bits instead of sets. *Example of producing dets format data using stim's python API:* >>> import pathlib >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X 1 ... M 0 0 0 0 1 1 1 1 0 0 1 1 0 1 0 1 ... """).compile_sampler().sample_write(shots=3, filepath=path, format="dets") ... with open(path) as f: ... print(f.read().strip()) shot M4 M5 M6 M7 M10 M11 M13 M15 shot M4 M5 M6 M7 M10 M11 M13 M15 shot M4 M5 M6 M7 M10 M11 M13 M15 >>> with tempfile.TemporaryDirectory() as d: ... path = str(pathlib.Path(d) / "tmp.dat") ... stim.Circuit(""" ... X_ERROR(1) 1 ... M 0 1 2 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... DETECTOR rec[-3] ... OBSERVABLE_INCLUDE(5) rec[-2] ... """).compile_detector_sampler().sample_write(shots=2, filepath=path, format="dets", append_observables=True) ... with open(path) as f: ... print(f.read().strip()) shot D1 L5 shot D1 L5 )HELP", R"PYTHON( from typing import List def save_dets(shots: List[List[bool]], num_detectors: int, num_observables: int): output = "" for shot in shots: assert len(shot) == num_detectors + num_observables detectors = shot[:num_detectors] observables = shot[num_detectors:] output += "shot" for k in range(num_detectors): if shot[k]: output += " D" + str(k) for k in range(num_observables): if shot[num_detectors + k]: output += " L" + str(k) output += "\n" return output )PYTHON", R"PYTHON( from typing import List def parse_dets(data: str, num_detectors: int, num_observables: int) -> List[List[bool]]: shots = [] for line in data.split('\n'): if not line.strip(): continue assert line.startswith('shot') line = line[4:].strip() shot = [False] * (num_detectors + num_observables) if line: for term in line.split(' '): c = term[0] v = int(term[1:]) if c == 'D': assert 0 <= v < num_detectors shot[v] = True elif c == 'L': assert 0 <= v < num_observables shot[num_detectors + v] = True else: raise NotImplementedError(c) shots.append(shot) return shots )PYTHON", }, }, }; return mapping; } ================================================ FILE: src/stim/io/stim_data_formats.h ================================================ #ifndef _STIM_IO_STIM_DATA_FORMATS_H #define _STIM_IO_STIM_DATA_FORMATS_H #include #include namespace stim { enum class SampleFormat { SAMPLE_FORMAT_01, SAMPLE_FORMAT_B8, SAMPLE_FORMAT_PTB64, SAMPLE_FORMAT_HITS, SAMPLE_FORMAT_R8, SAMPLE_FORMAT_DETS, }; struct FileFormatData { const char *name; SampleFormat id; const char *help; const char *help_python_save; const char *help_python_parse; }; const std::map &format_name_to_enum_map(); } // namespace stim #endif ================================================ FILE: src/stim/main.perf.cc ================================================ // 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. #include #include #include #include "stim/perf.perf.h" #include "stim/util_bot/arg_parse.h" using namespace stim; RegisteredBenchmark *running_benchmark = nullptr; std::vector *all_registered_benchmarks_data = nullptr; uint64_t registry_initialized = 0; /// Describe quantity as an SI-prefixed value with two significant figures. std::string si2(double val) { char unit = ' '; if (val < 1) { if (val < 1) { val *= 1000; unit = 'm'; } if (val < 1) { val *= 1000; unit = 'u'; } if (val < 1) { val *= 1000; unit = 'n'; } if (val < 1) { val *= 1000; unit = 'p'; } } else { if (val > 1000) { val /= 1000; unit = 'k'; } if (val > 1000) { val /= 1000; unit = 'M'; } if (val > 1000) { val /= 1000; unit = 'G'; } if (val > 1000) { val /= 1000; unit = 'T'; } } std::stringstream ss; if (1 <= val && val < 10) { ss << (size_t)val << '.' << ((size_t)(val * 10) % 10); } else if (10 <= val && val < 100) { ss << ' ' << (size_t)val; } else if (100 <= val && val < 1000) { ss << (size_t)(val / 10) * 10; } else { ss << val; } ss << ' ' << unit; return ss.str(); } static std::vector known_arguments{"--only", "--target_seconds"}; void find_benchmarks(std::string_view filter, std::vector &out) { bool found = false; if (!filter.empty() && filter[filter.size() - 1] == '*') { std::string_view start = filter.substr(0, filter.size() - 1); for (const auto &benchmark : *all_registered_benchmarks_data) { if (benchmark.name.substr(0, start.size()) == start) { out.push_back(benchmark); found = true; } } } else { for (const auto &benchmark : *all_registered_benchmarks_data) { if (benchmark.name == filter) { out.push_back(benchmark); found = true; } } } if (!found) { std::cerr << "No benchmark matching filter '" << filter << "'. Available benchmarks are:\n"; for (auto &benchmark : *all_registered_benchmarks_data) { std::cerr << " " << benchmark.name << "\n"; } exit(EXIT_FAILURE); } } double BENCHMARK_CONFIG_TARGET_SECONDS = 0.5; int main(int argc, const char **argv) { check_for_unknown_arguments(known_arguments, {}, nullptr, argc, argv); const char *only = find_argument("--only", argc, argv); BENCHMARK_CONFIG_TARGET_SECONDS = find_float_argument("--target_seconds", 0.5, 0, 10000, argc, argv); std::vector chosen_benchmarks; if (only == nullptr) { chosen_benchmarks = *all_registered_benchmarks_data; } else { std::string filter_text = only; std::vector filters{}; size_t s = 0; for (size_t k = 0;; k++) { if (only[k] == ',' || only[k] == '\0') { filters.push_back(filter_text.substr(s, k - s)); s = k + 1; } if (only[k] == '\0') { break; } } if (filters.empty()) { std::cerr << "No filters specified.\n"; exit(EXIT_FAILURE); } for (const auto &filter : filters) { find_benchmarks(filter, chosen_benchmarks); } } for (auto &benchmark : chosen_benchmarks) { running_benchmark = &benchmark; benchmark.func(); for (const auto &result : benchmark.results) { double actual_seconds_per_rep = result.total_seconds / result.total_reps; if (result.goal_seconds != -1) { int deviation = (int)round((log(result.goal_seconds) - log(actual_seconds_per_rep)) / (log(10) / 10.0)); std::cout << "["; for (int k = -20; k <= 20; k++) { if ((k < deviation && k < 0) || (k > deviation && k > 0)) { std::cout << '.'; } else if (k == deviation) { std::cout << '*'; } else if (k == 0) { std::cout << '|'; } else if (deviation < 0) { std::cout << '<'; } else { std::cout << '>'; } } std::cout << "] "; std::cout << si2(actual_seconds_per_rep) << "s"; std::cout << " (vs " << si2(result.goal_seconds) << "s) "; } else { std::cout << si2(actual_seconds_per_rep) << "s "; } for (const auto &e : result.marginal_rates) { const auto &multiplier = e.second; const auto &unit = e.first; std::cout << "(" << si2(result.total_reps / result.total_seconds * multiplier) << unit << "/s) "; } std::cout << benchmark.name << "\n"; if (benchmark.results.empty()) { std::cerr << "`benchmark_go` was not called from BENCH(" << benchmark.name << ")"; exit(EXIT_FAILURE); } } } if (all_registered_benchmarks_data != nullptr) { delete all_registered_benchmarks_data; all_registered_benchmarks_data = nullptr; } return 0; } ================================================ FILE: src/stim/main_namespaced.cc ================================================ // 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. #include "stim/main_namespaced.h" #include #include "stim/cmd/command_analyze_errors.h" #include "stim/cmd/command_convert.h" #include "stim/cmd/command_detect.h" #include "stim/cmd/command_diagram.h" #include "stim/cmd/command_explain_errors.h" #include "stim/cmd/command_gen.h" #include "stim/cmd/command_help.h" #include "stim/cmd/command_m2d.h" #include "stim/cmd/command_repl.h" #include "stim/cmd/command_sample.h" #include "stim/cmd/command_sample_dem.h" #include "stim/util_bot/arg_parse.h" using namespace stim; int stim::main(int argc, const char **argv) { try { const char *mode = argc > 1 ? argv[1] : ""; if (mode[0] == '-') { mode = ""; } auto is_mode = [&](const char *name) { if (name[0] == '-') { return find_argument(name, argc, argv) != nullptr || strcmp(mode, name + 2) == 0; } return strcmp(mode, name) == 0; }; if (is_mode("--help")) { return command_help(argc, argv); } bool mode_repl = is_mode("--repl"); bool mode_sample = is_mode("--sample"); bool mode_sample_dem = is_mode("sample_dem"); bool mode_diagram = is_mode("diagram"); bool mode_detect = is_mode("--detect"); bool mode_analyze_errors = is_mode("--analyze_errors"); bool mode_gen = is_mode("--gen"); bool mode_m2d = is_mode("--m2d"); bool mode_explain_errors = is_mode("--explain_errors"); bool old_mode_detector_hypergraph = find_bool_argument("--detector_hypergraph", argc, argv); if (old_mode_detector_hypergraph) { std::cerr << "[DEPRECATION] Use `stim analyze_errors` instead of `--detector_hypergraph`\n"; mode_analyze_errors = true; } bool mode_convert = is_mode("--convert"); int modes_picked = (mode_repl + mode_sample + mode_sample_dem + mode_detect + mode_analyze_errors + mode_gen + mode_m2d + mode_explain_errors + mode_diagram + mode_convert); if (modes_picked != 1) { std::cerr << "\033[31m"; if (modes_picked > 1) { std::cerr << "More than one mode was specified.\n\n"; } else { std::cerr << "No mode was given.\n\n"; } std::cerr << help_for(""); std::cerr << "\033[0m"; return EXIT_FAILURE; } if (mode_gen) { return command_gen(argc, argv); } if (mode_repl) { return command_repl(argc, argv); } if (mode_sample) { return command_sample(argc, argv); } if (mode_detect) { return command_detect(argc, argv); } if (mode_analyze_errors) { return command_analyze_errors(argc, argv); } if (mode_m2d) { return command_m2d(argc, argv); } if (mode_explain_errors) { return command_explain_errors(argc, argv); } if (mode_sample_dem) { return command_sample_dem(argc, argv); } if (mode_diagram) { return command_diagram(argc, argv); } if (mode_convert) { return command_convert(argc, argv); } throw std::out_of_range("Mode not handled."); } catch (const std::invalid_argument &ex) { std::string_view s = ex.what(); std::cerr << "\033[31m"; std::cerr << s; if (s.empty() || s.back() != '\n') { std::cerr << '\n'; } std::cerr << "\033[0m"; return EXIT_FAILURE; } } ================================================ FILE: src/stim/main_namespaced.h ================================================ /* * 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. */ #ifndef _STIM_MAIN_NAMESPACED_H #define _STIM_MAIN_NAMESPACED_H namespace stim { /// Stim's main method (in a namespace; not the global entrypoint main!). int main(int argc, const char **argv); } // namespace stim #endif ================================================ FILE: src/stim/main_namespaced.perf.cc ================================================ // 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. #include "stim/main_namespaced.h" #include "stim/perf.perf.h" #include "stim/simulators/frame_simulator.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/simulators/tableau_simulator.h" using namespace stim; Circuit make_rep_code(uint32_t distance, uint32_t rounds) { Circuit round_ops; for (uint32_t k = 0; k < distance - 1; k++) { round_ops.safe_append_u("CNOT", {2 * k, 2 * k + 1}); } for (uint32_t k = 0; k < distance - 1; k++) { round_ops.safe_append_ua("DEPOLARIZE2", {2 * k, 2 * k + 1}, 0.001); } for (uint32_t k = 1; k < distance; k++) { round_ops.safe_append_u("CNOT", {2 * k, 2 * k - 1}); } for (uint32_t k = 1; k < distance; k++) { round_ops.safe_append_ua("DEPOLARIZE2", {2 * k, 2 * k - 1}, 0.001); } for (uint32_t k = 0; k < distance - 1; k++) { round_ops.safe_append_ua("X_ERROR", {2 * k + 1}, 0.001); } for (uint32_t k = 0; k < distance - 1; k++) { round_ops.safe_append_u("MR", {2 * k + 1}); } Circuit detectors; for (uint32_t k = 1; k < distance; k++) { detectors.safe_append_u("DETECTOR", {k | TARGET_RECORD_BIT, (k + distance - 1) | TARGET_RECORD_BIT}); } Circuit result = round_ops + (round_ops + detectors) * (rounds - 1); for (uint32_t k = 0; k < distance; k++) { result.safe_append_ua("X_ERROR", {2 * k}, 0.001); } for (uint32_t k = 0; k < distance; k++) { result.safe_append_u("M", {2 * k}); } for (uint32_t k = 1; k < distance; k++) { result.safe_append_u( "DETECTOR", {k | TARGET_RECORD_BIT, (k + 1) | TARGET_RECORD_BIT, (k + distance) | TARGET_RECORD_BIT}); } result.safe_append_ua("OBSERVABLE_INCLUDE", {1 | TARGET_RECORD_BIT}, 0); return result; } BENCHMARK(main_sample1_tableau_rep_d1000_r100) { size_t distance = 1000; size_t rounds = 100; auto circuit = make_rep_code(distance, rounds); FILE *in = tmpfile(); FILE *out = tmpfile(); fprintf(in, "%s", circuit.str().data()); std::mt19937_64 rng(0); // NOLINT(cert-msc51-cpp) benchmark_go([&]() { rewind(in); rewind(out); TableauSimulator::sample_stream(in, out, SampleFormat::SAMPLE_FORMAT_B8, false, rng); }) .goal_millis(22) .show_rate("Samples", circuit.count_measurements()); } BENCHMARK(main_sample1_pauliframe_b8_rep_d1000_r100) { size_t distance = 1000; size_t rounds = 100; auto circuit = make_rep_code(distance, rounds); FILE *out = tmpfile(); std::mt19937_64 rng(0); // NOLINT(cert-msc51-cpp) simd_bits ref(0); benchmark_go([&]() { rewind(out); sample_batch_measurements_writing_results_to_disk(circuit, ref, 1, out, SampleFormat::SAMPLE_FORMAT_B8, rng); }) .goal_millis(9) .show_rate("Samples", circuit.count_measurements()); } BENCHMARK(main_sample1_detectors_b8_rep_d1000_r100) { size_t distance = 1000; size_t rounds = 100; auto circuit = make_rep_code(distance, rounds); FILE *out = tmpfile(); FILE *obs_out = tmpfile(); std::mt19937_64 rng(0); // NOLINT(cert-msc51-cpp) simd_bits ref(circuit.count_measurements()); benchmark_go([&]() { rewind(out); sample_batch_detection_events_writing_results_to_disk( circuit, 1, false, false, out, SampleFormat::SAMPLE_FORMAT_B8, rng, obs_out, SampleFormat::SAMPLE_FORMAT_B8); }) .goal_millis(11) .show_rate("Samples", circuit.count_measurements()); } BENCHMARK(main_sample256_pauliframe_b8_rep_d1000_r100) { size_t distance = 1000; size_t rounds = 100; auto circuit = make_rep_code(distance, rounds); FILE *out = tmpfile(); std::mt19937_64 rng(0); // NOLINT(cert-msc51-cpp) simd_bits ref(0); benchmark_go([&]() { rewind(out); sample_batch_measurements_writing_results_to_disk(circuit, ref, 256, out, SampleFormat::SAMPLE_FORMAT_B8, rng); }) .goal_millis(13) .show_rate("Samples", circuit.count_measurements()); } BENCHMARK(main_sample256_pauliframe_b8_rep_d1000_r1000_stream) { DebugForceResultStreamingRaii stream; size_t distance = 1000; size_t rounds = 1000; auto circuit = make_rep_code(distance, rounds); FILE *out = tmpfile(); std::mt19937_64 rng(0); // NOLINT(cert-msc51-cpp) simd_bits ref(0); benchmark_go([&]() { rewind(out); sample_batch_measurements_writing_results_to_disk(circuit, ref, 256, out, SampleFormat::SAMPLE_FORMAT_B8, rng); }) .goal_millis(360) .show_rate("Samples", circuit.count_measurements()); } BENCHMARK(main_sample256_detectors_b8_rep_d1000_r100) { size_t distance = 1000; size_t rounds = 100; auto circuit = make_rep_code(distance, rounds); FILE *out = tmpfile(); FILE *obs_out = tmpfile(); std::mt19937_64 rng(0); // NOLINT(cert-msc51-cpp) simd_bits ref(0); benchmark_go([&]() { rewind(out); sample_batch_detection_events_writing_results_to_disk( circuit, 256, false, false, out, SampleFormat::SAMPLE_FORMAT_B8, rng, obs_out, SampleFormat::SAMPLE_FORMAT_B8); }) .goal_millis(15) .show_rate("Samples", circuit.count_measurements()); } BENCHMARK(main_sample256_detectors_b8_rep_d1000_r1000_stream) { DebugForceResultStreamingRaii stream; size_t distance = 1000; size_t rounds = 1000; auto circuit = make_rep_code(distance, rounds); FILE *out = tmpfile(); FILE *obs_out = tmpfile(); std::mt19937_64 rng(0); // NOLINT(cert-msc51-cpp) simd_bits ref(0); benchmark_go([&]() { rewind(out); sample_batch_detection_events_writing_results_to_disk( circuit, 256, false, false, out, SampleFormat::SAMPLE_FORMAT_B8, rng, obs_out, SampleFormat::SAMPLE_FORMAT_B8); }) .goal_millis(360) .show_rate("Samples", circuit.count_measurements()); } ================================================ FILE: src/stim/main_namespaced.test.cc ================================================ // 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. #include "stim/main_namespaced.h" #include #include "gtest/gtest.h" #include "stim/main_namespaced.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; std::string stim::run_captured_stim_main(std::vector flags, std::string_view std_in_content) { // Setup input. RaiiTempNamedFile raii_temp_file(std_in_content); flags.push_back("--in"); flags.push_back(raii_temp_file.path.data()); return run_captured_stim_main(flags); } std::string stim::run_captured_stim_main(std::vector flags) { flags.insert(flags.begin(), "[PROGRAM_LOCATION_IGNORE]"); // Setup output. testing::internal::CaptureStdout(); testing::internal::CaptureStderr(); int result; try { result = stim::main(flags.size(), flags.data()); } catch (const std::exception &e) { return "[exception=" + std::string(e.what()) + "]" + testing::internal::GetCapturedStdout() + testing::internal::GetCapturedStderr(); } std::string out = testing::internal::GetCapturedStdout(); std::string err = testing::internal::GetCapturedStderr(); if (!err.empty()) { return out + "[stderr=" + err + "]"; } if (result != EXIT_SUCCESS) { return "[exit code != EXIT_SUCCESS]"; } return out; } std::string_view stim::trim(std::string_view text) { size_t s = 0; size_t e = text.size(); while (s < e && std::isspace(text[s])) { s++; } while (s < e && std::isspace(text[e - 1])) { e--; } return text.substr(s, e - s); } bool stim::matches(std::string actual, std::string pattern) { // Hackily work around C++ regex not supporting multiline matching. std::replace(actual.begin(), actual.end(), '\n', 'X'); std::replace(pattern.begin(), pattern.end(), '\n', 'X'); return std::regex_match(actual, std::regex("^" + pattern + "$")); } TEST(main, help_modes) { ASSERT_TRUE(matches(run_captured_stim_main({"--help"}), ".*Available stim commands.+")); ASSERT_TRUE(matches(run_captured_stim_main({"help"}), ".*Available stim commands.+")); ASSERT_TRUE(matches(run_captured_stim_main({}), ".+stderr.+No mode.+")); ASSERT_TRUE(matches(run_captured_stim_main({"--sample", "--repl"}), ".+stderr.+More than one mode.+")); ASSERT_TRUE(matches(run_captured_stim_main({"--sample", "--repl", "--detect"}), ".+stderr.+More than one mode.+")); ASSERT_TRUE(matches(run_captured_stim_main({"--help", "dhnsahddjoidsa"}), ".*Unrecognized.*")); ASSERT_TRUE(matches(run_captured_stim_main({"--help", "H"}), ".+Hadamard.+")); ASSERT_TRUE(matches(run_captured_stim_main({"--help", "sample"}), ".*Samples measurements from a circuit.+")); } TEST(main, bad_flag) { ASSERT_EQ( trim(run_captured_stim_main({"--gen", "--unknown"})), trim( "[stderr=\033[31mUnrecognized command line argument --unknown for `stim gen`.\n" "Recognized command line arguments for `stim gen`:\n" " --after_clifford_depolarization\n" " --after_reset_flip_probability\n" " --before_measure_flip_probability\n" " --before_round_data_depolarization\n" " --code\n" " --distance\n" " --in\n" " --out\n" " --rounds\n" " --task\n" "\033[0m]\n")); } ================================================ FILE: src/stim/main_namespaced.test.h ================================================ // 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. #ifndef _STIM_MAIN_NAMESPACED_TEST_H #define _STIM_MAIN_NAMESPACED_TEST_H #include #include namespace stim { std::string run_captured_stim_main(std::vector flags); std::string run_captured_stim_main(std::vector flags, std::string_view std_in_content); std::string_view trim(std::string_view text); bool matches(std::string actual, std::string pattern); } // namespace stim #endif ================================================ FILE: src/stim/mem/README.md ================================================ # `mem` directory This directory contains code related to efficiently packing data into memory. For example, it exposes `simd_bits` which manages an array of booleans that are bit-packed, padded, and aligned so that same-instruction-multiple-data constructs can be safely used on the bits. A key behind-the-scenes type is `simd_word`, which has different implementations depending on the instructions supported by the machine architecture. In particular, includes graceful degradation from AVX to SSE to raw 64 bit integers. ================================================ FILE: src/stim/mem/bit_ref.cc ================================================ // 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. #include "stim/mem/bit_ref.h" using namespace stim; bit_ref::bit_ref(void *base, size_t init_offset) : byte((uint8_t *)base + (init_offset / 8)), bit_index(init_offset & 7) { } ================================================ FILE: src/stim/mem/bit_ref.h ================================================ /* * 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. */ #ifndef _STIM_MEM_BIT_REF_H #define _STIM_MEM_BIT_REF_H #include #include namespace stim { /// A reference to a bit within a byte. /// /// Conceptually behaves the same as a `bool &`, as opposed to a `bool *`. For example, the `=` operator overwrites the /// contents of the bit being referenced instead of changing which bit is pointed to. /// /// This should behave essentially identically to the weird bit references that come out of a `std::vector`. struct bit_ref { uint8_t *byte; uint8_t bit_index; /// Construct a bit_ref from a pointer and a bit offset. /// The offset can be larger than a word. /// Automatically canonicalized so that the offset is less than 8. bit_ref(void *base, size_t offset); /// Copy assignment. inline bit_ref &operator=(bool value) { *byte &= ~((uint8_t)1 << bit_index); *byte |= (uint8_t)value << bit_index; return *this; } /// Copy assignment. inline bit_ref &operator=(const bit_ref &value) { *this = (bool)value; return *this; } /// Xor assignment. inline bit_ref &operator^=(bool value) { *byte ^= (uint8_t)value << bit_index; return *this; } /// Bitwise-and assignment. inline bit_ref &operator&=(bool value) { *byte &= ((uint8_t)value << bit_index) | ~(1 << bit_index); return *this; } /// Bitwise-or assignment. inline bit_ref &operator|=(bool value) { *byte |= (uint8_t)value << bit_index; return *this; } /// Swap assignment. inline void swap_with(bit_ref other) { bool b = (bool)other; other = (bool)*this; *this = b; } /// Implicit conversion to bool. inline operator bool() const { // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) return (*byte >> bit_index) & 1; } }; } // namespace stim #endif ================================================ FILE: src/stim/mem/bit_ref.test.cc ================================================ // 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. #include "stim/mem/bit_ref.h" #include "gtest/gtest.h" using namespace stim; TEST(bit_ref, get) { uint64_t word = 0; bit_ref b(&word, 5); ASSERT_EQ(b, false); word = 1; ASSERT_EQ(b, false); word = 32; ASSERT_EQ(b, true); } TEST(bit_ref, set) { uint64_t word = 0; bit_ref b(&word, 5); word = UINT64_MAX; b = false; ASSERT_EQ(word, UINT64_MAX - 32); } TEST(bit_ref, bit_xor) { uint64_t word = 0; bit_ref b2(&word, 2); bit_ref b5(&word, 5); b5 ^= 1; ASSERT_EQ(word, 32); b5 ^= 1; ASSERT_EQ(word, 0); b5 ^= 1; ASSERT_EQ(word, 32); b5 ^= 0; ASSERT_EQ(word, 32); b2 ^= 1; ASSERT_EQ(word, 36); b5 ^= 0; ASSERT_EQ(word, 36); b2 ^= 1; ASSERT_EQ(word, 32); b5 ^= 1; ASSERT_EQ(word, 0); } TEST(bit_ref, bit_or) { uint64_t word = 0; bit_ref b2(&word, 2); bit_ref b3(&word, 3); b2 |= 0; ASSERT_EQ(word, 0); b2 |= 1; ASSERT_EQ(word, 4); b3 |= 1; ASSERT_EQ(word, 12); word = 0; b3 |= 0; ASSERT_EQ(word, 0); b3 |= 1; ASSERT_EQ(word, 8); b3 |= 1; ASSERT_EQ(word, 8); b3 |= 0; ASSERT_EQ(word, 8); } TEST(bit_ref, bit_andr) { uint64_t word = 8; bit_ref b2(&word, 2); b2 &= 0; ASSERT_EQ(word, 8); b2 &= 1; ASSERT_EQ(word, 8); bit_ref b3(&word, 3); b3 &= 1; ASSERT_EQ(word, 8); b3 &= 0; ASSERT_EQ(word, 0); b3 &= 0; ASSERT_EQ(word, 0); b3 &= 1; ASSERT_EQ(word, 0); } TEST(bit_ref, swap_with) { uint64_t word = 8; bit_ref b3(&word, 3); bit_ref b5(&word, 5); b3.swap_with(b5); ASSERT_EQ(word, 32); b3.swap_with(b5); ASSERT_EQ(word, 8); word = 0; b3.swap_with(b5); ASSERT_EQ(word, 0); word = UINT64_MAX; b3.swap_with(b5); ASSERT_EQ(word, UINT64_MAX); } ================================================ FILE: src/stim/mem/bitword.h ================================================ /* * 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. */ #ifndef _STIM_MEM_BIT_WORD_H #define _STIM_MEM_BIT_WORD_H #include namespace stim { /// A `bitword` is a bag of bits that can be operated on in a SIMD-esque fashion /// by individual CPU instructions. /// /// This template would not have to exist, except that different architectures and /// operating systems expose different interfaces between native types like /// uint64_t and intrinsics like __m256i. For example, in some contexts, __m256i /// values can be operated on by operators (e.g. you can do `a ^= b`) while in /// other contexts this does not work. The bitword template implementations define /// a common set of methods required by stim to function, so that the same code /// can be compiled to use 256 bit registers or 64 bit registers as appropriate. template struct bitword; template inline bool operator==(const bitword &self, const bitword &other) { return self.to_u64_array() == other.to_u64_array(); } template inline bool operator<(const bitword &self, const bitword &other) { auto v1 = self.to_u64_array(); auto v2 = other.to_u64_array(); for (size_t k = 0; k < v1.size(); k++) { if (v1[k] != v2[k]) { return v1[k] < v2[k]; } } return false; } template inline bool operator!=(const bitword &self, const bitword &other) { return !(self == other); } template inline bool operator==(const bitword &self, int other) { return self == (bitword)other; } template inline bool operator!=(const bitword &self, int other) { return self != (bitword)other; } template inline bool operator==(const bitword &self, uint64_t other) { return self == (bitword)other; } template inline bool operator!=(const bitword &self, uint64_t other) { return self != (bitword)other; } template inline bool operator==(const bitword &self, int64_t other) { return self == (bitword)other; } template inline bool operator!=(const bitword &self, int64_t other) { return self != (bitword)other; } template std::ostream &operator<<(std::ostream &out, const bitword &v) { out << "bitword<" << W << ">{"; auto u = v.to_u64_array(); for (size_t k = 0; k < u.size(); k++) { for (size_t b = 0; b < 64; b++) { if ((b | k) && (b & 7) == 0) { out << ' '; } out << ".1"[(u[k] >> b) & 1]; } } out << '}'; return out; } template inline bitword operator<<(const bitword &self, uint64_t offset) { return self.shifted((int)offset); } template inline bitword operator>>(const bitword &self, uint64_t offset) { return self.shifted(-(int)offset); } template inline bitword &operator<<=(bitword &self, uint64_t offset) { self = (self << offset); return self; } template inline bitword &operator>>=(bitword &self, uint64_t offset) { self = (self >> offset); return self; } template inline bitword operator<<(const bitword &self, int offset) { return self.shifted((int)offset); } template inline bitword operator>>(const bitword &self, int offset) { return self.shifted(-(int)offset); } template inline bitword &operator<<=(bitword &self, int offset) { self = (self << offset); return self; } template inline bitword &operator>>=(bitword &self, int offset) { self = (self >> offset); return self; } template inline bitword operator&(const bitword &self, int mask) { return self & bitword(mask); } template inline bitword operator&(const bitword &self, uint64_t mask) { return self & bitword(mask); } template inline bitword operator&(const bitword &self, int64_t mask) { return self & bitword(mask); } template inline bitword operator|(const bitword &self, int mask) { return self | bitword(mask); } template inline bitword operator|(const bitword &self, uint64_t mask) { return self | bitword(mask); } template inline bitword operator|(const bitword &self, int64_t mask) { return self | bitword(mask); } template inline bitword operator^(const bitword &self, int mask) { return self ^ bitword(mask); } template inline bitword operator^(const bitword &self, uint64_t mask) { return self ^ bitword(mask); } template inline bitword operator^(const bitword &self, int64_t mask) { return self ^ bitword(mask); } template inline bitword andnot(const bitword &inv, const bitword &val) { return inv.andnot(val); } inline uint64_t andnot(uint64_t inv, uint64_t val) { return ~inv & val; } inline uint32_t andnot(uint32_t inv, uint32_t val) { return ~inv & val; } inline uint16_t andnot(uint16_t inv, uint16_t val) { return ~inv & val; } inline uint8_t andnot(uint8_t inv, uint8_t val) { return ~inv & val; } inline bool andnot(bool inv, bool val) { return !inv && val; } } // namespace stim #endif ================================================ FILE: src/stim/mem/bitword_128_sse.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SIMD_WORD_128_SSE_H #define _STIM_MEM_SIMD_WORD_128_SSE_H #ifdef __SSE2__ #include #include #include #include #include #include #include "stim/mem/bitword.h" namespace stim { /// Implements a 128 bit bitword using SSE instructions. template <> struct bitword<128> { constexpr static size_t BIT_SIZE = 128; constexpr static size_t BIT_POW = 7; union { __m128i val; uint8_t u8[16]; uint64_t u64[2]; }; static void *aligned_malloc(size_t bytes) { return _mm_malloc(bytes, sizeof(__m128i)); } static void aligned_free(void *ptr) { _mm_free(ptr); } inline bitword() : val(__m128i{}) { } inline bitword(__m128i val) : val(val) { } inline bitword(std::array val) : val{_mm_set_epi64x(val[1], val[0])} { } inline bitword(uint64_t val) : val{_mm_set_epi64x(0, val)} { } inline bitword(int64_t val) : val{_mm_set_epi64x(-(val < 0), val)} { } inline bitword(int val) : val{_mm_set_epi64x(-(val < 0), val)} { } inline static bitword<128> tile8(uint8_t pattern) { return {_mm_set1_epi8(pattern)}; } inline static bitword<128> tile16(uint16_t pattern) { return {_mm_set1_epi16(pattern)}; } inline static bitword<128> tile32(uint32_t pattern) { return {_mm_set1_epi32(pattern)}; } inline static bitword<128> tile64(uint64_t pattern) { return {_mm_set1_epi64x(pattern)}; } inline std::array to_u64_array() const { // I would use std::bit_cast here, but it failed to build in CI. // I would use '_mm_extract_epi64' here, but it failed to build in CI when using `-O3`. // Failures were on linux systems with gcc 12.2.0 uint64_t w0 = u64[0]; uint64_t w1 = u64[1]; return std::array{(uint64_t)w0, (uint64_t)w1}; } inline operator bool() const { // NOLINT(hicpp-explicit-conversions) auto words = to_u64_array(); return (bool)(words[0] | words[1]); } inline operator int() const { // NOLINT(hicpp-explicit-conversions) return (int64_t)*this; } inline operator uint64_t() const { // NOLINT(hicpp-explicit-conversions) auto words = to_u64_array(); if (words[1]) { throw std::invalid_argument("Too large for uint64_t"); } return words[0]; } inline operator int64_t() const { // NOLINT(hicpp-explicit-conversions) auto words = to_u64_array(); int64_t result = (int64_t)words[0]; uint64_t expected = result < 0 ? (uint64_t)-1 : (uint64_t)0; if (words[1] != expected) { throw std::invalid_argument("Out of bounds of int64_t"); } return result; } inline bitword<128> &operator^=(const bitword<128> &other) { val = _mm_xor_si128(val, other.val); return *this; } inline bitword<128> &operator&=(const bitword<128> &other) { val = _mm_and_si128(val, other.val); return *this; } inline bitword<128> &operator|=(const bitword<128> &other) { val = _mm_or_si128(val, other.val); return *this; } inline bitword<128> operator^(const bitword<128> &other) const { return {_mm_xor_si128(val, other.val)}; } inline bitword<128> operator&(const bitword<128> &other) const { return {_mm_and_si128(val, other.val)}; } inline bitword<128> operator|(const bitword<128> &other) const { return {_mm_or_si128(val, other.val)}; } inline bitword<128> andnot(const bitword<128> &other) const { return {_mm_andnot_si128(val, other.val)}; } inline bitword<128> operator~() const { return {_mm_xor_si128(val, _mm_set1_epi8(-1))}; } inline uint16_t popcount() const { auto words = to_u64_array(); return std::popcount(words[0]) + std::popcount(words[1]); } inline bitword<128> shifted(int offset) const { auto w = to_u64_array(); while (offset <= -64) { w[0] = w[1]; w[1] = 0; offset += 64; } while (offset >= 64) { w[1] = w[0]; w[0] = 0; offset -= 64; } __m128i low2high; __m128i high2low; if (offset < 0) { low2high = _mm_set_epi64x(0, w[1]); high2low = _mm_set_epi64x(w[1], w[0]); offset += 64; } else { low2high = _mm_set_epi64x(w[1], w[0]); high2low = _mm_set_epi64x(w[0], 0); } uint64_t m = (uint64_t{1} << offset) - uint64_t{1}; low2high = _mm_slli_epi64(low2high, offset); high2low = _mm_srli_epi64(high2low, 64 - offset); low2high = _mm_and_si128(low2high, _mm_set1_epi64x(~m)); high2low = _mm_and_si128(high2low, _mm_set1_epi64x(m)); return _mm_or_si128(low2high, high2low); } inline std::string str() const { std::stringstream out; out << *this; return out.str(); } template static void inplace_transpose_block_pass(bitword<128> *data, size_t stride, __m128i mask) { for (size_t k = 0; k < 128; k++) { if (k & shift) { continue; } bitword<128> &x = data[stride * k]; bitword<128> &y = data[stride * (k + shift)]; bitword<128> a = x & mask; bitword<128> b = _mm_andnot_si128(mask, x.val); bitword<128> c = y & mask; bitword<128> d = _mm_andnot_si128(mask, y.val); x = a | bitword<128>(_mm_slli_epi64(c.val, shift)); y = bitword<128>(_mm_srli_epi64(b.val, shift)) | d; } } static void inplace_transpose_block_pass64(bitword<128> *data, size_t stride) { uint64_t *ptr = (uint64_t *)data; stride <<= 1; for (size_t k = 0; k < 64; k++) { std::swap(ptr[stride * k + 1], ptr[stride * (k + 64)]); } } static void inplace_transpose_square(bitword<128> *data, size_t stride) { inplace_transpose_block_pass<1>(data, stride, _mm_set1_epi8(0x55)); inplace_transpose_block_pass<2>(data, stride, _mm_set1_epi8(0x33)); inplace_transpose_block_pass<4>(data, stride, _mm_set1_epi8(0xF)); inplace_transpose_block_pass<8>(data, stride, _mm_set1_epi16(0xFF)); inplace_transpose_block_pass<16>(data, stride, _mm_set1_epi32(0xFFFF)); inplace_transpose_block_pass<32>(data, stride, _mm_set1_epi64x(0xFFFFFFFF)); inplace_transpose_block_pass64(data, stride); } }; } // namespace stim #endif #endif ================================================ FILE: src/stim/mem/bitword_256_avx.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SIMD_WORD_256_AVX_H #define _STIM_MEM_SIMD_WORD_256_AVX_H #if __AVX2__ #include #include #include #include #include #include "stim/mem/bitword.h" namespace stim { /// Implements a 256 bit bitword using AVX instructions. template <> struct bitword<256> { constexpr static size_t BIT_SIZE = 256; constexpr static size_t BIT_POW = 8; union { __m256i val; uint8_t u8[32]; }; static void *aligned_malloc(size_t bytes) { return _mm_malloc(bytes, sizeof(__m256i)); } static void aligned_free(void *ptr) { _mm_free(ptr); } inline bitword() : val(__m256i{}) { } inline bitword(__m256i val) : val(val) { } inline bitword(std::array val) : val{_mm256_set_epi64x(val[3], val[2], val[1], val[0])} { } inline bitword(uint64_t val) : val{_mm256_set_epi64x(0, 0, 0, val)} { } inline bitword(int64_t val) : val{_mm256_set_epi64x(-(val < 0), -(val < 0), -(val < 0), val)} { } inline bitword(int val) : val{_mm256_set_epi64x(-(val < 0), -(val < 0), -(val < 0), val)} { } inline static bitword<256> tile8(uint8_t pattern) { return {_mm256_set1_epi8(pattern)}; } inline static bitword<256> tile16(uint16_t pattern) { return {_mm256_set1_epi16(pattern)}; } inline static bitword<256> tile32(uint32_t pattern) { return {_mm256_set1_epi32(pattern)}; } inline static bitword<256> tile64(uint64_t pattern) { return {_mm256_set1_epi64x(pattern)}; } inline std::array to_u64_array() const { return std::array{ (uint64_t)_mm256_extract_epi64(val, 0), (uint64_t)_mm256_extract_epi64(val, 1), (uint64_t)_mm256_extract_epi64(val, 2), (uint64_t)_mm256_extract_epi64(val, 3), }; } inline operator bool() const { // NOLINT(hicpp-explicit-conversions) auto words = to_u64_array(); return (bool)(words[0] | words[1] | words[2] | words[3]); } inline operator int() const { // NOLINT(hicpp-explicit-conversions) return (int64_t)*this; } inline operator uint64_t() const { // NOLINT(hicpp-explicit-conversions) auto words = to_u64_array(); if (words[1] || words[2] || words[3]) { throw std::invalid_argument("Too large for uint64_t"); } return words[0]; } inline operator int64_t() const { // NOLINT(hicpp-explicit-conversions) auto words = to_u64_array(); int64_t result = (int64_t)words[0]; uint64_t expected = result < 0 ? (uint64_t)-1 : (uint64_t)0; if (words[1] != expected || words[2] != expected || words[3] != expected) { throw std::invalid_argument("Out of bounds of int64_t"); } return result; } inline bitword<256> &operator^=(const bitword<256> &other) { val = _mm256_xor_si256(val, other.val); return *this; } inline bitword<256> &operator&=(const bitword<256> &other) { val = _mm256_and_si256(val, other.val); return *this; } inline bitword<256> &operator|=(const bitword<256> &other) { val = _mm256_or_si256(val, other.val); return *this; } inline bitword<256> operator^(const bitword<256> &other) const { return {_mm256_xor_si256(val, other.val)}; } inline bitword<256> operator&(const bitword<256> &other) const { return {_mm256_and_si256(val, other.val)}; } inline bitword<256> operator|(const bitword<256> &other) const { return {_mm256_or_si256(val, other.val)}; } inline bitword<256> andnot(const bitword<256> &other) const { return {_mm256_andnot_si256(val, other.val)}; } inline bitword<256> operator~() const { return {_mm256_xor_si256(val, _mm256_set1_epi8(-1))}; } inline uint16_t popcount() const { auto v = to_u64_array(); return std::popcount(v[0]) + std::popcount(v[1]) + std::popcount(v[2]) + (uint16_t)std::popcount(v[3]); } inline bitword<256> shifted(int offset) const { auto w = to_u64_array(); while (offset <= -64) { w[0] = w[1]; w[1] = w[2]; w[2] = w[3]; w[3] = 0; offset += 64; } while (offset >= 64) { w[3] = w[2]; w[2] = w[1]; w[1] = w[0]; w[0] = 0; offset -= 64; } __m256i low2high; __m256i high2low; if (offset < 0) { low2high = _mm256_set_epi64x(0, w[3], w[2], w[1]); high2low = _mm256_set_epi64x(w[3], w[2], w[1], w[0]); offset += 64; } else { low2high = _mm256_set_epi64x(w[3], w[2], w[1], w[0]); high2low = _mm256_set_epi64x(w[2], w[1], w[0], 0); } uint64_t m = (uint64_t{1} << offset) - uint64_t{1}; low2high = _mm256_slli_epi64(low2high, offset); high2low = _mm256_srli_epi64(high2low, 64 - offset); low2high = _mm256_and_si256(low2high, _mm256_set1_epi64x(~m)); high2low = _mm256_and_si256(high2low, _mm256_set1_epi64x(m)); return _mm256_or_si256(low2high, high2low); } inline std::string str() const { std::stringstream out; out << *this; return out.str(); } inline bool operator==(const bitword<256> &other) const { return to_u64_array() == other.to_u64_array(); } inline bool operator!=(const bitword<256> &other) const { return !(*this == other); } inline bool operator==(int other) const { return *this == (bitword<256>)other; } inline bool operator!=(int other) const { return *this != (bitword<256>)other; } inline bool operator==(uint64_t other) const { return *this == (bitword<256>)other; } inline bool operator!=(uint64_t other) const { return *this != (bitword<256>)other; } inline bool operator==(int64_t other) const { return *this == (bitword<256>)other; } inline bool operator!=(int64_t other) const { return *this != (bitword<256>)other; } template static void inplace_transpose_block_pass(bitword<256> *data, size_t stride, __m256i mask) { for (size_t k = 0; k < 256; k++) { if (k & shift) { continue; } bitword<256> &x = data[stride * k]; bitword<256> &y = data[stride * (k + shift)]; bitword<256> a = x & mask; bitword<256> b = _mm256_andnot_si256(mask, x.val); bitword<256> c = y & mask; bitword<256> d = _mm256_andnot_si256(mask, y.val); x = a | bitword<256>(_mm256_slli_epi64(c.val, shift)); y = bitword<256>(_mm256_srli_epi64(b.val, shift)) | d; } } static void inplace_transpose_block_pass_64_and_128(bitword<256> *data, size_t stride) { uint64_t *ptr = (uint64_t *)data; stride <<= 2; for (size_t k = 0; k < 64; k++) { std::swap(ptr[stride * (k + 64 * 0) + 1], ptr[stride * (k + 64 * 1) + 0]); std::swap(ptr[stride * (k + 64 * 0) + 2], ptr[stride * (k + 64 * 2) + 0]); std::swap(ptr[stride * (k + 64 * 0) + 3], ptr[stride * (k + 64 * 3) + 0]); std::swap(ptr[stride * (k + 64 * 1) + 2], ptr[stride * (k + 64 * 2) + 1]); std::swap(ptr[stride * (k + 64 * 1) + 3], ptr[stride * (k + 64 * 3) + 1]); std::swap(ptr[stride * (k + 64 * 2) + 3], ptr[stride * (k + 64 * 3) + 2]); } } static void inplace_transpose_square(bitword<256> *data, size_t stride) { inplace_transpose_block_pass<1>(data, stride, _mm256_set1_epi8(0x55)); inplace_transpose_block_pass<2>(data, stride, _mm256_set1_epi8(0x33)); inplace_transpose_block_pass<4>(data, stride, _mm256_set1_epi8(0xF)); inplace_transpose_block_pass<8>(data, stride, _mm256_set1_epi16(0xFF)); inplace_transpose_block_pass<16>(data, stride, _mm256_set1_epi32(0xFFFF)); inplace_transpose_block_pass<32>(data, stride, _mm256_set1_epi64x(0xFFFFFFFF)); inplace_transpose_block_pass_64_and_128(data, stride); } }; } // namespace stim #endif #endif ================================================ FILE: src/stim/mem/bitword_64.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SIMD_WORD_64_STD_H #define _STIM_MEM_SIMD_WORD_64_STD_H #include #include #include #include #include "stim/mem/bitword.h" #include "stim/mem/simd_util.h" namespace stim { /// Implements a 64 bit bitword using no architecture-specific instructions, just standard C++. template <> struct bitword<64> { constexpr static size_t BIT_SIZE = 64; constexpr static size_t BIT_POW = 6; union { uint64_t val; uint8_t u8[8]; }; static void *aligned_malloc(size_t bytes) { return malloc(bytes); } static void aligned_free(void *ptr) { free(ptr); } inline constexpr bitword() : val{} { } inline bitword(std::array val) : val{val[0]} { } inline constexpr bitword(uint64_t v) : val{v} { } inline constexpr bitword(int64_t v) : val{(uint64_t)v} { } inline constexpr bitword(int v) : val{(uint64_t)v} { } constexpr inline static bitword<64> tile64(uint64_t pattern) { return bitword<64>(pattern); } constexpr inline static bitword<64> tile8(uint8_t pattern) { return bitword<64>(tile64_helper(pattern, 8)); } inline std::array to_u64_array() const { return std::array{val}; } inline operator bool() const { // NOLINT(hicpp-explicit-conversions) return (bool)(val); } inline operator int() const { // NOLINT(hicpp-explicit-conversions) return (int)val; } inline operator uint64_t() const { // NOLINT(hicpp-explicit-conversions) return val; } inline operator int64_t() const { // NOLINT(hicpp-explicit-conversions) return (int64_t)val; } inline bitword<64> &operator^=(const bitword<64> &other) { val ^= other.val; return *this; } inline bitword<64> &operator&=(const bitword<64> &other) { val &= other.val; return *this; } inline bitword<64> &operator|=(const bitword<64> &other) { val |= other.val; return *this; } inline bitword<64> operator^(const bitword<64> &other) const { return bitword<64>(val ^ other.val); } inline bitword<64> operator&(const bitword<64> &other) const { return bitword<64>(val & other.val); } inline bitword<64> operator|(const bitword<64> &other) const { return bitword<64>(val | other.val); } inline bitword<64> andnot(const bitword<64> &other) const { return bitword<64>(~val & other.val); } inline bitword<64> operator~() const { return {~val}; } inline uint16_t popcount() const { return std::popcount(val); } inline std::string str() const { std::stringstream out; out << *this; return out.str(); } inline bitword<64> shifted(int offset) const { uint64_t v = val; if (offset >= 64 || offset <= -64) { v = 0; } else if (offset > 0) { v <<= offset; } else { v >>= -offset; } return bitword<64>{v}; } static void inplace_transpose_square(bitword<64> *data_block, size_t stride) { inplace_transpose_64x64((uint64_t *)data_block, stride); } }; } // namespace stim #endif ================================================ FILE: src/stim/mem/fixed_cap_vector.h ================================================ #ifndef _STIM_MEM_FIXED_CAP_VECTOR_H #define _STIM_MEM_FIXED_CAP_VECTOR_H #include #include #include #include namespace stim { /// A vector with a variable number of items but a fixed size backing array. template class FixedCapVector { std::array data; size_t num_used; public: FixedCapVector() : num_used(0) {}; FixedCapVector(std::initializer_list list) : num_used(0) { if (list.size() > max_size) { throw std::out_of_range("list.size() > max_size"); } for (auto &e : list) { push_back(std::move(e)); } } T &operator[](size_t index) { return data[index]; } const T &operator[](size_t index) const { return data[index]; } const T &front() const { if (num_used == 0) { throw std::out_of_range("Empty."); } return data[0]; } const T &back() const { if (num_used == 0) { throw std::out_of_range("Empty."); } return data[num_used - 1]; } T &front() { if (num_used == 0) { throw std::out_of_range("Empty."); } return data[0]; } T &back() { if (num_used == 0) { throw std::out_of_range("Empty."); } return data[num_used - 1]; } T *begin() { return &data[0]; } T *end() { return &data[0] + num_used; } const T *end() const { return &data[0] + num_used; } const T *begin() const { return &data[0]; } size_t size() const { return num_used; } bool empty() const { return num_used == 0; } T *find(const T &item) { auto p = begin(); while (p != end()) { if (*p == item) { break; } p++; } return p; } const T *find(const T &item) const { auto p = begin(); while (p != end()) { if (*p == item) { break; } p++; } return p; } void clear() { num_used = 0; } void push_back(const T &item) { if (num_used == data.size()) { throw std::out_of_range("CappedVector capacity exceeded."); } data[num_used] = item; num_used++; } void push_back(T &&item) { if (num_used == data.size()) { throw std::out_of_range("CappedVector capacity exceeded."); } data[num_used] = std::move(item); num_used++; } void pop_back() { if (num_used == 0) { throw std::out_of_range("Popped empty CappedVector."); } num_used -= 1; } bool operator==(const FixedCapVector &other) const { if (num_used != other.num_used) { return false; } for (size_t k = 0; k < num_used; k++) { if (data[k] != other.data[k]) { return false; } } return true; } bool operator!=(const FixedCapVector &other) const { return !(*this == other); } bool operator<(const FixedCapVector &other) const { if (num_used != other.num_used) { return num_used < other.num_used; } for (size_t k = 0; k < num_used; k++) { if (data[k] != other.data[k]) { return data[k] < other.data[k]; } } return false; } std::string str() const { std::stringstream ss; ss << *this; return ss.str(); } }; template std::ostream &operator<<(std::ostream &out, const FixedCapVector &v) { out << "FixedCapVector{"; bool first = true; for (const auto &t : v) { if (first) { first = false; } else { out << ", "; } out << t; } out << "}"; return out; } } // namespace stim #endif ================================================ FILE: src/stim/mem/fixed_cap_vector.test.cc ================================================ #include "stim/mem/fixed_cap_vector.h" #include "gtest/gtest.h" using namespace stim; TEST(FixedCapVector, basic_usage) { auto v = FixedCapVector(); const auto &c = v; ASSERT_EQ(v.empty(), true); ASSERT_EQ(v.size(), 0); ASSERT_EQ(v.begin(), v.end()); ASSERT_EQ(v.find("x"), v.end()); ASSERT_THROW({ v.front(); }, std::out_of_range); ASSERT_THROW({ v.back(); }, std::out_of_range); ASSERT_THROW({ c.front(); }, std::out_of_range); ASSERT_THROW({ c.back(); }, std::out_of_range); v.push_back("test"); ASSERT_EQ(v.empty(), false); ASSERT_EQ(v.size(), 1); ASSERT_EQ(*v.begin(), "test"); ASSERT_EQ(v.front(), "test"); ASSERT_EQ(v.back(), "test"); ASSERT_EQ(v.begin() + 1, v.end()); ASSERT_EQ(v.find("test"), v.begin()); ASSERT_EQ(v.find("other"), v.end()); ASSERT_EQ(v[0], "test"); ASSERT_EQ(c.empty(), false); ASSERT_EQ(c.size(), 1); ASSERT_EQ(*c.begin(), "test"); ASSERT_EQ(c.front(), "test"); ASSERT_EQ(c.back(), "test"); ASSERT_EQ(c.begin() + 1, c.end()); ASSERT_EQ(c.find("test"), c.begin()); ASSERT_EQ(c.find("other"), c.end()); ASSERT_EQ(c[0], "test"); v.push_back("1"); v.push_back("2"); v.push_back("3"); v.push_back("4"); ASSERT_THROW({ v.push_back("5"); }, std::out_of_range); ASSERT_EQ(v.find("2"), v.begin() + 2); v[0] = "new"; ASSERT_EQ(c[0], "new"); } TEST(FixedCapVector, push_pop) { auto v = FixedCapVector(); std::string v2 = "123"; v.push_back(v2); ASSERT_EQ(v2, "123"); ASSERT_EQ(v, (FixedCapVector{"123"})); v2.push_back('4'); v.push_back(std::move(v2)); ASSERT_EQ(v2, ""); ASSERT_EQ( v, (FixedCapVector{ "123", "1234", })); v.pop_back(); ASSERT_EQ( v, (FixedCapVector{ "123", })); v.pop_back(); ASSERT_EQ(v, (FixedCapVector{})); ASSERT_THROW({ v.pop_back(); }, std::out_of_range); } TEST(FixedCapVector, ordering) { auto v123 = FixedCapVector{1, 2, 3}; auto v12 = FixedCapVector{1, 2}; auto w12 = FixedCapVector{1, 2}; auto v423 = FixedCapVector{4, 2, 3}; ASSERT_LT(v123, v423); ASSERT_LT(v12, v123); ASSERT_TRUE((FixedCapVector{4, 2} < FixedCapVector{4, 2, 1})); ASSERT_TRUE((FixedCapVector{4, 2, 0} < FixedCapVector{4, 2, 1})); ASSERT_FALSE((FixedCapVector{4, 2, 2} < FixedCapVector{4, 2, 1})); ASSERT_TRUE((FixedCapVector{4, 1, 2} < FixedCapVector{4, 2, 1})); ASSERT_FALSE((FixedCapVector{4, 3, 2} < FixedCapVector{4, 2, 1})); ASSERT_FALSE((FixedCapVector{4, 3, 1} < FixedCapVector{4, 2, 2})); ASSERT_TRUE(v123 < v423); ASSERT_FALSE(v423 < v123); ASSERT_TRUE(v12 == w12); ASSERT_TRUE(!(v12 != w12)); ASSERT_TRUE(v12 != v123); ASSERT_TRUE(!(v12 == v123)); ASSERT_TRUE(v423 != v123); ASSERT_TRUE(!(v423 == v123)); } TEST(FixedCapVector, str) { ASSERT_EQ((FixedCapVector{1, 2, 3}.str()), "FixedCapVector{1, 2, 3}"); } ================================================ FILE: src/stim/mem/monotonic_buffer.h ================================================ /* * 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. */ #ifndef _STIM_MEM_MONOTONIC_BUFFER_H #define _STIM_MEM_MONOTONIC_BUFFER_H #include #include #include #include #include "stim/mem/span_ref.h" namespace stim { /// A memory resource that can efficiently incrementally accumulate data. /// /// There are three important types of "region" in play: the tail region, the current region, and old regions. /// /// The tail is for contiguous data being added incrementally into the buffer. /// When the tail grows beyond the currently available storage, more memory is allocated and the tail is is copied into /// the new memory so that it can stay contiguous. At any time, the tail can be discarded or committed. Discarding the /// tail allows the memory it was covering to be re-used when writing the next tail. Committing the tail permanently /// preserves that data (until the monotonic buffer is cleared or deconstructed) and also guarantees it will no longer /// move so pointers to it can be stored. /// /// The current region is a contiguous chunk of memory that the tail is being written into. /// When the tail grows beyond this region and triggers an allocation, the current region is relabelled as an old region /// and the newly allocated memory is now the current region. Each subsequent current region will be at least double the /// size of the previous one. /// /// The old regions are memory that has been finalized, and will be stored until the buffer is cleared or deconstructed. template struct MonotonicBuffer { /// Contiguous memory that is being appended to, but has not yet been committed. SpanRef tail; /// The current contiguous memory region with a mix of committed, staged, and unused memory. SpanRef cur; /// Old contiguous memory regions that have been committed and now need to be kept. std::vector> old_areas; /// Constructs an empty monotonic buffer. MonotonicBuffer() : tail(), cur(), old_areas() { } /// Constructs an empty monotonic buffer with initial capacity for its current region. MonotonicBuffer(size_t reserve) { ensure_available(reserve); } void _soft_clear() { cur.ptr_start = nullptr; cur.ptr_end = nullptr; tail.ptr_start = nullptr; tail.ptr_end = nullptr; old_areas.clear(); } void _hard_clear() { for (auto old : old_areas) { free(old.ptr_start); } if (cur.ptr_start != nullptr) { free(cur.ptr_start); } } ~MonotonicBuffer() { _hard_clear(); } MonotonicBuffer(MonotonicBuffer &&other) noexcept : tail(other.tail), cur(other.cur), old_areas(std::move(other.old_areas)) { other._soft_clear(); } MonotonicBuffer(const MonotonicBuffer &other) = delete; MonotonicBuffer &operator=(MonotonicBuffer &&other) noexcept { _hard_clear(); cur = other.cur; tail = other.tail; old_areas = std::move(other.old_areas); other._soft_clear(); return *this; } /// Invalidates all previous data and resets the class into a clean state. /// /// Happens to keep the current contiguous memory region and free old regions. void clear() { for (auto old : old_areas) { free(old.ptr_start); } old_areas.clear(); tail.ptr_end = tail.ptr_start = cur.ptr_start; } /// Returns the size of memory allocated and held by this monotonic buffer (in units of sizeof(T)). size_t total_allocated() const { size_t result = cur.size(); for (auto old : old_areas) { result += old.size(); } return result; } /// Appends and commits data. /// Requires the tail to be empty, to avoid bugs where previously staged data is committed. std::span take_copy(std::span data) { assert(tail.size() == 0); append_tail(data); return commit_tail(); } std::string_view take_copy(std::string_view data) { if (data.empty()) { return std::string_view(); } assert(tail.size() == 0); append_tail(SpanRef(&data[0], &data[0] + data.size())); SpanRef v = commit_tail(); return {v.ptr_start, v.size()}; } /// Adds a staged data item. void append_tail(T item) { ensure_available(1); *tail.ptr_end = item; tail.ptr_end++; } /// Adds staged data. void append_tail(SpanRef data) { ensure_available(data.size()); std::copy(data.begin(), data.end(), tail.ptr_end); tail.ptr_end += data.size(); } /// Throws away staged data, so its memory can be re-used. void discard_tail() { tail.ptr_end = tail.ptr_start; } /// Changes staged data into committed data that will be kept until the buffer is cleared or deconstructed. SpanRef commit_tail() { SpanRef result(tail); tail.ptr_start = tail.ptr_end; return result; } /// Ensures it is possible to stage at least `min_required` more items without more reallocations. void ensure_available(size_t min_required) { size_t available = cur.ptr_end - tail.ptr_end; if (available >= min_required) { return; } size_t alloc_count = std::max(min_required + tail.size(), cur.size() << 1); if (cur.ptr_start != nullptr) { old_areas.push_back(cur); } cur.ptr_start = (T *)malloc(alloc_count * sizeof(T)); cur.ptr_end = cur.ptr_start + alloc_count; // Staged data is not complete yet; keep it contiguous by copying it to the new larger memory region. size_t tail_size = tail.size(); if (tail_size) { std::move(tail.ptr_start, tail.ptr_end, cur.ptr_start); } tail = {cur.ptr_start, cur.ptr_start + tail_size}; } }; } // namespace stim #endif ================================================ FILE: src/stim/mem/monotonic_buffer.test.cc ================================================ // 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. #include "stim/mem/monotonic_buffer.h" #include "gtest/gtest.h" using namespace stim; TEST(pointer_range, equality) { int data[100]{}; SpanRef r1{&data[0], &data[10]}; SpanRef r2{&data[10], &data[20]}; SpanRef r4{&data[30], &data[50]}; ASSERT_TRUE(r1 == r2); ASSERT_FALSE(r1 != r2); ASSERT_EQ(r1, r2); ASSERT_NE(r1, r4); r2[0] = 1; ASSERT_EQ(data[10], 1); ASSERT_NE(r1, r2); ASSERT_TRUE(r1 != r2); ASSERT_FALSE(r1 == r2); ASSERT_NE(r1, r4); r2[0] = 0; ASSERT_EQ(r1, r2); r2[6] = 1; ASSERT_NE(r1, r2); } TEST(monotonic_buffer, append_tail) { MonotonicBuffer buf; for (size_t k = 0; k < 100; k++) { buf.append_tail(k); } SpanRef rng = buf.commit_tail(); ASSERT_EQ(rng.size(), 100); for (size_t k = 0; k < 100; k++) { ASSERT_EQ(rng[k], k); } } TEST(monotonic_buffer, ensure_available) { MonotonicBuffer buf; buf.append_tail(std::vector{1, 2, 3, 4}); buf.append_tail(std::vector{5, 6}); buf.append_tail(std::vector{7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); SpanRef rng = buf.commit_tail(); std::vector expected{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; SpanRef v = expected; ASSERT_EQ(rng, v); } ================================================ FILE: src/stim/mem/simd_bit_table.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SIMD_BIT_TABLE_H #define _STIM_MEM_SIMD_BIT_TABLE_H #include "stim/mem/simd_bits.h" namespace stim { /// A 2d array of bit-packed booleans, padded and aligned to make simd operations more efficient. /// /// The table contents are indexed by a major axis (not contiguous in memory) then a minor axis (contiguous in memory). /// /// Note that, due to the padding, the smallest table you can have is 256x256 bits (8 KiB). /// Technically the padding of the major axis is not necessary, but it's included so that transposing preserves size. /// /// Similar to simd_bits, simd_bit_table has no notion of the "intended" size of data, only the padded size. The /// intended size has to be stored separately. template struct simd_bit_table { size_t num_simd_words_major; size_t num_simd_words_minor; simd_bits data; /// Creates zero initialized table. simd_bit_table(size_t min_bits_major, size_t min_bits_minor); /// Creates a randomly initialized table. static simd_bit_table random( size_t num_randomized_major_bits, size_t num_randomized_minor_bits, std::mt19937_64 &rng); /// Creates a square table with 1s down the diagonal. static simd_bit_table identity(size_t n); /// Concatenates tables together to form a larger table. static simd_bit_table from_quadrants( size_t n, const simd_bit_table &upper_left, const simd_bit_table &upper_right, const simd_bit_table &lower_left, const simd_bit_table &lower_right); /// Parses a bit table from some text. /// /// Args: /// text: A paragraph of characters specifying the contents of a bit table. /// Each line is a row (a major index) of the table. /// Each position within a line is a column (a minor index) of the table. /// A '1' at character C of line L (counting from 0) indicates out[L][C] will be set. /// A '0', '.', or '_' indicates out[L][C] will not be set. /// Leading newlines and spaces at the beginning of the text are ignored. /// Leading spaces at the beginning of a line are ignored. /// Other characters result in errors. /// /// Returns: /// A simd_bit_table with cell contents corresponding to the text. static simd_bit_table from_text(const char *text, size_t min_rows = 0, size_t min_cols = 0); /// Resizes the table. Doesn't clear to zero. Does nothing if already the target size. void destructive_resize(size_t new_min_bits_major, size_t new_min_bits_minor); /// Copies the table into another table. /// /// It's safe for the other table to have a different size. /// When the other table has a different size, only the data at locations common to both /// tables are copied over. void copy_into_different_size_table(simd_bit_table &other) const; /// Resizes the table, keeping any data common to the old and new size and otherwise zeroing data. void resize(size_t new_min_bits_major, size_t new_min_bits_minor); /// Equality. bool operator==(const simd_bit_table &other) const; /// Inequality. bool operator!=(const simd_bit_table &other) const; /// Returns a reference to a row (column) of the table, when using row (column) major indexing. inline simd_bits_range_ref operator[](size_t major_index) { return data.word_range_ref(major_index * num_simd_words_minor, num_simd_words_minor); } /// Returns a const reference to a row (column) of the table, when using row (column) major indexing. inline const simd_bits_range_ref operator[](size_t major_index) const { return data.word_range_ref(major_index * num_simd_words_minor, num_simd_words_minor); } /// operator[] lets us extract a specified bit as (*this)[major_index][minor_index]. /// We can decompose these indicies as /// major_index = (major_index_high << bitword::BIT_POW) + major_index_low /// minor_index = (minor_index_high << bitword::BIT_POW) + minor_index_low /// As minor_index_low ranges from 0 to W-1, (*this)[major_index][minor_index] ranges over the /// bits of an aligned SIMD word. get_index_of_bitword returns the index in data.ptr_simd of /// the corresponding simd word. inline size_t get_index_of_bitword(size_t major_index_high, size_t major_index_low, size_t minor_index_high) const { size_t major_index = (major_index_high << bitword::BIT_POW) + major_index_low; return major_index * num_simd_words_minor + minor_index_high; } /// Square matrix multiplication (assumes row major indexing). n is the diameter of the matrix. simd_bit_table square_mat_mul(const simd_bit_table &rhs, size_t n) const; /// Square matrix inverse, assuming input is lower triangular. n is the diameter of the matrix. simd_bit_table inverse_assuming_lower_triangular(size_t n) const; /// Transposes the table inplace. void do_square_transpose(); /// Transposes the table out of place into a target location. void transpose_into(simd_bit_table &out) const; /// Transposes the table out of place. simd_bit_table transposed() const; /// Returns a subset of the table. simd_bit_table slice_maj(size_t maj_start_bit, size_t maj_stop_bit) const; /// Returns a copy of a column of the table. simd_bits read_across_majors_at_minor_index(size_t major_start, size_t major_stop, size_t minor_index) const; /// Concatenates the contents of the two tables, along the major axis. simd_bit_table concat_major(const simd_bit_table &second, size_t n_first, size_t n_second) const; /// Overwrites a range of the table with a range from another table with the same minor size. void overwrite_major_range_with( size_t dst_major_start, const simd_bit_table &src, size_t src_major_start, size_t num_major_indices) const; /// Sets all bits in the table to zero. void clear(); /// Number of 64 bit words in a column (row) assuming row (column) major indexing. inline size_t num_major_u64_padded() const { return num_simd_words_major * (sizeof(bitword) / sizeof(uint64_t)); } /// Number of 32 bit words in a column (row) assuming row (column) major indexing. inline size_t num_major_u32_padded() const { return num_simd_words_major * (sizeof(bitword) / sizeof(uint32_t)); } /// Number of 16 bit words in a column (row) assuming row (column) major indexing. inline size_t num_major_u16_padded() const { return num_simd_words_major * (sizeof(bitword) / sizeof(uint16_t)); } /// Number of 8 bit words in a column (row) assuming row (column) major indexing. inline size_t num_major_u8_padded() const { return num_simd_words_major * (sizeof(bitword) / sizeof(uint8_t)); } /// Number of bits in a column (row) assuming row (column) major indexing. inline size_t num_major_bits_padded() const { return num_simd_words_major * W; } /// Number of 64 bit words in a row (column) assuming row (column) major indexing. inline size_t num_minor_u64_padded() const { return num_simd_words_minor * (sizeof(bitword) / sizeof(uint64_t)); } /// Number of 32 bit words in a row (column) assuming row (column) major indexing. inline size_t num_minor_u32_padded() const { return num_simd_words_minor * (sizeof(bitword) / sizeof(uint32_t)); } /// Number of 16 bit words in a row (column) assuming row (column) major indexing. inline size_t num_minor_u16_padded() const { return num_simd_words_minor * (sizeof(bitword) / sizeof(uint16_t)); } /// Number of 8 bit words in a row (column) assuming row (column) major indexing. inline size_t num_minor_u8_padded() const { return num_simd_words_minor * (sizeof(bitword) / sizeof(uint8_t)); } /// Number of bits in a row (column) assuming row (column) major indexing. inline size_t num_minor_bits_padded() const { return num_simd_words_minor * W; } /// Returns a padded description of the table's contents. std::string str() const; /// Returns a truncated square description of the table's contents. std::string str(size_t n) const; /// Returns a truncated rectangle description of the table's contents. std::string str(size_t rows, size_t cols) const; }; template std::ostream &operator<<(std::ostream &out, const simd_bit_table &v); constexpr uint8_t lg(size_t k) { return k <= 1 ? 0 : lg(k >> 1) + 1; } } // namespace stim #include "stim/mem/simd_bit_table.inl" #endif ================================================ FILE: src/stim/mem/simd_bit_table.inl ================================================ // 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. #include #include #include #include namespace stim { template simd_bit_table::simd_bit_table(size_t min_bits_major, size_t min_bits_minor) : num_simd_words_major(min_bits_to_num_simd_words(min_bits_major)), num_simd_words_minor(min_bits_to_num_simd_words(min_bits_minor)), data(min_bits_to_num_bits_padded(min_bits_minor) * min_bits_to_num_bits_padded(min_bits_major)) { } template simd_bit_table simd_bit_table::identity(size_t n) { simd_bit_table result(n, n); for (size_t k = 0; k < n; k++) { result[k][k] = true; } return result; } template void simd_bit_table::clear() { data.clear(); } template bool simd_bit_table::operator==(const simd_bit_table &other) const { return num_simd_words_minor == other.num_simd_words_minor && num_simd_words_major == other.num_simd_words_major && data == other.data; } template bool simd_bit_table::operator!=(const simd_bit_table &other) const { return !(*this == other); } template simd_bit_table simd_bit_table::square_mat_mul(const simd_bit_table &rhs, size_t n) const { assert(num_major_bits_padded() >= n && num_minor_bits_padded() >= n); assert(rhs.num_major_bits_padded() >= n && rhs.num_minor_bits_padded() >= n); auto tmp = rhs.transposed(); simd_bit_table result(n, n); for (size_t row = 0; row < n; row++) { for (size_t col = 0; col < n; col++) { bitword acc{}; (*this)[row].for_each_word(tmp[col], [&](bitword &w1, bitword &w2) { acc ^= w1 & w2; }); result[row][col] = acc.popcount() & 1; } } return result; } template simd_bit_table simd_bit_table::inverse_assuming_lower_triangular(size_t n) const { assert(num_major_bits_padded() >= n && num_minor_bits_padded() >= n); simd_bit_table result = simd_bit_table::identity(n); simd_bits copy_row(num_minor_bits_padded()); for (size_t target = 0; target < n; target++) { copy_row = (*this)[target]; for (size_t pivot = 0; pivot < target; pivot++) { if (copy_row[pivot]) { copy_row ^= (*this)[pivot]; result[target] ^= result[pivot]; } } } return result; } template void exchange_low_indices(simd_bit_table &table) { for (size_t maj_high = 0; maj_high < table.num_simd_words_major; maj_high++) { for (size_t min_high = 0; min_high < table.num_simd_words_minor; min_high++) { size_t block_start = table.get_index_of_bitword(maj_high, 0, min_high); bitword::inplace_transpose_square(table.data.ptr_simd + block_start, table.num_simd_words_minor); } } } template void simd_bit_table::destructive_resize(size_t new_min_bits_major, size_t new_min_bits_minor) { num_simd_words_minor = min_bits_to_num_simd_words(new_min_bits_minor); num_simd_words_major = min_bits_to_num_simd_words(new_min_bits_major); data.destructive_resize(num_simd_words_minor * num_simd_words_major * W * W); } template void simd_bit_table::copy_into_different_size_table(simd_bit_table &other) const { size_t ni = num_simd_words_minor; size_t na = num_simd_words_major; size_t mi = other.num_simd_words_minor; size_t ma = other.num_simd_words_major; size_t num_min_bytes = std::min(ni, mi) * (W / 8); size_t num_maj = std::min(na, ma) * W; if (ni == mi) { memcpy(other.data.ptr_simd, data.ptr_simd, num_min_bytes * num_maj); } else { for (size_t maj = 0; maj < num_maj; maj++) { memcpy(other[maj].ptr_simd, (*this)[maj].ptr_simd, num_min_bytes); } } } template void simd_bit_table::resize(size_t new_min_bits_major, size_t new_min_bits_minor) { auto new_num_simd_words_minor = min_bits_to_num_simd_words(new_min_bits_minor); auto new_num_simd_words_major = min_bits_to_num_simd_words(new_min_bits_major); if (new_num_simd_words_major == num_simd_words_major && new_num_simd_words_minor == num_simd_words_minor) { return; } auto new_table = simd_bit_table(new_min_bits_major, new_min_bits_minor); copy_into_different_size_table(new_table); *this = std::move(new_table); } template void simd_bit_table::do_square_transpose() { assert(num_simd_words_minor == num_simd_words_major); // Current address tensor indices: [...min_low ...min_high ...maj_low ...maj_high] exchange_low_indices(*this); // Current address tensor indices: [...maj_low ...min_high ...min_low ...maj_high] // Permute data such that high address bits of majors and minors are exchanged. for (size_t maj_high = 0; maj_high < num_simd_words_major; maj_high++) { for (size_t min_high = maj_high + 1; min_high < num_simd_words_minor; min_high++) { for (size_t maj_low = 0; maj_low < W; maj_low++) { std::swap( data.ptr_simd[get_index_of_bitword(maj_high, maj_low, min_high)], data.ptr_simd[get_index_of_bitword(min_high, maj_low, maj_high)]); } } } // Current address tensor indices: [...maj_low ...maj_high ...min_low ...min_high] } template simd_bit_table simd_bit_table::transposed() const { simd_bit_table result(num_minor_bits_padded(), num_major_bits_padded()); transpose_into(result); return result; } template simd_bits simd_bit_table::read_across_majors_at_minor_index( size_t major_start, size_t major_stop, size_t minor_index) const { assert(major_stop >= major_start); assert(major_stop <= num_major_bits_padded()); assert(minor_index < num_minor_bits_padded()); simd_bits result(major_stop - major_start); for (size_t maj = major_start; maj < major_stop; maj++) { result[maj - major_start] = (*this)[maj][minor_index]; } return result; } template simd_bit_table simd_bit_table::slice_maj(size_t maj_start_bit, size_t maj_stop_bit) const { simd_bit_table result(maj_stop_bit - maj_start_bit, num_minor_bits_padded()); for (size_t k = maj_start_bit; k < maj_stop_bit; k++) { result[k - maj_start_bit] = (*this)[k]; } return result; } template void simd_bit_table::transpose_into(simd_bit_table &out) const { assert(out.num_simd_words_minor == num_simd_words_major); assert(out.num_simd_words_major == num_simd_words_minor); for (size_t maj_high = 0; maj_high < num_simd_words_major; maj_high++) { for (size_t min_high = 0; min_high < num_simd_words_minor; min_high++) { for (size_t maj_low = 0; maj_low < W; maj_low++) { size_t src_index = get_index_of_bitword(maj_high, maj_low, min_high); size_t dst_index = out.get_index_of_bitword(min_high, maj_low, maj_high); out.data.ptr_simd[dst_index] = data.ptr_simd[src_index]; } } } exchange_low_indices(out); } template simd_bit_table simd_bit_table::from_quadrants( size_t n, const simd_bit_table &upper_left, const simd_bit_table &upper_right, const simd_bit_table &lower_left, const simd_bit_table &lower_right) { assert(upper_left.num_minor_bits_padded() >= n && upper_left.num_major_bits_padded() >= n); assert(upper_right.num_minor_bits_padded() >= n && upper_right.num_major_bits_padded() >= n); assert(lower_left.num_minor_bits_padded() >= n && lower_left.num_major_bits_padded() >= n); assert(lower_right.num_minor_bits_padded() >= n && lower_right.num_major_bits_padded() >= n); simd_bit_table result(n << 1, n << 1); for (size_t row = 0; row < n; row++) { for (size_t col = 0; col < n; col++) { result[row][col] = upper_left[row][col]; result[row][col + n] = upper_right[row][col]; result[row + n][col] = lower_left[row][col]; result[row + n][col + n] = lower_right[row][col]; } } return result; } template std::string simd_bit_table::str(size_t rows, size_t cols) const { std::stringstream out; for (size_t row = 0; row < rows; row++) { if (row) { out << "\n"; } for (size_t col = 0; col < cols; col++) { out << ".1"[(*this)[row][col]]; } } return out.str(); } template std::string simd_bit_table::str(size_t n) const { return str(n, n); } template std::string simd_bit_table::str() const { return str(num_major_bits_padded(), num_minor_bits_padded()); } template simd_bit_table simd_bit_table::concat_major( const simd_bit_table &second, size_t n_first, size_t n_second) const { if (num_major_bits_padded() < n_first || second.num_major_bits_padded() < n_second || num_minor_bits_padded() != second.num_minor_bits_padded()) { throw std::invalid_argument("Size mismatch"); } simd_bit_table result(n_first + n_second, num_minor_bits_padded()); auto n1 = n_first * num_minor_u8_padded(); auto n2 = n_second * num_minor_u8_padded(); memcpy(result.data.u8, data.u8, n1); memcpy(result.data.u8 + n1, second.data.u8, n2); return result; } template void simd_bit_table::overwrite_major_range_with( size_t dst_major_start, const simd_bit_table &src, size_t src_major_start, size_t num_major_indices) const { assert(src.num_minor_bits_padded() == num_minor_bits_padded()); memcpy( data.u8 + dst_major_start * num_minor_u8_padded(), src.data.u8 + src_major_start * src.num_minor_u8_padded(), num_major_indices * num_minor_u8_padded()); } template simd_bit_table simd_bit_table::from_text(const char *text, size_t min_rows, size_t min_cols) { std::vector> lines; lines.push_back({}); // Skip indentation. while (*text == '\n' || *text == ' ') { text++; } for (const char *c = text; *c;) { if (*c == '\n') { lines.push_back({}); c++; // Skip indentation. while (*c == ' ') { c++; } } else if (*c == '0' || *c == '.' || *c == '_') { lines.back().push_back(false); c++; } else if (*c == '1') { lines.back().push_back(true); c++; } else { throw std::invalid_argument( "Expected indented characters from \"10._\\n\". Got '" + std::string(1, *c) + "'."); } } // Remove trailing newline. if (!lines.empty() && lines.back().empty()) { lines.pop_back(); } size_t num_cols = min_cols; for (const auto &v : lines) { num_cols = std::max(v.size(), num_cols); } size_t num_rows = std::max(min_rows, lines.size()); simd_bit_table out(num_rows, num_cols); for (size_t row = 0; row < lines.size(); row++) { for (size_t col = 0; col < lines[row].size(); col++) { out[row][col] = lines[row][col]; } } return out; } template simd_bit_table simd_bit_table::random( size_t num_randomized_major_bits, size_t num_randomized_minor_bits, std::mt19937_64 &rng) { simd_bit_table result(num_randomized_major_bits, num_randomized_minor_bits); for (size_t maj = 0; maj < num_randomized_major_bits; maj++) { result[maj].randomize(num_randomized_minor_bits, rng); } return result; } template std::ostream &operator<<(std::ostream &out, const stim::simd_bit_table &v) { for (size_t k = 0; k < v.num_major_bits_padded(); k++) { if (k) { out << '\n'; } out << v[k]; } return out; } } // namespace stim ================================================ FILE: src/stim/mem/simd_bit_table.perf.cc ================================================ // 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. #include "stim/mem/simd_bit_table.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(simd_bit_table_inplace_square_transpose_diam10K) { size_t n = 10 * 1000; simd_bit_table table(n, n); benchmark_go([&]() { table.do_square_transpose(); }) .goal_millis(6) .show_rate("Bits", n * n); } BENCHMARK(simd_bit_table_out_of_place_transpose_diam10K) { size_t n = 10 * 1000; simd_bit_table table(n, n); simd_bit_table out(n, n); benchmark_go([&]() { table.transpose_into(out); }) .goal_millis(12) .show_rate("Bits", n * n); } ================================================ FILE: src/stim/mem/simd_bit_table.test.cc ================================================ // 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. #include "stim/mem/simd_bit_table.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(simd_bit_table, creation, { simd_bit_table a(3, 3); ASSERT_EQ( a.str(3), "...\n" "...\n" "..."); a[1][2] = 1; ASSERT_EQ( a.str(3), "...\n" "..1\n" "..."); ASSERT_EQ( simd_bit_table::identity(3).str(3), "1..\n" ".1.\n" "..1"); ASSERT_EQ( simd_bit_table::identity(2).str(2), "1.\n" ".1"); ASSERT_EQ( simd_bit_table::identity(2).str(3, 4), "1...\n" ".1..\n" "...."); ASSERT_EQ(simd_bit_table::identity(2).str().substr(0, 5), "1...."); ASSERT_EQ(simd_bit_table(0, 5).str(), ""); ASSERT_EQ(simd_bit_table(5, 0).str().substr(0, 3), "\n\n\n"); ASSERT_EQ( simd_bit_table::from_text(R"TABLE( 1. 0._1 .1.. )TABLE") .str(5), "1....\n" "...1.\n" ".1...\n" ".....\n" "....."); simd_bit_table t = simd_bit_table::from_text("", 512, 256); ASSERT_EQ(t.num_minor_bits_padded(), 256); ASSERT_EQ(t.num_major_bits_padded(), 512); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, equality, { simd_bit_table a(3, 3); simd_bit_table b(3, 3); simd_bit_table c(511, 1000); simd_bit_table d(511, 1000); ASSERT_EQ(a, b); ASSERT_EQ(c, d); ASSERT_NE(a, c); a[0][1] = 1; ASSERT_NE(a, b); d[500][900] = 1; ASSERT_NE(c, d); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, multiplication, { simd_bit_table m1(3, 3); simd_bit_table m2(3, 3); m1[0][2] = true; m2[2][1] = true; auto m3 = m1.square_mat_mul(m2, 3); ASSERT_TRUE(m3[0][1]); ASSERT_EQ( m1.str(3), "..1\n" "...\n" "..."); ASSERT_EQ( m2.str(3), "...\n" "...\n" ".1."); ASSERT_EQ( m3.str(3), ".1.\n" "...\n" "..."); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, xor_row_into, { simd_bit_table m(500, 500); m[0][10] = true; m[0][490] = true; m[5][490] = true; m[1] ^= m[0]; m[1] ^= m[5]; ASSERT_EQ(m[1][10], true); ASSERT_EQ(m[1][490], false); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, inverse_assuming_lower_triangular, { auto m = simd_bit_table::identity(4); m[3][1] = true; ASSERT_EQ( m.str(4), "1...\n" ".1..\n" "..1.\n" ".1.1"); auto inv = m.inverse_assuming_lower_triangular(4); ASSERT_EQ(m.square_mat_mul(inv, 4), simd_bit_table::identity(4)); ASSERT_EQ(inv.square_mat_mul(m, 4), simd_bit_table::identity(4)); ASSERT_EQ( inv.str(4), "1...\n" ".1..\n" "..1.\n" ".1.1"); m[3][0] = true; ASSERT_EQ( m.str(4), "1...\n" ".1..\n" "..1.\n" "11.1"); inv = m.inverse_assuming_lower_triangular(4); ASSERT_EQ(m.square_mat_mul(inv, 4), simd_bit_table::identity(4)); ASSERT_EQ(inv.square_mat_mul(m, 4), simd_bit_table::identity(4)); ASSERT_EQ( inv.str(4), "1...\n" ".1..\n" "..1.\n" "11.1"); m[1][0] = true; ASSERT_EQ( m.str(4), "1...\n" "11..\n" "..1.\n" "11.1"); inv = m.inverse_assuming_lower_triangular(4); ASSERT_EQ(m.square_mat_mul(inv, 4), simd_bit_table::identity(4)); ASSERT_EQ(inv.square_mat_mul(m, 4), simd_bit_table::identity(4)); ASSERT_EQ( inv.str(4), "1...\n" "11..\n" "..1.\n" ".1.1"); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, transposed, { auto m = simd_bit_table::identity(4); m[3][1] = true; ASSERT_EQ( m.str(4), "1...\n" ".1..\n" "..1.\n" ".1.1"); auto trans = m; trans.do_square_transpose(); ASSERT_EQ( trans.str(4), "1...\n" ".1.1\n" "..1.\n" "...1"); auto trans2 = trans; trans2.do_square_transpose(); ASSERT_EQ(trans2, m); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, random, { auto rng = INDEPENDENT_TEST_RNG(); auto t = simd_bit_table::random(100, 90, rng); ASSERT_NE(t[99], simd_bits(90)); ASSERT_EQ(t[100], simd_bits(90)); t = t.transposed(); ASSERT_NE(t[89], simd_bits(100)); ASSERT_EQ(t[90], simd_bits(100)); ASSERT_NE(simd_bit_table::random(10, 10, rng), simd_bit_table::random(10, 10, rng)); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, slice_maj, { auto rng = INDEPENDENT_TEST_RNG(); auto m = simd_bit_table::random(100, 64, rng); auto s = m.slice_maj(5, 15); ASSERT_EQ(s[0], m[5]); ASSERT_EQ(s[9], m[14]); ASSERT_FALSE(s[10].not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, from_concat_major, { auto a = simd_bit_table::from_text(R"TABLE( 000111 101011 111111 000000 )TABLE"); auto b = simd_bit_table::from_text(R"TABLE( 100000 000001 000100 )TABLE"); ASSERT_EQ(a.concat_major(b, 4, 3), simd_bit_table::from_text(R"TABLE( 000111 101011 111111 000000 100000 000001 000100 )TABLE")); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, from_quadrants, { simd_bit_table t(2, 2); simd_bit_table z(2, 2); t[1][0] = true; ASSERT_EQ( simd_bit_table::from_quadrants(2, t, z, z, z).str(4), "....\n" "1...\n" "....\n" "...."); ASSERT_EQ( simd_bit_table::from_quadrants(2, z, t, z, z).str(4), "....\n" "..1.\n" "....\n" "...."); ASSERT_EQ( simd_bit_table::from_quadrants(2, z, z, t, z).str(4), "....\n" "....\n" "....\n" "1..."); ASSERT_EQ( simd_bit_table::from_quadrants(2, z, z, z, t).str(4), "....\n" "....\n" "....\n" "..1."); ASSERT_EQ( simd_bit_table::from_quadrants(2, t, t, t, t).str(4), "....\n" "1.1.\n" "....\n" "1.1."); }) TEST(simd_bit_table, lg) { ASSERT_EQ(lg(0), 0); ASSERT_EQ(lg(1), 0); ASSERT_EQ(lg(2), 1); ASSERT_EQ(lg(3), 1); ASSERT_EQ(lg(4), 2); ASSERT_EQ(lg(5), 2); ASSERT_EQ(lg(6), 2); ASSERT_EQ(lg(7), 2); ASSERT_EQ(lg(8), 3); ASSERT_EQ(lg(9), 3); } TEST_EACH_WORD_SIZE_W(simd_bit_table, destructive_resize, { auto rng = INDEPENDENT_TEST_RNG(); simd_bit_table table = simd_bit_table::random(5, 7, rng); const uint8_t *prev_pointer = table.data.u8; table.destructive_resize(5, 7); ASSERT_EQ(table.data.u8, prev_pointer); table.destructive_resize(1025, 7); ASSERT_NE(table.data.u8, prev_pointer); ASSERT_GE(table.num_major_bits_padded(), 1025); ASSERT_GE(table.num_minor_bits_padded(), 7); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, read_across_majors_at_minor_index, { auto rng = INDEPENDENT_TEST_RNG(); simd_bit_table table = simd_bit_table::random(5, 7, rng); simd_bits slice = table.read_across_majors_at_minor_index(2, 5, 1); ASSERT_GE(slice.num_bits_padded(), 4); ASSERT_EQ(slice[0], table[2][1]); ASSERT_EQ(slice[1], table[3][1]); ASSERT_EQ(slice[2], table[4][1]); ASSERT_EQ(slice[3], false); }) template bool is_table_overlap_identical(const simd_bit_table &a, const simd_bit_table &b) { size_t w_min = std::min(a.num_simd_words_minor, b.num_simd_words_minor); size_t n_maj = std::min(a.num_major_bits_padded(), b.num_major_bits_padded()); for (size_t k_maj = 0; k_maj < n_maj; k_maj++) { if (a[k_maj].word_range_ref(0, w_min) != b[k_maj].word_range_ref(0, w_min)) { return false; } } return true; } template bool is_table_zero_outside(const simd_bit_table &a, size_t num_major_bits, size_t num_minor_bits) { size_t num_major_words = min_bits_to_num_simd_words(num_major_bits); size_t num_minor_words = min_bits_to_num_simd_words(num_minor_bits); if (a.num_simd_words_minor > num_minor_words) { for (size_t k = 0; k < a.num_simd_words_major; k++) { if (a[k].word_range_ref(num_minor_words, a.num_simd_words_minor - num_minor_words).not_zero()) { return false; } } } for (size_t k = a.num_simd_words_major; k < num_major_words; k++) { if (a[k].not_zero()) { return false; } } return true; } TEST_EACH_WORD_SIZE_W(simd_bit_table, copy_into_different_size_table, { auto rng = INDEPENDENT_TEST_RNG(); auto check_size = [&](size_t w1, size_t h1, size_t w2, size_t h2) { simd_bit_table src = simd_bit_table::random(w1, h1, rng); simd_bit_table dst = simd_bit_table::random(w1, h1, rng); src.copy_into_different_size_table(dst); return is_table_overlap_identical(src, dst); }; EXPECT_TRUE(check_size(0, 0, 0, 0)); EXPECT_TRUE(check_size(64, 0, 0, 0)); EXPECT_TRUE(check_size(0, 64, 0, 0)); EXPECT_TRUE(check_size(0, 0, 64, 0)); EXPECT_TRUE(check_size(0, 0, 0, 64)); EXPECT_TRUE(check_size(64, 64, 64, 64)); EXPECT_TRUE(check_size(512, 64, 64, 64)); EXPECT_TRUE(check_size(64, 512, 64, 64)); EXPECT_TRUE(check_size(64, 64, 512, 64)); EXPECT_TRUE(check_size(64, 64, 64, 512)); EXPECT_TRUE(check_size(512, 512, 64, 64)); EXPECT_TRUE(check_size(512, 64, 512, 64)); EXPECT_TRUE(check_size(512, 64, 64, 512)); EXPECT_TRUE(check_size(64, 512, 512, 64)); EXPECT_TRUE(check_size(64, 512, 64, 512)); EXPECT_TRUE(check_size(64, 64, 512, 512)); }) TEST_EACH_WORD_SIZE_W(simd_bit_table, resize, { auto rng = INDEPENDENT_TEST_RNG(); auto check_size = [&](size_t w1, size_t h1, size_t w2, size_t h2) { simd_bit_table src = simd_bit_table::random(w1, h1, rng); simd_bit_table dst = src; dst.resize(w2, h2); return is_table_overlap_identical(src, dst) && is_table_zero_outside(dst, std::min(w1, w2), std::min(h1, h2)); }; EXPECT_TRUE(check_size(0, 0, 0, 0)); EXPECT_TRUE(check_size(64, 0, 0, 0)); EXPECT_TRUE(check_size(0, 64, 0, 0)); EXPECT_TRUE(check_size(0, 0, 64, 0)); EXPECT_TRUE(check_size(0, 0, 0, 64)); EXPECT_TRUE(check_size(64, 64, 64, 64)); EXPECT_TRUE(check_size(512, 64, 64, 64)); EXPECT_TRUE(check_size(64, 512, 64, 64)); EXPECT_TRUE(check_size(64, 64, 512, 64)); EXPECT_TRUE(check_size(64, 64, 64, 512)); EXPECT_TRUE(check_size(512, 512, 64, 64)); EXPECT_TRUE(check_size(512, 64, 512, 64)); EXPECT_TRUE(check_size(512, 64, 64, 512)); EXPECT_TRUE(check_size(64, 512, 512, 64)); EXPECT_TRUE(check_size(64, 512, 64, 512)); EXPECT_TRUE(check_size(64, 64, 512, 512)); }) ================================================ FILE: src/stim/mem/simd_bits.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SIMD_BITS_H #define _STIM_MEM_SIMD_BITS_H #include #include #include "stim/mem/bit_ref.h" #include "stim/mem/simd_bits_range_ref.h" namespace stim { /// Densely packed bits, allocated with alignment and padding enabling SIMD operations. /// /// Note that, due to the padding, the smallest simd_bits you can have is 256 bits (32 bytes) long. /// /// For performance, simd_bits does not store the "intended" size of the data, only the padded size. Any intended size /// has to be tracked separately. template struct simd_bits { size_t num_simd_words; union { // It is fair to say that this is the most dangerous block, or danger-enabling block, in the entire codebase. // C++ is very particular when it comes to touching the same memory as if it had multiple different types. // If you know how to make something *for sure work as a flexibly-accessible bag of bits*, please fix this. // In the meantime, always build with `-fno-strict-aliasing` and a short ritual prayer to the compiler gods. uint8_t *u8; uint64_t *u64; bitword *ptr_simd; }; /// Constructs a zero-initialized simd_bits with at least the given number of bits. explicit simd_bits(size_t min_bits); /// Frees allocated bits. ~simd_bits(); /// Copy constructor. simd_bits(const simd_bits &other); /// Copy constructor from range reference. simd_bits(const simd_bits_range_ref other); /// Move constructor. simd_bits(simd_bits &&other) noexcept; /// Copy assignment. simd_bits &operator=(const simd_bits &other); /// Copy assignment from range reference. simd_bits &operator=(const simd_bits_range_ref other); /// Move assignment. simd_bits &operator=(simd_bits &&other) noexcept; // Xor assignment. simd_bits &operator^=(const simd_bits_range_ref other); // Mask assignment. simd_bits &operator&=(const simd_bits_range_ref other); simd_bits &operator|=(const simd_bits_range_ref other); // Addition assigment simd_bits &operator+=(const simd_bits_range_ref other); simd_bits &operator-=(const simd_bits_range_ref other); // right shift assignment simd_bits &operator>>=(int offset); // left shift assignment simd_bits &operator<<=(int offset); // Swap assignment. simd_bits &swap_with(simd_bits_range_ref other); // Equality. bool operator==(const simd_bits_range_ref &other) const; bool operator==(const simd_bits &other) const; // Inequality. bool operator!=(const simd_bits_range_ref &other) const; bool operator!=(const simd_bits &other) const; /// Determines whether or not any of the bits in the simd_bits are non-zero. bool not_zero() const; // Arbitrary ordering. bool operator<(const simd_bits_range_ref other) const; void destructive_resize(size_t new_min_bits); void preserving_resize(size_t new_min_bits); /// Returns a reference to the bit at offset k. bit_ref operator[](size_t k); /// Returns a const reference to the bit at offset k. const bit_ref operator[](size_t k) const; /// Returns a reference to the bits in this simd_bits. operator simd_bits_range_ref(); /// Returns a const reference to the bits in this simd_bits. operator const simd_bits_range_ref() const; /// Returns a reference to a sub-range of the bits in this simd_bits. inline simd_bits_range_ref word_range_ref(size_t word_offset, size_t sub_num_simd_words) { return simd_bits_range_ref(ptr_simd + word_offset, sub_num_simd_words); } /// Returns a reference to a sub-range of the bits at the start of this simd_bits. inline simd_bits_range_ref prefix_ref(size_t unpadded_bit_length) { return simd_bits_range_ref(ptr_simd, min_bits_to_num_simd_words(unpadded_bit_length)); } /// Returns a const reference to a sub-range of the bits in this simd_bits. inline const simd_bits_range_ref word_range_ref(size_t word_offset, size_t sub_num_simd_words) const { return simd_bits_range_ref(ptr_simd + word_offset, sub_num_simd_words); } /// Returns the number of bits that are 1 in the bit range. size_t popcnt() const; /// Returns the power-of-two-ness of the number, or SIZE_MAX if the number has no 1s. size_t countr_zero() const; /// Inverts all bits in the range. void invert_bits(); /// Sets all bits in the range to zero. void clear(); /// Randomizes the contents of this simd_bits using the given random number generator, up to the given bit position. void randomize(size_t num_bits, std::mt19937_64 &rng); /// Returns a simd_bits with at least the given number of bits, with bits up to the given number of bits randomized. /// Padding bits beyond the minimum number of bits are not randomized. static simd_bits random(size_t min_bits, std::mt19937_64 &rng); /// Returns whether or not the two ranges have set bits in common. bool intersects(const simd_bits_range_ref other) const; /// Returns whether or not all bits that are set in `other` are also set in this range. bool is_subset_of_or_equal_to(const simd_bits_range_ref other) const; /// Writes bits from another location. /// Bits not part of the write are unchanged. void truncated_overwrite_from(simd_bits_range_ref other, size_t num_bits); /// Sets all bits at the given position and beyond it to 0. void clear_bits_past(size_t num_kept_bits); /// Returns a description of the contents of the simd_bits. std::string str() const; /// Number of 64 bit words in the range. inline size_t num_u64_padded() const { return num_simd_words * (sizeof(bitword) / sizeof(uint64_t)); } /// Number of 32 bit words in the range. inline size_t num_u32_padded() const { return num_simd_words * (sizeof(bitword) / sizeof(uint32_t)); } /// Number of 16 bit words in the range. inline size_t num_u16_padded() const { return num_simd_words * (sizeof(bitword) / sizeof(uint16_t)); } /// Number of 8 bit words in the range. inline size_t num_u8_padded() const { return num_simd_words * (sizeof(bitword) / sizeof(uint8_t)); } /// Number of bits in the range. inline size_t num_bits_padded() const { return num_simd_words * W; } uint64_t as_u64() const; template void for_each_set_bit(CALLBACK callback) const { size_t n = num_u64_padded(); for (size_t w = 0; w < n; w++) { uint64_t v = u64[w]; while (v) { size_t q = w * 64 + std::countr_zero(v); v &= v - 1; callback(q); } } } }; template simd_bits operator^(const simd_bits_range_ref a, const simd_bits_range_ref b); template simd_bits operator|(const simd_bits_range_ref a, const simd_bits_range_ref b); template simd_bits operator&(const simd_bits_range_ref a, const simd_bits_range_ref b); template simd_bits operator^(const simd_bits a, const simd_bits_range_ref b); template simd_bits operator|(const simd_bits a, const simd_bits_range_ref b); template simd_bits operator&(const simd_bits a, const simd_bits_range_ref b); template simd_bits operator^(const simd_bits_range_ref a, const simd_bits b); template simd_bits operator|(const simd_bits_range_ref a, const simd_bits b); template simd_bits operator&(const simd_bits_range_ref a, const simd_bits b); template simd_bits operator^(const simd_bits a, const simd_bits b); template simd_bits operator|(const simd_bits a, const simd_bits b); template simd_bits operator&(const simd_bits a, const simd_bits b); template std::ostream &operator<<(std::ostream &out, const simd_bits m); } // namespace stim #include "stim/mem/simd_bits.inl" #endif ================================================ FILE: src/stim/mem/simd_bits.inl ================================================ // 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. #include #include #include #include #include #include "stim/mem/simd_util.h" namespace stim { template uint64_t *malloc_aligned_padded_zeroed(size_t min_bits) { size_t num_u8 = min_bits_to_num_bits_padded(min_bits) >> 3; void *result = bitword::aligned_malloc(num_u8); memset(result, 0, num_u8); return (uint64_t *)result; } template simd_bits::simd_bits(size_t min_bits) : num_simd_words(min_bits_to_num_simd_words(min_bits)), u64(malloc_aligned_padded_zeroed(min_bits)) { } template simd_bits::simd_bits(const simd_bits &other) : num_simd_words(other.num_simd_words), u64(malloc_aligned_padded_zeroed(other.num_bits_padded())) { memcpy(u8, other.u8, num_u8_padded()); } template simd_bits::simd_bits(const simd_bits_range_ref other) : num_simd_words(other.num_simd_words), u64(malloc_aligned_padded_zeroed(other.num_bits_padded())) { memcpy(u8, other.u8, num_u8_padded()); } template simd_bits::simd_bits(simd_bits &&other) noexcept : num_simd_words(other.num_simd_words), u64(other.u64) { other.u64 = nullptr; other.num_simd_words = 0; } template simd_bits::~simd_bits() { if (u64 != nullptr) { bitword::aligned_free(u64); u64 = nullptr; num_simd_words = 0; } } template void simd_bits::clear() { simd_bits_range_ref(*this).clear(); } template void simd_bits::invert_bits() { simd_bits_range_ref(*this).invert_bits(); } template simd_bits &simd_bits::operator=(simd_bits &&other) noexcept { (*this).~simd_bits(); new (this) simd_bits(std::move(other)); return *this; } template simd_bits &simd_bits::operator=(const simd_bits &other) { *this = simd_bits_range_ref(other); return *this; } template simd_bits &simd_bits::operator=(const simd_bits_range_ref other) { // Avoid re-allocating if already the same size. if (num_simd_words == other.num_simd_words) { simd_bits_range_ref(*this) = other; return *this; } (*this).~simd_bits(); new (this) simd_bits(other); return *this; } template bool simd_bits::operator==(const simd_bits_range_ref &other) const { return simd_bits_range_ref(*this) == other; } template bool simd_bits::operator==(const simd_bits &other) const { return simd_bits_range_ref(*this) == simd_bits_range_ref(other); } template bool simd_bits::operator!=(const simd_bits_range_ref &other) const { return !(*this == other); } template bool simd_bits::operator!=(const simd_bits &other) const { return !(*this == other); } template simd_bits simd_bits::random(size_t min_bits, std::mt19937_64 &rng) { simd_bits result(min_bits); result.randomize(min_bits, rng); return result; } template void simd_bits::randomize(size_t num_bits, std::mt19937_64 &rng) { simd_bits_range_ref(*this).randomize(num_bits, rng); } template void simd_bits::truncated_overwrite_from(simd_bits_range_ref other, size_t num_bits) { simd_bits_range_ref(*this).truncated_overwrite_from(other, num_bits); } template void simd_bits::clear_bits_past(size_t num_kept_bits) { simd_bits_range_ref(*this).clear_bits_past(num_kept_bits); } template bit_ref simd_bits::operator[](size_t k) { return bit_ref(u64, k); } template const bit_ref simd_bits::operator[](size_t k) const { return bit_ref(u64, k); } template simd_bits::operator simd_bits_range_ref() { return simd_bits_range_ref(ptr_simd, num_simd_words); } template simd_bits::operator const simd_bits_range_ref() const { return simd_bits_range_ref(ptr_simd, num_simd_words); } template simd_bits operator^(const simd_bits_range_ref a, const simd_bits_range_ref b) { assert(a.num_simd_words == b.num_simd_words); simd_bits result(a.num_bits_padded()); ((simd_bits_range_ref)result).for_each_word(a, b, [](bitword &w0, bitword &w1, bitword &w2) { w0 = w1 ^ w2; }); return result; } template simd_bits operator|(const simd_bits_range_ref a, const simd_bits_range_ref b) { assert(a.num_simd_words == b.num_simd_words); simd_bits result(a.num_bits_padded()); ((simd_bits_range_ref)result).for_each_word(a, b, [](bitword &w0, bitword &w1, bitword &w2) { w0 = w1 | w2; }); return result; } template simd_bits operator&(const simd_bits_range_ref a, const simd_bits_range_ref b) { assert(a.num_simd_words == b.num_simd_words); simd_bits result(a.num_bits_padded()); ((simd_bits_range_ref)result).for_each_word(a, b, [](bitword &w0, bitword &w1, bitword &w2) { w0 = w1 & w2; }); return result; } template simd_bits operator^(const simd_bits a, const simd_bits_range_ref b) { return (const simd_bits_range_ref)a ^ (const simd_bits_range_ref)b; } template simd_bits operator^(const simd_bits_range_ref a, const simd_bits b) { return (const simd_bits_range_ref)a ^ (const simd_bits_range_ref)b; } template simd_bits operator^(const simd_bits a, const simd_bits b) { return (const simd_bits_range_ref)a ^ (const simd_bits_range_ref)b; } template simd_bits operator|(const simd_bits a, const simd_bits_range_ref b) { return (const simd_bits_range_ref)a | (const simd_bits_range_ref)b; } template simd_bits operator|(const simd_bits_range_ref a, const simd_bits b) { return (const simd_bits_range_ref)a | (const simd_bits_range_ref)b; } template simd_bits operator|(const simd_bits a, const simd_bits b) { return (const simd_bits_range_ref)a | (const simd_bits_range_ref)b; } template simd_bits operator&(const simd_bits a, const simd_bits_range_ref b) { return (const simd_bits_range_ref)a & (const simd_bits_range_ref)b; } template simd_bits operator&(const simd_bits_range_ref a, const simd_bits b) { return (const simd_bits_range_ref)a & (const simd_bits_range_ref)b; } template simd_bits operator&(const simd_bits a, const simd_bits b) { return (const simd_bits_range_ref)a & (const simd_bits_range_ref)b; } template bool simd_bits::operator<(const simd_bits_range_ref other) const { if (num_simd_words != other.num_simd_words) { return num_simd_words < other.num_simd_words; } for (size_t k = 0; k < num_simd_words; k++) { if (ptr_simd[k] != other.ptr_simd[k]) { return ptr_simd[k] < other.ptr_simd[k]; } } return false; } template simd_bits &simd_bits::operator^=(const simd_bits_range_ref other) { simd_bits_range_ref(*this) ^= other; return *this; } template simd_bits &simd_bits::operator&=(const simd_bits_range_ref other) { simd_bits_range_ref(*this) &= other; return *this; } template simd_bits &simd_bits::operator|=(const simd_bits_range_ref other) { simd_bits_range_ref(*this) |= other; return *this; } template simd_bits &simd_bits::operator+=(const simd_bits_range_ref other) { simd_bits_range_ref(*this) += other; return *this; } template simd_bits &simd_bits::operator-=(const simd_bits_range_ref other) { simd_bits_range_ref(*this) -= other; return *this; } template simd_bits &simd_bits::operator>>=(int offset) { simd_bits_range_ref(*this) >>= offset; return *this; } template simd_bits &simd_bits::operator<<=(int offset) { simd_bits_range_ref(*this) <<= offset; return *this; } template bool simd_bits::not_zero() const { return simd_bits_range_ref(*this).not_zero(); } template bool simd_bits::intersects(const simd_bits_range_ref other) const { return simd_bits_range_ref(*this).intersects(other); } template bool simd_bits::is_subset_of_or_equal_to(const simd_bits_range_ref other) const { return simd_bits_range_ref(*this).is_subset_of_or_equal_to(other); } template std::string simd_bits::str() const { return simd_bits_range_ref(*this).str(); } template simd_bits &simd_bits::swap_with(simd_bits_range_ref other) { simd_bits_range_ref(*this).swap_with(other); return *this; } template void simd_bits::destructive_resize(size_t new_min_bits) { if (min_bits_to_num_bits_padded(new_min_bits) == num_bits_padded()) { return; } *this = std::move(simd_bits(new_min_bits)); } template void simd_bits::preserving_resize(size_t new_min_bits) { if (min_bits_to_num_bits_padded(new_min_bits) == num_bits_padded()) { return; } simd_bits new_storage(new_min_bits); memcpy(new_storage.ptr_simd, ptr_simd, sizeof(bitword) * std::min(num_simd_words, new_storage.num_simd_words)); *this = std::move(new_storage); } template size_t simd_bits::popcnt() const { return simd_bits_range_ref(*this).popcnt(); } template uint64_t simd_bits::as_u64() const { return simd_bits_range_ref(*this).as_u64(); } template size_t simd_bits::countr_zero() const { return simd_bits_range_ref(*this).countr_zero(); } template std::ostream &operator<<(std::ostream &out, const simd_bits m) { return out << simd_bits_range_ref(m); } } // namespace stim ================================================ FILE: src/stim/mem/simd_bits.perf.cc ================================================ // 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. #include "stim/mem/simd_bits.h" #include #include "stim/perf.perf.h" using namespace stim; BENCHMARK(simd_bits_randomize_10K) { size_t n = 10 * 1000; simd_bits data(n); std::mt19937_64 rng(0); benchmark_go([&]() { data.randomize(n, rng); }) .goal_nanos(450) .show_rate("Bits", n); } BENCHMARK(simd_bits_xor_10K) { size_t n = 10 * 1000; simd_bits d1(n); simd_bits d2(n); benchmark_go([&]() { d2 ^= d1; }) .goal_nanos(20) .show_rate("Bits", n); } BENCHMARK(simd_bits_not_zero_100K) { size_t n = 10 * 1000; simd_bits d(n); d[600] = true; size_t total = 0; benchmark_go([&]() { total += d.not_zero(); }) .goal_nanos(32) .show_rate("Bits", n); if (total == 0) { std::cerr << "data dependency"; } } ================================================ FILE: src/stim/mem/simd_bits.test.cc ================================================ // 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. #include "stim/mem/simd_bits.h" #include #include "gtest/gtest.h" #include "stim/mem/simd_util.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(simd_bits, move, { simd_bits a(512); auto ptr = a.u64; simd_bits b(std::move(a)); ASSERT_EQ(b.u64, ptr); simd_bits c(1); c = std::move(b); ASSERT_EQ(c.u64, ptr); }) TEST_EACH_WORD_SIZE_W(simd_bits, construct, { simd_bits d(1024); ASSERT_NE(d.ptr_simd, nullptr); ASSERT_EQ(d.num_simd_words, 1024 / W); ASSERT_EQ(d.num_bits_padded(), 1024); ASSERT_EQ(d.num_u8_padded(), 128); ASSERT_EQ(d.num_u16_padded(), 64); ASSERT_EQ(d.num_u32_padded(), 32); ASSERT_EQ(d.num_u64_padded(), 16); }) TEST_EACH_WORD_SIZE_W(simd_bits, aliased_editing_and_bit_refs, { simd_bits d(1024); auto c = (char *)d.u8; const simd_bits &cref = d; ASSERT_EQ(c[0], 0); ASSERT_EQ(c[13], 0); d[5] = true; ASSERT_EQ(c[0], 32); d[0] = true; ASSERT_EQ(c[0], 33); d[100] = true; ASSERT_EQ(d[100], true); ASSERT_EQ(c[12], 16); c[12] = 0; ASSERT_EQ(d[100], false); ASSERT_EQ(cref[100], d[100]); }) TEST_EACH_WORD_SIZE_W(simd_bits, min_bits_to_num_bits_padded, { const auto &f = &min_bits_to_num_bits_padded; if (W == 256) { ASSERT_EQ(f(0), 0); ASSERT_EQ(f(1), 256); ASSERT_EQ(f(100), 256); ASSERT_EQ(f(255), 256); ASSERT_EQ(f(256), 256); ASSERT_EQ(f(257), 512); ASSERT_EQ(f((1 << 30) - 1), 1 << 30); ASSERT_EQ(f(1 << 30), 1 << 30); ASSERT_EQ(f((1 << 30) + 1), (1 << 30) + 256); } else if (W == 128) { ASSERT_EQ(f(0), 0); ASSERT_EQ(f(1), 128); ASSERT_EQ(f(100), 128); ASSERT_EQ(f(255), 256); ASSERT_EQ(f(256), 256); ASSERT_EQ(f(257), 256 + 128); ASSERT_EQ(f((1 << 30) - 1), 1 << 30); ASSERT_EQ(f(1 << 30), 1 << 30); ASSERT_EQ(f((1 << 30) + 1), (1 << 30) + 128); } else if (W == 64) { ASSERT_EQ(f(0), 0); ASSERT_EQ(f(1), 64); ASSERT_EQ(f(100), 128); ASSERT_EQ(f(255), 256); ASSERT_EQ(f(256), 256); ASSERT_EQ(f(257), 256 + 64); ASSERT_EQ(f((1 << 30) - 1), 1 << 30); ASSERT_EQ(f(1 << 30), 1 << 30); ASSERT_EQ(f((1 << 30) + 1), (1 << 30) + 64); } else { ASSERT_TRUE(false) << "Unrecognized size."; } }) TEST_EACH_WORD_SIZE_W(simd_bits, str, { simd_bits d(256); ASSERT_EQ( d.str(), "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________"); d[5] = true; ASSERT_EQ( d.str(), "_____1__________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________"); }) TEST_EACH_WORD_SIZE_W(simd_bits, randomize, { simd_bits d(1024); auto rng = INDEPENDENT_TEST_RNG(); d.randomize(64 + 57, rng); uint64_t mask = (1ULL << 57) - 1; // Randomized. ASSERT_NE(d.u64[0], 0); ASSERT_NE(d.u64[0], SIZE_MAX); ASSERT_NE(d.u64[1] & mask, 0); ASSERT_NE(d.u64[1] & mask, 0); // Not touched. ASSERT_EQ(d.u64[1] & ~mask, 0); ASSERT_EQ(d.u64[2], 0); ASSERT_EQ(d.u64[3], 0); for (size_t k = 0; k < d.num_u64_padded(); k++) { d.u64[k] = UINT64_MAX; } d.randomize(64 + 57, rng); // Randomized. ASSERT_NE(d.u64[0], 0); ASSERT_NE(d.u64[0], SIZE_MAX); ASSERT_NE(d.u64[1] & mask, 0); ASSERT_NE(d.u64[1] & mask, 0); // Not touched. ASSERT_EQ(d.u64[1] & ~mask, UINT64_MAX & ~mask); ASSERT_EQ(d.u64[2], UINT64_MAX); ASSERT_EQ(d.u64[3], UINT64_MAX); }) TEST_EACH_WORD_SIZE_W(simd_bits, xor_assignment, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits m0 = simd_bits::random(512, rng); simd_bits m1 = simd_bits::random(512, rng); simd_bits m2(512); m2 ^= m0; ASSERT_EQ(m0, m2); m2 ^= m1; for (size_t k = 0; k < m0.num_u64_padded(); k++) { ASSERT_EQ(m2.u64[k], m0.u64[k] ^ m1.u64[k]); } }) template void set_bits_from_u64_vector(simd_bits &bits, std::vector &vec) { for (size_t w = 0; w < bits.num_u64_padded(); w++) { for (size_t b = 0; b < 64; b++) { bits[w * 64 + b] |= (vec[w] & (1ULL << b)); } } } TEST_EACH_WORD_SIZE_W(simd_bits, add_assignment, { simd_bits m0(512); simd_bits m1(512); uint64_t all_set = 0xFFFFFFFFFFFFFFFFULL; uint64_t on_off = 0x0F0F0F0F0F0F0F0FULL; for (size_t word = 0; word < m0.num_u64_padded(); word++) { for (size_t k = 0; k < 64; k++) { if (word % 2 == 0) { m0[word * 64 + k] = all_set & (1ULL << k); m1[word * 64 + k] = all_set & (1ULL << k); } else { m0[word * 64 + k] = (bool)(on_off & (1ULL << k)); m1[word * 64 + k] = (bool)(on_off & (1ULL << k)); } } } m0 += m1; for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } if (word % 2 == 0) { ASSERT_EQ(pattern, 0xFFFFFFFFFFFFFFFEULL); } else { ASSERT_EQ(pattern, 0x1E1E1E1E1E1E1E1FULL); } } for (size_t k = 0; k < m0.num_u64_padded() / 2; k++) { m1.u64[2 * k] = 0ULL; m1.u64[2 * k + 1] = 0ULL; } m0 += m1; for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } if (word % 2 == 0) { ASSERT_EQ(pattern, 0xFFFFFFFFFFFFFFFEULL); } else { ASSERT_EQ(pattern, 0x1E1E1E1E1E1E1E1FULL); } } m0.clear(); m1.clear(); m1[0] = 1; for (int i = 0; i < 512; i++) { m0 += m1; } for (size_t k = 0; k < 64; k++) { if (k == 9) { ASSERT_EQ(m0[k], 1); } else { ASSERT_EQ(m0[k], 0); } } m0.clear(); for (size_t k = 0; k < 64; k++) { m0[k] = all_set & (1ULL << k); } m0 += m1; ASSERT_EQ(m0[0], 0); ASSERT_EQ(m0[64], 1); // Test carrying across multiple (>=2) words. size_t num_bits = 193; simd_bits add(num_bits); simd_bits one(num_bits); for (size_t word = 0; word < add.num_u64_padded() - 1; word++) { for (size_t k = 0; k < 64; k++) { add[word * 64 + k] = 1; } } one[0] = 1; add += one; // These should all overflow and carries should propagate to the last word. for (size_t k = 0; k < num_bits - 1; k++) { ASSERT_EQ(add[k], 0); } ASSERT_EQ(add[num_bits - 1], 1); // From python std::vector x{ 0ULL, 14988672980522980357ULL, 18446744073709551615ULL, 18446744073709551615ULL, 6866573900576593249ULL, 0ULL, 18446744073709551615ULL, 0ULL}; std::vector y{ 4413476325400229597ULL, 0ULL, 9428810821357656676ULL, 7863636477302268070ULL, 0ULL, 18446744073709551615ULL, 0ULL, 15077824728923429555ULL}; std::vector z{ 4413476325400229597ULL, 14988672980522980357ULL, 9428810821357656675ULL, 7863636477302268070ULL, 6866573900576593250ULL, 18446744073709551615ULL, 18446744073709551615ULL, 15077824728923429555ULL}; simd_bits a(512), b(512), ref(512); set_bits_from_u64_vector(a, x); set_bits_from_u64_vector(b, y); set_bits_from_u64_vector(ref, z); a += b; ASSERT_EQ(a, ref); }) template void set_random_words_to_all_set( simd_bits &bits, size_t num_bits, std::mt19937_64 &rng, std::uniform_real_distribution &dist_real) { bits.randomize(num_bits, rng); size_t max_bit = W; for (size_t iword = 0; iword < bits.num_simd_words; iword++) { double r = dist_real(rng); if (iword == bits.num_simd_words - 1) { max_bit = num_bits - W * iword; } if (r < 1.0 / 3.0) { double rall = dist_real(rng); if (rall > 0.5) { for (size_t k = 0; k < max_bit; k++) { bits[iword * W + k] = 1; } } else { for (size_t k = 0; k < max_bit; k++) { bits[iword * W + k] = 0; } } } } } TEST_EACH_WORD_SIZE_W(simd_bits, fuzz_add_assignment, { auto rng = INDEPENDENT_TEST_RNG(); // a + b == b + a std::uniform_real_distribution dist_real(0, 1); for (int i = 0; i < 10; i++) { std::uniform_int_distribution dist_bits(1, 1200); int num_bits = dist_bits(rng); simd_bits m1(num_bits), m2(num_bits); set_random_words_to_all_set(m1, num_bits, rng, dist_real); set_random_words_to_all_set(m2, num_bits, rng, dist_real); simd_bits ref1(m1), ref2(m2); m1 += ref2; m2 += ref1; ASSERT_EQ(m1, m2); } // (a + 1) + ~a = allset for (int i = 0; i < 10; i++) { std::uniform_int_distribution dist_bits(1, 1200); int num_bits = dist_bits(rng); simd_bits m1(num_bits); simd_bits zero(num_bits); simd_bits one(num_bits); one[0] = 1; set_random_words_to_all_set(m1, num_bits, rng, dist_real); simd_bits m2(m1); m2.invert_bits(); m1 += one; m1 += m2; ASSERT_EQ(m1, zero); } // m1 += x; m1 = ~m1; m1 += x; m1 is unchanged. for (int i = 0; i < 10; i++) { std::uniform_int_distribution dist_bits(1, 1200); int num_bits = dist_bits(rng); simd_bits m1(num_bits); m1.randomize(num_bits, rng); set_random_words_to_all_set(m1, num_bits, rng, dist_real); simd_bits ref(m1); simd_bits m2(num_bits); m1 += m2; m1.invert_bits(); m1 += m2; m1.invert_bits(); ASSERT_EQ(m1, ref); } // a + (b + c) == (a + b) + c for (int i = 0; i < 10; i++) { std::uniform_int_distribution dist_bits(1, 1200); int num_bits = dist_bits(rng); simd_bits alhs(num_bits); simd_bits blhs(num_bits); simd_bits clhs(num_bits); simd_bits arhs(num_bits); simd_bits brhs(num_bits); simd_bits crhs(num_bits); set_random_words_to_all_set(alhs, num_bits, rng, dist_real); arhs = alhs; set_random_words_to_all_set(blhs, num_bits, rng, dist_real); brhs = blhs; set_random_words_to_all_set(clhs, num_bits, rng, dist_real); crhs = clhs; blhs += clhs; alhs += blhs; arhs += brhs; arhs += crhs; ASSERT_EQ(alhs, arhs); } }) TEST_EACH_WORD_SIZE_W(simd_bits, right_shift_assignment, { simd_bits m0(512), m1(512); m0[511] = 1; m0 >>= 64; for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } if (word != m0.num_u64_padded() - 2) { ASSERT_EQ(pattern, 0ULL); } else { ASSERT_EQ(pattern, uint64_t{1} << 63); } } m1 = m0; m1 >>= 0; for (size_t k = 0; k < 512; k++) { ASSERT_EQ(m0[k], m1[k]); } m0.clear(); uint64_t on_off = 0xAAAAAAAAAAAAAAAAULL; for (size_t word = 0; word < m0.num_u64_padded(); word++) { for (size_t k = 0; k < 64; k++) { m0[word * 64 + k] = (bool)(on_off & (1ULL << k)); } } m0 >>= 1; for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } ASSERT_EQ(pattern, 0x5555555555555555ULL); } m0.clear(); for (size_t word = 0; word < m0.num_u64_padded(); word++) { for (size_t k = 0; k < 64; k++) { m0[word * 64 + k] = (bool)(on_off & (1ULL << k)); } } m0 >>= 128; for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } if (word < 6) { ASSERT_EQ(pattern, 0xAAAAAAAAAAAAAAAA); } else { ASSERT_EQ(pattern, 0ULL); } } }) TEST_EACH_WORD_SIZE_W(simd_bits, fuzz_right_shift_assignment, { auto rng = INDEPENDENT_TEST_RNG(); for (int i = 0; i < 5; i++) { std::uniform_int_distribution dist_bits(1, 1200); int num_bits = dist_bits(rng); simd_bits m1(num_bits), m2(num_bits); m1.randomize(num_bits, rng); m2 = m1; std::uniform_int_distribution dist_shift(0, (int)m1.num_bits_padded()); size_t shift = dist_shift(rng); m1 >>= shift; for (size_t k = 0; k < m1.num_bits_padded() - shift; k++) { ASSERT_EQ(m1[k], m2[k + shift]); } for (size_t k = m1.num_bits_padded() - shift; k < m1.num_bits_padded(); k++) { ASSERT_EQ(m1[k], 0); } } }) TEST_EACH_WORD_SIZE_W(simd_bits, left_shift_assignment, { simd_bits m0(512), m1(512); for (size_t w = 0; w < m0.num_u64_padded(); w++) { m0.u64[w] = 0xAAAAAAAAAAAAAAAAULL; } m0 <<= 1; m1 = m0; m1 <<= 0; for (size_t k = 0; k < 512; k++) { ASSERT_EQ(m0[k], m1[k]); } for (size_t w = 0; w < m0.num_u64_padded(); w++) { if (w == 0) { ASSERT_EQ(m0.u64[w], 0x5555555555555554ULL); } else { ASSERT_EQ(m0.u64[w], 0x5555555555555555ULL); } } m0 <<= 63; for (size_t w = 0; w < m0.num_u64_padded(); w++) { if (w == 0) { ASSERT_EQ(m0.u64[w], 0ULL); } else { ASSERT_EQ(m0.u64[w], 0xAAAAAAAAAAAAAAAAULL); } } m0 <<= 488; ASSERT_TRUE(!m0.not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bits, fuzz_left_shift_assignment, { auto rng = INDEPENDENT_TEST_RNG(); for (int i = 0; i < 5; i++) { std::uniform_int_distribution dist_bits(1, 1200); int num_bits = dist_bits(rng); simd_bits m1(num_bits), m2(num_bits); m1.randomize(num_bits, rng); m2 = m1; std::uniform_int_distribution dist_shift(0, (int)m1.num_bits_padded()); size_t shift = dist_shift(rng); m1 <<= shift; for (size_t k = 0; k < m1.num_bits_padded() - shift; k++) { ASSERT_EQ(m1[k + shift], m2[k]); } for (size_t k = 0; k < shift; k++) { ASSERT_EQ(m1[k], 0); } } }) TEST_EACH_WORD_SIZE_W(simd_bits, assignment, { simd_bits m0(512); simd_bits m1(512); auto rng = INDEPENDENT_TEST_RNG(); m0.randomize(512, rng); m1.randomize(512, rng); auto old_m1 = m1.u64[0]; ASSERT_NE(m0, m1); m0 = m1; ASSERT_EQ(m0, m1); ASSERT_EQ(m0.u64[0], old_m1); ASSERT_EQ(m1.u64[0], old_m1); }) TEST_EACH_WORD_SIZE_W(simd_bits, equality, { simd_bits m0(512); simd_bits m1(512); simd_bits m4(1024); ASSERT_TRUE(m0 == m1); ASSERT_FALSE(m0 != m1); ASSERT_FALSE(m0 == m4); ASSERT_TRUE(m0 != m4); m1[505] = true; ASSERT_FALSE(m0 == m1); ASSERT_TRUE(m0 != m1); m0[505] = true; ASSERT_TRUE(m0 == m1); ASSERT_FALSE(m0 != m1); }) TEST_EACH_WORD_SIZE_W(simd_bits, swap_with, { simd_bits m0(512); simd_bits m1(512); simd_bits m2(512); simd_bits m3(512); auto rng = INDEPENDENT_TEST_RNG(); m0.randomize(512, rng); m1.randomize(512, rng); m2 = m0; m3 = m1; ASSERT_EQ(m0, m2); ASSERT_EQ(m1, m3); m0.swap_with(m1); ASSERT_EQ(m0, m3); ASSERT_EQ(m1, m2); }) TEST_EACH_WORD_SIZE_W(simd_bits, clear, { simd_bits m0(512); auto rng = INDEPENDENT_TEST_RNG(); m0.randomize(512, rng); ASSERT_TRUE(m0.not_zero()); m0.clear(); ASSERT_TRUE(!m0.not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bits, not_zero, { simd_bits m0(512); ASSERT_FALSE(m0.not_zero()); m0[5] = true; ASSERT_TRUE(m0.not_zero()); m0[511] = true; ASSERT_TRUE(m0.not_zero()); m0[5] = false; ASSERT_TRUE(m0.not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bits, word_range_ref, { simd_bits d(1024); const simd_bits &cref = d; auto r1 = d.word_range_ref(1, 2); auto r2 = d.word_range_ref(2, 2); r1[1] = true; ASSERT_TRUE(!r2.not_zero()); auto k = W + 1; ASSERT_EQ(r1[k], false); r2[1] = true; ASSERT_EQ(r1[k], true); ASSERT_EQ(cref.word_range_ref(1, 2)[k], true); }) TEST_EACH_WORD_SIZE_W(simd_bits, invert_bits, { simd_bits r(100); r[5] = true; r.invert_bits(); for (size_t k = 0; k < 100; k++) { ASSERT_EQ(r[k], k != 5); } }) TEST_EACH_WORD_SIZE_W(simd_bits, mask_assignment_and, { simd_bits a(4); simd_bits b(4); a[2] = true; a[3] = true; b[1] = true; b[3] = true; b &= a; simd_bits expected(4); expected[3] = true; ASSERT_EQ(b, expected); }) TEST_EACH_WORD_SIZE_W(simd_bits, mask_assignment_or, { simd_bits a(4); simd_bits b(4); a[2] = true; a[3] = true; b[1] = true; b[3] = true; b |= a; simd_bits expected(4); expected[1] = true; expected[2] = true; expected[3] = true; ASSERT_EQ(b, expected); }) TEST_EACH_WORD_SIZE_W(simd_bits, truncated_overwrite_from, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits dat = simd_bits::random(1024, rng); simd_bits mut = simd_bits::random(1024, rng); simd_bits old = mut; mut.truncated_overwrite_from(dat, 455); for (size_t k = 0; k < 1024; k++) { ASSERT_EQ(mut[k], k < 455 ? dat[k] : old[k]) << k; } }) TEST_EACH_WORD_SIZE_W(simd_bits, popcnt, { simd_bits data(1024); ASSERT_EQ(data.popcnt(), 0); data[101] = 1; ASSERT_EQ(data.popcnt(), 1); data[0] = 1; ASSERT_EQ(data.popcnt(), 2); data.u64[8] = 0xFFFFFFFFFFFFFFFFULL; ASSERT_EQ(data.popcnt(), 66); ASSERT_EQ(simd_bits(0).popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(simd_bits, countr_zero, { simd_bits data(1024); ASSERT_EQ(data.countr_zero(), SIZE_MAX); data[1000] = 1; ASSERT_EQ(data.countr_zero(), 1000); data[101] = 1; ASSERT_EQ(data.countr_zero(), 101); data[260] = 1; ASSERT_EQ(data.countr_zero(), 101); data[0] = 1; ASSERT_EQ(data.countr_zero(), 0); }) TEST_EACH_WORD_SIZE_W(simd_bits, prefix_ref, { simd_bits data(1024); auto prefix = data.prefix_ref(257); ASSERT_TRUE(prefix.num_bits_padded() >= 257); ASSERT_TRUE(prefix.num_bits_padded() < 1024); ASSERT_FALSE(data[0]); prefix[0] = true; ASSERT_TRUE(data[0]); }) TEST_EACH_WORD_SIZE_W(simd_bits, out_of_place_bit_masking, { simd_bits m0(4); simd_bits m1(4); m0.ptr_simd[0] = simd_word(0b0101); m1.ptr_simd[0] = simd_word(0b0011); ASSERT_EQ((m0 & m1).ptr_simd[0], 0b0001); ASSERT_EQ((m0 | m1).ptr_simd[0], 0b0111); ASSERT_EQ((m0 ^ m1).ptr_simd[0], 0b0110); }) TEST_EACH_WORD_SIZE_W(simd_bits, destructive_resize, { simd_bits m0(512); m0[0] = true; ASSERT_TRUE(m0.not_zero()); m0.destructive_resize(256); ASSERT_FALSE(m0.not_zero()); m0[0] = true; ASSERT_TRUE(m0.not_zero()); m0.destructive_resize(512); ASSERT_FALSE(m0.not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bits, preserving_resize, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits m0(256); m0.randomize(m0.num_bits_padded(), rng); auto copy = m0; m0.preserving_resize(512); ASSERT_EQ(m0.ptr_simd[0], copy.ptr_simd[0]); m0.preserving_resize(256); ASSERT_EQ(m0, copy); }) ================================================ FILE: src/stim/mem/simd_bits_range_ref.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SIMD_BITS_RANGE_REF_H #define _STIM_MEM_SIMD_BITS_RANGE_REF_H #include #include #include #include #include "stim/mem/bit_ref.h" #include "stim/mem/simd_word.h" namespace stim { template constexpr size_t min_bits_to_num_bits_padded(size_t min_bits) { return (min_bits + (sizeof(bitword) * 8 - 1)) & ~(sizeof(bitword) * 8 - 1); } template constexpr size_t min_bits_to_num_simd_words(size_t min_bits) { return (min_bits_to_num_bits_padded(min_bits) / sizeof(bitword)) >> 3; } /// A reference to a range of bits that support SIMD operations (e.g. they are aligned and padded correctly). /// /// Conceptually behaves the same as a reference like `int &`, as opposed to a pointer like `int *`. For example, the /// `=` operator overwrites the contents of the range being referenced instead of changing which range is pointed to. template struct simd_bits_range_ref { union { // It is fair to say that this is the most dangerous block, or danger-enabling block, in the entire codebase. // C++ is very particular when it comes to touching the same memory as if it had multiple different types. // If you know how to make something *for sure work as a flexibly-accessible bag of bits*, please fix this. // In the meantime, always build with `-fno-strict-aliasing` and a short ritual prayer to the compiler gods. uint8_t *const u8; uint64_t *const u64; bitword *const ptr_simd; }; const size_t num_simd_words; /// Construct a simd_bits_range_ref from a given pointer and word count. simd_bits_range_ref(bitword *ptr_simd, size_t num_simd_words); /// Overwrite assignment. simd_bits_range_ref operator=( const simd_bits_range_ref other); // NOLINT(cppcoreguidelines-c-copy-assignment-signature,misc-unconventional-assign-operator) /// Xor assignment. simd_bits_range_ref operator^=(const simd_bits_range_ref other); /// Mask assignment. simd_bits_range_ref operator&=(const simd_bits_range_ref other); simd_bits_range_ref operator|=(const simd_bits_range_ref other); // Addition assigment simd_bits_range_ref operator+=(const simd_bits_range_ref other); simd_bits_range_ref operator-=(const simd_bits_range_ref other); // Shift assigment simd_bits_range_ref operator>>=(int offset); simd_bits_range_ref operator<<=(int offset); /// Swap assignment. void swap_with(simd_bits_range_ref other); /// Equality. bool operator==(const simd_bits_range_ref &other) const; /// Inequality. bool operator!=(const simd_bits_range_ref &other) const; /// Determines whether or not any of the bits in the referenced range are non-zero. bool not_zero() const; /// Treats the referenced range as an unsigned integer, and returns it as a uint64_t. /// If the integer is too large to fit, an exception is raised. uint64_t as_u64() const; /// Returns a reference to a given bit within the referenced range. inline bit_ref operator[](size_t k) { return bit_ref(u8, k); } /// Returns a const reference to a given bit within the referenced range. inline const bit_ref operator[](size_t k) const { return bit_ref(u8, k); } /// Returns a reference to a sub-range of the bits at the start of this simd_bits. inline simd_bits_range_ref prefix_ref(size_t unpadded_bit_length) { return simd_bits_range_ref(ptr_simd, min_bits_to_num_simd_words(unpadded_bit_length)); } /// Returns a reference to a sub-range of the bits in the referenced range. inline simd_bits_range_ref word_range_ref(size_t word_offset, size_t sub_num_simd_words) { return simd_bits_range_ref(ptr_simd + word_offset, sub_num_simd_words); } /// Returns a const reference to a sub-range of the bits in the referenced range. inline const simd_bits_range_ref word_range_ref(size_t word_offset, size_t sub_num_simd_words) const { return simd_bits_range_ref(ptr_simd + word_offset, sub_num_simd_words); } /// Inverts all bits in the referenced range. void invert_bits(); /// Sets all bits in the referenced range to zero. void clear(); /// Randomizes the bits in the referenced range, up to the given bit count. Leaves further bits unchanged. void randomize(size_t num_bits, std::mt19937_64 &rng); /// Returns the number of bits that are 1 in the bit range. size_t popcnt() const; /// Returns the power-of-two-ness of the number, or SIZE_MAX if the number has no 1s. size_t countr_zero() const; /// Returns whether or not the two ranges have set bits in common. bool intersects(const simd_bits_range_ref other) const; /// Returns whether or not all bits that are set in `other` are also set in this range. bool is_subset_of_or_equal_to(const simd_bits_range_ref other) const; /// Writes bits from another location. /// Bits not part of the write are unchanged. void truncated_overwrite_from(simd_bits_range_ref other, size_t num_bits); /// Returns a description of the contents of the range. std::string str() const; /// Number of 64 bit words in the referenced range. inline size_t num_u64_padded() const { return num_simd_words * (sizeof(bitword) / sizeof(uint64_t)); } /// Number of 32 bit words in the referenced range. inline size_t num_u32_padded() const { return num_simd_words * (sizeof(bitword) / sizeof(uint32_t)); } /// Number of 16 bit words in the referenced range. inline size_t num_u16_padded() const { return num_simd_words * (sizeof(bitword) / sizeof(uint16_t)); } /// Number of 8 bit words in the referenced range. inline size_t num_u8_padded() const { return num_simd_words * (sizeof(bitword) / sizeof(uint8_t)); } /// Number of bits in the referenced range. inline size_t num_bits_padded() const { return num_simd_words * sizeof(bitword) << 3; } /// Sets all bits at the given position and beyond it to 0. void clear_bits_past(size_t num_kept_bits); /// Runs a function on each word in the range, in sequential order. /// /// The words are passed by reference and have type bitword. /// /// This is a boilerplate reduction method. It could be an iterator, but when experimenting I found that the /// compiler seemed much more amenable to inline the function in the way I wanted when using this approach rather /// than iterators. /// /// Example: /// size_t simd_popcount(simd_bits_range_ref data) { /// size_t popcount = 0; /// data.for_each_word([&](auto &w) { /// popcount += w.popcount(); /// }); /// return popcount; /// } /// /// HACK: Templating the function type makes inlining significantly more likely. template inline void for_each_word(FUNC body) const { auto *v0 = ptr_simd; auto *v0_end = v0 + num_simd_words; while (v0 != v0_end) { body(*v0); v0++; } } /// Runs a function on paired up words from two ranges, in sequential order. /// /// The words are passed by reference and have type bitword. /// /// This is a boilerplate reduction method. It could be an iterator, but when experimenting I found that the /// compiler seemed much more amenable to inline the function in the way I wanted when using this approach rather /// than iterators. /// /// Example: /// void xor_left_into_right(simd_bits_range_ref data1, simd_bits_range_ref data2) { /// data1.for_each_word(data2, [&](auto &w1, auto &w2) { /// w2 ^= w1; /// }); /// } /// /// HACK: Templating the function type makes inlining significantly more likely. template inline void for_each_word(simd_bits_range_ref other, FUNC body) const { auto *v0 = ptr_simd; auto *v1 = other.ptr_simd; auto *v0_end = v0 + num_simd_words; while (v0 != v0_end) { body(*v0, *v1); v0++; v1++; } } /// Runs a function on paired up words from three ranges, in sequential order. /// /// The words are passed by reference and have type bitword. /// /// This is a boilerplate reduction method. It could be an iterator, but when experimenting I found that the /// compiler seemed much more amenable to inline the function in the way I wanted when using this approach rather /// than iterators. /// /// Example: /// void xor_intersection_into_last(simd_bits_range_ref data1, simd_bits_range_ref data2, simd_bits_range_ref /// data3) { /// data1.for_each_word(data2, data3, [&](auto &w1, auto &w2, auto &w3) { /// w3 ^= w1 & w2; /// }); /// } /// /// HACK: Templating the function type makes inlining significantly more likely. template inline void for_each_word(simd_bits_range_ref other1, simd_bits_range_ref other2, FUNC body) const { auto *v0 = ptr_simd; auto *v1 = other1.ptr_simd; auto *v2 = other2.ptr_simd; auto *v0_end = v0 + num_simd_words; while (v0 != v0_end) { body(*v0, *v1, *v2); v0++; v1++; v2++; } } /// Runs a function on paired up words from four ranges, in sequential order. /// /// The words are passed by reference and have type bitword. /// /// This is a boilerplate reduction method. It could be an iterator, but when experimenting I found that the /// compiler seemed much more amenable to inline the function in the way I wanted when using this approach rather /// than iterators. /// /// Example: /// void xor_union_into_last( /// simd_bits_range_ref data1, simd_bits_range_ref data2, simd_bits_range_ref data3, simd_bits_range_ref /// data4) { /// data1.for_each_word(data2, data3, data4, [&](auto &w1, auto &w2, auto &w3, auto &w4) { /// w4 ^= w1 | w2 | w3; /// }); /// } /// /// HACK: Templating the function type makes inlining significantly more likely. template inline void for_each_word( simd_bits_range_ref other1, simd_bits_range_ref other2, simd_bits_range_ref other3, FUNC body) const { auto *v0 = ptr_simd; auto *v1 = other1.ptr_simd; auto *v2 = other2.ptr_simd; auto *v3 = other3.ptr_simd; auto *v0_end = v0 + num_simd_words; while (v0 != v0_end) { body(*v0, *v1, *v2, *v3); v0++; v1++; v2++; v3++; } } /// Runs a function on paired up words from five ranges, in sequential order. /// /// The words are passed by reference and have type bitword. /// /// This is a boilerplate reduction method. It could be an iterator, but when experimenting I found that the /// compiler seemed much more amenable to inline the function in the way I wanted when using this approach rather /// than iterators. /// /// Example: /// void xor_union_into_last( /// simd_bits_range_ref data1, /// simd_bits_range_ref data2, /// simd_bits_range_ref data3, /// simd_bits_range_ref data4, /// simd_bits_range_ref data5) { /// data1.for_each_word(data2, data3, data4, data5, [&](auto &w1, auto &w2, auto &w3, auto &w4, auto &w5) { /// w5 ^= w1 | w2 | w3 | w4; /// }); /// } /// /// HACK: Templating the function type makes inlining significantly more likely. template inline void for_each_word( simd_bits_range_ref other1, simd_bits_range_ref other2, simd_bits_range_ref other3, simd_bits_range_ref other4, FUNC body) const { auto *v0 = ptr_simd; auto *v1 = other1.ptr_simd; auto *v2 = other2.ptr_simd; auto *v3 = other3.ptr_simd; auto *v4 = other4.ptr_simd; auto *v0_end = v0 + num_simd_words; while (v0 != v0_end) { body(*v0, *v1, *v2, *v3, *v4); v0++; v1++; v2++; v3++; v4++; } } template void for_each_set_bit(CALLBACK callback) { size_t n = num_u64_padded(); for (size_t w = 0; w < n; w++) { uint64_t v = u64[w]; while (v) { size_t q = w * 64 + std::countr_zero(v); v &= v - 1; callback(q); } } } }; /// Writes a description of the contents of the range to `out`. template std::ostream &operator<<(std::ostream &out, const simd_bits_range_ref m); } // namespace stim #include "stim/mem/simd_bits_range_ref.inl" #endif ================================================ FILE: src/stim/mem/simd_bits_range_ref.inl ================================================ // 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. #include #include #include "stim/mem/simd_util.h" namespace stim { template simd_bits_range_ref::simd_bits_range_ref(bitword *ptr_simd_init, size_t num_simd_words) : ptr_simd(ptr_simd_init), num_simd_words(num_simd_words) { } template simd_bits_range_ref simd_bits_range_ref::operator^=(const simd_bits_range_ref other) { for_each_word(other, [](bitword &w0, bitword &w1) { w0 ^= w1; }); return *this; } template simd_bits_range_ref simd_bits_range_ref::operator|=(const simd_bits_range_ref other) { for_each_word(other, [](bitword &w0, bitword &w1) { w0 |= w1; }); return *this; } template simd_bits_range_ref simd_bits_range_ref::operator&=(const simd_bits_range_ref other) { for_each_word(other, [](bitword &w0, bitword &w1) { w0 &= w1; }); return *this; } template simd_bits_range_ref simd_bits_range_ref::operator=(const simd_bits_range_ref other) { memcpy(ptr_simd, other.ptr_simd, num_u8_padded()); return *this; } template simd_bits_range_ref simd_bits_range_ref::operator+=(const simd_bits_range_ref other) { size_t num_u64 = num_u64_padded(); uint64_t carry{0}; for (size_t w = 0; w < num_u64; w++) { uint64_t val_before = u64[w]; u64[w] += other.u64[w] + carry; carry = u64[w] < val_before || (carry & (val_before == u64[w])); } return *this; } template simd_bits_range_ref simd_bits_range_ref::operator-=(const simd_bits_range_ref other) { invert_bits(); *this += other; invert_bits(); return *this; } template simd_bits_range_ref simd_bits_range_ref::operator>>=(int offset) { uint64_t incoming_word; uint64_t cur_word; if (offset == 0) { return *this; } while (offset >= 64) { incoming_word = 0ULL; for (int w = num_u64_padded() - 1; w >= 0; w--) { cur_word = u64[w]; u64[w] = incoming_word; incoming_word = cur_word; } offset -= 64; } if (offset == 0) { return *this; } incoming_word = 0ULL; for (int w = num_u64_padded() - 1; w >= 0; w--) { cur_word = u64[w]; u64[w] >>= offset; u64[w] |= incoming_word << (64 - offset); incoming_word = cur_word & ((uint64_t{1} << offset) - 1); } return *this; } template simd_bits_range_ref simd_bits_range_ref::operator<<=(int offset) { uint64_t incoming_word; uint64_t cur_word; if (offset == 0) { return *this; } while (offset >= 64) { incoming_word = 0ULL; for (size_t w = 0; w < num_u64_padded(); w++) { cur_word = u64[w]; u64[w] = incoming_word; incoming_word = cur_word; } offset -= 64; } if (offset == 0) { return *this; } incoming_word = 0ULL; for (size_t w = 0; w < num_u64_padded(); w++) { cur_word = u64[w]; u64[w] <<= offset; u64[w] |= incoming_word; incoming_word = (cur_word >> (64 - offset)); } return *this; } template void simd_bits_range_ref::swap_with(simd_bits_range_ref other) { for_each_word(other, [](bitword &w0, bitword &w1) { std::swap(w0, w1); }); } template void simd_bits_range_ref::invert_bits() { auto mask = bitword::tile8(0xFF); for_each_word([&mask](bitword &w) { w ^= mask; }); } template void simd_bits_range_ref::clear() { memset(u8, 0, num_u8_padded()); } template bool simd_bits_range_ref::operator==(const simd_bits_range_ref &other) const { return num_simd_words == other.num_simd_words && memcmp(ptr_simd, other.ptr_simd, num_u8_padded()) == 0; } template bool simd_bits_range_ref::not_zero() const { bitword acc{}; for_each_word([&acc](bitword &w) { acc |= w; }); return (bool)acc; } template bool simd_bits_range_ref::operator!=(const simd_bits_range_ref &other) const { return !(*this == other); } template std::ostream &operator<<(std::ostream &out, const simd_bits_range_ref m) { for (size_t k = 0; k < m.num_bits_padded(); k++) { out << "_1"[m[k]]; } return out; } template std::string simd_bits_range_ref::str() const { std::stringstream ss; ss << *this; return ss.str(); } template void simd_bits_range_ref::randomize(size_t num_bits, std::mt19937_64 &rng) { auto n = num_bits >> 6; for (size_t k = 0; k < n; k++) { u64[k] = rng(); } auto leftover = num_bits & 63; if (leftover) { uint64_t mask = ((uint64_t)1 << leftover) - 1; u64[n] &= ~mask; u64[n] |= rng() & mask; } } template void simd_bits_range_ref::truncated_overwrite_from(simd_bits_range_ref other, size_t num_bits) { size_t n8 = num_bits >> 3; memcpy(u8, other.u8, n8); if (num_bits & 7) { uint8_t m8 = uint8_t{0xFF} >> (8 - (num_bits & 7)); u8[n8] &= ~m8; u8[n8] |= other.u8[n8] & m8; } } template void simd_bits_range_ref::clear_bits_past(size_t num_kept_bits) { if (num_kept_bits >= num_bits_padded()) { return; } size_t n8 = num_kept_bits >> 3; if (num_kept_bits & 7) { uint8_t m8 = uint8_t{0xFF} >> (8 - (num_kept_bits & 7)); u8[n8] &= m8; memset(u8 + n8 + 1, 0, num_u8_padded() - n8 - 1); } else { memset(u8 + n8, 0, num_u8_padded() - n8); } } template size_t simd_bits_range_ref::popcnt() const { auto end = u64 + num_u64_padded(); size_t result = 0; for (const uint64_t *p = u64; p != end; p++) { result += std::popcount(*p); } return result; } template size_t simd_bits_range_ref::countr_zero() const { size_t n = num_u64_padded(); for (size_t k = 0; k < n; k++) { uint64_t u = u64[k]; if (u) { for (size_t r = 0; r < 64; r++) { if ((u >> r) & 1) { return r + 64 * k; } } } } return SIZE_MAX; } template bool simd_bits_range_ref::intersects(const simd_bits_range_ref other) const { size_t n = std::min(num_u64_padded(), other.num_u64_padded()); uint64_t v = 0; for (size_t k = 0; k < n; k++) { v |= u64[k] & other.u64[k]; } return v != 0; } template bool simd_bits_range_ref::is_subset_of_or_equal_to(const simd_bits_range_ref other) const { size_t n = std::min(num_u64_padded(), other.num_u64_padded()); uint64_t v = 0; for (size_t k = 0; k < n; k++) { v |= u64[k] & ~other.u64[k]; } for (size_t k = other.num_u64_padded(); k < num_u64_padded(); k++) { v |= u64[k]; } return v == 0; } template uint64_t simd_bits_range_ref::as_u64() const { size_t n64 = num_u64_padded(); for (size_t k = 1; k < n64; k++) { if (u64[k]) { throw std::invalid_argument("Too large to fit into a uint64_t."); } } size_t n1 = std::min(num_bits_padded(), (size_t)64); uint64_t v = 0; for (size_t k = 0; k < n1; k++) { v ^= uint64_t{(*this)[k]} << k; } return v; } } // namespace stim ================================================ FILE: src/stim/mem/simd_bits_range_ref.test.cc ================================================ // 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. #include "stim/mem/simd_bits_range_ref.h" #include "gtest/gtest.h" #include "stim/mem/simd_bits.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, construct, { alignas(64) std::array data{}; simd_bits_range_ref ref((bitword *)data.data(), sizeof(data) / (W / 8)); ASSERT_EQ(ref.ptr_simd, (bitword *)&data[0]); ASSERT_EQ(ref.num_simd_words, 16 * sizeof(uint64_t) / sizeof(bitword)); ASSERT_EQ(ref.num_bits_padded(), 1024); ASSERT_EQ(ref.num_u8_padded(), 128); ASSERT_EQ(ref.num_u16_padded(), 64); ASSERT_EQ(ref.num_u32_padded(), 32); ASSERT_EQ(ref.num_u64_padded(), 16); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, aliased_editing_and_bit_refs, { alignas(64) std::array data{}; auto c = (char *)&data; simd_bits_range_ref ref((bitword *)data.data(), sizeof(data) / sizeof(bitword)); const simd_bits_range_ref cref((bitword *)data.data(), sizeof(data) / sizeof(bitword)); ASSERT_EQ(c[0], 0); ASSERT_EQ(c[13], 0); ref[5] = true; ASSERT_EQ(c[0], 32); ref[0] = true; ASSERT_EQ(c[0], 33); ref[100] = true; ASSERT_EQ(ref[100], true); ASSERT_EQ(c[12], 16); c[12] = 0; ASSERT_EQ(ref[100], false); ASSERT_EQ(cref[100], ref[100]); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, str, { alignas(64) std::array data{}; simd_bits_range_ref ref((bitword *)data.data(), sizeof(data) / sizeof(bitword)); ASSERT_EQ( ref.str(), "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________"); ref[5] = 1; ASSERT_EQ( ref.str(), "_____1__________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________" "________________________________________________________________"); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, randomize, { alignas(64) std::array data{}; simd_bits_range_ref ref((bitword *)data.data(), sizeof(data) / sizeof(bitword)); auto rng = INDEPENDENT_TEST_RNG(); ref.randomize(64 + 57, rng); uint64_t mask = (1ULL << 57) - 1; // Randomized. ASSERT_NE(ref.u64[0], 0); ASSERT_NE(ref.u64[0], SIZE_MAX); ASSERT_NE(ref.u64[1] & mask, 0); ASSERT_NE(ref.u64[1] & mask, 0); // Not touched. ASSERT_EQ(ref.u64[1] & ~mask, 0); ASSERT_EQ(ref.u64[2], 0); ASSERT_EQ(ref.u64[3], 0); for (size_t k = 0; k < ref.num_u64_padded(); k++) { ref.u64[k] = UINT64_MAX; } ref.randomize(64 + 57, rng); // Randomized. ASSERT_NE(ref.u64[0], 0); ASSERT_NE(ref.u64[0], SIZE_MAX); ASSERT_NE(ref.u64[1] & mask, 0); ASSERT_NE(ref.u64[1] & mask, 0); // Not touched. ASSERT_EQ(ref.u64[1] & ~mask, UINT64_MAX & ~mask); ASSERT_EQ(ref.u64[2], UINT64_MAX); ASSERT_EQ(ref.u64[3], UINT64_MAX); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, xor_assignment, { alignas(64) std::array data{}; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword) / 3); simd_bits_range_ref m1((bitword *)&data[8], sizeof(data) / sizeof(bitword) / 3); simd_bits_range_ref m2((bitword *)&data[16], sizeof(data) / sizeof(bitword) / 3); auto rng = INDEPENDENT_TEST_RNG(); m0.randomize(512, rng); m1.randomize(512, rng); ASSERT_NE(m0, m1); ASSERT_NE(m0, m2); m2 ^= m0; ASSERT_EQ(m0, m2); m2 ^= m1; for (size_t k = 0; k < m0.num_u64_padded(); k++) { ASSERT_EQ(m2.u64[k], m0.u64[k] ^ m1.u64[k]); } }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, assignment, { alignas(64) std::array data{}; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword) / 2); simd_bits_range_ref m1((bitword *)&data[8], sizeof(data) / sizeof(bitword) / 2); auto rng = INDEPENDENT_TEST_RNG(); m0.randomize(512, rng); m1.randomize(512, rng); auto old_m1 = m1.u64[0]; ASSERT_NE(m0, m1); m0 = m1; ASSERT_EQ(m0, m1); ASSERT_EQ(m0.u64[0], old_m1); ASSERT_EQ(m1.u64[0], old_m1); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, equality, { alignas(64) std::array data{}; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword) / 4); simd_bits_range_ref m1((bitword *)&data[8], sizeof(data) / sizeof(bitword) / 4); simd_bits_range_ref m4((bitword *)&data[16], sizeof(data) / sizeof(bitword) / 2); ASSERT_TRUE(m0 == m1); ASSERT_FALSE(m0 != m1); ASSERT_FALSE(m0 == m4); ASSERT_TRUE(m0 != m4); m1[505] = 1; ASSERT_FALSE(m0 == m1); ASSERT_TRUE(m0 != m1); m0[505] = 1; ASSERT_TRUE(m0 == m1); ASSERT_FALSE(m0 != m1); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, add_assignment, { alignas(64) std::array data{ 0xFFFFFFFFFFFFFFFFULL, 0x0F0F0F0F0F0F0F0FULL, 0xFFFFFFFFFFFFFFFFULL, 0x0F0F0F0F0F0F0F0FULL, 0xFFFFFFFFFFFFFFFFULL, 0x0F0F0F0F0F0F0F0FULL, 0xFFFFFFFFFFFFFFFFULL, 0x0F0F0F0F0F0F0F0FULL}; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword) / 2); simd_bits_range_ref m1((bitword *)&data[4], sizeof(data) / sizeof(bitword) / 2); m0 += m1; for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } if (word % 2 == 0) { ASSERT_EQ(pattern, 0xFFFFFFFFFFFFFFFEULL); } else { ASSERT_EQ(pattern, 0x1E1E1E1E1E1E1E1FULL); } } }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, right_shift_assignment, { alignas(64) std::array data{ 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, }; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword)); m0 >>= 1; for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } ASSERT_EQ(pattern, 0x5555555555555555ULL); } m0 >>= 511; ASSERT_TRUE(!m0.not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, left_shift_assignment, { alignas(64) std::array data{ 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, 0xAAAAAAAAAAAAAAAAULL, }; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword)); m0 <<= 1; for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } if (word == 0) { ASSERT_EQ(pattern, 0x5555555555555554ULL); } else { ASSERT_EQ(pattern, 0x5555555555555555ULL); } } m0 <<= 63; for (size_t w = 0; w < m0.num_u64_padded(); w++) { if (w == 0) { ASSERT_EQ(m0.u64[w], 0ULL); } else { ASSERT_EQ(m0.u64[w], 0xAAAAAAAAAAAAAAAAULL); } } for (size_t word = 0; word < m0.num_u64_padded(); word++) { uint64_t pattern = 0ULL; for (size_t k = 0; k < 64; k++) { pattern |= (uint64_t{m0[word * 64 + k]} << k); } if (word == 0) { ASSERT_EQ(pattern, 0ULL); } else { ASSERT_EQ(pattern, 0xAAAAAAAAAAAAAAAAULL); } } m0 <<= 488; ASSERT_TRUE(!m0.not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, swap_with, { alignas(64) std::array data{}; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword) / 4); simd_bits_range_ref m1((bitword *)&data[8], sizeof(data) / sizeof(bitword) / 4); simd_bits_range_ref m2((bitword *)&data[16], sizeof(data) / sizeof(bitword) / 4); simd_bits_range_ref m3((bitword *)&data[24], sizeof(data) / sizeof(bitword) / 4); auto rng = INDEPENDENT_TEST_RNG(); m0.randomize(512, rng); m1.randomize(512, rng); m2 = m0; m3 = m1; ASSERT_EQ(m0, m2); ASSERT_EQ(m1, m3); m0.swap_with(m1); ASSERT_EQ(m0, m3); ASSERT_EQ(m1, m2); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, clear, { alignas(64) std::array data{}; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword)); auto rng = INDEPENDENT_TEST_RNG(); m0.randomize(512, rng); ASSERT_TRUE(m0.not_zero()); m0.clear(); ASSERT_TRUE(!m0.not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, not_zero256, { alignas(64) std::array data{}; simd_bits_range_ref m0((bitword *)&data[0], sizeof(data) / sizeof(bitword)); ASSERT_FALSE(m0.not_zero()); m0[5] = true; ASSERT_TRUE(m0.not_zero()); m0[511] = true; ASSERT_TRUE(m0.not_zero()); m0[5] = false; ASSERT_TRUE(m0.not_zero()); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, word_range_ref, { bitword d[sizeof(uint64_t) * 16 / sizeof(bitword)]{}; simd_bits_range_ref ref(d, sizeof(d) / sizeof(bitword)); const simd_bits_range_ref cref(d, sizeof(d) / sizeof(bitword)); auto r1 = ref.word_range_ref(1, 2); auto r2 = ref.word_range_ref(2, 2); r1[1] = true; ASSERT_TRUE(!r2.not_zero()); auto k = sizeof(bitword) * 8 + 1; ASSERT_EQ(r1[k], false); r2[1] = true; ASSERT_EQ(r1[k], true); ASSERT_EQ(cref.word_range_ref(1, 2)[k], true); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, for_each_set_bit, { simd_bits data(256); simd_bits_range_ref ref(data); ref[5] = true; ref[101] = true; std::vector hits; ref.for_each_set_bit([&](size_t k) { hits.push_back(k); }); ASSERT_EQ(hits, (std::vector{5, 101})); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, truncated_overwrite_from, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits dat = simd_bits::random(1024, rng); simd_bits mut = simd_bits::random(1024, rng); simd_bits old = mut; simd_bits_range_ref(mut).truncated_overwrite_from(dat, 455); for (size_t k = 0; k < 1024; k++) { ASSERT_EQ(mut[k], k < 455 ? dat[k] : old[k]) << k; } }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, popcnt, { simd_bits data(1024); simd_bits_range_ref ref(data); ASSERT_EQ(ref.popcnt(), 0); data[101] = 1; ASSERT_EQ(ref.popcnt(), 1); data[0] = 1; ASSERT_EQ(ref.popcnt(), 2); data.u64[8] = 0xFFFFFFFFFFFFFFFFULL; ASSERT_EQ(ref.popcnt(), 66); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, countr_zero, { simd_bits data(1024); simd_bits_range_ref ref(data); ASSERT_EQ(ref.countr_zero(), SIZE_MAX); data[1000] = 1; ASSERT_EQ(ref.countr_zero(), 1000); data[101] = 1; ASSERT_EQ(ref.countr_zero(), 101); data[260] = 1; ASSERT_EQ(ref.countr_zero(), 101); data[0] = 1; ASSERT_EQ(ref.countr_zero(), 0); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, intersects, { simd_bits data(1024); simd_bits other(512); simd_bits_range_ref ref(data); ASSERT_EQ(data.intersects(other), false); ASSERT_EQ(ref.intersects(other), false); other[511] = true; ASSERT_EQ(data.intersects(other), false); ASSERT_EQ(ref.intersects(other), false); data[513] = true; ASSERT_EQ(data.intersects(other), false); ASSERT_EQ(ref.intersects(other), false); data[511] = true; ASSERT_EQ(data.intersects(other), true); ASSERT_EQ(ref.intersects(other), true); data[101] = true; ASSERT_EQ(data.intersects(other), true); ASSERT_EQ(ref.intersects(other), true); other[101] = true; ASSERT_EQ(data.intersects(other), true); ASSERT_EQ(ref.intersects(other), true); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, is_subset_of_or_equal_to, { simd_bits data(1024); simd_bits other(512); simd_bits_range_ref ref(data); ASSERT_EQ(data.is_subset_of_or_equal_to(other), true); ASSERT_EQ(ref.is_subset_of_or_equal_to(other), true); other[511] = true; ASSERT_EQ(data.is_subset_of_or_equal_to(other), true); ASSERT_EQ(ref.is_subset_of_or_equal_to(other), true); data[511] = true; ASSERT_EQ(data.is_subset_of_or_equal_to(other), true); ASSERT_EQ(ref.is_subset_of_or_equal_to(other), true); other[511] = false; ASSERT_EQ(data.is_subset_of_or_equal_to(other), false); ASSERT_EQ(ref.is_subset_of_or_equal_to(other), false); other[511] = true; data[513] = true; ASSERT_EQ(data.is_subset_of_or_equal_to(other), false); ASSERT_EQ(ref.is_subset_of_or_equal_to(other), false); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, prefix_ref, { simd_bits data(1024); simd_bits_range_ref ref(data); auto prefix = ref.prefix_ref(257); ASSERT_TRUE(prefix.num_bits_padded() >= 257); ASSERT_TRUE(prefix.num_bits_padded() < 1024); ASSERT_FALSE(data[0]); prefix[0] = true; ASSERT_TRUE(data[0]); }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, as_u64, { simd_bits data(1024); simd_bits_range_ref ref(data); ASSERT_EQ(data.as_u64(), 0); ASSERT_EQ(ref.as_u64(), 0); ref[63] = 1; ASSERT_EQ(data.as_u64(), uint64_t{1} << 63); ASSERT_EQ(ref.as_u64(), uint64_t{1} << 63); ASSERT_EQ(data.word_range_ref(0, 0).as_u64(), 0); ASSERT_EQ(data.word_range_ref(0, 1).as_u64(), uint64_t{1} << 63); ASSERT_EQ(data.word_range_ref(0, 2).as_u64(), uint64_t{1} << 63); ASSERT_EQ(data.word_range_ref(1, 1).as_u64(), 0); ASSERT_EQ(data.word_range_ref(1, 2).as_u64(), 0); ref[64] = 1; ASSERT_THROW({ data.as_u64(); }, std::invalid_argument); ASSERT_THROW({ data.word_range_ref(0, 2).as_u64(); }, std::invalid_argument); ASSERT_THROW({ ref.as_u64(); }, std::invalid_argument); if (data.num_simd_words > 2) { ASSERT_EQ(data.word_range_ref(2, 1).as_u64(), 0); } }) TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, clear_bits_past, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits data(1024); simd_bits copy(1024); for (size_t c = 700; c < 710; c++) { data.randomize(1024, rng); copy = data; simd_bits_range_ref(copy).clear_bits_past(c); for (size_t k = c; k < 1024; k++) { data[k] = 0; } } ASSERT_EQ(data, copy); }) ================================================ FILE: src/stim/mem/simd_util.cc ================================================ // 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. #include "stim/mem/simd_util.h" template void inplace_transpose_64x64_pass(uint64_t *data, size_t stride) { for (size_t k = 0; k < 64; k++) { if (k & shift) { continue; } uint64_t &x = data[stride * k]; uint64_t &y = data[stride * (k + shift)]; uint64_t a = x & mask; uint64_t b = x & ~mask; uint64_t c = y & mask; uint64_t d = y & ~mask; x = a | (c << shift); y = (b >> shift) | d; } } void stim::inplace_transpose_64x64(uint64_t *data, size_t stride) { inplace_transpose_64x64_pass<0x5555555555555555UL, 1>(data, stride); inplace_transpose_64x64_pass<0x3333333333333333UL, 2>(data, stride); inplace_transpose_64x64_pass<0x0F0F0F0F0F0F0F0FUL, 4>(data, stride); inplace_transpose_64x64_pass<0x00FF00FF00FF00FFUL, 8>(data, stride); inplace_transpose_64x64_pass<0x0000FFFF0000FFFFUL, 16>(data, stride); inplace_transpose_64x64_pass<0x00000000FFFFFFFFUL, 32>(data, stride); } ================================================ FILE: src/stim/mem/simd_util.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SIMD_UTIL_H #define _STIM_MEM_SIMD_UTIL_H #include #include namespace stim { constexpr uint64_t tile64_helper(uint64_t val, size_t shift) { return shift >= 64 ? val : tile64_helper(val | (val << shift), shift << 1); } constexpr uint64_t interleave_mask(size_t step) { return tile64_helper((uint64_t{1} << step) - 1, step << 1); } inline uint64_t spread_bytes_32_to_64(uint32_t v) { uint64_t r = (((uint64_t)v << 16) | v) & 0xFFFF0000FFFFULL; return ((r << 8) | r) & 0xFF00FF00FF00FFULL; } void inplace_transpose_64x64(uint64_t *data, size_t stride); } // namespace stim #endif ================================================ FILE: src/stim/mem/simd_util.test.cc ================================================ // 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. #include "stim/mem/simd_util.h" #include #include "gtest/gtest.h" #include "stim/mem/simd_bit_table.h" #include "stim/mem/simd_bits.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; template simd_bits reference_transpose_of(size_t bit_width, const simd_bits &data) { assert(!(bit_width & 0xFF)); auto expected = simd_bits(bit_width * bit_width); for (size_t i = 0; i < bit_width; i++) { for (size_t j = 0; j < bit_width; j++) { expected[i * bit_width + j] = data[j * bit_width + i]; } } return expected; } template uint8_t determine_permutation_bit_else_0xFF(const std::function &)> &func, uint8_t bit) { auto data = simd_bits(1 << A); data[1 << bit] = true; func(data); uint32_t seen = 0; for (size_t k = 0; k < 1 << A; k++) { if (data[k]) { seen++; } } if (seen != 1) { return 0xFF; } for (uint8_t k = 0; k < A; k++) { if (data[1 << k]) { return k; } } return 0xFF; } template std::string determine_if_function_performs_bit_permutation_helper( const std::function &)> &func, const std::array &bit_permutation) { size_t area = 1 << A; auto data = simd_bits::random(area, INDEPENDENT_TEST_RNG()); auto expected = simd_bits(area); for (size_t k_in = 0; k_in < area; k_in++) { size_t k_out = 0; for (size_t bit = 0; bit < A; bit++) { if ((k_in >> bit) & 1) { k_out ^= 1 << bit_permutation[bit]; } } expected[k_out] = data[k_in]; } func(data); bool result = data == expected; if (!result) { std::stringstream ss; std::array perm; ss << "actual permutation:"; for (uint8_t k = 0; k < A; k++) { auto v = (uint32_t)determine_permutation_bit_else_0xFF(func, k); if (v == 0xFF) { ss << " [not a permutation],"; } else { ss << " " << v << ","; } perm[k] = v; } ss << "\n"; if (perm == bit_permutation) { ss << "[BUT PERMUTATION ACTS INCORRECTLY ON SOME BITS.]\n"; } return ss.str(); } return ""; } template void EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION( const std::function &)> &func, const std::array &bit_permutation) { size_t area = 1 << A; auto rng = INDEPENDENT_TEST_RNG(); auto data = simd_bits::random(area, rng); auto expected = simd_bits(area); for (size_t k_in = 0; k_in < area; k_in++) { size_t k_out = 0; for (size_t bit = 0; bit < A; bit++) { if ((k_in >> bit) & 1) { k_out ^= 1 << bit_permutation[bit]; } } expected[k_out] = data[k_in]; } func(data); if (data != expected) { std::stringstream ss; std::array perm; ss << "\nexpected permutation:"; for (const auto &e : bit_permutation) { ss << " " << (int)e << ","; } ss << "\n actual permutation:"; for (uint8_t k = 0; k < A; k++) { auto v = (uint32_t)determine_permutation_bit_else_0xFF(func, k); if (v == 0xFF) { ss << " [not a permutation],"; } else { ss << " " << v << ","; } perm[k] = v; } ss << "\n"; if (perm == bit_permutation) { ss << "[BUT PERMUTATION ACTS INCORRECTLY ON SOME BITS.]\n"; } EXPECT_TRUE(false) << ss.str(); } } TEST(simd_util, inplace_transpose_64x64) { constexpr size_t W = 64; auto rng = INDEPENDENT_TEST_RNG(); simd_bits data = simd_bits::random(64 * 64, rng); simd_bits copy = data; inplace_transpose_64x64(copy.u64, 1); for (size_t i = 0; i < 64; i++) { for (size_t j = 0; j < 64; j++) { ASSERT_EQ(data[i * 64 + j], copy[j * 64 + i]); } } } TEST_EACH_WORD_SIZE_W(simd_util, inplace_transpose, { if (W == 64) { EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<12, W>( [](simd_bits &d) { inplace_transpose_64x64(d.u64, 1); }, { 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<13, W>( [](simd_bits &d) { inplace_transpose_64x64(d.u64, 1); inplace_transpose_64x64(d.u64 + 64, 1); }, { 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 12, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<13, W>( [](simd_bits &d) { inplace_transpose_64x64(d.u64, 2); inplace_transpose_64x64(d.u64 + 1, 2); }, { 7, 8, 9, 10, 11, 12, 6, 0, 1, 2, 3, 4, 5, }); } else if (W == 128) { EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<14, W>( [](simd_bits &d) { bitword::inplace_transpose_square(d.ptr_simd, 1); }, { 7, 8, 9, 10, 11, 12, 13, 0, 1, 2, 3, 4, 5, 6, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<15, W>( [](simd_bits &d) { bitword::inplace_transpose_square(d.ptr_simd, 1); bitword::inplace_transpose_square(d.ptr_simd + 128, 1); }, { 7, 8, 9, 10, 11, 12, 13, 0, 1, 2, 3, 4, 5, 6, 14, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<15, W>( [](simd_bits &d) { bitword::inplace_transpose_square(d.ptr_simd, 2); bitword::inplace_transpose_square(d.ptr_simd + 1, 2); }, { 8, 9, 10, 11, 12, 13, 14, 7, 0, 1, 2, 3, 4, 5, 6, }); } else if (W == 256) { EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<16, W>( [](simd_bits &d) { bitword::inplace_transpose_square(d.ptr_simd, 1); }, { 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<17, W>( [](simd_bits &d) { bitword::inplace_transpose_square(d.ptr_simd, 1); bitword::inplace_transpose_square(d.ptr_simd + 256, 1); }, { 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 16, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<17, W>( [](simd_bits &d) { bitword::inplace_transpose_square(d.ptr_simd, 2); bitword::inplace_transpose_square(d.ptr_simd + 1, 2); }, { 9, 10, 11, 12, 13, 14, 15, 16, 8, 0, 1, 2, 3, 4, 5, 6, 7, }); } }) TEST_EACH_WORD_SIZE_W(simd_util, simd_bit_table_transpose, { EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<20, W>( [](simd_bits &d) { d = reference_transpose_of(1024, d); }, { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<18, W>( [](simd_bits &d) { simd_bit_table t(512, 512); t.data = d; t.do_square_transpose(); d = t.data; }, { 9, 10, 11, 12, 13, 14, 15, 16, 17, 0, 1, 2, 3, 4, 5, 6, 7, 8, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<20, W>( [](simd_bits &d) { simd_bit_table t(1024, 1024); t.data = d; t.do_square_transpose(); d = t.data; }, { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, }); EXPECT_FUNCTION_PERFORMS_ADDRESS_BIT_PERMUTATION<19, W>( [](simd_bits &d) { simd_bit_table t(512, 1024); t.data = d; auto t2 = t.transposed(); d = t2.data; }, { 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 6, 7, 8, }); }) TEST(simd_util, interleave_mask) { ASSERT_EQ(interleave_mask(1), 0x5555555555555555ULL); ASSERT_EQ(interleave_mask(2), 0x3333333333333333ULL); ASSERT_EQ(interleave_mask(4), 0x0F0F0F0F0F0F0F0FULL); ASSERT_EQ(interleave_mask(8), 0x00FF00FF00FF00FFULL); ASSERT_EQ(interleave_mask(16), 0x0000FFFF0000FFFFULL); ASSERT_EQ(interleave_mask(32), 0x00000000FFFFFFFFULL); } ================================================ FILE: src/stim/mem/simd_word.cc ================================================ // 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. #include "stim/mem/simd_word.h" ================================================ FILE: src/stim/mem/simd_word.h ================================================ /* * 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. */ #include #include #ifndef _STIM_MEM_SIMD_WORD_H #define _STIM_MEM_SIMD_WORD_H #include "stim/mem/bitword_128_sse.h" #include "stim/mem/bitword_256_avx.h" #include "stim/mem/bitword_64.h" namespace stim { #if __AVX2__ constexpr size_t MAX_BITWORD_WIDTH = 256; #elif __SSE2__ constexpr size_t MAX_BITWORD_WIDTH = 128; #else constexpr size_t MAX_BITWORD_WIDTH = 64; #endif template using simd_word = bitword; } // namespace stim #endif ================================================ FILE: src/stim/mem/simd_word.perf.cc ================================================ // 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. #include "stim/mem/simd_word.h" #include "stim/mem/simd_bits.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(simd_compat_popcnt) { simd_bits d(1024 * 256); std::mt19937_64 rng(0); d.randomize(d.num_bits_padded(), rng); uint64_t optimization_blocker = 0; benchmark_go([&]() { d[300] ^= true; for (size_t k = 0; k < d.num_simd_words; k++) { optimization_blocker += d.ptr_simd[k].popcount(); } }) .goal_micros(1.5) .show_rate("Bits", d.num_bits_padded()); if (optimization_blocker == 0) { std::cout << '!'; } } ================================================ FILE: src/stim/mem/simd_word.test.cc ================================================ // 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. #include "stim/mem/simd_word.h" #include #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; union WordOr64 { simd_word w; uint64_t p[sizeof(simd_word) / sizeof(uint64_t)]; WordOr64() : p() { } }; TEST_EACH_WORD_SIZE_W(simd_word_pick, popcount, { WordOr64 v; auto n = sizeof(simd_word) * 2; for (size_t expected = 0; expected <= n; expected++) { std::vector bits{}; for (size_t i = 0; i < n; i++) { bits.push_back(i < expected); } for (size_t reps = 0; reps < 10; reps++) { std::shuffle(bits.begin(), bits.end(), INDEPENDENT_TEST_RNG()); for (size_t i = 0; i < n; i++) { v.p[i >> 6] = 0; } for (size_t i = 0; i < n; i++) { v.p[i >> 6] |= bits[i] << (i & 63); } ASSERT_EQ(v.w.popcount(), expected); } } }) TEST_EACH_WORD_SIZE_W(simd_word_pick, operator_bool, { bitword w{}; auto p = &w.u8[0]; ASSERT_EQ((bool)w, false); p[0] = 5; ASSERT_EQ((bool)w, true); p[0] = 0; if (bitword::BIT_SIZE > 64) { p[1] = 5; ASSERT_EQ((bool)w, true); p[1] = 0; ASSERT_EQ((bool)w, false); } }) TEST_EACH_WORD_SIZE_W(simd_word, integer_conversions, { ASSERT_EQ((uint64_t)(simd_word{23}), 23); ASSERT_EQ((uint64_t)(simd_word{(uint64_t)23}), 23); ASSERT_EQ((int64_t)(simd_word{(uint64_t)23}), 23); ASSERT_EQ((uint64_t)(simd_word{(int64_t)23}), 23); ASSERT_EQ((int64_t)(simd_word{(int64_t)23}), 23); ASSERT_EQ((int64_t)(simd_word{(int64_t)-23}), -23); if (W > 64) { ASSERT_THROW( { uint64_t u = (uint64_t)(simd_word{(int64_t)-23}); std::cerr << u; }, std::invalid_argument); } simd_word w0{(uint64_t)0}; simd_word w1{(uint64_t)1}; simd_word w2{(uint64_t)2}; ASSERT_EQ((uint64_t)(w1 | w2), 3); ASSERT_EQ((uint64_t)w1, 1); ASSERT_EQ((uint64_t)w2, 2); ASSERT_EQ((int)(w1 | w2), 3); ASSERT_EQ((int)w0, 0); ASSERT_EQ((int)w1, 1); ASSERT_EQ((int)w2, 2); ASSERT_EQ((bool)w0, false); ASSERT_EQ((bool)w1, true); ASSERT_EQ((bool)w2, true); }) TEST_EACH_WORD_SIZE_W(simd_word, equality, { ASSERT_TRUE((simd_word{1}) == (simd_word{1})); ASSERT_FALSE((simd_word{1}) == (simd_word{2})); ASSERT_TRUE((simd_word{1}) != (simd_word{2})); ASSERT_FALSE((simd_word{1}) != (simd_word{1})); }) TEST_EACH_WORD_SIZE_W(simd_word, shifting, { simd_word w{1}; for (size_t k = 0; k < W; k++) { std::array expected{}; expected[k / 64] = uint64_t{1} << (k % 64); EXPECT_EQ((w << static_cast(k)).to_u64_array(), expected) << k; if (k > 0) { EXPECT_EQ(((w << (static_cast(k) - 1)) << 1).to_u64_array(), expected) << k; } EXPECT_EQ(w, (w << static_cast(k)) >> static_cast(k)) << k; } ASSERT_EQ(w << 0, 1); ASSERT_EQ(w >> 0, 1); ASSERT_EQ(w << 1, 2); ASSERT_EQ(w >> 1, 0); ASSERT_EQ(w << 2, 4); ASSERT_EQ(w >> 2, 0); ASSERT_EQ((w << 5) >> 5, 1); ASSERT_EQ((w >> 5) << 5, 0); ASSERT_EQ((w << static_cast(W - 1)) << 1, 0); ASSERT_EQ((w << static_cast(W - 1)) >> static_cast(W - 1), 1); }) TEST_EACH_WORD_SIZE_W(simd_word, masking, { simd_word w{0b10011}; ASSERT_EQ((w & 1), 1); ASSERT_EQ((w & 2), 2); ASSERT_EQ((w & 4), 0); ASSERT_EQ((w ^ 1), 0b10010); ASSERT_EQ((w ^ 2), 0b10001); ASSERT_EQ((w ^ 4), 0b10111); ASSERT_EQ((w | 1), 0b10011); ASSERT_EQ((w | 2), 0b10011); ASSERT_EQ((w | 4), 0b10111); }) TEST_EACH_WORD_SIZE_W(simd_word, ordering, { ASSERT_TRUE(simd_word(1) < simd_word(2)); ASSERT_TRUE(!(simd_word(2) < simd_word(2))); ASSERT_TRUE(!(simd_word(3) < simd_word(2))); }) TEST_EACH_WORD_SIZE_W(simd_word, from_u64_array, { std::array expected; for (size_t k = 0; k < expected.size(); k++) { expected[k] = k * 3 + 1; } simd_word w(expected); std::array actual = w.to_u64_array(); ASSERT_EQ(actual, expected); }) ================================================ FILE: src/stim/mem/simd_word.test.h ================================================ /// Test utilities for working with various sizes of simd words. #include "gtest/gtest.h" #define TEST_EACH_WORD_SIZE_UP_TO_64(test_suite, test_name, ...) \ TEST(test_suite, test_name##_64) { \ constexpr size_t W = 64; \ __VA_ARGS__ \ } #define TEST_EACH_WORD_SIZE_UP_TO_128(test_suite, test_name, ...) \ TEST(test_suite, test_name##_128) { \ constexpr size_t W = 128; \ __VA_ARGS__ \ } \ TEST(test_suite, test_name##_64) { \ constexpr size_t W = 64; \ __VA_ARGS__ \ } #define TEST_EACH_WORD_SIZE_UP_TO_256(test_suite, test_name, ...) \ TEST(test_suite, test_name##_256) { \ constexpr size_t W = 256; \ __VA_ARGS__ \ } \ TEST(test_suite, test_name##_128) { \ constexpr size_t W = 128; \ __VA_ARGS__ \ } \ TEST(test_suite, test_name##_64) { \ constexpr size_t W = 64; \ __VA_ARGS__ \ } #if __AVX2__ #define TEST_EACH_WORD_SIZE_W(test_suite, test_name, ...) \ TEST_EACH_WORD_SIZE_UP_TO_256(test_suite, test_name, __VA_ARGS__) #elif __SSE2__ #define TEST_EACH_WORD_SIZE_W(test_suite, test_name, ...) \ TEST_EACH_WORD_SIZE_UP_TO_128(test_suite, test_name, __VA_ARGS__) #else #define TEST_EACH_WORD_SIZE_W(test_suite, test_name, ...) \ TEST_EACH_WORD_SIZE_UP_TO_64(test_suite, test_name, __VA_ARGS__) #endif ================================================ FILE: src/stim/mem/span_ref.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SPAN_REF_H #define _STIM_MEM_SPAN_REF_H #include #include #include #include #include namespace stim { /// Exposes a collection-like interface to a range of memory delineated by start/end pointers. /// /// A major difference between the semantics of this class and the std::span class added in C++20 is that this class /// defines equality and ordering operators that depend on *what is pointed to* instead of the value of the pointers /// themselves. Two range refs aren't equal because they have the same pointers, they're equal because they point /// to ranges that have the same contents. In other words, this class really acts more like a *reference* than like a /// pointer. template struct SpanRef { T *ptr_start; T *ptr_end; SpanRef() : ptr_start(nullptr), ptr_end(nullptr) { } SpanRef(T *begin, T *end) : ptr_start(begin), ptr_end(end) { } // Implicit conversions. SpanRef(T *singleton) : ptr_start(singleton), ptr_end(singleton + 1) { } SpanRef(const SpanRef::type> &other) : ptr_start(other.ptr_start), ptr_end(other.ptr_end) { } SpanRef(std::span &items) : ptr_start(items.data()), ptr_end(items.data() + items.size()) { } SpanRef(const std::span::type> &items) : ptr_start(items.data()), ptr_end(items.data() + items.size()) { } SpanRef(std::vector &items) : ptr_start(items.data()), ptr_end(items.data() + items.size()) { } SpanRef(const std::vector::type> &items) : ptr_start(items.data()), ptr_end(items.data() + items.size()) { } template SpanRef(std::array &items) : ptr_start(items.data()), ptr_end(items.data() + items.size()) { } template SpanRef(const std::array::type, K> &items) : ptr_start(items.data()), ptr_end(items.data() + items.size()) { } operator std::span() { return {ptr_start, ptr_end}; } operator std::span() const { return {ptr_start, ptr_end}; } SpanRef sub(size_t start_offset, size_t end_offset) const { return SpanRef(ptr_start + start_offset, ptr_start + end_offset); } size_t size() const { return ptr_end - ptr_start; } const T *begin() const { return ptr_start; } const T *end() const { return ptr_end; } const T &back() const { return *(ptr_end - 1); } const T &front() const { return *ptr_start; } bool empty() const { return ptr_end == ptr_start; } T *begin() { return ptr_start; } T *end() { return ptr_end; } T &back() { return *(ptr_end - 1); } T &front() { return *ptr_start; } const T &operator[](size_t index) const { return ptr_start[index]; } T &operator[](size_t index) { return ptr_start[index]; } bool operator==(const SpanRef &other) const { size_t n = size(); if (n != other.size()) { return false; } for (size_t k = 0; k < n; k++) { if (ptr_start[k] != other[k]) { return false; } } return true; } bool operator==(const SpanRef::type> &other) const { return SpanRef(ptr_start, ptr_end) == SpanRef(other.ptr_start, other.ptr_end); } bool operator!=(const SpanRef &other) const { return !(*this == other); } bool operator!=(const SpanRef::type> &other) const { return !(*this == other); } std::string str() const { std::stringstream ss; ss << *this; return ss.str(); } /// Lexicographic ordering. bool operator<(const SpanRef &other) const { auto n = std::min(size(), other.size()); for (size_t k = 0; k < n; k++) { if ((*this)[k] != other[k]) { return (*this)[k] < other[k]; } } return size() < other.size(); } bool operator<(const SpanRef::type> &other) const { return SpanRef(ptr_start, ptr_end) < SpanRef(other.ptr_start, other.ptr_end); } }; template std::ostream &operator<<(std::ostream &out, const stim::SpanRef &v) { bool first = false; out << "SpanRef{"; for (const auto &item : v) { if (!first) { first = true; } else { out << ", "; } out << item; } out << "}"; return out; } } // namespace stim #endif ================================================ FILE: src/stim/mem/sparse_xor_vec.cc ================================================ /* * 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. */ #include "stim/mem/sparse_xor_vec.h" ================================================ FILE: src/stim/mem/sparse_xor_vec.h ================================================ /* * 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. */ #ifndef _STIM_MEM_SPARSE_XOR_VEC_H #define _STIM_MEM_SPARSE_XOR_VEC_H #include #include #include #include #include #include #include #include "stim/mem/monotonic_buffer.h" namespace stim { /// Merges the elements of two sorted buffers into an output buffer while cancelling out duplicate items. /// /// \param sorted_in1: Pointer range covering the first sorted list. /// \param sorted_in2: Pointer range covering the second sorted list. /// \param out: Where to write the output. Must have size of at least sorted_in1.size() + sorted_in2.size(). /// \return: A pointer to the end of the output (one past the last place written). template inline T *xor_merge_sort(SpanRef sorted_in1, SpanRef sorted_in2, T *out) { // Interleave sorted src and dst into a sorted work buffer. const T *p1 = sorted_in1.ptr_start; const T *p2 = sorted_in2.ptr_start; const T *end1 = sorted_in1.ptr_end; const T *end2 = sorted_in2.ptr_end; while (p1 != end1) { if (p2 == end2 || *p1 < *p2) { *out++ = *p1++; } else if (*p2 < *p1) { *out++ = *p2++; } else { // Same value in both lists. Cancels itself out. p1++; p2++; } } while (p2 != end2) { *out++ = *p2++; } return out; } template inline SpanRef inplace_xor_sort(SpanRef items) { std::sort(items.begin(), items.end()); size_t new_size = 0; for (size_t k = 0; k < items.size(); k++) { if (new_size > 0 && items[k] == items[new_size - 1]) { new_size--; } else { if (k != new_size) { std::swap(items[new_size], items[k]); } new_size++; } } return items.sub(0, new_size); } template bool is_subset_of_sorted(SpanRef subset, SpanRef superset) { const T *p_sub = subset.ptr_start; const T *p_sup = superset.ptr_start; const T *end_sub = subset.ptr_end; const T *end_sup = superset.ptr_end; while (p_sub != end_sub) { if (p_sup == end_sup || *p_sub < *p_sup) { return false; } else if (*p_sup < *p_sub) { p_sup++; } else { // Same value in both lists. Cancels itself out. p_sub++; p_sup++; } } return true; } template inline void xor_merge_sort_temp_buffer_callback( SpanRef sorted_items_1, SpanRef sorted_items_2, CALLBACK handler) { constexpr size_t STACK_SIZE = 64; size_t max = sorted_items_1.size() + sorted_items_2.size(); if (max > STACK_SIZE) { T *begin = new T[max]; T *end = xor_merge_sort(sorted_items_1, sorted_items_2, begin); handler(SpanRef(begin, end)); delete[] begin; } else { T data[STACK_SIZE]; T *begin = &data[0]; T *end = xor_merge_sort(sorted_items_1, sorted_items_2, begin); handler(SpanRef(begin, end)); } } template struct SparseXorVec; template std::ostream &operator<<(std::ostream &out, const SparseXorVec &v); template inline void xor_item_into_sorted_vec(const T &item, std::vector &sorted_items) { // Just do a linear scan to find the insertion point, instead of a binary search. // This is faster at small sizes, and complexity is linear anyways due to the shifting of later items. for (size_t k = 0; k < sorted_items.size(); k++) { const auto &v = sorted_items[k]; if (v < item) { continue; } else if (v == item) { sorted_items.erase(sorted_items.begin() + k); return; } else { sorted_items.insert(sorted_items.begin() + k, item); return; } } sorted_items.push_back(item); } /// A sparse set of integers that supports efficient xoring (computing the symmetric difference). template struct SparseXorVec { public: // Sorted list of entries. std::vector sorted_items; SparseXorVec() = default; SparseXorVec(std::vector &&vec) : sorted_items(std::move(vec)) { } bool empty() const { return sorted_items.empty(); } void set_to_xor_merge_sort(SpanRef sorted_items1, SpanRef sorted_items2) { sorted_items.resize(sorted_items1.size() + sorted_items2.size()); auto written = xor_merge_sort(sorted_items, sorted_items1, sorted_items2); sorted_items.resize(written.size()); } bool is_superset_of(SpanRef subset) const { return is_subset_of_sorted(subset, (SpanRef)sorted_items); } bool contains(const T &item) const { return std::find(sorted_items.begin(), sorted_items.end(), item) != sorted_items.end(); } void xor_sorted_items(SpanRef sorted) { xor_merge_sort_temp_buffer_callback(range(), sorted, [&](SpanRef result) { sorted_items.clear(); sorted_items.insert(sorted_items.end(), result.begin(), result.end()); }); } void clear() { sorted_items.clear(); } void xor_item(const T &item) { xor_item_into_sorted_vec(item, sorted_items); } SparseXorVec &operator^=(const SparseXorVec &other) { xor_sorted_items(other.range()); return *this; } SparseXorVec operator^(const SparseXorVec &other) const { SparseXorVec result; result.sorted_items.resize(size() + other.size()); auto n = xor_merge_sort(range(), other.range(), result.begin()) - result.begin(); result.sorted_items.resize(n); return result; } SparseXorVec operator^(const T &other) const { return xor_helper(&other, 1); } bool operator<(const SparseXorVec &other) const { return range() < other.range(); } inline size_t size() const { return sorted_items.size(); } inline T *begin() { return sorted_items.data(); } inline T *end() { return sorted_items.data() + size(); } inline const T *begin() const { return sorted_items.data(); } inline const T *end() const { return sorted_items.data() + size(); } bool operator==(const std::vector &other) const { return sorted_items == other; } bool operator!=(const std::vector &other) const { return sorted_items != other; } bool operator==(const SparseXorVec &other) const { return sorted_items == other.sorted_items; } bool operator!=(const SparseXorVec &other) const { return sorted_items != other.sorted_items; } SpanRef range() const { return {begin(), end()}; } std::string str() const { std::stringstream ss; ss << *this; return ss.str(); } void check_invariants() const { for (size_t k = 1; k < sorted_items.size(); k++) { if (!(sorted_items[k - 1] < sorted_items[k])) { throw std::invalid_argument(str() + " is not unique and sorted."); } } } }; template std::ostream &operator<<(std::ostream &out, const SparseXorVec &v) { bool first = false; out << "SparseXorVec{"; for (const auto &item : v) { if (!first) { first = true; } else { out << ", "; } out << item; } out << "}"; return out; } } // namespace stim #endif ================================================ FILE: src/stim/mem/sparse_xor_vec.perf.cc ================================================ // 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. #include "stim/mem/sparse_xor_vec.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(SparseXorTable_SmallRowXor_1000) { size_t n = 1000; std::vector> table(n); for (uint32_t k = 0; k < n; k++) { table[k].xor_item(k); table[k].xor_item(k + 1); table[k].xor_item(k + 4); table[k].xor_item(k + 8); table[k].xor_item(k + 15); } benchmark_go([&]() { for (size_t k = 1; k < n; k++) { table[k - 1] ^= table[k]; } for (size_t k = n; --k > 1;) { table[k - 1] ^= table[k]; } }) .goal_micros(35) .show_rate("RowXors", n * 2) .show_rate("WordXors", n * 3); } BENCHMARK(SparseXorVec_XorItem) { SparseXorVec buf; std::vector data{2, 5, 9, 5, 3, 6, 10}; benchmark_go([&]() { for (auto d : data) { buf.xor_item(d); } }) .goal_nanos(30) .show_rate("Item", data.size()); } ================================================ FILE: src/stim/mem/sparse_xor_vec.test.cc ================================================ // 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. #include "stim/mem/sparse_xor_vec.h" #include #include "gtest/gtest.h" #include "stim/mem/simd_util.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(sparse_xor_table, inplace_xor) { SparseXorVec v1; SparseXorVec v2; v1.xor_item(1); v1.xor_item(3); v2.xor_item(3); v2.xor_item(2); ASSERT_EQ(v1.sorted_items, (std::vector{1, 3})); ASSERT_EQ(v2.sorted_items, (std::vector{2, 3})); v1 ^= v2; ASSERT_EQ(v1.sorted_items, (std::vector{1, 2})); ASSERT_EQ(v2.sorted_items, (std::vector{2, 3})); } TEST(sparse_xor_table, grow) { SparseXorVec v1; SparseXorVec v2; v1.xor_item(1); v1.xor_item(3); v1.xor_item(6); v2.xor_item(2); v2.xor_item(3); v2.xor_item(4); v2.xor_item(5); v1 ^= v2; ASSERT_EQ(v1.sorted_items, (std::vector{1, 2, 4, 5, 6})); } TEST(sparse_xor_table, historical_failure_case) { SparseXorVec v1; SparseXorVec v2; v1.xor_item(1); v1.xor_item(2); v1.xor_item(3); v1.xor_item(6); v1.xor_item(9); v2.xor_item(2); v1.xor_item(2); ASSERT_EQ(v1.sorted_items, (std::vector{1, 3, 6, 9})); ASSERT_EQ(v2.sorted_items, (std::vector{2})); } TEST(sparse_xor_table, comparison) { SparseXorVec v1; v1.xor_item(1); v1.xor_item(3); SparseXorVec v2; v2.xor_item(1); ASSERT_TRUE(v1 != v2); ASSERT_TRUE(!(v1 == v2)); ASSERT_TRUE(v2 < v1); ASSERT_TRUE(!(v1 < v2)); v2.xor_item(4); ASSERT_TRUE(v1 != v2); ASSERT_TRUE(!(v1 == v2)); ASSERT_TRUE(!(v2 < v1)); ASSERT_TRUE(v1 < v2); v2.xor_item(4); v2.xor_item(3); ASSERT_TRUE(v1 == v2); ASSERT_TRUE(!(v1 != v2)); ASSERT_TRUE(!(v2 < v1)); ASSERT_TRUE(!(v1 < v2)); } TEST(sparse_xor_table, str) { SparseXorVec v; ASSERT_EQ(v.str(), "SparseXorVec{}"); v.xor_item(5); ASSERT_EQ(v.str(), "SparseXorVec{5}"); v.xor_item(2); ASSERT_EQ(v.str(), "SparseXorVec{2, 5}"); v.xor_item(5000); ASSERT_EQ(v.str(), "SparseXorVec{2, 5, 5000}"); } TEST(sparse_xor_table, empty) { SparseXorVec v1; ASSERT_TRUE(v1.empty()); v1.xor_item(1); ASSERT_FALSE(v1.empty()); v1.xor_item(3); ASSERT_FALSE(v1.empty()); } TEST(sparse_xor_vec, is_subset_of_sorted) { ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{}, (SpanRef)std::vector{})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{}, (SpanRef)std::vector{0})); ASSERT_FALSE(is_subset_of_sorted( (SpanRef)std::vector{0}, (SpanRef)std::vector{})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{0}, (SpanRef)std::vector{0})); ASSERT_FALSE(is_subset_of_sorted( (SpanRef)std::vector{1}, (SpanRef)std::vector{0})); ASSERT_FALSE(is_subset_of_sorted( (SpanRef)std::vector{0}, (SpanRef)std::vector{1})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{0}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{1}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_FALSE(is_subset_of_sorted( (SpanRef)std::vector{2}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{5}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{6}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_FALSE(is_subset_of_sorted( (SpanRef)std::vector{7}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{8}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_FALSE(is_subset_of_sorted( (SpanRef)std::vector{9}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{0, 5}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_TRUE(is_subset_of_sorted( (SpanRef)std::vector{0, 1, 6}, (SpanRef)std::vector{0, 1, 5, 6, 8})); ASSERT_FALSE(is_subset_of_sorted( (SpanRef)std::vector{0, 2, 6}, (SpanRef)std::vector{0, 1, 5, 6, 8})); } TEST(sparse_xor_vec, is_superset_of) { ASSERT_TRUE((SparseXorVec{{0}}).is_superset_of(SparseXorVec{{0}}.range())); ASSERT_TRUE((SparseXorVec{{0, 1}}).is_superset_of(SparseXorVec{{0}}.range())); ASSERT_FALSE((SparseXorVec{{0}}).is_superset_of(SparseXorVec{{0, 1}}.range())); } TEST(sparse_xor_vec, contains) { ASSERT_TRUE((SparseXorVec{{0}}).contains(0)); ASSERT_TRUE((SparseXorVec{{2}}).contains(2)); ASSERT_TRUE((SparseXorVec{{0, 2}}).contains(0)); ASSERT_TRUE((SparseXorVec{{0, 2}}).contains(2)); ASSERT_FALSE((SparseXorVec{{0, 2}}).contains(1)); ASSERT_FALSE((SparseXorVec{{}}).contains(0)); ASSERT_FALSE((SparseXorVec{{1}}).contains(0)); } TEST(sparse_xor_vec, inplace_xor_sort) { auto f = [](std::vector v) -> std::vector { SpanRef s = v; auto r = inplace_xor_sort(s); v.resize(r.size()); return v; }; ASSERT_EQ(f({}), (std::vector({}))); ASSERT_EQ(f({5}), (std::vector({5}))); ASSERT_EQ(f({5, 5}), (std::vector({}))); ASSERT_EQ(f({5, 5, 5}), (std::vector({5}))); ASSERT_EQ(f({5, 5, 5, 5}), (std::vector({}))); ASSERT_EQ(f({5, 4, 5, 5}), (std::vector({4, 5}))); ASSERT_EQ(f({4, 5, 5, 5}), (std::vector({4, 5}))); ASSERT_EQ(f({5, 5, 5, 4}), (std::vector({4, 5}))); ASSERT_EQ(f({4, 5, 5, 4}), (std::vector({}))); ASSERT_EQ(f({3, 5, 5, 4}), (std::vector({3, 4}))); } ================================================ FILE: src/stim/perf.perf.h ================================================ /* * 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. */ #ifndef _STIM_PERF_PERF_H #define _STIM_PERF_PERF_H #include #include #include #include extern double BENCHMARK_CONFIG_TARGET_SECONDS; struct BenchmarkResult { double total_seconds; size_t total_reps; std::vector> marginal_rates; double goal_seconds; BenchmarkResult(double total_seconds, size_t total_reps) : total_seconds(total_seconds), total_reps(total_reps), marginal_rates(), goal_seconds(-1) { } BenchmarkResult &show_rate(std::string_view new_unit_name, double new_multiplier) { marginal_rates.emplace_back(new_unit_name, new_multiplier); return *this; } BenchmarkResult &goal_nanos(double nanos) { goal_seconds = nanos / 1000 / 1000 / 1000; return *this; } BenchmarkResult &goal_micros(double micros) { goal_seconds = micros / 1000 / 1000; return *this; } BenchmarkResult &goal_millis(double millis) { goal_seconds = millis / 1000; return *this; } }; struct RegisteredBenchmark { std::string name; std::function func; std::vector results; }; extern RegisteredBenchmark *running_benchmark; extern std::vector *all_registered_benchmarks_data; extern uint64_t registry_initialized; inline void add_benchmark(RegisteredBenchmark benchmark) { if (all_registered_benchmarks_data == nullptr || registry_initialized != 4620243525989388168ULL) { registry_initialized = 4620243525989388168ULL; all_registered_benchmarks_data = new std::vector(); } all_registered_benchmarks_data->push_back(benchmark); } #define BENCHMARK(name) \ void BENCH_##name##_METHOD(); \ struct BENCH_STARTUP_TYPE_##name { \ BENCH_STARTUP_TYPE_##name() { \ add_benchmark({#name, BENCH_##name##_METHOD}); \ } \ }; \ static BENCH_STARTUP_TYPE_##name BENCH_STARTUP_INSTANCE_##name; \ void BENCH_##name##_METHOD() // HACK: Templating the body function type makes inlining significantly more likely. template BenchmarkResult &benchmark_go(FUNC body) { size_t total_reps = 0; double total_seconds = 0.0; double target_wait_time_seconds = BENCHMARK_CONFIG_TARGET_SECONDS; for (size_t rep_limit = 1; total_seconds < target_wait_time_seconds; rep_limit *= 100) { double remaining_time = target_wait_time_seconds - total_seconds; size_t reps = rep_limit; if (total_seconds > 0) { reps = (size_t)(remaining_time * total_reps / total_seconds); if (reps < total_reps * 0.1) { break; } if (reps > rep_limit) { reps = rep_limit; } if (reps < 1) { reps = 1; } } auto start = std::chrono::steady_clock::now(); for (size_t rep = 0; rep < reps; rep++) { body(); } auto end = std::chrono::steady_clock::now(); auto micros = std::chrono::duration_cast(end - start).count(); total_reps += reps; total_seconds += (double)micros / 1000.0 / 1000.0; } running_benchmark->results.push_back({total_seconds, total_reps}); return running_benchmark->results.back(); } #endif ================================================ FILE: src/stim/py/README.md ================================================ # `py` directory This directory contains code related to exposing a Python 3 API for stim, using `pybind11`. ================================================ FILE: src/stim/py/base.pybind.cc ================================================ // 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. #include "stim/py/base.pybind.h" #include #include "stim/util_bot/probability_util.h" using namespace stim; std::mt19937_64 stim_pybind::make_py_seeded_rng(const pybind11::object &seed) { if (seed.is_none()) { return externally_seeded_rng(); } try { uint64_t s = pybind11::cast(seed) ^ INTENTIONAL_VERSION_SEED_INCOMPATIBILITY; return std::mt19937_64(s); } catch (const pybind11::cast_error &) { throw std::invalid_argument("Expected seed to be None or a 64 bit unsigned integer."); } } bool stim_pybind::normalize_index_or_slice( const pybind11::object &index_or_slice, size_t length, pybind11::ssize_t *start, pybind11::ssize_t *step, pybind11::ssize_t *slice_length) { try { *start = pybind11::cast(index_or_slice); if (*start < 0) { *start += length; } if (*start < 0 || (size_t)*start >= length) { throw std::out_of_range( "Index " + std::to_string(pybind11::cast(index_or_slice)) + " not in range for sequence of length " + std::to_string(length) + "."); } *step = 0; *slice_length = 1; return false; } catch (const pybind11::cast_error &) { } pybind11::slice slice; try { slice = pybind11::cast(index_or_slice); } catch (const pybind11::cast_error &ex) { throw pybind11::type_error("Expected an integer index or a slice."); } pybind11::ssize_t stop; if (!slice.compute(length, start, &stop, step, slice_length)) { throw pybind11::error_already_set(); } return true; } SampleFormat stim_pybind::format_to_enum(std::string_view format) { auto found_format = format_name_to_enum_map().find(format); if (found_format == format_name_to_enum_map().end()) { std::stringstream msg; msg << "Unrecognized output format: '" << format << "'. Recognized formats are:\n"; for (const auto &kv : format_name_to_enum_map()) { msg << " " << kv.first << "\n"; } throw std::invalid_argument(msg.str()); } return found_format->second.id; } ================================================ FILE: src/stim/py/base.pybind.h ================================================ // 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. #ifndef _STIM_PY_BASE_PYBIND_H #define _STIM_PY_BASE_PYBIND_H #include #include #include #include #include #include #include "stim/circuit/circuit.h" #include "stim/cmd/command_help.h" #include "stim/io/stim_data_formats.h" namespace stim_pybind { std::mt19937_64 make_py_seeded_rng(const pybind11::object &seed); stim::SampleFormat format_to_enum(std::string_view format); /// Converts a python index or slice into a form that's easier to consume. /// /// In particular, replaces negative indices with non-negative indices. /// When the object is an integer, it is treated as a slice over a single /// index. The boolean returned from the method can be used to distinguish /// a single-index slice from an integer index. /// /// The result can be consumed as follows: /// /// pybind11::ssize_t slice_start; /// pybind11::ssize_t slice_step; /// pybind11::ssize_t slice_length; /// bool was_slice = normalize_index_or_slice( /// obj, /// collection_length, /// &slice_start, /// &slice_step, /// &slice_length) /// for (size_t k = 0; k < (size_t)slice_length; k++) { /// size_t target_k = index + step * k; /// auto &target = collection[slice_start + slice_step*k]; /// ... /// } /// /// Args: /// index_or_slice: An int or slice object. /// length: The length of the collection being indexed or sliced. /// start: Output address for the offset to use when looping. /// The value written to this pointer will be non-negative // (unless an exception is thrown). /// step: Output address for the stride to use when looping. /// The value written to this pointer may be negative. /// slice_length: Output address for how many iterations to run the loop. /// The value written to this pointer will be non-negative // (unless an exception is thrown). /// /// Returns: /// True: the given object was a slice. /// False: the given object was an integer index. /// /// Raises: /// invalid_argument: The given object was not a valid slice or index for /// the given collection length; bool normalize_index_or_slice( const pybind11::object &index_or_slice, size_t length, pybind11::ssize_t *start, pybind11::ssize_t *step, pybind11::ssize_t *slice_length); template pybind11::tuple tuple_tree(const std::vector &val, size_t offset = 0) { // A workaround for https://github.com/pybind/pybind11/issues/1928 if (offset >= val.size()) { return pybind11::make_tuple(); } if (offset + 1 == val.size()) { return pybind11::make_tuple(val[offset]); } return pybind11::make_tuple(val[offset], tuple_tree(val, offset + 1)); } } // namespace stim_pybind #endif ================================================ FILE: src/stim/py/compiled_detector_sampler.pybind.cc ================================================ // 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. #include "stim/py/compiled_detector_sampler.pybind.h" #include "stim/circuit/circuit.pybind.h" #include "stim/io/raii_file.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" #include "stim/simulators/frame_simulator.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/simulators/tableau_simulator.h" using namespace stim; using namespace stim_pybind; CompiledDetectorSampler::CompiledDetectorSampler(Circuit init_circuit, std::mt19937_64 &&rng) : circuit_stats(init_circuit.compute_stats()), circuit(std::move(init_circuit)), frame_sim(circuit_stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 0, std::move(rng)) { } pybind11::object CompiledDetectorSampler::sample_to_numpy( size_t num_shots, bool prepend_observables, bool append_observables, bool separate_observables, bool bit_packed, pybind11::object dets_out, pybind11::object obs_out) { if (separate_observables && (append_observables || prepend_observables)) { throw std::invalid_argument( "Can't specify separate_observables=True with append_observables=True or prepend_observables=True"); } { pybind11::gil_scoped_release release; frame_sim.configure_for(circuit_stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, num_shots); frame_sim.reset_all(); frame_sim.do_circuit(circuit); } const auto &det_data = frame_sim.det_record.storage; const auto &obs_data = frame_sim.obs_record; pybind11::object py_obs_data = pybind11::none(); if (separate_observables || !obs_out.is_none()) { py_obs_data = simd_bit_table_to_numpy(obs_data, circuit_stats.num_observables, num_shots, bit_packed, true, obs_out); } pybind11::object py_det_data = pybind11::none(); if (append_observables || prepend_observables) { size_t num_concat = circuit_stats.num_detectors; simd_bit_table concat_data = det_data; if (append_observables) { concat_data = concat_data.concat_major(obs_data, num_concat, circuit_stats.num_observables); num_concat += circuit_stats.num_observables; } if (prepend_observables) { concat_data = obs_data.concat_major(concat_data, circuit_stats.num_observables, num_concat); num_concat += circuit_stats.num_observables; } py_det_data = simd_bit_table_to_numpy(concat_data, num_concat, num_shots, bit_packed, true, dets_out); } else { py_det_data = simd_bit_table_to_numpy(det_data, circuit_stats.num_detectors, num_shots, bit_packed, true, dets_out); } if (separate_observables) { return pybind11::make_tuple(py_det_data, py_obs_data); } else { return py_det_data; } } void CompiledDetectorSampler::sample_write( size_t num_samples, pybind11::object filepath_obj, std::string_view format, bool prepend_observables, bool append_observables, pybind11::object obs_out_filepath_obj, std::string_view obs_out_format) { auto f = format_to_enum(format); auto py_path = pybind11::module::import("pathlib").attr("Path"); if (pybind11::isinstance(filepath_obj, py_path)) { filepath_obj = pybind11::str(filepath_obj); } if (pybind11::isinstance(obs_out_filepath_obj, py_path)) { obs_out_filepath_obj = pybind11::str(obs_out_filepath_obj); } std::string_view filepath; if (pybind11::isinstance(filepath_obj)) { filepath = pybind11::cast(filepath_obj); } else { std::stringstream ss; ss << "Don't know how to write to "; ss << pybind11::repr(filepath_obj); throw std::invalid_argument(ss.str()); } std::string_view obs_out_filepath_view; if (pybind11::isinstance(obs_out_filepath_obj)) { obs_out_filepath_view = pybind11::cast(obs_out_filepath_obj); } else if (obs_out_filepath_obj.is_none()) { // Empty string view does the right thing. } else { std::stringstream ss; ss << "Don't know how to write observables to "; ss << pybind11::repr(obs_out_filepath_obj); throw std::invalid_argument(ss.str()); } RaiiFile out(filepath, "wb"); RaiiFile obs_out(obs_out_filepath_view, "wb"); auto parsed_obs_out_format = format_to_enum(obs_out_format); sample_batch_detection_events_writing_results_to_disk( circuit, num_samples, prepend_observables, append_observables, out.f, f, frame_sim.rng, obs_out.f, parsed_obs_out_format); } std::string CompiledDetectorSampler::repr() const { std::stringstream result; result << "stim.CompiledDetectorSampler("; result << circuit_repr(circuit); result << ")"; return result.str(); } CompiledDetectorSampler stim_pybind::py_init_compiled_detector_sampler( const Circuit &circuit, const pybind11::object &seed) { return CompiledDetectorSampler(circuit, make_py_seeded_rng(seed)); } pybind11::class_ stim_pybind::pybind_compiled_detector_sampler(pybind11::module &m) { return pybind11::class_( m, "CompiledDetectorSampler", "An analyzed stabilizer circuit whose detection events can be sampled quickly."); } void stim_pybind::pybind_compiled_detector_sampler_methods( pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init(&py_init_compiled_detector_sampler), pybind11::arg("circuit"), pybind11::kw_only(), pybind11::arg("seed") = pybind11::none(), clean_doc_string(R"DOC( Creates an object that can sample the detection events from a circuit. Args: circuit: The circuit to sample from. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. Returns: An initialized stim.CompiledDetectorSampler. Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(1.0) 0 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[ True]]) )DOC") .data()); c.def( "sample", [](CompiledDetectorSampler &self, size_t shots, bool prepend, bool append, bool separate_observables, bool bit_packed, pybind11::object dets_out, pybind11::object obs_out) { return self.sample_to_numpy(shots, prepend, append, separate_observables, bit_packed, dets_out, obs_out); }, pybind11::arg("shots"), pybind11::kw_only(), pybind11::arg("prepend_observables") = false, pybind11::arg("append_observables") = false, pybind11::arg("separate_observables") = false, pybind11::arg("bit_packed") = false, pybind11::arg("dets_out") = pybind11::none(), pybind11::arg("obs_out") = pybind11::none(), clean_doc_string(R"DOC( @signature def sample(self, shots: int, *, prepend_observables: bool = False, append_observables: bool = False, separate_observables: bool = False, bit_packed: bool = False, dets_out: Optional[np.ndarray] = None, obs_out: Optional[np.ndarray] = None) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: Returns a numpy array containing a batch of detector samples from the circuit. The circuit must define the detectors using DETECTOR instructions. Observables defined by OBSERVABLE_INCLUDE instructions can also be included in the results as honorary detectors. Args: shots: The number of times to sample every detector in the circuit. separate_observables: Defaults to False. When set to True, the return value is a (detection_events, observable_flips) tuple instead of a flat detection_events array. prepend_observables: Defaults to false. When set, observables are included with the detectors and are placed at the start of the results. append_observables: Defaults to false. When set, observables are included with the detectors and are placed at the end of the results. bit_packed: Returns a uint8 numpy array with 8 bits per byte, instead of a bool_ numpy array with 1 bit per byte. Uses little endian packing. dets_out: Defaults to None. Specifies a pre-allocated numpy array to write the detection event data into. This array must have the correct shape and dtype. obs_out: Defaults to None. Specifies a pre-allocated numpy array to write the observable flip data into. This array must have the correct shape and dtype. Returns: A numpy array or tuple of numpy arrays containing the samples. if separate_observables=False and bit_packed=False: A single numpy array. dtype=bool_ shape=( shots, num_detectors + num_observables * ( append_observables + prepend_observables), ) The bit for detection event `m` in shot `s` is at result[s, m] if separate_observables=False and bit_packed=True: A single numpy array. dtype=uint8 shape=( shots, math.ceil((num_detectors + num_observables * ( append_observables + prepend_observables)) / 8), ) The bit for detection event `m` in shot `s` is at (result[s, m // 8] >> (m % 8)) & 1 if separate_observables=True and bit_packed=False: A (dets, obs) tuple. dets.dtype=bool_ dets.shape=(shots, num_detectors) obs.dtype=bool_ obs.shape=(shots, num_observables) The bit for detection event `m` in shot `s` is at dets[s, m] The bit for observable `m` in shot `s` is at obs[s, m] if separate_observables=True and bit_packed=True: A (dets, obs) tuple. dets.dtype=uint8 dets.shape=(shots, math.ceil(num_detectors / 8)) obs.dtype=uint8 obs.shape=(shots, math.ceil(num_observables / 8)) The bit for detection event `m` in shot `s` is at (dets[s, m // 8] >> (m % 8)) & 1 The bit for observable `m` in shot `s` is at (obs[s, m // 8] >> (m % 8)) & 1 Examples: >>> import stim >>> c = stim.Circuit(''' ... H 0 ... CNOT 0 1 ... X_ERROR(1.0) 0 ... M 0 1 ... DETECTOR rec[-1] rec[-2] ... ''') >>> s = c.compile_detector_sampler() >>> s.sample(shots=1) array([[ True]]) )DOC") .data()); c.def( "sample_bit_packed", [](CompiledDetectorSampler &self, size_t shots, bool prepend, bool append) { return self.sample_to_numpy(shots, prepend, append, false, true, pybind11::none(), pybind11::none()); }, pybind11::arg("shots"), pybind11::kw_only(), pybind11::arg("prepend_observables") = false, pybind11::arg("append_observables") = false, clean_doc_string(R"DOC( [DEPRECATED] Use sampler.sample(..., bit_packed=True) instead. Returns a numpy array containing bit packed detector samples from the circuit. The circuit must define the detectors using DETECTOR instructions. Observables defined by OBSERVABLE_INCLUDE instructions can also be included in the results as honorary detectors. Args: shots: The number of times to sample every detector in the circuit. prepend_observables: Defaults to false. When set, observables are included with the detectors and are placed at the start of the results. append_observables: Defaults to false. When set, observables are included with the detectors and are placed at the end of the results. Returns: A numpy array with `dtype=uint8` and `shape=(shots, n)` where `n` is `num_detectors + num_observables*(append_observables+prepend_observables)`. The bit for detection event `m` in shot `s` is at `result[s, (m // 8)] & 2**(m % 8)`. )DOC") .data()); c.def( "sample_write", &CompiledDetectorSampler::sample_write, pybind11::arg("shots"), pybind11::kw_only(), pybind11::arg("filepath"), pybind11::arg("format") = "01", pybind11::arg("prepend_observables") = false, pybind11::arg("append_observables") = false, pybind11::arg("obs_out_filepath") = pybind11::none(), pybind11::arg("obs_out_format") = "01", clean_doc_string(R"DOC( @signature def sample_write(self, shots: int, *, filepath: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', obs_out_filepath: Optional[Union[str, pathlib.Path]] = None, obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', prepend_observables: bool = False, append_observables: bool = False) -> None: Samples detection events from the circuit and writes them to a file. Args: shots: The number of times to sample every measurement in the circuit. filepath: The file to write the results to. format: The output format to write the results with. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". obs_out_filepath: Sample observables as part of each shot, and write them to this file. This keeps the observable data separate from the detector data. obs_out_format: If writing the observables to a file, this is the format to write them in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". prepend_observables: Sample observables as part of each shot, and put them at the start of the detector data. append_observables: Sample observables as part of each shot, and put them at the end of the detector data. Returns: None. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f"{d}/tmp.dat" ... c = stim.Circuit(''' ... X_ERROR(1) 0 ... M 0 1 ... DETECTOR rec[-2] ... DETECTOR rec[-1] ... ''') ... c.compile_detector_sampler().sample_write( ... shots=3, ... filepath=path, ... format="dets") ... with open(path) as f: ... print(f.read(), end='') shot D0 shot D0 shot D0 )DOC") .data()); c.def( "__repr__", &CompiledDetectorSampler::repr, "Returns valid python code evaluating to an equivalent `stim.CompiledDetectorSampler`."); } ================================================ FILE: src/stim/py/compiled_detector_sampler.pybind.h ================================================ // 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. #ifndef _STIM_PY_COMPILED_DETECTOR_SAMPLER_PYBIND_H #define _STIM_PY_COMPILED_DETECTOR_SAMPLER_PYBIND_H #include #include #include #include "stim/circuit/circuit.h" #include "stim/mem/simd_bits.h" #include "stim/simulators/frame_simulator.h" namespace stim_pybind { struct CompiledDetectorSampler { stim::CircuitStats circuit_stats; stim::Circuit circuit; stim::FrameSimulator frame_sim; CompiledDetectorSampler() = delete; CompiledDetectorSampler(const CompiledDetectorSampler &) = delete; CompiledDetectorSampler(CompiledDetectorSampler &&) = default; CompiledDetectorSampler(stim::Circuit circuit, std::mt19937_64 &&rng); pybind11::object sample_to_numpy( size_t num_shots, bool prepend_observables, bool append_observables, bool separate_observables, bool bit_packed, pybind11::object dets_out, pybind11::object obs_out); void sample_write( size_t num_samples, pybind11::object filepath_obj, std::string_view format, bool prepend_observables, bool append_observables, pybind11::object obs_out_filepath_obj, std::string_view obs_out_format); std::string repr() const; }; pybind11::class_ pybind_compiled_detector_sampler(pybind11::module &m); void pybind_compiled_detector_sampler_methods(pybind11::module &m, pybind11::class_ &c); CompiledDetectorSampler py_init_compiled_detector_sampler(const stim::Circuit &circuit, const pybind11::object &seed); } // namespace stim_pybind #endif ================================================ FILE: src/stim/py/compiled_detector_sampler_pybind_test.py ================================================ # 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. import pathlib import tempfile import numpy as np import pytest import stim def test_compiled_detector_sampler_trivial(): c = stim.Circuit(""" X_ERROR(1) 0 M 0 DETECTOR rec[-1] """) np.testing.assert_array_equal( c.compile_detector_sampler().sample(shots=2), np.array([ [1], [1], ], dtype=np.bool_)) def test_compiled_detector_sampler_sample(): c = stim.Circuit(""" X_ERROR(1) 0 M 0 1 2 DETECTOR rec[-3] rec[-2] DETECTOR rec[-3] rec[-1] DETECTOR rec[-1] rec[-2] OBSERVABLE_INCLUDE(0) rec[-3] OBSERVABLE_INCLUDE(3) rec[-2] rec[-1] OBSERVABLE_INCLUDE(0) rec[-2] """) np.testing.assert_array_equal( c.compile_detector_sampler().sample(5), np.array([ [1, 1, 0], [1, 1, 0], [1, 1, 0], [1, 1, 0], [1, 1, 0], ], dtype=np.bool_)) np.testing.assert_array_equal( c.compile_detector_sampler().sample(5, prepend_observables=True), np.array([ [1, 0, 0, 0, 1, 1, 0], [1, 0, 0, 0, 1, 1, 0], [1, 0, 0, 0, 1, 1, 0], [1, 0, 0, 0, 1, 1, 0], [1, 0, 0, 0, 1, 1, 0], ], dtype=np.bool_)) np.testing.assert_array_equal( c.compile_detector_sampler().sample(5, append_observables=True), np.array([ [1, 1, 0, 1, 0, 0, 0], [1, 1, 0, 1, 0, 0, 0], [1, 1, 0, 1, 0, 0, 0], [1, 1, 0, 1, 0, 0, 0], [1, 1, 0, 1, 0, 0, 0], ], dtype=np.bool_)) dets, obs = c.compile_detector_sampler().sample(5, separate_observables=True) np.testing.assert_array_equal( dets, np.array([ [1, 1, 0], [1, 1, 0], [1, 1, 0], [1, 1, 0], [1, 1, 0], ], dtype=np.bool_)) np.testing.assert_array_equal( obs, np.array([ [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0], ], dtype=np.uint8)) dets, obs = c.compile_detector_sampler().sample(5, separate_observables=True, bit_packed=True) np.testing.assert_array_equal( dets, np.array([ [3], [3], [3], [3], [3], ], dtype=np.uint8)) np.testing.assert_array_equal( obs, np.array([ [1], [1], [1], [1], [1], ], dtype=np.uint8)) np.testing.assert_array_equal( c.compile_detector_sampler().sample(5, append_observables=True, prepend_observables=True), np.array([ [1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0], [1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0], [1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0], [1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0], [1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0], ], dtype=np.bool_)) np.testing.assert_array_equal( c.compile_detector_sampler().sample_bit_packed(5), np.array([ [0b011], [0b011], [0b011], [0b011], [0b011], ], dtype=np.uint8)) np.testing.assert_array_equal( c.compile_detector_sampler().sample(5, bit_packed=True), np.array([ [0b011], [0b011], [0b011], [0b011], [0b011], ], dtype=np.uint8)) with tempfile.TemporaryDirectory() as d: path = f"{d}/tmp.dat" c.compile_detector_sampler().sample_write(5, filepath=path, format='b8') with open(path, 'rb') as f: assert f.read() == b'\x03' * 5 c.compile_detector_sampler().sample_write(5, filepath=pathlib.Path(path), format='b8') with open(path, 'rb') as f: assert f.read() == b'\x03' * 5 c.compile_detector_sampler().sample_write(5, filepath=path, format='01', prepend_observables=True) with open(path, 'r') as f: assert f.readlines() == ['1000110\n'] * 5 c.compile_detector_sampler().sample_write(5, filepath=path, format='01', append_observables=True) with open(path, 'r') as f: assert f.readlines() == ['1101000\n'] * 5 def test_write_obs_file(): c = stim.Circuit(""" X_ERROR(1) 1 MR 0 1 DETECTOR rec[-2] DETECTOR rec[-2] DETECTOR rec[-2] DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-2] OBSERVABLE_INCLUDE(1) rec[-1] OBSERVABLE_INCLUDE(2) rec[-2] """) r: stim.CompiledDetectorSampler = c.compile_detector_sampler() with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) r.sample_write( shots=100, filepath=str(d / 'det'), format='dets', obs_out_filepath=str(d / 'obs'), obs_out_format='hits', ) with open(d / 'det') as f: assert f.read() == 'shot D3\n' * 100 with open(d / 'obs') as f: assert f.read() == '1\n' * 100 with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) r.sample_write( shots=100, filepath=d / 'det', format='dets', obs_out_filepath=d / 'obs', obs_out_format='hits', ) with open(d / 'det') as f: assert f.read() == 'shot D3\n' * 100 with open(d / 'obs') as f: assert f.read() == '1\n' * 100 def test_detector_sampler_actually_fills_array(): circuit = stim.Circuit(''' X_ERROR(1) 0 M 0 DETECTOR rec[-1] ''') sampler = circuit.compile_detector_sampler() detector_data = sampler.sample(shots=10000) assert np.all(detector_data) def test_manual_output_buffer(): circuit = stim.Circuit(''' X_ERROR(1) 0 M 0 DETECTOR DETECTOR DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR DETECTOR DETECTOR rec[-1] DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''') sampler = circuit.compile_detector_sampler() with pytest.raises(ValueError): sampler.sample(shots=17, dets_out=np.zeros(shape=(17, 8), dtype=np.bool_)) with pytest.raises(ValueError): sampler.sample(shots=17, dets_out=np.zeros(shape=(17, 10), dtype=np.bool_)) with pytest.raises(ValueError): sampler.sample(shots=17, dets_out=np.zeros(shape=(18, 9), dtype=np.bool_)) with pytest.raises(ValueError): sampler.sample(shots=17, dets_out=np.zeros(shape=(16, 9), dtype=np.bool_)) with pytest.raises(ValueError): sampler.sample(shots=17, dets_out=np.zeros(shape=(17, 9), dtype=np.uint8)) buf = np.zeros(shape=(17, 9), dtype=np.bool_) ret = sampler.sample(shots=17, dets_out=buf) assert ret is buf assert np.array_equal(buf, [[0, 0, 1, 1, 1, 0, 0, 1, 1]] * 17) buf = np.zeros(shape=(17, 2), dtype=np.uint8) ret = sampler.sample( shots=17, dets_out=buf, bit_packed=True, ) assert ret is buf assert np.array_equal(buf, [[0b10011100, 0b1]] * 17) buf = np.zeros(shape=(2, 17), dtype=np.uint8).transpose() ret = sampler.sample( shots=17, dets_out=buf, bit_packed=True, ) assert ret is buf assert np.array_equal(buf, [[0b10011100, 0b1]] * 17) buf = np.zeros(shape=(17, 9), dtype=np.bool_) buf2 = np.zeros(shape=(17, 1), dtype=np.bool_) ret = sampler.sample( shots=17, dets_out=buf, obs_out=buf2, ) assert ret is buf assert np.array_equal(buf, [[0, 0, 1, 1, 1, 0, 0, 1, 1]] * 17) assert np.array_equal(buf2, [[1]] * 17) buf = np.zeros(shape=(17, 9), dtype=np.bool_) buf2 = np.zeros(shape=(17, 1), dtype=np.bool_) ret, ret2 = sampler.sample( shots=17, dets_out=buf, obs_out=buf2, separate_observables=True, ) assert ret is buf assert ret2 is buf2 assert np.array_equal(buf, [[0, 0, 1, 1, 1, 0, 0, 1, 1]] * 17) assert np.array_equal(buf2, [[1]] * 17) buf = np.zeros(shape=(17, 10), dtype=np.bool_) buf2 = np.zeros(shape=(17, 1), dtype=np.bool_) ret = sampler.sample( shots=17, dets_out=buf, obs_out=buf2, append_observables=True, ) assert ret is buf assert np.array_equal(buf, [[0, 0, 1, 1, 1, 0, 0, 1, 1, 1]] * 17) assert np.array_equal(buf2, [[1]] * 17) buf = np.zeros(shape=(10, 17), dtype=np.bool_).transpose() buf2 = np.zeros(shape=(17, 1), dtype=np.bool_) ret = sampler.sample( shots=17, dets_out=buf, obs_out=buf2, append_observables=True, ) assert ret is buf assert np.array_equal(buf, [[0, 0, 1, 1, 1, 0, 0, 1, 1, 1]] * 17) assert np.array_equal(buf2, [[1]] * 17) ================================================ FILE: src/stim/py/compiled_measurement_sampler.pybind.cc ================================================ // 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. #include "stim/py/compiled_measurement_sampler.pybind.h" #include "stim/circuit/circuit.pybind.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/simulators/tableau_simulator.h" using namespace stim; using namespace stim_pybind; CompiledMeasurementSampler::CompiledMeasurementSampler( simd_bits ref_sample, Circuit circuit, bool skip_reference_sample, std::mt19937_64 &&rng) : ref_sample(ref_sample), circuit(circuit), skip_reference_sample(skip_reference_sample), rng(std::move(rng)) { } pybind11::object CompiledMeasurementSampler::sample_to_numpy(size_t num_shots, bool bit_packed) { simd_bit_table sample = sample_batch_measurements(circuit, ref_sample, num_shots, rng, false); size_t bits_per_sample = circuit.count_measurements(); return simd_bit_table_to_numpy(sample, bits_per_sample, num_shots, bit_packed, true, pybind11::none()); } void CompiledMeasurementSampler::sample_write(size_t num_samples, std::string_view filepath, std::string_view format) { auto f = format_to_enum(format); FILE *out = fopen(std::string(filepath).c_str(), "wb"); if (out == nullptr) { throw std::invalid_argument("Failed to open '" + std::string(filepath) + "' to write."); } sample_batch_measurements_writing_results_to_disk(circuit, ref_sample, num_samples, out, f, rng); fclose(out); } std::string CompiledMeasurementSampler::repr() const { std::stringstream result; result << "stim.CompiledMeasurementSampler("; result << circuit_repr(circuit); if (skip_reference_sample) { result << ", skip_reference_sample=True"; } result << ")"; return result.str(); } pybind11::class_ stim_pybind::pybind_compiled_measurement_sampler(pybind11::module &m) { return pybind11::class_( m, "CompiledMeasurementSampler", "An analyzed stabilizer circuit whose measurements can be sampled quickly."); } CompiledMeasurementSampler stim_pybind::py_init_compiled_sampler( const Circuit &circuit, bool skip_reference_sample, const pybind11::object &seed, const pybind11::object &reference_sample) { if (reference_sample.is_none()) { simd_bits ref_sample = skip_reference_sample ? simd_bits(circuit.count_measurements()) : TableauSimulator::reference_sample_circuit(circuit); return CompiledMeasurementSampler(ref_sample, circuit, skip_reference_sample, make_py_seeded_rng(seed)); } else { if (skip_reference_sample) { throw std::invalid_argument("skip_reference_sample = True but reference_sample is not None."); } uint64_t num_bits = circuit.count_measurements(); simd_bits ref_sample(num_bits); simd_bits_range_ref ref_sample_ref(ref_sample); memcpy_bits_from_numpy_to_simd(num_bits, reference_sample, ref_sample_ref); return CompiledMeasurementSampler(ref_sample, circuit, skip_reference_sample, make_py_seeded_rng(seed)); } } void stim_pybind::pybind_compiled_measurement_sampler_methods( pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init(&py_init_compiled_sampler), pybind11::arg("circuit"), pybind11::kw_only(), pybind11::arg("skip_reference_sample") = false, pybind11::arg("seed") = pybind11::none(), pybind11::arg("reference_sample") = pybind11::none(), clean_doc_string(R"DOC( Creates a measurement sampler for the given circuit. The sampler uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the sampler, as a baseline for deriving more samples using an error propagation simulator. Args: circuit: The stim circuit to sample from. skip_reference_sample: Defaults to False. When set to True, the reference sample used by the sampler is initialized to all-zeroes instead of being collected from the circuit. This means that the results returned by the sampler are actually whether or not each measurement was *flipped*, instead of true measurement results. Forcing an all-zero reference sample is useful when you are only interested in error propagation and don't want to have to deal with the fact that some measurements want to be On when no errors occur. It is also useful when you know for sure that the all-zero result is actually a possible result from the circuit (under noiseless execution), meaning it is a valid reference sample as good as any other. Computing the reference sample is the most time consuming and memory intensive part of simulating the circuit, so promising that the simulator can safely skip that step is an effective optimization. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how many shots are taken. For example, taking 10 shots and then 90 shots will give different results from taking 100 shots in one call. reference_sample: The data to xor into the measurement flips produced by the frame simulator, in order to produce proper measurement results. This can either be specified as an `np.bool_` array or a bit packed `np.uint8` array (little endian). Under normal conditions, the reference sample should be a valid noiseless sample of the circuit, such as the one returned by `circuit.reference_sample()`. If this argument is not provided, the reference sample will be set to `circuit.reference_sample()`, unless `skip_reference_sample=True` is used, in which case it will be set to all-zeros. Returns: An initialized stim.CompiledMeasurementSampler. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[ True, False, True, True]]) )DOC") .data()); c.def( "sample", [](CompiledMeasurementSampler &self, size_t shots, bool bit_packed) { return self.sample_to_numpy(shots, bit_packed); }, pybind11::arg("shots"), pybind11::kw_only(), pybind11::arg("bit_packed") = false, clean_doc_string(R"DOC( @signature def sample(self, shots: int, *, bit_packed: bool = False) -> np.ndarray: Samples a batch of measurement samples from the circuit. Args: shots: The number of times to sample every measurement in the circuit. bit_packed: Returns a uint8 numpy array with 8 bits per byte, instead of a bool_ numpy array with 1 bit per byte. Uses little endian packing. Returns: A numpy array containing the samples. If bit_packed=False: dtype=bool_ shape=(shots, circuit.num_measurements) The bit for measurement `m` in shot `s` is at result[s, m] If bit_packed=True: dtype=uint8 shape=(shots, math.ceil(circuit.num_measurements / 8)) The bit for measurement `m` in shot `s` is at (result[s, m // 8] >> (m % 8)) & 1 Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') >>> s = c.compile_sampler() >>> s.sample(shots=1) array([[ True, False, True, True]]) )DOC") .data()); c.def( "sample_bit_packed", [](CompiledMeasurementSampler &self, size_t shots) { return self.sample_to_numpy(shots, true); }, pybind11::arg("shots"), clean_doc_string(R"DOC( [DEPRECATED] Use sampler.sample(..., bit_packed=True) instead. @signature def sample_bit_packed(self, shots: int) -> np.ndarray: Samples a bit packed batch of measurement samples from the circuit. Args: shots: The number of times to sample every measurement in the circuit. Returns: A numpy array with `dtype=uint8` and `shape=(shots, (num_measurements + 7) // 8)`. The bit for measurement `m` in shot `s` is at `result[s, (m // 8)] & 2**(m % 8)`. Examples: >>> import stim >>> c = stim.Circuit(''' ... X 0 1 2 3 4 5 6 7 10 ... M 0 1 2 3 4 5 6 7 8 9 10 ... ''') >>> s = c.compile_sampler() >>> s.sample_bit_packed(shots=1) array([[255, 4]], dtype=uint8) )DOC") .data()); c.def( "sample_write", &CompiledMeasurementSampler::sample_write, pybind11::arg("shots"), pybind11::kw_only(), pybind11::arg("filepath"), pybind11::arg("format") = "01", clean_doc_string(R"DOC( @signature def sample_write(self, shots: int, *, filepath: Union[str, pathlib.Path], format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01') -> None: Samples measurements from the circuit and writes them to a file. Examples: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f"{d}/tmp.dat" ... c = stim.Circuit(''' ... X 0 2 3 ... M 0 1 2 3 ... ''') ... c.compile_sampler().sample_write(5, filepath=path, format="01") ... with open(path) as f: ... print(f.read(), end='') 1011 1011 1011 1011 1011 Args: shots: The number of times to sample every measurement in the circuit. filepath: The file to write the results to. format: The output format to write the results with. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". Returns: None. )DOC") .data()); c.def( "__repr__", &CompiledMeasurementSampler::repr, "Returns text that is a valid python expression evaluating to an equivalent " "`stim.CompiledMeasurementSampler`."); } ================================================ FILE: src/stim/py/compiled_measurement_sampler.pybind.h ================================================ // 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. #ifndef _STIM_PY_COMPILED_MEASUREMENT_SAMPLER_PYBIND_H #define _STIM_PY_COMPILED_MEASUREMENT_SAMPLER_PYBIND_H #include #include #include #include "stim/circuit/circuit.h" #include "stim/mem/simd_bits.h" namespace stim_pybind { struct CompiledMeasurementSampler { const stim::simd_bits ref_sample; const stim::Circuit circuit; const bool skip_reference_sample; std::mt19937_64 rng; CompiledMeasurementSampler() = delete; CompiledMeasurementSampler(const CompiledMeasurementSampler &) = delete; CompiledMeasurementSampler(CompiledMeasurementSampler &&) = default; CompiledMeasurementSampler( stim::simd_bits ref_sample, stim::Circuit circuit, bool skip_reference_sample, std::mt19937_64 &&rng); pybind11::object sample_to_numpy(size_t num_shots, bool bit_packed); void sample_write(size_t num_samples, std::string_view filepath, std::string_view format); std::string repr() const; }; pybind11::class_ pybind_compiled_measurement_sampler(pybind11::module &m); void pybind_compiled_measurement_sampler_methods(pybind11::module &m, pybind11::class_ &c); CompiledMeasurementSampler py_init_compiled_sampler( const stim::Circuit &circuit, bool skip_reference_sample, const pybind11::object &seed, const pybind11::object &reference_sample); } // namespace stim_pybind #endif ================================================ FILE: src/stim/py/compiled_measurement_sampler_pybind_test.py ================================================ # 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. import tempfile import numpy as np import pytest import stim def test_compiled_measurement_sampler_sample(): c = stim.Circuit(""" X 1 M 0 1 2 3 """) np.testing.assert_array_equal( c.compile_sampler().sample(5), np.array([ [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], ], dtype=np.bool_)) np.testing.assert_array_equal( c.compile_sampler().sample(5, bit_packed=True), np.array([ [0b00010], [0b00010], [0b00010], [0b00010], [0b00010], ], dtype=np.uint8)) np.testing.assert_array_equal( c.compile_sampler().sample_bit_packed(5), np.array([ [0b00010], [0b00010], [0b00010], [0b00010], [0b00010], ], dtype=np.uint8)) def test_measurements_vs_resets(): assert not np.any(stim.Circuit(""" RX 0 RY 1 RZ 2 H 0 H_YZ 1 M 0 1 2 """).compile_sampler().sample(shots=100)) assert not np.any(stim.Circuit(""" H 0 H_YZ 1 MRX 0 MRY 1 MRZ 2 H 0 H_YZ 1 M 0 1 2 """).compile_sampler().sample(shots=100)) assert not np.any(stim.Circuit(""" H 0 H_YZ 1 MRX 0 MRY 1 MRZ 2 """).compile_sampler().sample(shots=100)) def test_sample_write(): c = stim.Circuit(""" X 0 4 5 M 0 1 2 3 4 5 6 """) with tempfile.TemporaryDirectory() as d: path = f"{d}/tmp.dat" c.compile_sampler().sample_write(5, filepath=path, format='b8') with open(path, 'rb') as f: assert f.read() == b'\x31' * 5 c.compile_sampler().sample_write(5, filepath=path, format='01') with open(path, 'r') as f: assert f.readlines() == ['1000110\n'] * 5 def test_skip_reference_sample(): np.testing.assert_array_equal( stim.Circuit("X 0\nM 0").compile_sampler().sample(1), [[True]], ) np.testing.assert_array_equal( stim.Circuit("X 0\nM 0").compile_sampler(skip_reference_sample=False).sample(1), [[True]], ) np.testing.assert_array_equal( stim.Circuit("X 0\nM 0").compile_sampler(skip_reference_sample=True).sample(1), [[False]], ) np.testing.assert_array_equal( stim.Circuit("X_ERROR(1) 0\nM 0").compile_sampler(skip_reference_sample=False).sample(1), [[True]], ) np.testing.assert_array_equal( stim.Circuit("X_ERROR(1) 0\nM 0").compile_sampler(skip_reference_sample=True).sample(1), [[True]], ) def test_reference_sample_init(): np.testing.assert_array_equal( stim.Circuit("X 0\nM 0").compile_sampler(reference_sample=None).sample(1), [[True]], ) circuit = stim.Circuit("X 0\nM 0") ref_sample = np.array([False]) np.testing.assert_array_equal( circuit.compile_sampler(reference_sample=ref_sample).sample(1), [[False]], ) ref_sample = circuit.reference_sample() np.testing.assert_array_equal( circuit.compile_sampler(reference_sample=ref_sample).sample(1), [[True]], ) circuit = stim.Circuit("X_ERROR(1) 0\n M 0") ref_sample = np.array([False]) np.testing.assert_array_equal( circuit.compile_sampler(reference_sample=ref_sample).sample(1), [[True]], ) ref_sample = circuit.reference_sample() np.testing.assert_array_equal( circuit.compile_sampler(reference_sample=ref_sample).sample(1), [[True]], ) with pytest.raises(ValueError): circuit.compile_sampler(reference_sample=ref_sample, skip_reference_sample=True) circuit = stim.Circuit("H 0\n X 1\n CNOT 0 1\n H 0 1\n MPP X0*X1") ref_sample = circuit.reference_sample() np.testing.assert_array_equal( circuit.compile_sampler(reference_sample=ref_sample, seed=0).sample(10), circuit.compile_sampler(reference_sample=None, seed=0).sample(10), ) circuit = stim.Circuit("H 0\n X 1\n CNOT 0 1\n H 0 1 2\n MPP Y1*Y2 X1*X2 Z1*Z2") ref_sample = circuit.reference_sample() np.testing.assert_array_equal( circuit.compile_sampler(reference_sample=ref_sample, seed=0).sample(11), circuit.compile_sampler(seed=0).sample(11), ) def test_repr(): assert repr(stim.Circuit(""" X 0 M 0 """).compile_sampler()) == """stim.CompiledMeasurementSampler(stim.Circuit(''' X 0 M 0 '''))""" assert repr(stim.Circuit(""" X 0 M 0 """).compile_sampler(skip_reference_sample=True)) == """stim.CompiledMeasurementSampler(stim.Circuit(''' X 0 M 0 '''), skip_reference_sample=True)""" def test_circuit_sampler_actually_fills_array(): circuit = stim.Circuit(''' X_ERROR(1) 0 M 0 DETECTOR rec[-1] ''') sampler = circuit.compile_detector_sampler() measure_data = sampler.sample(shots=10000) assert np.all(measure_data) ================================================ FILE: src/stim/py/march.pybind.cc ================================================ #include "stim/py/march.pybind.h" #include #ifdef _WIN32 // Windows #include #define cpuid(info, x) __cpuidex(info, x, 0) #elif (defined(__arm64__) && defined(__APPLE__)) || defined(__aarch64__) || defined(_ARCH_PPC) // macOS ARM64 and IBM PowerPC (dummied out) void cpuid(int info[4], int infoType) { info[0] = 0; info[1] = 0; info[2] = 0; info[3] = 0; } #else // GCC Intrinsics #include void cpuid(int info[4], int infoType) { __cpuid_count(infoType, 0, info[0], info[1], info[2], info[3]); } #endif std::string detect_march() { // From: https://en.wikipedia.org/wiki/CPUID constexpr int EAX = 0; constexpr int EBX = 1; constexpr int EDX = 3; constexpr int INFO_HIGHEST_FUNCTION_PARAMETER = 0; constexpr int INFO_PROCESSOR_FEATURE_BITS = 1; constexpr int INFO_EXTENDED_FEATURES = 7; constexpr int avx2_bit_in_ebx = 1 << 5; constexpr int sse2_bit_in_edx = 1 << 26; int regs[4]; cpuid(regs, INFO_HIGHEST_FUNCTION_PARAMETER); auto max_info_param = regs[EAX]; if (max_info_param >= INFO_EXTENDED_FEATURES) { cpuid(regs, INFO_EXTENDED_FEATURES); if (regs[EBX] & avx2_bit_in_ebx) { return "avx2"; } } if (max_info_param >= INFO_PROCESSOR_FEATURE_BITS) { cpuid(regs, INFO_PROCESSOR_FEATURE_BITS); if (regs[EDX] & sse2_bit_in_edx) { return "sse2"; } } return "polyfill"; } PYBIND11_MODULE(_detect_machine_architecture, m) { m.doc() = R"pbdoc( Helper code for detecting AVX/SSE instruction support for Stim. )pbdoc"; m.def("_UNSTABLE_detect_march", &detect_march); } ================================================ FILE: src/stim/py/march.pybind.h ================================================ #ifndef _STIM_PY_MARCH_PYBIND_H #define _STIM_PY_MARCH_PYBIND_H #include /// Returns a string indicating the capabilities of the CPU (the M-achine ARCH-itecture). std::string detect_march(); #endif ================================================ FILE: src/stim/py/numpy.pybind.cc ================================================ // 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. #include "stim/py/numpy.pybind.h" using namespace stim; using namespace stim_pybind; static pybind11::object transposed_simd_bit_table_to_numpy_uint8( const simd_bit_table &table, size_t num_major_in, size_t num_minor_in, pybind11::object out_buffer) { size_t num_major_bytes_in = (num_major_in + 7) / 8; if (out_buffer.is_none()) { auto numpy = pybind11::module::import("numpy"); out_buffer = numpy.attr("empty")(pybind11::make_tuple(num_minor_in, num_major_bytes_in), numpy.attr("uint8")); } if (!pybind11::isinstance>(out_buffer)) { throw std::invalid_argument("Output buffer wasn't a numpy.ndarray[np.uint8]."); } auto buf = pybind11::cast>(out_buffer); if (buf.ndim() != 2) { throw std::invalid_argument("Output buffer wasn't two dimensional."); } if ((size_t)buf.shape(0) != num_minor_in || (size_t)buf.shape(1) != num_major_bytes_in) { std::stringstream ss; ss << "Expected output buffer to have shape=(" << num_minor_in << ", " << num_major_bytes_in << ")"; ss << " but its shape is (" << buf.shape(0) << ", " << buf.shape(1) << ")."; throw std::invalid_argument(ss.str()); } if (num_major_in && num_minor_in) { auto stride = buf.strides(1); for (size_t minor_in = 0; minor_in < num_minor_in; minor_in++) { auto ptr = buf.mutable_data(minor_in, 0); for (size_t major_in = 0; major_in < num_major_in; major_in += 8) { uint8_t v = 0; for (size_t b = 0; b < 8 && major_in + b < num_major_in; b++) { bool bit = table[major_in + b][minor_in]; v |= bit << b; } *ptr = v; ptr += stride; } } } return out_buffer; } static pybind11::object transposed_simd_bit_table_to_numpy_bool8( const simd_bit_table &table, size_t num_major_in, size_t num_minor_in, pybind11::object out_buffer) { if (out_buffer.is_none()) { auto numpy = pybind11::module::import("numpy"); out_buffer = numpy.attr("empty")(pybind11::make_tuple(num_minor_in, num_major_in), numpy.attr("bool_")); } if (!pybind11::isinstance>(out_buffer)) { throw std::invalid_argument("Output buffer wasn't a numpy.ndarray[np.bool_]."); } auto buf = pybind11::cast>(out_buffer); if (buf.ndim() != 2) { throw std::invalid_argument("Output buffer wasn't two dimensional."); } if ((size_t)buf.shape(0) != num_minor_in || (size_t)buf.shape(1) != num_major_in) { std::stringstream ss; ss << "Expected output buffer to have shape=(" << num_minor_in << ", " << num_major_in << ")"; ss << " but its shape is (" << buf.shape(0) << ", " << buf.shape(1) << ")."; throw std::invalid_argument(ss.str()); } if (num_major_in && num_minor_in) { auto stride = buf.strides(0); for (size_t major = 0; major < num_major_in; major++) { auto row = table[major]; auto ptr = buf.mutable_data(0, major); for (size_t minor = 0; minor < num_minor_in; minor++) { *ptr = row[minor]; ptr += stride; } } } return out_buffer; } static pybind11::object simd_bit_table_to_numpy_uint8( const simd_bit_table &table, size_t num_major, size_t num_minor, pybind11::object out_buffer) { size_t num_minor_bytes = (num_minor + 7) / 8; if (out_buffer.is_none()) { auto numpy = pybind11::module::import("numpy"); out_buffer = numpy.attr("empty")(pybind11::make_tuple(num_major, num_minor_bytes), numpy.attr("uint8")); } if (!pybind11::isinstance>(out_buffer)) { throw std::invalid_argument("Output buffer wasn't a numpy.ndarray[np.uint8]."); } auto buf = pybind11::cast>(out_buffer); if (buf.ndim() != 2) { throw std::invalid_argument("Output buffer wasn't two dimensional."); } if ((size_t)buf.shape(0) != num_major || (size_t)buf.shape(1) != num_minor_bytes) { std::stringstream ss; ss << "Expected output buffer to have shape=(" << num_major << ", " << num_minor_bytes << ")"; ss << " but its shape is (" << buf.shape(0) << ", " << buf.shape(1) << ")."; throw std::invalid_argument(ss.str()); } uint8_t mask = 0b11111111; if (num_minor & 7) { mask = (1 << (num_minor & 7)) - 1; } if (num_major && num_minor) { auto stride = buf.strides(1); if (stride == 1) { for (size_t major = 0; major < num_major; major++) { auto row = table[major]; memcpy(buf.mutable_data(major, 0), row.u8, num_minor_bytes); *buf.mutable_data(major, num_minor_bytes - 1) &= mask; } } else { for (size_t major = 0; major < num_major; major++) { auto row = table[major]; auto ptr = buf.mutable_data(major, 0); for (size_t minor = 0; minor < num_minor_bytes; minor += 1) { *ptr = row.u8[minor]; ptr += stride; } *(ptr - stride) &= mask; } } } return out_buffer; } static pybind11::object simd_bit_table_to_numpy_bool8( const simd_bit_table &table, size_t num_major, size_t num_minor, pybind11::object out_buffer) { if (out_buffer.is_none()) { auto numpy = pybind11::module::import("numpy"); out_buffer = numpy.attr("empty")(pybind11::make_tuple(num_major, num_minor), numpy.attr("bool_")); } if (!pybind11::isinstance>(out_buffer)) { throw std::invalid_argument("Output buffer wasn't a numpy.ndarray[np.bool_]."); } auto buf = pybind11::cast>(out_buffer); if (buf.ndim() != 2) { throw std::invalid_argument("Output buffer wasn't two dimensional."); } if ((size_t)buf.shape(0) != num_major || (size_t)buf.shape(1) != num_minor) { std::stringstream ss; ss << "Expected output buffer to have shape=(" << num_major << ", " << num_minor << ")"; ss << " but its shape is (" << buf.shape(0) << ", " << buf.shape(1) << ")."; throw std::invalid_argument(ss.str()); } if (num_major && num_minor) { auto stride = buf.strides(1); for (size_t major = 0; major < num_major; major++) { auto row = table[major]; auto out_ptr = buf.mutable_data(major, 0); for (size_t minor = 0; minor < num_minor; minor++) { *out_ptr = row[minor]; out_ptr += stride; } } } return out_buffer; } pybind11::object stim_pybind::simd_bit_table_to_numpy( const simd_bit_table &table, size_t num_major, size_t num_minor, bool bit_pack_result, bool transposed, pybind11::object out_buffer) { if (transposed) { if (bit_pack_result) { return transposed_simd_bit_table_to_numpy_uint8(table, num_major, num_minor, out_buffer); } else { return transposed_simd_bit_table_to_numpy_bool8(table, num_major, num_minor, out_buffer); } } else { if (bit_pack_result) { return simd_bit_table_to_numpy_uint8(table, num_major, num_minor, out_buffer); } else { return simd_bit_table_to_numpy_bool8(table, num_major, num_minor, out_buffer); } } } void stim_pybind::memcpy_bits_from_numpy_to_simd_bit_table( size_t num_major, size_t num_minor, const pybind11::object &src, stim::simd_bit_table &dst) { if (pybind11::isinstance>(src)) { auto arr = pybind11::cast>(src); size_t num_minor_bytes = (num_minor + 7) / 8; auto u = arr.unchecked(); for (size_t major = 0; major < num_major; major++) { auto row = dst[major]; for (size_t minor_byte = 0; minor_byte < num_minor_bytes; minor_byte++) { uint8_t v = u(major, minor_byte); row.u8[minor_byte] = v; } // Clear overwrite. for (size_t k = num_minor; k < num_minor_bytes * 8; k++) { row[k] = false; } } } else if (pybind11::isinstance>(src)) { auto arr = pybind11::cast>(src); auto u = arr.unchecked(); for (size_t major = 0; major < num_major; major++) { auto row = dst[major]; for (size_t minor = 0; minor < num_minor; minor++) { row[minor] = u(major, minor); } } } else { throw std::invalid_argument("Expected a 2-dimensional numpy array with dtype=np.uint8 or dtype=np.bool_"); } } simd_bit_table bit_packed_numpy_uint8_array_to_transposed_simd_table( const pybind11::array_t &data_u8, size_t expected_bits_per_shot, size_t *num_shots_out) { if (data_u8.ndim() != 2) { throw std::invalid_argument("data must be a 2-dimensional numpy array with dtype=np.uint8 or dtype=np.bool_"); } size_t num_shots = data_u8.shape(0); *num_shots_out = num_shots; size_t expected_bytes_per_shot = (expected_bits_per_shot + 7) / 8; size_t actual_bytes_per_shot = data_u8.shape(1); if (actual_bytes_per_shot != expected_bytes_per_shot) { std::stringstream ss; ss << "Expected " << expected_bits_per_shot << " bits per shot. "; ss << "Got bit packed data (dtype=np.uint8) but data.shape[1]="; ss << actual_bytes_per_shot << " != math.ceil(" << expected_bits_per_shot << " / 8)=" << expected_bytes_per_shot; throw std::invalid_argument(ss.str()); } simd_bit_table result(actual_bytes_per_shot * 8, num_shots); auto u = data_u8.unchecked(); for (size_t a = 0; a < num_shots; a++) { for (size_t b = 0; b < actual_bytes_per_shot; b++) { uint8_t v = u(a, b); for (size_t k = 0; k < 8; k++) { result[b * 8 + k][a] |= ((v >> k) & 1) != 0; } } } return result; } simd_bit_table bit_packed_numpy_bool8_array_to_transposed_simd_table( const pybind11::array_t &data_bool8, size_t expected_bits_per_shot, size_t *num_shots_out) { size_t num_shots = data_bool8.shape(0); *num_shots_out = num_shots; if (data_bool8.ndim() != 2) { throw std::invalid_argument("data must be a 2-dimensional numpy array with dtype=np.uint8 or dtype=np.bool_"); } size_t actual_bits_per_shot = data_bool8.shape(1); if (actual_bits_per_shot != expected_bits_per_shot) { std::stringstream ss; ss << "Expected " << expected_bits_per_shot << " bits per shot. "; ss << "Got unpacked boolean data (dtype=np.bool_) but data.shape[1]=" << actual_bits_per_shot; throw std::invalid_argument(ss.str()); } simd_bit_table result(actual_bits_per_shot, num_shots); auto u = data_bool8.unchecked(); for (size_t a = 0; a < num_shots; a++) { for (size_t b = 0; b < actual_bits_per_shot; b++) { result[b][a] |= u(a, b); } } return result; } simd_bit_table stim_pybind::numpy_array_to_transposed_simd_table( const pybind11::object &data, size_t bits_per_shot, size_t *num_shots_out) { if (pybind11::isinstance>(data)) { return bit_packed_numpy_uint8_array_to_transposed_simd_table( pybind11::cast>(data), bits_per_shot, num_shots_out); } else if (pybind11::isinstance>(data)) { return bit_packed_numpy_bool8_array_to_transposed_simd_table( pybind11::cast>(data), bits_per_shot, num_shots_out); } else { throw std::invalid_argument("data must be a 2-dimensional numpy array with dtype=np.uint8 or dtype=np.bool_"); } } pybind11::object bits_to_numpy_bool8(simd_bits_range_ref bits, size_t num_bits) { bool *buffer = new bool[num_bits]; for (size_t minor = 0; minor < num_bits; minor++) { buffer[minor] = bits[minor]; } pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast(f); }); return pybind11::array_t({(pybind11::ssize_t)num_bits}, {(pybind11::ssize_t)1}, buffer, free_when_done); } pybind11::object bits_to_numpy_uint8_packed(simd_bits_range_ref bits, size_t num_bits) { size_t num_bytes = (num_bits + 7) / 8; uint8_t *buffer = new uint8_t[num_bytes]; memcpy(buffer, bits.u8, num_bytes); pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast(f); }); return pybind11::array_t({(pybind11::ssize_t)num_bytes}, {(pybind11::ssize_t)1}, buffer, free_when_done); } pybind11::object stim_pybind::simd_bits_to_numpy( simd_bits_range_ref bits, size_t num_bits, bool bit_packed) { if (bit_packed) { return bits_to_numpy_uint8_packed(bits, num_bits); } return bits_to_numpy_bool8(bits, num_bits); } void stim_pybind::memcpy_bits_from_numpy_to_simd( size_t num_bits, const pybind11::object &src, simd_bits_range_ref dst) { if (pybind11::isinstance>(src)) { auto arr = pybind11::cast>(src); if (arr.ndim() == 1) { size_t num_bytes = (num_bits + 7) / 8; auto u = arr.unchecked(); for (size_t k = 0; k < num_bytes; k++) { uint8_t v = u(k); dst.u8[k] = v; } // Clear overwrite. for (size_t k = num_bits; k < num_bytes * 8; k++) { dst[k] = false; } return; } } else if (pybind11::isinstance>(src)) { auto arr = pybind11::cast>(src); if (arr.ndim() == 1) { auto u = arr.unchecked(); for (size_t k = 0; k < num_bits; k++) { dst[k] = u(k); } return; } } throw std::invalid_argument("Expected a 1-dimensional numpy array with dtype=np.uint8 or dtype=np.bool_"); } ================================================ FILE: src/stim/py/numpy.pybind.h ================================================ #ifndef _STIM_PY_NUMPY_PYBIND_H #define _STIM_PY_NUMPY_PYBIND_H #include "stim/mem/simd_bit_table.h" #include "stim/py/base.pybind.h" namespace stim_pybind { stim::simd_bit_table numpy_array_to_transposed_simd_table( const pybind11::object &data, size_t expected_bits_per_shot, size_t *num_shots_out); pybind11::object simd_bit_table_to_numpy( const stim::simd_bit_table &table, size_t num_major, size_t num_minor, bool bit_pack_result, bool transposed, pybind11::object out_buffer); void memcpy_bits_from_numpy_to_simd_bit_table( size_t num_major, size_t num_minor, const pybind11::object &src, stim::simd_bit_table &dst); pybind11::object simd_bits_to_numpy( stim::simd_bits_range_ref bits, size_t num_bits, bool bit_packed); void memcpy_bits_from_numpy_to_simd( size_t num_bits, const pybind11::object &src, stim::simd_bits_range_ref dst); } // namespace stim_pybind #endif ================================================ FILE: src/stim/py/stim.pybind.cc ================================================ // 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. #include #include #include "stim/circuit/circuit.pybind.h" #include "stim/circuit/circuit_instruction.pybind.h" #include "stim/circuit/circuit_repeat_block.pybind.h" #include "stim/circuit/gate_target.pybind.h" #include "stim/cmd/command_diagram.pybind.h" #include "stim/dem/dem_instruction.pybind.h" #include "stim/dem/detector_error_model.pybind.h" #include "stim/dem/detector_error_model_repeat_block.pybind.h" #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/gates/gates.pybind.h" #include "stim/io/read_write.pybind.h" #include "stim/main_namespaced.h" #include "stim/py/base.pybind.h" #include "stim/py/compiled_detector_sampler.pybind.h" #include "stim/py/compiled_measurement_sampler.pybind.h" #include "stim/py/march.pybind.h" #include "stim/simulators/dem_sampler.pybind.h" #include "stim/simulators/frame_simulator.pybind.h" #include "stim/simulators/matched_error.pybind.h" #include "stim/simulators/measurements_to_detection_events.pybind.h" #include "stim/simulators/tableau_simulator.pybind.h" #include "stim/stabilizers/clifford_string.pybind.h" #include "stim/stabilizers/flow.pybind.h" #include "stim/stabilizers/pauli_string.pybind.h" #include "stim/stabilizers/pauli_string_iter.pybind.h" #include "stim/stabilizers/tableau.h" #include "stim/stabilizers/tableau.pybind.h" #include "stim/stabilizers/tableau_iter.pybind.h" #define xstr_literal(s) str_literal(s) #define str_literal(s) #s using namespace stim; using namespace stim_pybind; GateTarget target_rec(int32_t lookback) { return GateTarget::rec(lookback); } GateTarget target_inv(const pybind11::object &qubit) { if (pybind11::isinstance(qubit)) { return !pybind11::cast(qubit); } return GateTarget::qubit(pybind11::cast(qubit), true); } GateTarget target_x(const pybind11::object &qubit, bool invert) { if (pybind11::isinstance(qubit)) { auto t = pybind11::cast(qubit); if (!t.is_qubit_target()) { throw std::invalid_argument("result of stim.target_x(" + t.str() + ") is not defined"); } return GateTarget::x(t.qubit_value(), t.is_inverted_result_target() ^ invert); } return GateTarget::x(pybind11::cast(qubit), invert); } GateTarget target_y(const pybind11::object &qubit, bool invert) { if (pybind11::isinstance(qubit)) { auto t = pybind11::cast(qubit); if (!t.is_qubit_target()) { throw std::invalid_argument("result of stim.target_y(" + t.str() + ") is not defined"); } return GateTarget::y(t.qubit_value(), t.is_inverted_result_target() ^ invert); } return GateTarget::y(pybind11::cast(qubit), invert); } GateTarget target_z(const pybind11::object &qubit, bool invert) { if (pybind11::isinstance(qubit)) { auto t = pybind11::cast(qubit); if (!t.is_qubit_target()) { throw std::invalid_argument("result of stim.target_z(" + t.str() + ") is not defined"); } return GateTarget::z(t.qubit_value(), t.is_inverted_result_target() ^ invert); } return GateTarget::z(pybind11::cast(qubit), invert); } std::vector target_combined_paulis(const pybind11::object &paulis, bool invert) { std::vector result; if (pybind11::isinstance(paulis)) { const FlexPauliString &ps = pybind11::cast(paulis); if (ps.imag) { std::stringstream ss; ss << "Imaginary sign: paulis="; ss << paulis; throw std::invalid_argument(ss.str()); } invert ^= ps.value.sign; for (size_t q = 0; q < ps.value.num_qubits; q++) { bool x = ps.value.xs[q]; bool z = ps.value.zs[q]; if (x | z) { result.push_back(GateTarget::pauli_xz(q, x, z)); result.push_back(GateTarget::combiner()); } } } else { for (const auto &h : paulis) { if (pybind11::isinstance(h)) { GateTarget g = pybind11::cast(h); if (g.pauli_type() != 'I') { if (g.is_inverted_result_target()) { invert ^= true; g.data ^= TARGET_INVERTED_BIT; } result.push_back(g); result.push_back(GateTarget::combiner()); continue; } } std::stringstream ss; ss << "Expected a pauli string or iterable of stim.GateTarget but got this when iterating: "; ss << h; throw std::invalid_argument(ss.str()); } } if (result.empty()) { std::stringstream ss; ss << "Identity pauli product: paulis="; ss << paulis; throw std::invalid_argument(ss.str()); } result.pop_back(); if (invert) { result[0].data ^= TARGET_INVERTED_BIT; } return result; } GateTarget target_pauli(uint32_t qubit_index, const pybind11::object &pauli, bool invert) { if ((qubit_index & TARGET_VALUE_MASK) != qubit_index) { std::stringstream ss; ss << "qubit_index=" << qubit_index << " is too large. Maximum qubit index is " << TARGET_VALUE_MASK << "."; throw std::invalid_argument(ss.str()); } if (pybind11::isinstance(pauli)) { std::string_view p = pybind11::cast(pauli); if (p == "X" || p == "x") { return GateTarget::x(qubit_index, invert); } else if (p == "Y" || p == "y") { return GateTarget::y(qubit_index, invert); } else if (p == "Z" || p == "z") { return GateTarget::z(qubit_index, invert); } else if (p == "I") { return GateTarget::qubit(qubit_index, invert); } } else { try { uint8_t p = pybind11::cast(pauli); if (p == 1) { return GateTarget::x(qubit_index, invert); } else if (p == 2) { return GateTarget::y(qubit_index, invert); } else if (p == 3) { return GateTarget::z(qubit_index, invert); } else if (p == 0) { return GateTarget::qubit(qubit_index, invert); } } catch (const pybind11::cast_error &ex) { // Wasn't an integer. } } std::stringstream ss; ss << "Expected pauli in [0, 1, 2, 3, *'IXYZxyz'] but got pauli=" << pauli; throw std::invalid_argument(ss.str()); } GateTarget target_sweep_bit(uint32_t qubit) { return GateTarget::sweep_bit(qubit); } int stim_main(const std::vector &args) { pybind11::scoped_ostream_redirect redirect_out(std::cout, pybind11::module_::import("sys").attr("stdout")); pybind11::scoped_ostream_redirect redirect_err(std::cerr, pybind11::module_::import("sys").attr("stderr")); std::vector argv; argv.push_back("stim.main"); for (const auto &arg : args) { argv.push_back(arg.c_str()); } return stim::main(argv.size(), argv.data()); } pybind11::object raw_format_data_solo(const FileFormatData &data) { pybind11::dict result; result["name"] = data.name; result["parse_example"] = data.help_python_parse; result["save_example"] = data.help_python_save; result["help"] = data.help; return std::move(result); } pybind11::dict raw_format_data() { pybind11::dict result; for (const auto &kv : format_name_to_enum_map()) { result[pybind11::str(kv.first)] = raw_format_data_solo(kv.second); } return result; } void top_level(pybind11::module &m) { m.def( "target_rec", &target_rec, pybind11::arg("lookback_index"), clean_doc_string(R"DOC( Returns a measurement record target with the given lookback. Measurement record targets are used to refer back to the measurement record; the list of measurements that have been performed so far. Measurement record targets always specify an index relative to the *end* of the measurement record. The latest measurement is `stim.target_rec(-1)`, the next most recent measurement is `stim.target_rec(-2)`, and so forth. Indexing is done this way in order to make it possible to write loops. Args: lookback_index: A negative integer indicating how far to look back, relative to the end of the measurement record. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("M", [5, 7, 11]) >>> circuit.append("CX", [stim.target_rec(-2), 3]) >>> circuit stim.Circuit(''' M 5 7 11 CX rec[-2] 3 ''') )DOC") .data()); m.def( "target_inv", &target_inv, pybind11::arg("qubit_index"), clean_doc_string(R"DOC( @signature def target_inv(qubit_index: Union[int, stim.GateTarget]) -> stim.GateTarget: Returns a target flagged as inverted. Inverted targets are used to indicate measurement results should be flipped. Args: qubit_index: The underlying qubit index of the inverted target. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("M", [2, stim.target_inv(3)]) >>> circuit stim.Circuit(''' M 2 !3 ''') For example, the '!1' in 'M 0 !1 2' is qubit 1 flagged as inverted, meaning the measurement result from qubit 1 should be inverted when reported. )DOC") .data()); m.def( "target_combiner", &GateTarget::combiner, clean_doc_string(R"DOC( Returns a target combiner that can be used to build Pauli products. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*Y3*Z5 ''') )DOC") .data()); m.def( "target_x", &target_x, pybind11::arg("qubit_index"), pybind11::arg("invert") = false, clean_doc_string(R"DOC( @signature def target_x(qubit_index: Union[int, stim.GateTarget], invert: bool = False) -> stim.GateTarget: Returns a Pauli X target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') )DOC") .data()); m.def( "target_y", &target_y, pybind11::arg("qubit_index"), pybind11::arg("invert") = false, clean_doc_string(R"DOC( @signature def target_y(qubit_index: Union[int, stim.GateTarget], invert: bool = False) -> stim.GateTarget: Returns a Pauli Y target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') )DOC") .data()); m.def( "target_z", &target_z, pybind11::arg("qubit_index"), pybind11::arg("invert") = false, clean_doc_string(R"DOC( @signature def target_z(qubit_index: Union[int, stim.GateTarget], invert: bool = False) -> stim.GateTarget: Returns a Pauli Z target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. invert: Defaults to False. If True, the target is inverted (indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_x(2), ... stim.target_combiner(), ... stim.target_y(3, invert=True), ... stim.target_combiner(), ... stim.target_z(5), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3*Z5 ''') )DOC") .data()); m.def( "target_pauli", &target_pauli, pybind11::arg("qubit_index"), pybind11::arg("pauli"), pybind11::arg("invert") = false, clean_doc_string(R"DOC( @signature def target_pauli(qubit_index: int, pauli: Union[str, int], invert: bool = False) -> stim.GateTarget: Returns a pauli target that can be passed into `stim.Circuit.append`. Args: qubit_index: The qubit that the Pauli applies to. pauli: The pauli gate to use. This can either be a string identifying the pauli by name ("x", "X", "y", "Y", "z", or "Z") or an integer following the convention (1=X, 2=Y, 3=Z). Setting this argument to "I" or to 0 will return a qubit target instead of a pauli target. invert: Defaults to False. If True, the target is inverted (like "!X10"), indicating that, for example, measurement results should be inverted). Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... stim.target_pauli(2, "X"), ... stim.target_combiner(), ... stim.target_pauli(3, "y", invert=True), ... stim.target_pauli(5, 3), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3 Z5 ''') >>> circuit.append("M", [ ... stim.target_pauli(7, "I"), ... ]) >>> circuit stim.Circuit(''' MPP X2*!Y3 Z5 M 7 ''') )DOC") .data()); m.def( "target_combined_paulis", &target_combined_paulis, pybind11::arg("paulis"), pybind11::arg("invert") = false, clean_doc_string(R"DOC( @signature def target_combined_paulis(paulis: Union[stim.PauliString, List[stim.GateTarget]], invert: bool = False) -> stim.GateTarget: Returns a list of targets encoding a pauli product for instructions like MPP. Args: paulis: The paulis to encode into the targets. This can be a `stim.PauliString` or a list of pauli targets from `stim.target_x`, `stim.target_pauli`, etc. invert: Defaults to False. If True, the product is inverted (like "!X2*Y3"). Note that this is in addition to any inversions specified by the `paulis` argument. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("MPP", [ ... *stim.target_combined_paulis(stim.PauliString("-XYZ")), ... *stim.target_combined_paulis([stim.target_x(2), stim.target_y(5)]), ... *stim.target_combined_paulis([stim.target_z(9)], invert=True), ... ]) >>> circuit stim.Circuit(''' MPP !X0*Y1*Z2 X2*Y5 !Z9 ''') )DOC") .data()); m.def( "target_sweep_bit", &target_sweep_bit, pybind11::arg("sweep_bit_index"), clean_doc_string(R"DOC( Returns a sweep bit target that can be passed into `stim.Circuit.append`. Args: sweep_bit_index: The index of the sweep bit to target. Examples: >>> import stim >>> circuit = stim.Circuit() >>> circuit.append("CX", [stim.target_sweep_bit(2), 5]) >>> circuit stim.Circuit(''' CX sweep[2] 5 ''') )DOC") .data()); m.def( "main", &stim_main, pybind11::kw_only(), pybind11::arg("command_line_args"), clean_doc_string(R"DOC( Runs the command line tool version of stim on the given arguments. Note that by default any input will be read from stdin, any output will print to stdout (as opposed to being intercepted). For most commands, you can use arguments like `--out` to write to a file instead of stdout and `--in` to read from a file instead of stdin. Returns: An exit code (0 means success, not zero means failure). Raises: A large variety of errors, depending on what you are doing and how it failed! Beware that many errors are caught by the main method itself and printed to stderr, with the only indication that something went wrong being the return code. Example: >>> import stim >>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... path = f'{d}/tmp.out' ... return_code = stim.main(command_line_args=[ ... "gen", ... "--code=repetition_code", ... "--task=memory", ... "--rounds=1000", ... "--distance=2", ... "--out", ... path, ... ]) ... assert return_code == 0 ... with open(path) as f: ... print(f.read(), end='') # Generated repetition_code circuit. # task: memory # rounds: 1000 # distance: 2 # before_round_data_depolarization: 0 # before_measure_flip_probability: 0 # after_reset_flip_probability: 0 # after_clifford_depolarization: 0 # layout: # L0 Z1 d2 # Legend: # d# = data qubit # L# = data qubit with logical observable crossing # Z# = measurement qubit R 0 1 2 TICK CX 0 1 TICK CX 2 1 TICK MR 1 DETECTOR(1, 0) rec[-1] REPEAT 999 { TICK CX 0 1 TICK CX 2 1 TICK MR 1 SHIFT_COORDS(0, 1) DETECTOR(1, 0) rec[-1] rec[-2] } M 0 2 DETECTOR(1, 1) rec[-1] rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] )DOC") .data()); m.def("_UNSTABLE_raw_format_data", &raw_format_data); } PYBIND11_MODULE(STIM_PYBIND11_MODULE_NAME, m) { m.attr("__version__") = xstr_literal(VERSION_INFO); m.doc() = R"pbdoc( Stim: A fast stabilizer circuit library. )pbdoc"; /// class registration happens before function/method /// registration. If a class references another before it is /// registered, method signatures can get messed up. For example, /// if DetectorErrorModel is defined after Circuit then /// Circuit.detector_error_model's return type is described as /// `stim::DetectorErrorModel` instead of `stim.DetectorErrorModel`. /// class definitions auto c_dem_sampler = pybind_dem_sampler(m); auto c_compiled_detector_sampler = pybind_compiled_detector_sampler(m); auto c_compiled_measurement_sampler = pybind_compiled_measurement_sampler(m); auto c_compiled_m2d_converter = pybind_compiled_measurements_to_detection_events_converter(m); auto c_clifford_string = pybind_clifford_string(m); auto c_pauli_string = pybind_pauli_string(m); auto c_pauli_string_iter = pybind_pauli_string_iter(m); auto c_tableau = pybind_tableau(m); auto c_tableau_iter = pybind_tableau_iter(m); auto c_circuit_gate_target = pybind_circuit_gate_target(m); auto c_gate_data = pybind_gate_data(m); auto c_circuit_instruction = pybind_circuit_instruction(m); auto c_circuit_repeat_block = pybind_circuit_repeat_block(m); auto c_circuit = pybind_circuit(m); auto c_detector_error_model_instruction = pybind_detector_error_model_instruction(m); auto c_detector_error_model_target = pybind_detector_error_model_target(m); auto c_detector_error_model_repeat_block = pybind_detector_error_model_repeat_block(m); auto c_detector_error_model = pybind_detector_error_model(m); auto c_tableau_simulator = pybind_tableau_simulator(m); auto c_frame_simulator = pybind_frame_simulator(m); auto c_circuit_error_location_stack_frame = pybind_circuit_error_location_stack_frame(m); auto c_gate_target_with_coords = pybind_gate_target_with_coords(m); auto c_dem_target_with_coords = pybind_dem_target_with_coords(m); auto c_flipped_measurement = pybind_flipped_measurement(m); auto c_circuit_targets_inside_instruction = pybind_circuit_targets_inside_instruction(m); auto c_circuit_error_location = pybind_circuit_error_location(m); auto c_circuit_error_location_methods = pybind_explained_error(m); auto c_flow = pybind_flow(m); auto c_diagram_helper = pybind_diagram(m); /// top level function definitions top_level(m); pybind_read_write(m); // method definitions pybind_circuit_instruction_methods(m, c_circuit_instruction); pybind_circuit_gate_target_methods(m, c_circuit_gate_target); pybind_gate_data_methods(m, c_gate_data); pybind_circuit_repeat_block_methods(m, c_circuit_repeat_block); pybind_circuit_methods(m, c_circuit); pybind_circuit_methods_extra(m, c_circuit); pybind_tableau_iter_methods(m, c_tableau_iter); pybind_dem_sampler_methods(m, c_dem_sampler); pybind_detector_error_model_instruction_methods(m, c_detector_error_model_instruction); pybind_detector_error_model_repeat_block_methods(m, c_detector_error_model_repeat_block); pybind_detector_error_model_target_methods(m, c_detector_error_model_target); pybind_detector_error_model_methods(m, c_detector_error_model); pybind_tableau_methods(m, c_tableau); pybind_pauli_string_methods(m, c_pauli_string); pybind_clifford_string_methods(m, c_clifford_string); pybind_pauli_string_iter_methods(m, c_pauli_string_iter); pybind_compiled_detector_sampler_methods(m, c_compiled_detector_sampler); pybind_compiled_measurement_sampler_methods(m, c_compiled_measurement_sampler); pybind_compiled_measurements_to_detection_events_converter_methods(m, c_compiled_m2d_converter); pybind_tableau_simulator_methods(m, c_tableau_simulator); pybind_frame_simulator_methods(m, c_frame_simulator); pybind_circuit_error_location_stack_frame_methods(m, c_circuit_error_location_stack_frame); pybind_gate_target_with_coords_methods(m, c_gate_target_with_coords); pybind_dem_target_with_coords_methods(m, c_dem_target_with_coords); pybind_flipped_measurement_methods(m, c_flipped_measurement); pybind_circuit_targets_inside_instruction_methods(m, c_circuit_targets_inside_instruction); pybind_circuit_error_location_methods(m, c_circuit_error_location); pybind_explained_error_methods(m, c_circuit_error_location_methods); pybind_flow_methods(m, c_flow); pybind_diagram_methods(m, c_diagram_helper); } ================================================ FILE: src/stim/py/stim_pybind_test.py ================================================ # 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. import pathlib import tempfile import doctest import numpy as np import pytest import types import stim import re def test_version(): assert re.match(r"^\d\.\d+", stim.__version__) def test_targets(): t = stim.target_x(5) assert isinstance(t, stim.GateTarget) assert t.is_x_target and t.value == 5 assert not t.is_y_target t = stim.target_y(6) assert isinstance(t, stim.GateTarget) assert t.is_y_target and t.value == 6 t = stim.target_z(5) assert isinstance(t, stim.GateTarget) assert t.is_z_target and t.value == 5 t = stim.target_inv(5) assert isinstance(t, stim.GateTarget) assert t.is_inverted_result_target and t.value == 5 t = stim.target_rec(-5) assert isinstance(t, stim.GateTarget) assert t.is_measurement_record_target and not t.is_inverted_result_target and t.value == -5 t = stim.target_sweep_bit(4) assert isinstance(t, stim.GateTarget) assert t.is_sweep_bit_target and not t.is_inverted_result_target and t.value == 4 t = stim.target_combiner() assert isinstance(t, stim.GateTarget) def test_gate_data(): data = stim.gate_data() assert len(data) == 81 assert data["CX"].name == "CX" assert data["CX"].aliases == ["CNOT", "CX", "ZCX"] assert data["X"].is_unitary assert "CNOT" not in data def test_format_data(): format_data = stim._UNSTABLE_raw_format_data() assert len(format_data) >= 6 # Check that example code has needed imports. for k, v in format_data.items(): exec(v["parse_example"], {}, {}) exec(v["save_example"], {}, {}) # Collect sample methods. ctx = {} for k, v in format_data.items(): exec(v["parse_example"], {}, ctx) exec(v["save_example"], {}, ctx) # Manually test example save/parse methods. data = [[0, 1, 1, 0], [0, 0, 0, 0]] saved = "0110\n0000\n" assert ctx["save_01"](data) == saved assert ctx["parse_01"](saved) == data saved = b"\x01\x00\x01\x04" assert ctx["save_r8"](data) == saved assert ctx["parse_r8"](saved, bits_per_shot=4) == data saved = b"\x06\x00" assert ctx["save_b8"](data) == saved assert ctx["parse_b8"](saved, bits_per_shot=4) == data saved = "1,2\n\n" assert ctx["save_hits"](data) == saved assert ctx["parse_hits"](saved, bits_per_shot=4) == data saved = "shot D1 L0\nshot\n" assert ctx["save_dets"](data, num_detectors=2, num_observables=2) == saved assert ctx["parse_dets"](saved, num_detectors=2, num_observables=2) == data big_data = [data[0]] + [data[1]] * 36 + [[1, 0, 1, 0]] * 4 + [data[1]] * (64 - 41) saved = (b"\x00\x00\x00\x00\xe0\x01\x00\x00" b"\x01\x00\x00\x00\x00\x00\x00\x00" b"\x01\x00\x00\x00\xe0\x01\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00") assert ctx["save_ptb64"](big_data) == saved assert ctx["parse_ptb64"](saved, bits_per_shot=4) == big_data # Check that python examples in help strings are correct. for k, v in format_data.items(): mod = types.ModuleType('stim_test_fake') mod.f = lambda: 5 mod.__test__ = {"f": mod.f} mod.f.__doc__ = v["help"] assert doctest.testmod(mod).failed == 0, k def test_main_write_to_file(): with tempfile.TemporaryDirectory() as d: p = pathlib.Path(d) / 'tmp' assert stim.main(command_line_args=[ "gen", "--code=repetition_code", "--task=memory", "--rounds=1000", "--distance=2", f"--out={p}" ]) == 0 with open(p) as f: assert "Generated repetition_code" in f.read() def test_main_help(capsys): assert stim.main(command_line_args=["help"]) == 0 captured = capsys.readouterr() assert captured.err == "" assert 'Available stim commands' in captured.out def test_main_redirects_stdout(capsys): assert stim.main(command_line_args=[ "gen", "--code=repetition_code", "--task=memory", "--rounds=1000", "--distance=2", ]) == 0 captured = capsys.readouterr() assert "Generated repetition_code" in captured.out assert captured.err == "" def test_main_redirects_stderr(capsys): assert stim.main(command_line_args=[ "gen", "--code=XXXXX", "--task=memory", "--rounds=1000", "--distance=2", ]) == 1 captured = capsys.readouterr() assert captured.out == "" assert "Unrecognized value 'XXXXX'" in captured.err def test_target_methods_accept_gate_targets(): assert stim.target_inv(stim.GateTarget(5)) == stim.target_inv(5) assert stim.target_inv(stim.target_inv(5)) == stim.GateTarget(5) assert stim.target_inv(stim.target_x(5)) == stim.target_x(5, invert=True) assert stim.target_inv(stim.target_y(5)) == stim.target_y(5, invert=True) assert stim.target_inv(stim.target_z(5)) == stim.target_z(5, invert=True) assert stim.target_x(stim.GateTarget(5)) == stim.target_x(5) assert stim.target_x(stim.target_inv(stim.GateTarget(5))) == stim.target_x(5, invert=True) assert stim.target_x(stim.GateTarget(5), invert=True) == stim.target_x(5, invert=True) assert stim.target_x(stim.target_inv(stim.GateTarget(5)), invert=True) == stim.target_x(5) assert stim.target_y(stim.GateTarget(5)) == stim.target_y(5) assert stim.target_y(stim.target_inv(stim.GateTarget(5))) == stim.target_y(5, invert=True) assert stim.target_y(stim.GateTarget(5), invert=True) == stim.target_y(5, invert=True) assert stim.target_y(stim.target_inv(stim.GateTarget(5)), invert=True) == stim.target_y(5) assert stim.target_z(stim.GateTarget(5)) == stim.target_z(5) assert stim.target_z(stim.target_inv(stim.GateTarget(5))) == stim.target_z(5, invert=True) assert stim.target_z(stim.GateTarget(5), invert=True) == stim.target_z(5, invert=True) assert stim.target_z(stim.target_inv(stim.GateTarget(5)), invert=True) == stim.target_z(5) with pytest.raises(ValueError): stim.target_inv(stim.target_sweep_bit(4)) with pytest.raises(ValueError): stim.target_inv(stim.target_rec(-4)) with pytest.raises(ValueError): stim.target_x(stim.target_sweep_bit(4)) with pytest.raises(ValueError): stim.target_y(stim.target_sweep_bit(4)) with pytest.raises(ValueError): stim.target_z(stim.target_sweep_bit(4)) def test_target_pauli(): assert stim.target_pauli(2, "I") == stim.GateTarget(2) assert stim.target_pauli(2, "X") == stim.target_x(2) assert stim.target_pauli(2, "Y") == stim.target_y(2) assert stim.target_pauli(2, "Z") == stim.target_z(2) assert stim.target_pauli(5, "x") == stim.target_x(5) assert stim.target_pauli(2, "y") == stim.target_y(2) assert stim.target_pauli(2, "z") == stim.target_z(2) assert stim.target_pauli(2, 0) == stim.GateTarget(2) assert stim.target_pauli(2, 1) == stim.target_x(2) assert stim.target_pauli(2, 2) == stim.target_y(2) assert stim.target_pauli(2, 3) == stim.target_z(2) assert stim.target_pauli(2, 3, True) == stim.target_z(2, True) assert stim.target_pauli(qubit_index=2, pauli=3, invert=True) == stim.target_z(2, True) assert stim.target_pauli(5, np.array([2], dtype=np.uint8)[0]) == stim.target_y(5) assert stim.target_pauli(5, np.array([2], dtype=np.uint32)[0]) == stim.target_y(5) assert stim.target_pauli(5, np.array([2], dtype=np.int16)[0]) == stim.target_y(5) with pytest.raises(ValueError, match="too large"): stim.target_pauli(2**31, 'X') with pytest.raises(ValueError, match="Expected pauli"): stim.target_pauli(5, 'F') with pytest.raises(ValueError, match="Expected pauli"): stim.target_pauli(5, np.array([257], dtype=np.uint32)[0]) def test_target_combined_paulis(): assert stim.target_combined_paulis(stim.PauliString("XYZ")) == [ stim.target_x(0), stim.target_combiner(), stim.target_y(1), stim.target_combiner(), stim.target_z(2), ] assert stim.target_combined_paulis(stim.PauliString("X"), True) == [ stim.target_x(0, True), ] assert stim.target_combined_paulis(stim.PauliString("-XYIZ")) == [ stim.target_x(0, invert=True), stim.target_combiner(), stim.target_y(1), stim.target_combiner(), stim.target_z(3), ] assert stim.target_combined_paulis(stim.PauliString("-XYIZ"), True) == [ stim.target_x(0), stim.target_combiner(), stim.target_y(1), stim.target_combiner(), stim.target_z(3), ] assert stim.target_combined_paulis([stim.target_x(5), stim.target_z(9)]) == [ stim.target_x(5), stim.target_combiner(), stim.target_z(9), ] assert stim.target_combined_paulis([stim.target_x(5, True), stim.target_z(9)]) == [ stim.target_x(5, True), stim.target_combiner(), stim.target_z(9), ] assert stim.target_combined_paulis([stim.target_x(5), stim.target_z(9, True)]) == [ stim.target_x(5, True), stim.target_combiner(), stim.target_z(9), ] assert stim.target_combined_paulis([stim.target_x(5), stim.target_z(9)], True) == [ stim.target_x(5, True), stim.target_combiner(), stim.target_z(9), ] assert stim.target_combined_paulis([stim.target_y(4)]) == [ stim.target_y(4), ] with pytest.raises(ValueError, match="Expected a pauli string"): stim.target_combined_paulis([stim.target_rec(-2)]) with pytest.raises(ValueError, match="Expected a pauli string"): stim.target_combined_paulis([object()]) with pytest.raises(ValueError, match="Identity pauli product"): stim.target_combined_paulis([]) with pytest.raises(ValueError, match="Identity pauli product"): stim.target_combined_paulis(stim.PauliString(0)) with pytest.raises(ValueError, match="Identity pauli product"): stim.target_combined_paulis(stim.PauliString(10)) with pytest.raises(ValueError, match="Imaginary"): stim.target_combined_paulis(stim.PauliString("iX")) ================================================ FILE: src/stim/search/graphlike/algo.cc ================================================ // 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. #include "stim/search/graphlike/algo.h" #include #include #include #include #include "stim/search/graphlike/edge.h" #include "stim/search/graphlike/graph.h" #include "stim/search/graphlike/node.h" #include "stim/search/graphlike/search_state.h" using namespace stim; using namespace stim::impl_search_graphlike; DetectorErrorModel backtrack_path(const std::unordered_map &back_map, const SearchState &final_state) { DetectorErrorModel out; auto cur_state = final_state; while (true) { const auto &prev_state = back_map.at(cur_state); cur_state.append_transition_as_error_instruction_to(prev_state, out); if (prev_state.is_undetected()) { break; } cur_state = prev_state; } std::sort(out.instructions.begin(), out.instructions.end()); return out; } DetectorErrorModel stim::shortest_graphlike_undetectable_logical_error( const DetectorErrorModel &model, bool ignore_ungraphlike_errors) { Graph graph = Graph::from_dem(model, ignore_ungraphlike_errors); SearchState empty_search_state(graph.num_observables); if (graph.distance_1_error_mask.not_zero()) { DetectorErrorModel out; SearchState s1(NO_NODE_INDEX, NO_NODE_INDEX, graph.distance_1_error_mask); s1.append_transition_as_error_instruction_to(empty_search_state, out); return out; } std::queue queue; std::unordered_map back_map; // Mark the vacuous dead-end state as already seen. back_map.emplace(empty_search_state, empty_search_state); // Search starts from any and all edges crossing an observable. for (size_t node1 = 0; node1 < graph.nodes.size(); node1++) { for (const auto &e : graph.nodes[node1].edges) { uint64_t node2 = e.opposite_node_index; if (node1 < node2 && e.crossing_observable_mask.not_zero()) { SearchState start{node1, node2, e.crossing_observable_mask}; queue.push(start); back_map.emplace(start, empty_search_state); } } } // Breadth first search for a symptomless state that has a frame change. for (; !queue.empty(); queue.pop()) { SearchState cur = queue.front(); assert(cur.det_active != NO_NODE_INDEX); for (const auto &e : graph.nodes[cur.det_active].edges) { SearchState next(e.opposite_node_index, cur.det_held, e.crossing_observable_mask ^ cur.obs_mask); if (!back_map.emplace(next, cur).second) { continue; } if (next.is_undetected()) { assert(next.obs_mask.not_zero()); // Otherwise, it would have already been in back_map. return backtrack_path(back_map, next); } else { if (next.det_active == NO_NODE_INDEX) { // Just resolved one out of two excitations. Move on to the second excitation. std::swap(next.det_active, next.det_held); } queue.push(next); } } } std::stringstream err_msg; err_msg << "Failed to find any graphlike logical errors."; if (graph.num_observables == 0) { err_msg << "\n WARNING: NO OBSERVABLES. The circuit or detector error model didn't define any observables, " "making it vacuously impossible to find a logical error."; } if (graph.nodes.size() == 0) { err_msg << "\n WARNING: NO DETECTORS. The circuit or detector error model didn't define any detectors."; } if (model.count_errors() == 0) { err_msg << "\n WARNING: NO ERRORS. The circuit or detector error model didn't include any errors, making it " "vacuously impossible to find a logical error."; } else { bool edges = 0; for (const auto &n : graph.nodes) { edges |= n.edges.size() > 0; } if (!edges) { err_msg << "\n WARNING: NO GRAPHLIKE ERRORS. Although the circuit or detector error model does define " "some errors, none of them are graphlike (i.e. have at most two detection events), making it " "vacuously impossible to find a graphlike logical error."; } } throw std::invalid_argument(err_msg.str()); } ================================================ FILE: src/stim/search/graphlike/algo.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_GRAPHLIKE_ALGO_H #define _STIM_SEARCH_GRAPHLIKE_ALGO_H #include "stim/dem/detector_error_model.h" namespace stim { /// Finds a list of graphlike errors from the model that form an undetectable logical error. /// /// An error is graphlike if it has at most 2 symptoms (detector indices). /// /// The components of composite errors like D0 ^ D1 D2 are included in the set of graphlike errors being considered when /// trying to find an undetectable error (even if that specific graphlike error component doesn't occur on its own /// outside a composite error anywhere in the model). /// /// Args: /// model: The detector error model to search for undetectable errors. /// ignore_ungraphlike_errors: Determines whether or not error components with more than 2 symptoms should raise an /// exception, or just be ignored as if they weren't there. /// /// Returns: /// A detector error model containing only the error mechanisms that cause the undetectable logical error. /// Note that the error mechanisms will have their probabilities set to 1 (indicating they are necessary). DetectorErrorModel shortest_graphlike_undetectable_logical_error( const DetectorErrorModel &model, bool ignore_ungraphlike_errors); } // namespace stim #endif ================================================ FILE: src/stim/search/graphlike/algo.perf.cc ================================================ // 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. #include "stim/gen/gen_surface_code.h" #include "stim/perf.perf.h" #include "stim/search/search.h" #include "stim/simulators/error_analyzer.h" using namespace stim; BENCHMARK(find_graphlike_logical_error_surface_code_d25) { CircuitGenParameters params(25, 25, "rotated_memory_x"); params.after_clifford_depolarization = 0.001; params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.before_round_data_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; auto model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); size_t total = 0; benchmark_go([&]() { total += stim::shortest_graphlike_undetectable_logical_error(model, false).instructions.size(); }).goal_millis(35); if (total % 25 != 0 || total == 0) { std::cout << "bad"; } } BENCHMARK(find_graphlike_logical_error_surface_code_d11_r1000) { CircuitGenParameters params(1000, 11, "rotated_memory_x"); params.after_clifford_depolarization = 0.001; params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.before_round_data_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; auto model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); size_t total = 0; benchmark_go([&]() { total += stim::shortest_graphlike_undetectable_logical_error(model, false).instructions.size(); }).goal_millis(100); if (total % 11 != 0 || total == 0) { std::cout << "bad"; } } ================================================ FILE: src/stim/search/graphlike/algo.test.cc ================================================ // 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. #include "stim/search/graphlike/algo.h" #include "gtest/gtest.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/search/graphlike/edge.h" #include "stim/simulators/error_analyzer.h" using namespace stim; using namespace stim::impl_search_graphlike; TEST(shortest_graphlike_undetectable_logical_error, no_error) { // No error. ASSERT_THROW( { stim::shortest_graphlike_undetectable_logical_error(DetectorErrorModel(), false); }, std::invalid_argument); // No undetectable error. ASSERT_THROW( { stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 L0 )MODEL"), false); }, std::invalid_argument); // No logical flips. ASSERT_THROW( { stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 error(0.1) D0 D1 error(0.1) D1 )MODEL"), false); }, std::invalid_argument); } TEST(shortest_graphlike_undetectable_logical_error, distance_1) { ASSERT_EQ( stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) L0 )MODEL"), false), DetectorErrorModel(R"MODEL( error(1) L0 )MODEL")); } TEST(shortest_graphlike_undetectable_logical_error, distance_2) { ASSERT_EQ( stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 error(0.1) D0 L0 )MODEL"), false), DetectorErrorModel(R"MODEL( error(1) D0 error(1) D0 L0 )MODEL")); ASSERT_EQ( stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 L0 error(0.1) D0 L1 )MODEL"), false), DetectorErrorModel(R"MODEL( error(1) D0 L0 error(1) D0 L1 )MODEL")); ASSERT_EQ( stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 D1 L0 error(0.1) D0 D1 L1 )MODEL"), false), DetectorErrorModel(R"MODEL( error(1) D0 D1 L0 error(1) D0 D1 L1 )MODEL")); ASSERT_EQ( stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 D1 L1 error(0.1) D0 D1 L0 )MODEL"), false), DetectorErrorModel(R"MODEL( error(1) D0 D1 L0 error(1) D0 D1 L1 )MODEL")); } TEST(shortest_graphlike_undetectable_logical_error, distance_3) { ASSERT_EQ( stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 error(0.1) D0 D1 L0 error(0.1) D1 )MODEL"), false), DetectorErrorModel(R"MODEL( error(1) D0 error(1) D0 D1 L0 error(1) D1 )MODEL")); ASSERT_EQ( stim::shortest_graphlike_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D1 error(0.1) D1 D0 L0 error(0.1) D0 )MODEL"), false), DetectorErrorModel(R"MODEL( error(1) D0 error(1) D0 D1 L0 error(1) D1 )MODEL")); } TEST(shortest_graphlike_undetectable_logical_error, surface_code) { CircuitGenParameters params(5, 5, "rotated_memory_x"); params.after_clifford_depolarization = 0.001; params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.before_round_data_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; auto graphlike_model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); auto ungraphlike_model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, false, true, false, 0.0, false, true); ASSERT_EQ(stim::shortest_graphlike_undetectable_logical_error(graphlike_model, false).instructions.size(), 5); ASSERT_EQ(stim::shortest_graphlike_undetectable_logical_error(graphlike_model, true).instructions.size(), 5); ASSERT_EQ(stim::shortest_graphlike_undetectable_logical_error(ungraphlike_model, true).instructions.size(), 5); // Throw due to ungraphlike errors. ASSERT_THROW( { stim::shortest_graphlike_undetectable_logical_error(ungraphlike_model, false); }, std::invalid_argument); } TEST(shortest_graphlike_undetectable_logical_error, repetition_code) { CircuitGenParameters params(10, 7, "memory"); params.before_round_data_depolarization = 0.01; auto circuit = generate_rep_code_circuit(params).circuit; auto graphlike_model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); ASSERT_EQ(stim::shortest_graphlike_undetectable_logical_error(graphlike_model, false).instructions.size(), 7); } TEST(shortest_graphlike_undetectable_logical_error, many_observables) { Circuit circuit(R"CIRCUIT( MPP Z0*Z1 Z1*Z2 Z2*Z3 Z3*Z4 X_ERROR(0.1) 0 1 2 3 4 MPP Z0*Z1 Z1*Z2 Z2*Z3 Z3*Z4 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] M 4 OBSERVABLE_INCLUDE(1200) rec[-1] )CIRCUIT"); auto graphlike_model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); auto err = stim::shortest_graphlike_undetectable_logical_error(graphlike_model, false); ASSERT_EQ(err.instructions.size(), 5); } ================================================ FILE: src/stim/search/graphlike/edge.cc ================================================ // 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. #include "stim/search/graphlike/edge.h" #include #include #include "stim/search/graphlike/node.h" using namespace stim; using namespace stim::impl_search_graphlike; std::string Edge::str() const { std::stringstream result; result << *this; return result.str(); } bool Edge::operator==(const Edge &other) const { return opposite_node_index == other.opposite_node_index && crossing_observable_mask == other.crossing_observable_mask; } bool Edge::operator!=(const Edge &other) const { return !(*this == other); } std::ostream &stim::impl_search_graphlike::operator<<(std::ostream &out, const Edge &v) { if (v.opposite_node_index == NO_NODE_INDEX) { out << "[boundary]"; } else { out << "D" << v.opposite_node_index; } auto m = v.crossing_observable_mask; for (size_t k = 0; k < m.num_bits_padded(); k++) { if (m[k]) { out << " L" << k; } } return out; } ================================================ FILE: src/stim/search/graphlike/edge.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_GRAPHLIKE_EDGE_H #define _STIM_SEARCH_GRAPHLIKE_EDGE_H #include #include #include #include "stim/mem/simd_bits.h" namespace stim { namespace impl_search_graphlike { /// A graphlike edge from a detector error model. struct Edge { uint64_t opposite_node_index; simd_bits<64> crossing_observable_mask; std::string str() const; bool operator==(const Edge &other) const; bool operator!=(const Edge &other) const; }; std::ostream &operator<<(std::ostream &out, const Edge &v); } // namespace impl_search_graphlike } // namespace stim #endif ================================================ FILE: src/stim/search/graphlike/edge.test.cc ================================================ // 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. #include "stim/search/graphlike/edge.h" #include "gtest/gtest.h" #include "stim/search/graphlike/node.h" using namespace stim; using namespace stim::impl_search_graphlike; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(search_graphlike, Edge) { Edge e1{NO_NODE_INDEX, obs_mask(0)}; Edge e2{1, obs_mask(0)}; Edge e3{NO_NODE_INDEX, obs_mask(1)}; Edge e4{NO_NODE_INDEX, obs_mask(5)}; ASSERT_EQ(e1.str(), "[boundary]"); ASSERT_EQ(e2.str(), "D1"); ASSERT_EQ(e3.str(), "[boundary] L0"); ASSERT_EQ(e4.str(), "[boundary] L0 L2"); ASSERT_TRUE(e1 == e1); ASSERT_TRUE(!(e1 == e2)); ASSERT_FALSE(e1 == e2); ASSERT_FALSE(!(e1 == e1)); ASSERT_EQ(e1, (Edge{NO_NODE_INDEX, obs_mask(0)})); ASSERT_EQ(e2, e2); ASSERT_EQ(e3, e3); ASSERT_NE(e1, e3); } ================================================ FILE: src/stim/search/graphlike/graph.cc ================================================ // 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. #include "stim/search/graphlike/graph.h" #include #include #include using namespace stim; using namespace stim::impl_search_graphlike; std::string Graph::str() const { std::stringstream result; result << *this; return result.str(); } void Graph::add_outward_edge(size_t src, uint64_t dst, const simd_bits<64> &obs_mask) { assert(src < nodes.size()); auto &node = nodes[src]; // Don't add duplicate edges. // Note: the neighbor list is expected to be short, so we do a linear scan instead of e.g. a binary search. for (const auto &e : node.edges) { if (e.opposite_node_index == dst && e.crossing_observable_mask == obs_mask) { return; } } node.edges.push_back({dst, obs_mask}); } void Graph::add_edges_from_targets_with_no_separators( SpanRef targets, bool ignore_ungraphlike_errors) { FixedCapVector detectors; simd_bits<64> obs_mask(num_observables); // Collect detectors and observables. for (const auto &t : targets) { if (t.is_relative_detector_id()) { if (detectors.size() == 2) { if (ignore_ungraphlike_errors) { return; } throw std::invalid_argument( "The detector error model contained a non-graphlike error mechanism.\n" "You can ignore such errors using `ignore_ungraphlike_errors`.\n" "You can use `decompose_errors` when converting a circuit into a model " "to ensure no such errors are present.\n"); } detectors.push_back(t.raw_id()); } else if (t.is_observable_id()) { obs_mask[t.raw_id()] ^= true; } } // Add edges between detector nodes. if (detectors.size() == 1) { add_outward_edge(detectors[0], NO_NODE_INDEX, obs_mask); } else if (detectors.size() == 2) { add_outward_edge(detectors[0], detectors[1], obs_mask); add_outward_edge(detectors[1], detectors[0], obs_mask); } else if (detectors.empty() && !distance_1_error_mask.not_zero() && obs_mask.not_zero()) { distance_1_error_mask = obs_mask; } } void Graph::add_edges_from_separable_targets(SpanRef targets, bool ignore_ungraphlike_errors) { const DemTarget *prev = targets.begin(); const DemTarget *cur = targets.begin(); while (true) { if (cur == targets.end() || cur->is_separator()) { if (ignore_ungraphlike_errors && cur != targets.end()) { return; } add_edges_from_targets_with_no_separators({prev, cur}, ignore_ungraphlike_errors); prev = cur + 1; } if (cur == targets.end()) { break; } cur++; } } Graph Graph::from_dem(const DetectorErrorModel &model, bool ignore_ungraphlike_errors) { Graph result(model.count_detectors(), model.count_observables()); model.iter_flatten_error_instructions([&](const DemInstruction &e) { if (e.arg_data[0] != 0) { result.add_edges_from_separable_targets(e.target_data, ignore_ungraphlike_errors); } }); return result; } bool Graph::operator==(const Graph &other) const { return nodes == other.nodes && num_observables == other.num_observables && distance_1_error_mask == other.distance_1_error_mask; } bool Graph::operator!=(const Graph &other) const { return !(*this == other); } Graph::Graph(size_t node_count, size_t num_observables) : nodes(node_count), num_observables(num_observables), distance_1_error_mask(num_observables) { } Graph::Graph(std::vector nodes, size_t num_observables, simd_bits<64> distance_1_error_mask) : nodes(std::move(nodes)), num_observables(num_observables), distance_1_error_mask(std::move(distance_1_error_mask)) { } std::ostream &stim::impl_search_graphlike::operator<<(std::ostream &out, const Graph &v) { for (size_t k = 0; k < v.nodes.size(); k++) { out << k << ":\n" << v.nodes[k]; } return out; } ================================================ FILE: src/stim/search/graphlike/graph.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_GRAPHLIKE_GRAPH_H #define _STIM_SEARCH_GRAPHLIKE_GRAPH_H #include "stim/dem/detector_error_model.h" #include "stim/search/graphlike/node.h" namespace stim { namespace impl_search_graphlike { struct Graph { std::vector nodes; size_t num_observables; simd_bits<64> distance_1_error_mask; explicit Graph(size_t node_count, size_t num_observables); Graph(std::vector nodes, size_t num_observables, simd_bits<64> distance_1_error_mask); void add_outward_edge(size_t src, uint64_t dst, const simd_bits<64> &obs_mask); void add_edges_from_targets_with_no_separators(SpanRef targets, bool ignore_ungraphlike_errors); void add_edges_from_separable_targets(SpanRef targets, bool ignore_ungraphlike_errors); static Graph from_dem(const DetectorErrorModel &model, bool ignore_ungraphlike_errors); bool operator==(const Graph &other) const; bool operator!=(const Graph &other) const; std::string str() const; }; std::ostream &operator<<(std::ostream &out, const Graph &v); } // namespace impl_search_graphlike } // namespace stim #endif ================================================ FILE: src/stim/search/graphlike/graph.test.cc ================================================ // 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. #include "stim/search/graphlike/graph.h" #include "gtest/gtest.h" using namespace stim; using namespace stim::impl_search_graphlike; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(search_graphlike, DemAdjGraph_equality) { ASSERT_TRUE(Graph(1, 5) == Graph(1, 5)); ASSERT_TRUE(Graph(1, 5) != Graph(2, 5)); ASSERT_TRUE(Graph(1, 5) != Graph(1, 4)); ASSERT_FALSE(Graph(1, 5) == Graph(2, 5)); ASSERT_FALSE(Graph(1, 5) != Graph(1, 5)); Graph a(1, 5); Graph b(1, 5); ASSERT_EQ(a, b); b.distance_1_error_mask = obs_mask(1); ASSERT_NE(a, b); } TEST(search_graphlike, DemAdjGraph_add_outward_edge) { Graph g(3, 64); g.add_outward_edge(1, 2, obs_mask(3)); ASSERT_EQ( g, (Graph{ std::vector{ Node{}, Node{{Edge{2, obs_mask(3)}}}, Node{}, }, 64, obs_mask(0)})); g.add_outward_edge(1, 2, obs_mask(3)); ASSERT_EQ( g, (Graph{ std::vector{ Node{}, Node{{Edge{2, obs_mask(3)}}}, Node{}, }, 64, obs_mask(0)})); g.add_outward_edge(1, 2, obs_mask(4)); ASSERT_EQ( g, (Graph{ std::vector{ Node{}, Node{{Edge{2, obs_mask(3)}, Edge{2, obs_mask(4)}}}, Node{}, }, 64, obs_mask(0)})); g.add_outward_edge(2, 1, obs_mask(3)); ASSERT_EQ( g, (Graph{ std::vector{ Node{}, Node{{Edge{2, obs_mask(3)}, Edge{2, obs_mask(4)}}}, Node{{Edge{1, obs_mask(3)}}}, }, 64, obs_mask(0)})); g.add_outward_edge(2, NO_NODE_INDEX, obs_mask(3)); ASSERT_EQ( g, (Graph{ std::vector{ Node{}, Node{{Edge{2, obs_mask(3)}, Edge{2, obs_mask(4)}}}, Node{{Edge{1, obs_mask(3)}, Edge{NO_NODE_INDEX, obs_mask(3)}}}, }, 64, obs_mask(0)})); } TEST(search_graphlike, DemAdjGraph_add_edges_from_targets_with_no_separators) { Graph g(4, 64); g.add_edges_from_targets_with_no_separators(std::vector{DemTarget::relative_detector_id(1)}, false); ASSERT_EQ( g, (Graph{ { Node{}, Node{{Edge{NO_NODE_INDEX, obs_mask(0)}}}, Node{}, Node{}, }, 64, obs_mask(0)})); g.add_edges_from_targets_with_no_separators( std::vector{ DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(3), DemTarget::observable_id(5), }, false); ASSERT_EQ( g, (Graph{ { Node{}, Node{{Edge{NO_NODE_INDEX, obs_mask(0)}, Edge{3, obs_mask(32)}}}, Node{}, Node{{Edge{1, obs_mask(32)}}}, }, 64, obs_mask(0)})); g.add_edges_from_targets_with_no_separators( std::vector{ DemTarget::observable_id(3), DemTarget::observable_id(7), }, false); ASSERT_EQ( g, (Graph{ { Node{}, Node{{Edge{NO_NODE_INDEX, obs_mask(0)}, Edge{3, obs_mask(32)}}}, Node{}, Node{{Edge{1, obs_mask(32)}}}, }, 64, obs_mask((1 << 3) + (1 << 7))})); Graph same_g = g; std::vector too_big{ DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), DemTarget::relative_detector_id(3), }; ASSERT_THROW({ same_g.add_edges_from_targets_with_no_separators(too_big, false); }, std::invalid_argument); ASSERT_EQ(g, same_g); same_g.add_edges_from_targets_with_no_separators(too_big, true); ASSERT_EQ(g, same_g); } TEST(search_graphlike, DemAdjGraph_str) { Graph g{ { Node{}, Node{{Edge{NO_NODE_INDEX, obs_mask(0)}, Edge{3, obs_mask(32)}}}, Node{}, Node{{Edge{1, obs_mask(32)}}}, }, 64, obs_mask(0)}; ASSERT_EQ( g.str(), "0:\n" "1:\n" " [boundary]\n" " D3 L5\n" "2:\n" "3:\n" " D1 L5\n"); } TEST(search_graphlike, DemAdjGraph_add_edges_from_separable_targets) { Graph g(4, 64); g.add_edges_from_separable_targets( std::vector{ DemTarget::relative_detector_id(1), DemTarget::separator(), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), DemTarget::observable_id(4), }, false); ASSERT_EQ( g, (Graph{ { Node{}, Node{{Edge{NO_NODE_INDEX, obs_mask(0)}, Edge{2, obs_mask(16)}}}, Node{{Edge{1, obs_mask(16)}}}, Node{}, }, 64, obs_mask(0)})); } TEST(search_graphlike, DemAdjGraph_from_dem) { ASSERT_EQ( Graph::from_dem( DetectorErrorModel(R"MODEL( error(0.1) D0 repeat 3 { error(0.1) D0 D1 shift_detectors 1 } error(0.1) D0 L7 error(0.1) D2 ^ D3 D4 L2 detector D5 )MODEL"), false), Graph( { Node{{Edge{NO_NODE_INDEX, obs_mask(0)}, Edge{1, obs_mask(0)}}}, Node{{Edge{0, obs_mask(0)}, Edge{2, obs_mask(0)}}}, Node{{Edge{1, obs_mask(0)}, Edge{3, obs_mask(0)}}}, Node{{Edge{2, obs_mask(0)}, Edge{NO_NODE_INDEX, obs_mask(128)}}}, Node{}, Node{{Edge{NO_NODE_INDEX, obs_mask(0)}}}, Node{{Edge{7, obs_mask(4)}}}, Node{{Edge{6, obs_mask(4)}}}, Node{}, }, 8, obs_mask(0))); } TEST(search_graphlike, add_edges_from_separable_targets_ignore) { Graph actual(10, 64); Graph expected(10, 64); DetectorErrorModel d(R"DEM( error(0.125) D0 ^ D4 D6 )DEM"); ASSERT_EQ(actual, expected); actual.add_edges_from_separable_targets(d.instructions[0].target_data, true); ASSERT_EQ(actual, expected); actual.add_edges_from_separable_targets(d.instructions[0].target_data, false); expected.add_outward_edge(4, 6, obs_mask(0)); expected.add_outward_edge(6, 4, obs_mask(0)); expected.add_outward_edge(0, NO_NODE_INDEX, obs_mask(0)); ASSERT_EQ(actual, expected); } ================================================ FILE: src/stim/search/graphlike/node.cc ================================================ // 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. #include "stim/search/graphlike/node.h" #include #include using namespace stim; using namespace stim::impl_search_graphlike; std::string Node::str() const { std::stringstream result; result << *this; return result.str(); } bool Node::operator==(const Node &other) const { return edges == other.edges; } bool Node::operator!=(const Node &other) const { return !(*this == other); } std::ostream &stim::impl_search_graphlike::operator<<(std::ostream &out, const Node &v) { for (const auto &e : v.edges) { out << " " << e << "\n"; } return out; } ================================================ FILE: src/stim/search/graphlike/node.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_GRAPHLIKE_NODE_H #define _STIM_SEARCH_GRAPHLIKE_NODE_H #include #include "stim/search/graphlike/edge.h" namespace stim { namespace impl_search_graphlike { constexpr uint64_t NO_NODE_INDEX = UINT64_MAX; /// Adjacency list representation of a detector from a detector error model. /// Only includes graphlike edges. struct Node { std::vector edges; std::string str() const; bool operator==(const Node &other) const; bool operator!=(const Node &other) const; }; std::ostream &operator<<(std::ostream &out, const Node &v); } // namespace impl_search_graphlike } // namespace stim #endif ================================================ FILE: src/stim/search/graphlike/node.test.cc ================================================ // 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. #include "stim/search/graphlike/node.h" #include "gtest/gtest.h" using namespace stim; using namespace stim::impl_search_graphlike; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(search_graphlike, Node) { Node n1{}; Node n2{{Edge{NO_NODE_INDEX, obs_mask(0)}}}; Node n3{{Edge{1, obs_mask(5)}, Edge{NO_NODE_INDEX, obs_mask(8)}}}; ASSERT_EQ(n1.str(), ""); ASSERT_EQ(n2.str(), " [boundary]\n"); ASSERT_EQ(n3.str(), " D1 L0 L2\n [boundary] L3\n"); ASSERT_TRUE(n1 == n1); ASSERT_TRUE(!(n1 == n2)); ASSERT_FALSE(n1 == n2); ASSERT_FALSE(!(n1 == n1)); ASSERT_EQ(n1, (Node{})); ASSERT_EQ(n2, n2); ASSERT_EQ(n3, n3); ASSERT_NE(n1, n3); } ================================================ FILE: src/stim/search/graphlike/search_state.cc ================================================ // 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. #include "stim/search/graphlike/search_state.h" #include #include #include "stim/search/graphlike/node.h" using namespace stim; using namespace stim::impl_search_graphlike; std::string SearchState::str() const { std::stringstream result; result << *this; return result.str(); } SearchState::SearchState(size_t num_observables) : det_active(NO_NODE_INDEX), det_held(NO_NODE_INDEX), obs_mask(num_observables) { } SearchState::SearchState(uint64_t det_active, uint64_t det_held, simd_bits<64> obs_mask) : det_active(det_active), det_held(det_held), obs_mask(std::move(obs_mask)) { } bool SearchState::is_undetected() const { return det_active == det_held; } SearchState SearchState::canonical() const { if (det_active < det_held) { return SearchState{det_active, det_held, obs_mask}; } else if (det_active > det_held) { return SearchState{det_held, det_active, obs_mask}; } else { return SearchState{NO_NODE_INDEX, NO_NODE_INDEX, obs_mask}; } } void SearchState::append_transition_as_error_instruction_to(const SearchState &other, DetectorErrorModel &out) const { // Extract detector indices while cancelling duplicates. std::array nodes{det_active, det_held, other.det_active, other.det_held, NO_NODE_INDEX}; std::sort(nodes.begin(), nodes.end()); for (size_t k = 0; k < 4; k++) { if (nodes[k] == nodes[k + 1]) { k++; } else { out.target_buf.append_tail(DemTarget::relative_detector_id(nodes[k])); } } // Extract logical observable indices. auto dif_mask = obs_mask ^ other.obs_mask; for (size_t k = 0; k < dif_mask.num_bits_padded(); k++) { if (dif_mask[k]) { out.target_buf.append_tail(DemTarget::observable_id(k)); } } out.arg_buf.append_tail(1); out.instructions.push_back( DemInstruction{ .arg_data = out.arg_buf.commit_tail(), .target_data = out.target_buf.commit_tail(), .tag = "", .type = DemInstructionType::DEM_ERROR, }); } bool SearchState::operator==(const SearchState &other) const { SearchState a = canonical(); SearchState b = other.canonical(); return a.det_active == b.det_active && a.det_held == b.det_held && a.obs_mask == b.obs_mask; } bool SearchState::operator!=(const SearchState &other) const { return !(*this == other); } bool SearchState::operator<(const SearchState &other) const { SearchState a = canonical(); SearchState b = other.canonical(); if (a.det_active != b.det_active) { return a.det_active < b.det_active; } if (a.det_held != b.det_held) { return a.det_held < b.det_held; } return a.obs_mask < b.obs_mask; } std::ostream &stim::impl_search_graphlike::operator<<(std::ostream &out, const SearchState &v) { if (v.is_undetected()) { out << "[no symptoms] "; } else { if (v.det_active != NO_NODE_INDEX) { out << "D" << v.det_active << " "; } if (v.det_held != NO_NODE_INDEX) { out << "D" << v.det_held << " "; } } for (size_t k = 0; k < v.obs_mask.num_bits_padded(); k++) { if (v.obs_mask[k]) { out << "L" << k << " "; } } return out; } ================================================ FILE: src/stim/search/graphlike/search_state.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_GRAPHLIKE_SEARCH_STATE_H #define _STIM_SEARCH_GRAPHLIKE_SEARCH_STATE_H #include "stim/dem/detector_error_model.h" #include "stim/mem/simd_bits.h" namespace stim { namespace impl_search_graphlike { struct SearchState { uint64_t det_active; // The detection event being moved around in an attempt to remove it (or NO_NODE_INDEX). uint64_t det_held; // The detection event being left in the same place (or NO_NODE_INDEX). simd_bits<64> obs_mask; // The accumulated frame changes from moving the detection events around. SearchState() = delete; SearchState(size_t num_observables); SearchState(uint64_t det_active, uint64_t det_held, simd_bits<64> obs_mask); bool is_undetected() const; SearchState canonical() const; void append_transition_as_error_instruction_to(const SearchState &other, DetectorErrorModel &out) const; bool operator==(const SearchState &other) const; bool operator!=(const SearchState &other) const; bool operator<(const SearchState &other) const; std::string str() const; }; std::ostream &operator<<(std::ostream &out, const SearchState &v); inline void hash_combine(size_t &h, uint64_t x) { h ^= std::hash{}(x) + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); // mimic Boost's hash-combine function } struct SearchStateHash { size_t operator()(const SearchState &s) const { SearchState c = s.canonical(); size_t h = std::hash{}(c.det_active); hash_combine(h, c.det_held); for (size_t i = 0; i < c.obs_mask.num_u64_padded(); i++) { hash_combine(h, c.obs_mask.u64[i]); } return h; } }; } // namespace impl_search_graphlike } // namespace stim #endif ================================================ FILE: src/stim/search/graphlike/search_state.test.cc ================================================ // 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. #include "stim/search/graphlike/search_state.h" #include "gtest/gtest.h" #include "stim/search/graphlike/node.h" using namespace stim; using namespace stim::impl_search_graphlike; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(search_graphlike, DemAdjGraphSearchState_construct) { SearchState r(64); ASSERT_EQ(r.det_active, NO_NODE_INDEX); ASSERT_EQ(r.det_held, NO_NODE_INDEX); ASSERT_EQ((uint64_t)r.obs_mask.ptr_simd[0], 0); SearchState r2(2, 1, obs_mask(3)); ASSERT_EQ(r2.det_active, 2); ASSERT_EQ(r2.det_held, 1); ASSERT_EQ((uint64_t)r2.obs_mask.ptr_simd[0], 3); } TEST(search_graphlike, DemAdjGraphSearchState_is_undetected) { ASSERT_FALSE(SearchState(1, 2, obs_mask(3)).is_undetected()); ASSERT_FALSE(SearchState(1, 2, obs_mask(2)).is_undetected()); ASSERT_FALSE(SearchState(1, 2, obs_mask(0)).is_undetected()); ASSERT_TRUE(SearchState(1, 1, obs_mask(3)).is_undetected()); ASSERT_TRUE(SearchState(NO_NODE_INDEX, NO_NODE_INDEX, obs_mask(32)).is_undetected()); ASSERT_TRUE(SearchState(NO_NODE_INDEX, NO_NODE_INDEX, obs_mask(0)).is_undetected()); } TEST(search_graphlike, DemAdjGraphSearchState_canonical) { SearchState a = SearchState(1, 2, obs_mask(3)).canonical(); ASSERT_EQ(a.det_active, 1); ASSERT_EQ(a.det_held, 2); ASSERT_EQ(a.obs_mask, obs_mask(3)); a = SearchState(2, 1, obs_mask(3)).canonical(); ASSERT_EQ(a.det_active, 1); ASSERT_EQ(a.det_held, 2); ASSERT_EQ(a.obs_mask, obs_mask(3)); a = SearchState(1, 1, obs_mask(3)).canonical(); ASSERT_EQ(a.det_active, NO_NODE_INDEX); ASSERT_EQ(a.det_held, NO_NODE_INDEX); ASSERT_EQ(a.obs_mask, obs_mask(3)); a = SearchState(1, 1, obs_mask(1)).canonical(); ASSERT_EQ(a.det_active, NO_NODE_INDEX); ASSERT_EQ(a.det_held, NO_NODE_INDEX); ASSERT_EQ(a.obs_mask, obs_mask(1)); a = SearchState(1, NO_NODE_INDEX, obs_mask(1)).canonical(); ASSERT_EQ(a.det_active, 1); ASSERT_EQ(a.det_held, NO_NODE_INDEX); ASSERT_EQ(a.obs_mask, obs_mask(1)); } TEST(search_graphlike, DemAdjGraphSearchState_append_transition_as_error_instruction_to) { DetectorErrorModel out; SearchState(1, 2, obs_mask(9)).append_transition_as_error_instruction_to(SearchState(1, 2, obs_mask(16)), out); ASSERT_EQ(out, DetectorErrorModel(R"MODEL( error(1) L0 L3 L4 )MODEL")); SearchState(1, 2, obs_mask(9)).append_transition_as_error_instruction_to(SearchState(3, 2, obs_mask(9)), out); ASSERT_EQ(out, DetectorErrorModel(R"MODEL( error(1) L0 L3 L4 error(1) D1 D3 )MODEL")); SearchState(1, 2, obs_mask(9)) .append_transition_as_error_instruction_to(SearchState(1, NO_NODE_INDEX, obs_mask(9)), out); ASSERT_EQ(out, DetectorErrorModel(R"MODEL( error(1) L0 L3 L4 error(1) D1 D3 error(1) D2 )MODEL")); SearchState(NO_NODE_INDEX, NO_NODE_INDEX, obs_mask(0)) .append_transition_as_error_instruction_to(SearchState(1, NO_NODE_INDEX, obs_mask(9)), out); ASSERT_EQ(out, DetectorErrorModel(R"MODEL( error(1) L0 L3 L4 error(1) D1 D3 error(1) D2 error(1) D1 L0 L3 )MODEL")); SearchState(1, 1, obs_mask(0)).append_transition_as_error_instruction_to(SearchState(2, 2, obs_mask(4)), out); ASSERT_EQ(out, DetectorErrorModel(R"MODEL( error(1) L0 L3 L4 error(1) D1 D3 error(1) D2 error(1) D1 L0 L3 error(1) L2 )MODEL")); } TEST(search_graphlike, DemAdjGraphSearchState_canonical_equality) { SearchState v1{1, 2, obs_mask(3)}; SearchState v2{1, 4, obs_mask(3)}; ASSERT_TRUE(v1 == v1); ASSERT_FALSE(v1 == v2); ASSERT_FALSE(v1 != v1); ASSERT_TRUE(v1 != v2); ASSERT_EQ(v1, SearchState(2, 1, obs_mask(3))); ASSERT_NE(v1, SearchState(1, NO_NODE_INDEX, obs_mask(3))); ASSERT_EQ(SearchState(NO_NODE_INDEX, NO_NODE_INDEX, obs_mask(0)), SearchState(1, 1, obs_mask(0))); ASSERT_EQ(SearchState(3, 3, obs_mask(0)), SearchState(1, 1, obs_mask(0))); ASSERT_NE(SearchState(3, 3, obs_mask(1)), SearchState(1, 1, obs_mask(0))); ASSERT_EQ(SearchState(3, 3, obs_mask(1)), SearchState(1, 1, obs_mask(1))); ASSERT_EQ(SearchState(2, NO_NODE_INDEX, obs_mask(3)), SearchState(NO_NODE_INDEX, 2, obs_mask(3))); } TEST(search_graphlike, DemAdjGraphSearchState_canonical_ordering) { ASSERT_TRUE(SearchState(1, 999, obs_mask(999)) < SearchState(101, 102, obs_mask(103))); ASSERT_TRUE(SearchState(999, 1, obs_mask(999)) < SearchState(101, 102, obs_mask(103))); ASSERT_TRUE(SearchState(101, 1, obs_mask(999)) < SearchState(101, 102, obs_mask(103))); ASSERT_TRUE(SearchState(102, 1, obs_mask(999)) < SearchState(101, 102, obs_mask(103))); ASSERT_TRUE(SearchState(101, 102, obs_mask(3)) < SearchState(101, 102, obs_mask(103))); ASSERT_FALSE(SearchState(101, 102, obs_mask(103)) < SearchState(101, 102, obs_mask(103))); ASSERT_FALSE(SearchState(101, 104, obs_mask(103)) < SearchState(101, 102, obs_mask(103))); ASSERT_FALSE(SearchState(101, 102, obs_mask(104)) < SearchState(101, 102, obs_mask(103))); } TEST(search_graphlike, DemAdjGraphSearchState_str) { ASSERT_EQ(SearchState(1, 2, obs_mask(3)).str(), "D1 D2 L0 L1 "); } TEST(search_graphlike, SearchStateHash_operator) { ASSERT_EQ(SearchStateHash{}(SearchState(1, 2, obs_mask(3))), SearchStateHash{}(SearchState(2, 1, obs_mask(3)))); ASSERT_EQ(SearchStateHash{}(SearchState(1, 2, obs_mask(3))), SearchStateHash{}(SearchState(1, 2, obs_mask(3)))); ASSERT_NE(SearchStateHash{}(SearchState(1, 2, obs_mask(3))), SearchStateHash{}(SearchState(2, 2, obs_mask(3)))); ASSERT_NE(SearchStateHash{}(SearchState(1, 2, obs_mask(3))), SearchStateHash{}(SearchState(1, 2, obs_mask(4)))); } ================================================ FILE: src/stim/search/hyper/algo.cc ================================================ // 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. #include "stim/search/hyper/algo.h" #include #include #include #include #include "stim/search/graphlike/algo.h" #include "stim/search/hyper/edge.h" #include "stim/search/hyper/graph.h" #include "stim/search/hyper/search_state.h" using namespace stim; using namespace stim::impl_search_hyper; DetectorErrorModel backtrack_path(const std::map &back_map, SearchState &&final_state) { DetectorErrorModel out; SearchState cur_state = std::move(final_state); while (true) { auto prev_state = back_map.at(cur_state); cur_state.append_transition_as_error_instruction_to(prev_state, out); if (prev_state.dets.empty()) { break; } cur_state = prev_state; } std::sort(out.instructions.begin(), out.instructions.end()); // Because of the search truncation, the same step may have been taken twice at different states. for (size_t k = 0; k < out.instructions.size() - 1; k++) { if (out.instructions[k].target_data == out.instructions[k + 1].target_data) { out.instructions.erase(out.instructions.begin() + k); out.instructions.erase(out.instructions.begin() + k); k -= 1; } } return out; } DetectorErrorModel stim::find_undetectable_logical_error( const DetectorErrorModel &model, size_t dont_explore_detection_event_sets_with_size_above, size_t dont_explore_edges_with_degree_above, bool dont_explore_edges_increasing_symptom_degree) { if (dont_explore_edges_with_degree_above == 2 && dont_explore_detection_event_sets_with_size_above == 2) { return stim::shortest_graphlike_undetectable_logical_error(model, true); } Graph graph = Graph::from_dem(model, dont_explore_edges_with_degree_above); auto empty_search_state = SearchState{{}, simd_bits<64>(graph.num_observables)}; if (graph.distance_1_error_mask.not_zero()) { DetectorErrorModel out; SearchState s1{{}, graph.distance_1_error_mask}; s1.append_transition_as_error_instruction_to(empty_search_state, out); return out; } std::queue queue; std::map back_map; // Mark the vacuous dead-end state as already seen. back_map.emplace(empty_search_state, empty_search_state); // Search starts from any and all edges crossing an observable. for (size_t node = 0; node < graph.nodes.size(); node++) { for (const auto &e : graph.nodes[node].edges) { if (e.crossing_observable_mask.not_zero() && e.nodes.sorted_items[0] == node) { SearchState start{e.nodes, e.crossing_observable_mask}; if (start.dets.size() <= dont_explore_detection_event_sets_with_size_above) { queue.push(start); } back_map.emplace(start, empty_search_state); } } } // Breadth first search for a symptomless state that has a frame change. for (; !queue.empty(); queue.pop()) { SearchState cur = queue.front(); assert(!cur.dets.empty()); size_t active_node = cur.dets.sorted_items[0]; for (const auto &e : graph.nodes[active_node].edges) { SearchState next{e.nodes ^ cur.dets, e.crossing_observable_mask ^ cur.obs_mask}; if (next.dets.size() > dont_explore_detection_event_sets_with_size_above) { continue; } if (dont_explore_edges_increasing_symptom_degree && next.dets.size() > cur.dets.size()) { continue; } if (!back_map.emplace(next, cur).second) { continue; } if (next.dets.empty()) { assert(next.obs_mask.not_zero()); // Otherwise, it would have already been in back_map. return backtrack_path(back_map, std::move(next)); } queue.push(std::move(next)); } } std::stringstream err_msg; err_msg << "Failed to find any logical errors."; if (graph.num_observables == 0) { err_msg << "\n WARNING: NO OBSERVABLES. The circuit or detector error model didn't define any observables, " "making it vacuously impossible to find a logical error."; } if (graph.nodes.size() == 0) { err_msg << "\n WARNING: NO DETECTORS. The circuit or detector error model didn't define any detectors."; } if (model.count_errors() == 0) { err_msg << "\n WARNING: NO ERRORS. The circuit or detector error model didn't include any errors, making it " "vacuously impossible to find a logical error."; } throw std::invalid_argument(err_msg.str()); } ================================================ FILE: src/stim/search/hyper/algo.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_HYPER_ALGO_H #define _STIM_SEARCH_HYPER_ALGO_H #include #include "stim/dem/detector_error_model.h" namespace stim { /// Finds a list of errors from the model that form an undetectable logical error. /// /// Note that this search considers hyper errors, and so if it is not truncated it has exponential /// runtime. It is important to carefully consider how you are truncating the space to trade off /// cost versus quality of result. /// /// Beware that the choice of logical observable can interact with the truncation options. Using /// different observables can change whether or not the search succeeds, even if those observables /// are equal modulo the stabilizers of the code. This is because the edges crossing logical /// observables are used as starting points for the search, and starting from different places along /// a path will result in different numbers of symptoms in intermediate states as the search /// progresses. For example, if the logical observable is next to a boundary, then the starting /// edges are likely boundary edges (degree 1) with 'room to grow', whereas if the observable was /// running through the bulk then the starting edges will have degree at least 2. /// /// To perform a graphlike search use: /// dont_explore_detection_event_sets_with_size_above = 2 /// dont_explore_edges_with_degree_above = 2 /// dont_explore_edges_increasing_symptom_degree = [anything] /// /// Args: /// model: The detector error model to search for undetectable errors. /// dont_explore_detection_event_sets_with_size_above: Truncates the search space by refusing to /// cross an edge (i.e. add an error) when doing so would produce an intermediate state that /// has more detection events than this limit. /// dont_explore_edges_with_degree_above: Truncates the search space by refusing to consider /// errors that cause a lot of detection events. For example, you may only want to consider /// graphlike errors which have two or fewer detection events. /// dont_explore_edges_increasing_symptom_degree: Truncates the search space by refusing to /// cross an edge (i.e. add an error) when doing so would produce an intermediate state that /// has more detection events that the previous intermediate state. This massively improves /// the efficiency of the search because instead of, for example, exploring all n^4 possible /// detection event sets with 4 symptoms, the search will attempt to cancel out symptoms one /// by one. /// /// Returns: /// A detector error model containing only the error mechanisms that cause the undetectable /// logical error. The error mechanisms will have their probabilities set to 1 (indicating that /// they are necessary) and will not suggest a decomposition. DetectorErrorModel find_undetectable_logical_error( const DetectorErrorModel &model, size_t dont_explore_detection_event_sets_with_size_above, size_t dont_explore_edges_with_degree_above, bool dont_explore_edges_increasing_symptom_degree); } // namespace stim #endif ================================================ FILE: src/stim/search/hyper/algo.test.cc ================================================ // 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. #include "stim/search/hyper/algo.h" #include "gtest/gtest.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/simulators/error_analyzer.h" using namespace stim; TEST(find_undetectable_logical_error, no_error) { // No error. ASSERT_THROW( { stim::find_undetectable_logical_error(DetectorErrorModel(), SIZE_MAX, SIZE_MAX, false); }, std::invalid_argument); // No undetectable error. ASSERT_THROW( { stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 L0 )MODEL"), SIZE_MAX, SIZE_MAX, false); }, std::invalid_argument); // No logical flips. ASSERT_THROW( { stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 error(0.1) D0 D1 error(0.1) D1 )MODEL"), SIZE_MAX, SIZE_MAX, false); }, std::invalid_argument); } TEST(find_undetectable_logical_error, distance_1) { ASSERT_EQ( stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) L0 )MODEL"), SIZE_MAX, SIZE_MAX, false), DetectorErrorModel(R"MODEL( error(1) L0 )MODEL")); } TEST(find_undetectable_logical_error, distance_2) { ASSERT_EQ( stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 error(0.1) D0 L0 )MODEL"), SIZE_MAX, SIZE_MAX, false), DetectorErrorModel(R"MODEL( error(1) D0 error(1) D0 L0 )MODEL")); ASSERT_EQ( stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 L0 error(0.1) D0 L1 )MODEL"), SIZE_MAX, SIZE_MAX, false), DetectorErrorModel(R"MODEL( error(1) D0 L0 error(1) D0 L1 )MODEL")); ASSERT_EQ( stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 D1 L0 error(0.1) D0 D1 L1 )MODEL"), SIZE_MAX, SIZE_MAX, false), DetectorErrorModel(R"MODEL( error(1) D0 D1 L0 error(1) D0 D1 L1 )MODEL")); ASSERT_EQ( stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 D1 L1 error(0.1) D0 D1 L0 )MODEL"), SIZE_MAX, SIZE_MAX, false), DetectorErrorModel(R"MODEL( error(1) D0 D1 L0 error(1) D0 D1 L1 )MODEL")); } TEST(find_undetectable_logical_error, distance_3) { ASSERT_EQ( stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 error(0.1) D0 D1 L0 error(0.1) D1 )MODEL"), SIZE_MAX, SIZE_MAX, false), DetectorErrorModel(R"MODEL( error(1) D0 error(1) D0 D1 L0 error(1) D1 )MODEL")); ASSERT_EQ( stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D1 error(0.1) D1 D0 L0 error(0.1) D0 )MODEL"), SIZE_MAX, SIZE_MAX, false), DetectorErrorModel(R"MODEL( error(1) D0 error(1) D0 D1 L0 error(1) D1 )MODEL")); } TEST(find_undetectable_logical_error, hyper_error) { ASSERT_EQ( stim::find_undetectable_logical_error( DetectorErrorModel(R"MODEL( error(0.1) D0 D1 error(0.1) D0 D1 D2 D3 error(0.1) D2 D3 D4 D5 L0 error(0.1) D4 D5 D6 D7 error(0.1) D6 D7 D8 D9 error(0.1) D8 error(0.1) D9 )MODEL"), 4, 4, true), DetectorErrorModel(R"MODEL( error(1) D0 D1 error(1) D0 D1 D2 D3 error(1) D2 D3 D4 D5 L0 error(1) D4 D5 D6 D7 error(1) D6 D7 D8 D9 error(1) D8 error(1) D9 )MODEL")); } TEST(find_undetectable_logical_error, surface_code) { CircuitGenParameters params(5, 5, "rotated_memory_x"); params.after_clifford_depolarization = 0.001; params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.before_round_data_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; auto graphlike_model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, false, false, true); auto ungraphlike_model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, false, true, false, false, false, true); ASSERT_EQ(stim::find_undetectable_logical_error(graphlike_model, 4, 4, true).instructions.size(), 5); ASSERT_EQ(stim::find_undetectable_logical_error(ungraphlike_model, 4, 4, true).instructions.size(), 5); } TEST(find_undetectable_logical_error, repetition_code) { CircuitGenParameters params(10, 7, "memory"); params.before_round_data_depolarization = 0.01; auto circuit = generate_rep_code_circuit(params).circuit; auto graphlike_model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); ASSERT_EQ(stim::find_undetectable_logical_error(graphlike_model, 4, 4, false).instructions.size(), 7); } TEST(find_undetectable_logical_error, many_observables) { Circuit circuit(R"CIRCUIT( MPP Z0*Z1 Z1*Z2 Z2*Z3 Z3*Z4 X_ERROR(0.1) 0 1 2 3 4 MPP Z0*Z1 Z1*Z2 Z2*Z3 Z3*Z4 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] M 4 OBSERVABLE_INCLUDE(1200) rec[-1] )CIRCUIT"); auto graphlike_model = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); auto err = stim::find_undetectable_logical_error(graphlike_model, 4, 4, false); ASSERT_EQ(err.instructions.size(), 5); } ================================================ FILE: src/stim/search/hyper/edge.cc ================================================ // 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. #include "stim/search/hyper/edge.h" #include #include using namespace stim; using namespace stim::impl_search_hyper; std::string Edge::str() const { std::stringstream result; result << *this; return result.str(); } bool Edge::operator==(const Edge &other) const { return nodes == other.nodes && crossing_observable_mask == other.crossing_observable_mask; } bool Edge::operator!=(const Edge &other) const { return !(*this == other); } std::ostream &stim::impl_search_hyper::operator<<(std::ostream &out, const Edge &v) { bool sep = false; if (v.nodes.empty()) { out << "[silent]"; sep = true; } else if (v.nodes.size() == 1) { out << "[boundary]"; sep = true; } for (const auto &t : v.nodes) { if (sep) { out << ' '; } sep = true; out << "D" << t; } for (size_t k = 0; k < v.crossing_observable_mask.num_bits_padded(); k++) { if (v.crossing_observable_mask[k]) { if (sep) { out << ' '; } sep = true; out << "L" << k; } } return out; } ================================================ FILE: src/stim/search/hyper/edge.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_HYPER_EDGE_H #define _STIM_SEARCH_HYPER_EDGE_H #include #include #include #include "stim/mem/simd_bits.h" #include "stim/mem/sparse_xor_vec.h" namespace stim { namespace impl_search_hyper { struct Edge { SparseXorVec nodes; simd_bits<64> crossing_observable_mask; std::string str() const; bool operator==(const Edge &other) const; bool operator!=(const Edge &other) const; }; std::ostream &operator<<(std::ostream &out, const Edge &v); } // namespace impl_search_hyper } // namespace stim #endif ================================================ FILE: src/stim/search/hyper/edge.test.cc ================================================ // 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. #include "stim/search/hyper/edge.h" #include "gtest/gtest.h" using namespace stim; using namespace stim::impl_search_hyper; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(search_decay, Edge) { Edge e1{{{}}, obs_mask(0)}; Edge e2{{{1}}, obs_mask(0)}; Edge e3{{{}}, obs_mask(1)}; Edge e4{{{1, 2}}, obs_mask(5)}; ASSERT_EQ(e1.str(), "[silent]"); ASSERT_EQ(e2.str(), "[boundary] D1"); ASSERT_EQ(e3.str(), "[silent] L0"); ASSERT_EQ(e4.str(), "D1 D2 L0 L2"); ASSERT_TRUE(e1 == e1); ASSERT_TRUE(!(e1 == e2)); ASSERT_FALSE(e1 == e2); ASSERT_FALSE(!(e1 == e1)); ASSERT_EQ(e1, (Edge{{{}}, obs_mask(0)})); ASSERT_EQ(e2, e2); ASSERT_EQ(e3, e3); ASSERT_NE(e1, e3); } ================================================ FILE: src/stim/search/hyper/graph.cc ================================================ // 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. #include "stim/search/hyper/graph.h" #include #include #include using namespace stim; using namespace stim::impl_search_hyper; std::string Graph::str() const { std::stringstream result; result << *this; return result.str(); } void Graph::add_edge_from_dem_targets(SpanRef targets, size_t dont_explore_edges_with_degree_above) { Edge edge{{}, simd_bits<64>(num_observables)}; for (const auto &t : targets) { if (t.is_relative_detector_id()) { edge.nodes.xor_item(t.val()); } else if (t.is_observable_id()) { edge.crossing_observable_mask[t.val()] ^= true; } } if (edge.nodes.size() > dont_explore_edges_with_degree_above) { return; } if (edge.nodes.empty() && edge.crossing_observable_mask.not_zero()) { distance_1_error_mask = edge.crossing_observable_mask; } for (const auto &n : edge.nodes) { nodes[n].edges.push_back(edge); } } Graph Graph::from_dem(const DetectorErrorModel &model, size_t dont_explore_edges_with_degree_above) { Graph result(model.count_detectors(), model.count_observables()); model.iter_flatten_error_instructions([&](const DemInstruction &e) { if (e.arg_data[0] != 0) { result.add_edge_from_dem_targets(e.target_data, dont_explore_edges_with_degree_above); } }); return result; } bool Graph::operator==(const Graph &other) const { return nodes == other.nodes && num_observables == other.num_observables && distance_1_error_mask == other.distance_1_error_mask; } bool Graph::operator!=(const Graph &other) const { return !(*this == other); } Graph::Graph(size_t node_count, size_t num_observables) : nodes(node_count), num_observables(num_observables), distance_1_error_mask(simd_bits<64>(num_observables)) { } Graph::Graph(std::vector nodes, size_t num_observables, simd_bits<64> distance_1_error_mask) : nodes(std::move(nodes)), num_observables(num_observables), distance_1_error_mask(std::move(distance_1_error_mask)) { } std::ostream &stim::impl_search_hyper::operator<<(std::ostream &out, const Graph &v) { for (size_t k = 0; k < v.nodes.size(); k++) { out << k << ":\n" << v.nodes[k]; } return out; } ================================================ FILE: src/stim/search/hyper/graph.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_HYPER_GRAPH_H #define _STIM_SEARCH_HYPER_GRAPH_H #include "stim/dem/detector_error_model.h" #include "stim/search/hyper/node.h" namespace stim { namespace impl_search_hyper { struct Graph { std::vector nodes; size_t num_observables; simd_bits<64> distance_1_error_mask; explicit Graph(size_t node_count, size_t num_observables); Graph(std::vector nodes, size_t num_observables, simd_bits<64> distance_1_error_mask); void add_edge_from_dem_targets(SpanRef targets, size_t dont_explore_edges_with_degree_above); static Graph from_dem(const DetectorErrorModel &model, size_t dont_explore_edges_with_degree_above); bool operator==(const Graph &other) const; bool operator!=(const Graph &other) const; std::string str() const; }; std::ostream &operator<<(std::ostream &out, const Graph &v); } // namespace impl_search_hyper } // namespace stim #endif ================================================ FILE: src/stim/search/hyper/graph.test.cc ================================================ // 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. #include "stim/search/hyper/graph.h" #include "gtest/gtest.h" using namespace stim; using namespace stim::impl_search_hyper; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(search_hyper_graph, equality) { ASSERT_TRUE(Graph(1, 64) == Graph(1, 64)); ASSERT_TRUE(Graph(1, 64) != Graph(2, 64)); ASSERT_TRUE(Graph(1, 64) != Graph(1, 32)); ASSERT_FALSE(Graph(1, 64) == Graph(2, 64)); ASSERT_FALSE(Graph(1, 64) != Graph(1, 64)); Graph a(1, 64); Graph b(1, 64); ASSERT_EQ(a, b); b.distance_1_error_mask[0] = true; ASSERT_NE(a, b); } TEST(search_hyper_graph, add_edge_from_dem_targets) { Graph g(3, 64); g.add_edge_from_dem_targets(DetectorErrorModel("error(0.01) D0 D1 L3 ^ D0").instructions[0].target_data, SIZE_MAX); ASSERT_EQ( g, (Graph{ std::vector{ Node{}, Node{{Edge{{{1}}, obs_mask(8)}}}, Node{}, }, 64, obs_mask(0)})); g.add_edge_from_dem_targets(DetectorErrorModel("error(0.01) D0 D1 D2 L0").instructions[0].target_data, SIZE_MAX); ASSERT_EQ( g, (Graph{ std::vector{ Node{{Edge{{{0, 1, 2}}, obs_mask(1)}}}, Node{{Edge{{{1}}, obs_mask(8)}, Edge{{{0, 1, 2}}, obs_mask(1)}}}, Node{{Edge{{{0, 1, 2}}, obs_mask(1)}}}, }, 64, obs_mask(0)})); } TEST(search_hyper_graph, str) { Graph g{ { Node{}, Node{{Edge{{{1}}, obs_mask(0)}, Edge{{{1, 3}}, obs_mask(32)}}}, Node{}, Node{{Edge{{{1, 3}}, obs_mask(32)}}}, }, 64, obs_mask(0)}; ASSERT_EQ( g.str(), "0:\n" "1:\n" " [boundary] D1\n" " D1 D3 L5\n" "2:\n" "3:\n" " D1 D3 L5\n"); } TEST(search_hyper_graph, from_dem) { DetectorErrorModel dem(R"MODEL( error(0.1) D0 repeat 3 { error(0.1) D0 D1 shift_detectors 1 } error(0.1) D0 L7 error(0.1) D2 ^ D3 D4 L2 detector D5 )MODEL"); ASSERT_EQ( Graph::from_dem(dem, SIZE_MAX), Graph( { Node{{Edge{{{0}}, obs_mask(0)}, Edge{{{0, 1}}, obs_mask(0)}}}, Node{{Edge{{{0, 1}}, obs_mask(0)}, Edge{{{1, 2}}, obs_mask(0)}}}, Node{{Edge{{{1, 2}}, obs_mask(0)}, Edge{{{2, 3}}, obs_mask(0)}}}, Node{{Edge{{{2, 3}}, obs_mask(0)}, Edge{{{3}}, obs_mask(128)}}}, Node{}, Node{{Edge{{{5, 6, 7}}, obs_mask(4)}}}, Node{{Edge{{{5, 6, 7}}, obs_mask(4)}}}, Node{{Edge{{{5, 6, 7}}, obs_mask(4)}}}, Node{}, }, 8, obs_mask(0))); ASSERT_EQ( Graph::from_dem(dem, 2), Graph( { Node{{Edge{{{0}}, obs_mask(0)}, Edge{{{0, 1}}, obs_mask(0)}}}, Node{{Edge{{{0, 1}}, obs_mask(0)}, Edge{{{1, 2}}, obs_mask(0)}}}, Node{{Edge{{{1, 2}}, obs_mask(0)}, Edge{{{2, 3}}, obs_mask(0)}}}, Node{{Edge{{{2, 3}}, obs_mask(0)}, Edge{{{3}}, obs_mask(128)}}}, Node{}, Node{}, Node{}, Node{}, Node{}, }, 8, obs_mask(0))); ASSERT_EQ( Graph::from_dem(dem, 1), Graph( { Node{{Edge{{{0}}, obs_mask(0)}}}, Node{}, Node{}, Node{{Edge{{{3}}, obs_mask(128)}}}, Node{}, Node{}, Node{}, Node{}, Node{}, }, 8, obs_mask(0))); } ================================================ FILE: src/stim/search/hyper/node.cc ================================================ // 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. #include "stim/search/hyper/node.h" #include #include using namespace stim; using namespace stim::impl_search_hyper; std::string Node::str() const { std::stringstream result; result << *this; return result.str(); } bool Node::operator==(const Node &other) const { return edges == other.edges; } bool Node::operator!=(const Node &other) const { return !(*this == other); } std::ostream &stim::impl_search_hyper::operator<<(std::ostream &out, const Node &v) { for (const auto &e : v.edges) { out << " " << e << "\n"; } return out; } ================================================ FILE: src/stim/search/hyper/node.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_HYPER_NODE_H #define _STIM_SEARCH_HYPER_NODE_H #include #include "stim/search/hyper/edge.h" namespace stim { namespace impl_search_hyper { struct Node { std::vector edges; std::string str() const; bool operator==(const Node &other) const; bool operator!=(const Node &other) const; }; std::ostream &operator<<(std::ostream &out, const Node &v); } // namespace impl_search_hyper } // namespace stim #endif ================================================ FILE: src/stim/search/hyper/node.test.cc ================================================ // 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. #include "stim/search/hyper/node.h" #include "gtest/gtest.h" using namespace stim; using namespace stim::impl_search_hyper; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(search_decay, Node) { Node n1{}; Node n2{{Edge{{{2}}, obs_mask(0)}}}; Node n3{{Edge{{{1, 3}}, obs_mask(5)}, Edge{{{3}}, obs_mask(8)}}}; ASSERT_EQ(n1.str(), ""); ASSERT_EQ(n2.str(), " [boundary] D2\n"); ASSERT_EQ(n3.str(), " D1 D3 L0 L2\n [boundary] D3 L3\n"); ASSERT_TRUE(n1 == n1); ASSERT_TRUE(!(n1 == n2)); ASSERT_FALSE(n1 == n2); ASSERT_FALSE(!(n1 == n1)); ASSERT_EQ(n1, (Node{})); ASSERT_EQ(n2, n2); ASSERT_EQ(n3, n3); ASSERT_NE(n1, n3); } ================================================ FILE: src/stim/search/hyper/search_state.cc ================================================ // 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. #include "stim/search/hyper/search_state.h" #include #include "stim/search/hyper/node.h" using namespace stim; using namespace stim::impl_search_hyper; std::string SearchState::str() const { std::stringstream result; result << *this; return result.str(); } void SearchState::append_transition_as_error_instruction_to(const SearchState &other, DetectorErrorModel &out) const { // Extract detector indices while cancelling duplicates. SparseXorVec dif = dets ^ other.dets; for (const auto &n : dif) { out.target_buf.append_tail(DemTarget::relative_detector_id(n)); } // Extract logical observable indices. auto dif_mask = obs_mask ^ other.obs_mask; for (size_t k = 0; k < dif_mask.num_bits_padded(); k++) { if (dif_mask[k]) { out.target_buf.append_tail(DemTarget::observable_id(k)); } } // Default probability to 1. out.arg_buf.append_tail(1); out.instructions.push_back( DemInstruction{ .arg_data = out.arg_buf.commit_tail(), .target_data = out.target_buf.commit_tail(), .tag = "", .type = DemInstructionType::DEM_ERROR, }); } bool SearchState::operator==(const SearchState &other) const { return dets == other.dets && obs_mask == other.obs_mask; } bool SearchState::operator!=(const SearchState &other) const { return !(*this == other); } bool SearchState::operator<(const SearchState &other) const { if (dets != other.dets) { return dets < other.dets; } return obs_mask < other.obs_mask; } std::ostream &stim::impl_search_hyper::operator<<(std::ostream &out, const SearchState &v) { if (v.dets.empty()) { out << "[no symptoms] "; } else { for (const auto &d : v.dets) { out << "D" << d << " "; } } for (size_t k = 0; k < v.obs_mask.num_bits_padded(); k++) { if (v.obs_mask[k]) { out << "L" << k << " "; } } return out; } ================================================ FILE: src/stim/search/hyper/search_state.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_HYPER_SEARCH_STATE_H #define _STIM_SEARCH_HYPER_SEARCH_STATE_H #include "stim/dem/detector_error_model.h" #include "stim/mem/simd_bits.h" #include "stim/mem/sparse_xor_vec.h" namespace stim { namespace impl_search_hyper { struct SearchState { SparseXorVec dets; simd_bits<64> obs_mask; void append_transition_as_error_instruction_to(const SearchState &other, DetectorErrorModel &out) const; bool operator==(const SearchState &other) const; bool operator!=(const SearchState &other) const; bool operator<(const SearchState &other) const; std::string str() const; }; std::ostream &operator<<(std::ostream &out, const SearchState &v); } // namespace impl_search_hyper } // namespace stim #endif ================================================ FILE: src/stim/search/hyper/search_state.test.cc ================================================ // 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. #include "stim/search/hyper/search_state.h" #include "gtest/gtest.h" #include "stim/search/hyper/node.h" using namespace stim; using namespace stim::impl_search_hyper; static simd_bits<64> obs_mask(uint64_t v) { simd_bits<64> result(64); result.ptr_simd[0] = v; return result; } TEST(search_hyper_search_state, append_transition_as_error_instruction_to) { DetectorErrorModel out; SearchState{{{1, 2}}, obs_mask(9)}.append_transition_as_error_instruction_to( SearchState{{{1, 2}}, obs_mask(16)}, out); ASSERT_EQ(out, DetectorErrorModel(R"MODEL( error(1) L0 L3 L4 )MODEL")); SearchState{{{}}, obs_mask(9)}.append_transition_as_error_instruction_to( SearchState{{{1, 2, 4}}, obs_mask(16)}, out); ASSERT_EQ(out, DetectorErrorModel(R"MODEL( error(1) L0 L3 L4 error(1) D1 D2 D4 L0 L3 L4 )MODEL")); SearchState{{{1, 2}}, obs_mask(9)}.append_transition_as_error_instruction_to( SearchState{{{2, 3}}, obs_mask(9)}, out); ASSERT_EQ(out, DetectorErrorModel(R"MODEL( error(1) L0 L3 L4 error(1) D1 D2 D4 L0 L3 L4 error(1) D1 D3 )MODEL")); } TEST(search_hyper_search_state, equality) { SearchState v1{{{1, 2}}, obs_mask(3)}; SearchState v2{{{1, 4}}, obs_mask(3)}; ASSERT_TRUE(v1 == v1); ASSERT_FALSE(v1 == v2); ASSERT_FALSE(v1 != v1); ASSERT_TRUE(v1 != v2); ASSERT_NE(v1, (SearchState{{{1}}, obs_mask(3)})); ASSERT_NE(v1, (SearchState{{{1, 2}}, obs_mask(4)})); } TEST(search_hyper_search_state, ordering) { ASSERT_TRUE((SearchState{{{1}}, obs_mask(999)}) < (SearchState{{{1, 2}}, obs_mask(999)})); ASSERT_TRUE((SearchState{{{1, 999}}, obs_mask(999)}) < (SearchState{{{101, 102}}, obs_mask(103)})); ASSERT_TRUE((SearchState{{{1, 999}}, obs_mask(999)}) < (SearchState{{{101, 102}}, obs_mask(103)})); ASSERT_TRUE((SearchState{{{1, 101}}, obs_mask(999)}) < (SearchState{{{101, 102}}, obs_mask(103)})); ASSERT_TRUE((SearchState{{{1, 102}}, obs_mask(999)}) < (SearchState{{{101, 102}}, obs_mask(103)})); ASSERT_TRUE((SearchState{{{101, 102}}, obs_mask(3)}) < (SearchState{{{101, 102}}, obs_mask(103)})); ASSERT_FALSE((SearchState{{{101, 102}}, obs_mask(103)}) < (SearchState{{{101, 102}}, obs_mask(103)})); ASSERT_FALSE((SearchState{{{101, 104}}, obs_mask(103)}) < (SearchState{{{101, 102}}, obs_mask(103)})); ASSERT_FALSE((SearchState{{{101, 102}}, obs_mask(104)}) < (SearchState{{{101, 102}}, obs_mask(103)})); } TEST(search_hyper_search_state, str) { ASSERT_EQ((SearchState{{{1, 2}}, obs_mask(3)}.str()), "D1 D2 L0 L1 "); } ================================================ FILE: src/stim/search/sat/wcnf.cc ================================================ // 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. #include "stim/search/sat/wcnf.h" using namespace stim; typedef double Weight; constexpr Weight HARD_CLAUSE_WEIGHT = -1.0; const std::string UNSAT_WCNF_STR = "p wcnf 1 2 3\n3 -1 0\n3 1 0\n"; constexpr size_t BOOL_LITERAL_FALSE = SIZE_MAX - 1; constexpr size_t BOOL_LITERAL_TRUE = SIZE_MAX; struct BoolRef { size_t variable = BOOL_LITERAL_FALSE; bool negated = false; BoolRef operator~() const { return {variable, !negated}; } static BoolRef False() { return {BOOL_LITERAL_FALSE, false}; } static BoolRef True() { return {BOOL_LITERAL_TRUE, false}; } }; struct Clause { std::vector vars; Weight weight = HARD_CLAUSE_WEIGHT; void add_var(BoolRef x) { vars.push_back(x); } }; struct MaxSATInstance { size_t num_variables = 0; Weight max_weight = 0; std::vector clauses; BoolRef new_bool() { return {num_variables++}; } void add_clause(Clause &clause) { if (clause.weight != HARD_CLAUSE_WEIGHT) { if (clause.weight <= 0) { throw std::invalid_argument("Clauses must have positive weight or HARD_CLAUSE_WEIGHT."); } max_weight = std::max(max_weight, clause.weight); } clauses.push_back(clause); } BoolRef Xor(BoolRef &x, BoolRef &y) { if (x.variable == BOOL_LITERAL_FALSE) { return y; } if (x.variable == BOOL_LITERAL_TRUE) { return ~y; } if (y.variable == BOOL_LITERAL_FALSE) { return x; } if (y.variable == BOOL_LITERAL_TRUE) { return ~x; } BoolRef z = new_bool(); // Forbid strings (x, y, z) such that z != XOR(x, y) { Clause clause; // hard clause (x, y, z) != (0, 0, 1) clause.add_var(x); clause.add_var(y); clause.add_var(~z); add_clause(clause); } { Clause clause; // hard clause (x, y, z) != (0, 1, 0) clause.add_var(x); clause.add_var(~y); clause.add_var(z); add_clause(clause); } { Clause clause; // hard clause (x, y, z) != (1, 0, 0) clause.add_var(~x); clause.add_var(y); clause.add_var(z); add_clause(clause); } { Clause clause; // hard clause (x, y, z) != (1, 1, 1) clause.add_var(~x); clause.add_var(~y); clause.add_var(~z); add_clause(clause); } return z; } size_t quantized_weight(bool weighted, size_t quantization, size_t top, Weight weight) { if (weight == HARD_CLAUSE_WEIGHT) { // Hard clause return top; } // Soft clause if (!weighted) { // For unweighted problems, all soft clauses have weight 1. return 1; } return std::round(weight / max_weight * (double)quantization); } std::string to_wdimacs(bool weighted, size_t quantization) { // 'top' is a special weight used to indicate a hard clause. // Should be at least the sum of the weights of all soft clauses plus 1. size_t top; if (weighted) { top = 1 + quantization * clauses.size(); } else { top = 1 + clauses.size(); } // WDIMACS header format: p wcnf nbvar nbclauses top // see http://www.maxhs.org/docs/wdimacs.html std::stringstream ss; ss << "p wcnf " << num_variables << " " << clauses.size() << " " << top << "\n"; // Add clauses, 1 on each line. for (const auto &clause : clauses) { // WDIMACS clause format: weight var1 var2 ... // To show negation of a variable, the index should be negated. size_t qw = quantized_weight(weighted, quantization, top, clause.weight); // There is no need to add a clause with zero weight. // This can happen if the error has probability 0.5 or if the quantization is too // small to accomodate the entire dynamic range of weight values. if (qw == 0) continue; ss << qw; for (size_t i = 0; i < clause.vars.size(); ++i) { BoolRef var = clause.vars[i]; // Variables are 1-indexed if (var.negated) { ss << " -" << (var.variable + 1); } else { ss << " " << (var.variable + 1); } } // Each clause ends with 0 ss << " 0\n"; } return ss.str(); } }; std::string sat_problem_as_wcnf_string(const DetectorErrorModel &model, bool weighted, size_t quantization) { MaxSATInstance inst; if (weighted and quantization < 1) { throw std::invalid_argument("for weighted problems, quantization must be >= 1"); } if (!weighted and quantization != 0) { throw std::invalid_argument("for unweighted problems, quantization must be == 0"); } size_t num_observables = model.count_observables(); size_t num_detectors = model.count_detectors(); size_t num_errors = model.count_errors(); if (num_observables == 0 or num_errors == 0) { return UNSAT_WCNF_STR; } MaxSATInstance instance; // Create a boolean variable for each error, which indicates whether it is activated. std::vector errors_activated; for (size_t i = 0; i < num_errors; ++i) { errors_activated.push_back(instance.new_bool()); } std::vector detectors_activated(num_detectors, BoolRef::False()); std::vector observables_flipped(num_observables, BoolRef::False()); size_t error_index = 0; model.iter_flatten_error_instructions([&](const DemInstruction &e) { if (!weighted or e.arg_data[0] != 0) { BoolRef err_x = errors_activated[error_index]; // Add parity contribution to the detectors and observables for (const auto &t : e.target_data) { if (t.is_relative_detector_id()) { detectors_activated[t.val()] = instance.Xor(detectors_activated[t.val()], err_x); } else if (t.is_observable_id()) { observables_flipped[t.val()] = instance.Xor(observables_flipped[t.val()], err_x); } } // Add a soft clause for this error Clause clause; double p = e.arg_data[0]; if (weighted) { // Weighted search if (p < 0.5) { // If the probability < 0.5, the weight should be positive // and we add the clause for the error to be inactive. clause.add_var(~err_x); clause.weight = -std::log(p / (1 - p)); instance.add_clause(clause); } else if (p == 0.5) { // If the probability == 0.5, the error can be included "for free" // so we don't bother adding any soft clause. } else { // If the probability is > 0.5, the cost is negative so we emulate this by // inverting the sign of the weight and negating the clause so that the // clause has a positive weight. clause.add_var(err_x); clause.weight = -std::log((1 - p) / p); instance.add_clause(clause); } } else { // For unweighted search the error should be soft clause should be that // the error is inactive. clause.add_var(~err_x); clause.weight = 1.0; instance.add_clause(clause); } } ++error_index; }); assert(error_index == num_errors); // Add a hard clause for each detector to be inactive for (size_t d = 0; d < num_detectors; ++d) { Clause clause; if (detectors_activated[d].variable == BOOL_LITERAL_FALSE) continue; clause.add_var(~detectors_activated[d]); instance.add_clause(clause); } // Add a hard clause for any observable to be flipped Clause clause; for (size_t i = 0; i < num_observables; ++i) { clause.add_var(observables_flipped[i]); } instance.add_clause(clause); return instance.to_wdimacs(weighted, quantization); } // Should ignore weights entirely and minimize the cardinality. std::string stim::shortest_error_sat_problem(const DetectorErrorModel &model, std::string_view format) { if (format != "WDIMACS") { throw std::invalid_argument("Unsupported format."); } return sat_problem_as_wcnf_string(model, /*weighted=*/false, /*quantization=*/0); } std::string stim::likeliest_error_sat_problem( const DetectorErrorModel &model, int quantization, std::string_view format) { if (format != "WDIMACS") { throw std::invalid_argument("Unsupported format."); } if (quantization < 1) { throw std::invalid_argument("Must have quantization >= 1"); } return sat_problem_as_wcnf_string(model, /*weighted=*/true, quantization); } ================================================ FILE: src/stim/search/sat/wcnf.h ================================================ #ifndef _STIM_SEARCH_SAT_WCNF_H #define _STIM_SEARCH_SAT_WCNF_H #include "stim/dem/detector_error_model.h" namespace stim { /// Generates a maxSAT problem instance in .wcnf format from a DetectorErrorModel, such that the optimal value of the /// instance corresponds to the minimum distance of the protocol. /// /// The .wcnf (weighted CNF) file format is widely /// accepted by numerous maxSAT solvers. For example, the solvers in the 2023 maxSAT competition: /// https://maxsat-evaluations.github.io/2023/descriptions.html Note that erformance can greatly vary among solvers. The /// conversion involves encoding XOR constraints into CNF clauses using standard techniques. /// /// Args: /// model: The detector error model to be converted into .wcnf format for minimum distance calculation. /// weighted: (default = false) If false, all errors have cost 1. If true, errors are given a cost based /// on their likelihood cost scaled by the weight scale factor divided by the maximum cost value. /// if false, must set weight_scale_factor to 0. If true, must set weight_scale_factor >= 1. /// weight_scale_factor: The scaling factor used for quantization. (default = 0 for unweighted). /// /// Returns: /// A string which is interpreted as the contents of a .wcnf file. This should be written to a file which can then /// be passed to various maxSAT solvers to determine the minimum distance of the protocol represented by the model. /// The optimal value found by the solver corresponds to the minimum distance of the error correction protocol. In /// other words, the smallest number of errors that cause a logical observable flip without any detection events. /// /// Note: /// The use of .wcnf format offers significant flexibility in choosing a maxSAT solver, but it also means that /// users must separately manage the process of selecting and running the solver. This approach is designed to /// sidestep the need for direct integration with any particular solver and allow /// for experimentation with different solvers to achieve the best performance. std::string shortest_error_sat_problem(const DetectorErrorModel &model, std::string_view format = "WDIMACS"); std::string likeliest_error_sat_problem( const DetectorErrorModel &model, int quantization = 10, std::string_view format = "WDIMACS"); } // namespace stim #endif ================================================ FILE: src/stim/search/sat/wcnf.test.cc ================================================ #include "stim/search/sat/wcnf.h" #include "gtest/gtest.h" using namespace stim; const std::string unsatisfiable_wdimacs = "p wcnf 1 2 3\n3 -1 0\n3 1 0\n"; TEST(shortest_error_sat_problem, no_error) { // No errors. Should get an unsatisfiable formula. ASSERT_EQ(stim::shortest_error_sat_problem(DetectorErrorModel()), "p wcnf 1 2 3\n3 -1 0\n3 1 0\n"); } TEST(shortest_error_sat_problem, single_obs_no_dets) { std::string wcnf = stim::shortest_error_sat_problem(DetectorErrorModel(R"DEM( error(0.1) L0 )DEM")); EXPECT_EQ( wcnf, R"WDIMACS(p wcnf 1 2 3 1 -1 0 3 1 0 )WDIMACS"); } TEST(shortest_error_sat_problem, single_dets_no_obs) { std::string wcnf = stim::shortest_error_sat_problem(DetectorErrorModel(R"DEM( error(0.1) D0 )DEM")); EXPECT_EQ(wcnf, unsatisfiable_wdimacs); } TEST(shortest_error_sat_problem, no_dets_no_obs) { std::string wcnf = stim::shortest_error_sat_problem(DetectorErrorModel(R"DEM( error(0.1) )DEM")); EXPECT_EQ(wcnf, unsatisfiable_wdimacs); } TEST(shortest_error_sat_problem, no_errors) { std::string wcnf = stim::shortest_error_sat_problem(DetectorErrorModel(R"DEM()DEM")); EXPECT_EQ(wcnf, unsatisfiable_wdimacs); } TEST(shortest_error_sat_problem, single_detector_single_observable) { // To test that it ignores weights entirely, we try several combinations of weights. for (std::string dem_str : { R"DEM( error(0.1) D0 L0 error(0.1) D0 )DEM", R"DEM( error(1.0) D0 L0 error(0) D0 )DEM", R"DEM( error(0.5) D0 L0 error(0.999) D0 )DEM", R"DEM( error(0.001) D0 L0 error(0.999) D0 )DEM", R"DEM( error(0) D0 L0 error(0) D0 )DEM", R"DEM( error(0.5) D0 L0 error(0.5) D0 )DEM"}) { std::string wcnf = stim::shortest_error_sat_problem(DetectorErrorModel(dem_str.c_str())); // x_0 -- error 0 occurred // x_1 -- error 1 occurred // x_2 -- XOR of x_0 and x_1 // There should be 2 soft clauses: // soft clause NOT(x_0) with weight 1 // soft clause NOT(x_0) with weight 1 // There should be 4 hard clauses to forbid the strings such that x_2 != XOR(x_0, x_1): // hard clause x != (0, 0, 1) // hard clause x != (0, 1, 0) // hard clause x != (1, 0, 0) // hard clause x != (1, 1, 1) // Plus 1 hard clause to ensure detector is not flipped // hard clause -x_2 // Plus 1 hard clause to ensure an observable is flipped: // hard clause x_0 // This gives a total of 8 clauses // The top value should be at least 1 + 1 + 1 = 3. In our implementation ends up being 9. std::stringstream expected; // WDIMACS header format: p wcnf nbvar nbclauses top expected << "p wcnf 3 8 9\n"; // Soft clause expected << "1 -1 0\n"; // Hard clauses expected << "9 1 2 -3 0\n"; expected << "9 1 -2 3 0\n"; expected << "9 -1 2 3 0\n"; expected << "9 -1 -2 -3 0\n"; // Soft clause expected << "1 -2 0\n"; // Hard clause for the detector not to be flipped expected << "9 -3 0\n"; // Hard clause for the observable flipped expected << "9 1 0\n"; ASSERT_EQ(wcnf, expected.str()); // The optimal value of this wcnf file should be 2, but we don't have // a maxSAT solver to be able to test it here. } } TEST(likeliest_error_sat_problem, no_error) { // No errors. Should get an unsatisfiable formula. ASSERT_EQ(stim::likeliest_error_sat_problem(DetectorErrorModel()), unsatisfiable_wdimacs); } TEST(likeliest_error_sat_problem, single_detector_single_observable) { std::string wcnf = stim::likeliest_error_sat_problem(DetectorErrorModel(R"DEM( error(0.1) D0 L0 error(0.1) D0 )DEM")); // There should be 3 variables: x = (x_0, x_1, x_2) // x_0 -- error 0 occurred // x_1 -- error 1 occurred // x_2 -- XOR of x_0 and x_1 // There should be 2 soft clauses: // soft clause NOT(x_0) with weight 1 // soft clause NOT(x_0) with weight 1 // There should be 4 hard clauses to forbid the strings such that x_2 != XOR(x_0, x_1): // hard clause x != (0, 0, 1) // hard clause x != (0, 1, 0) // hard clause x != (1, 0, 0) // hard clause x != (1, 1, 1) // Plus 1 hard clause to ensure detector is not flipped // hard clause -x_2 // Plus 1 hard clause to ensure an observable is flipped: // hard clause x_0 // This gives a total of 8 clauses // The top value should be at least 1 + 1 + 1 = 3. In our implementation ends up being 9. std::stringstream expected; // WDIMACS header format: p wcnf nbvar nbclauses top expected << "p wcnf 3 8 81\n"; // Soft clause expected << "10 -1 0\n"; // Hard clauses expected << "81 1 2 -3 0\n"; expected << "81 1 -2 3 0\n"; expected << "81 -1 2 3 0\n"; expected << "81 -1 -2 -3 0\n"; // Soft clause expected << "10 -2 0\n"; // Hard clause for the detector not to be flipped expected << "81 -3 0\n"; // Hard clause for the observable flipped expected << "81 1 0\n"; ASSERT_EQ(wcnf, expected.str()); // The optimal value of this wcnf file should be 20, but we don't have // a maxSAT solver to be able to test it here. } TEST(likeliest_error_sat_problem, single_detector_single_observable_large_probability) { std::string wcnf = stim::likeliest_error_sat_problem(DetectorErrorModel(R"DEM( error(0.1) D0 L0 error(0.9) D0 )DEM")); // There should be 3 variables: x = (x_0, x_1, x_2) // x_0 -- error 0 occurred // x_1 -- error 1 occurred // x_2 -- XOR of x_0 and x_1 // There should be 2 soft clauses: // soft clause NOT(x_0) with weight 1 // soft clause NOT(x_0) with weight 1 // There should be 4 hard clauses to forbid the strings such that x_2 != XOR(x_0, x_1): // hard clause x != (0, 0, 1) // hard clause x != (0, 1, 0) // hard clause x != (1, 0, 0) // hard clause x != (1, 1, 1) // Plus 1 hard clause to ensure detector is not flipped // hard clause -x_2 // Plus 1 hard clause to ensure an observable is flipped: // hard clause x_0 // This gives a total of 8 clauses // The top value should be at least 1 + 1 + 1 = 3. In our implementation ends up being 9. std::stringstream expected; // WDIMACS header format: p wcnf nbvar nbclauses top expected << "p wcnf 3 8 81\n"; // Soft clause expected << "10 -1 0\n"; // Hard clauses expected << "81 1 2 -3 0\n"; expected << "81 1 -2 3 0\n"; expected << "81 -1 2 3 0\n"; expected << "81 -1 -2 -3 0\n"; // Soft clause for the 2nd error to flip, with weight 10. // This is because the probability of that error is 0.9. expected << "10 2 0\n"; // Hard clause for the detector not to be flipped expected << "81 -3 0\n"; // Hard clause for the observable flipped expected << "81 1 0\n"; ASSERT_EQ(wcnf, expected.str()); // The optimal value of this wcnf file should be 20, but we don't have // a maxSAT solver to be able to test it here. } TEST(likeliest_error_sat_problem, single_detector_single_observable_half_probability) { std::string wcnf = stim::likeliest_error_sat_problem(DetectorErrorModel(R"DEM( error(0.1) D0 L0 error(0.5) D0 )DEM")); // There should be 3 variables: x = (x_0, x_1, x_2) // x_0 -- error 0 occurred // x_1 -- error 1 occurred // x_2 -- XOR of x_0 and x_1 // There should be 2 soft clauses: // soft clause NOT(x_0) with weight 1 // soft clause NOT(x_0) with weight 1 // There should be 4 hard clauses to forbid the strings such that x_2 != XOR(x_0, x_1): // hard clause x != (0, 0, 1) // hard clause x != (0, 1, 0) // hard clause x != (1, 0, 0) // hard clause x != (1, 1, 1) // Plus 1 hard clause to ensure detector is not flipped // hard clause -x_2 // Plus 1 hard clause to ensure an observable is flipped: // hard clause x_0 // This gives a total of 8 clauses // The top value should be at least 1 + 1 + 1 = 3. In our implementation ends up being 9. std::stringstream expected; // WDIMACS header format: p wcnf nbvar nbclauses top expected << "p wcnf 3 7 71\n"; // Soft clause expected << "10 -1 0\n"; // Hard clauses expected << "71 1 2 -3 0\n"; expected << "71 1 -2 3 0\n"; expected << "71 -1 2 3 0\n"; expected << "71 -1 -2 -3 0\n"; // // Soft clause for the 2nd error to flip, with weight 10. // // This is because the probability of that error is 0.9. // expected << "10 2 0\n"; // Hard clause for the detector not to be flipped expected << "71 -3 0\n"; // Hard clause for the observable flipped expected << "71 1 0\n"; ASSERT_EQ(wcnf, expected.str()); // The optimal value of this wcnf file should be 20, but we don't have // a maxSAT solver to be able to test it here. } ================================================ FILE: src/stim/search/search.h ================================================ /* * 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. */ #ifndef _STIM_SEARCH_SEARCH_H #define _STIM_SEARCH_SEARCH_H #include "stim/search/graphlike/algo.h" #include "stim/search/hyper/algo.h" #include "stim/search/sat/wcnf.h" #endif ================================================ FILE: src/stim/simulators/README.md ================================================ # `simulators` directory This directory contains various quantum circuit simulators, which package up disparate functionality from elsewhere in the codebase into useful objects. ================================================ FILE: src/stim/simulators/dem_sampler.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_DEM_SAMPLER_H #define _STIM_SIMULATORS_DEM_SAMPLER_H #include #include "stim/dem/detector_error_model.h" #include "stim/io/stim_data_formats.h" #include "stim/mem/simd_bit_table.h" namespace stim { /// Performs high performance bulk sampling of a detector error model. /// /// The template parameter, W, represents the SIMD width template struct DemSampler { DetectorErrorModel model; uint64_t num_detectors; uint64_t num_observables; uint64_t num_errors; std::mt19937_64 rng; // TODO: allow these buffers to be streamed instead of entirely stored in memory. simd_bit_table det_buffer; simd_bit_table obs_buffer; simd_bit_table err_buffer; size_t num_stripes; /// Compiles a sampler for the given detector error model. DemSampler(DetectorErrorModel model, std::mt19937_64 &&rng, size_t min_stripes); /// Clears the buffers and refills them with sampled shot data. void resample(bool replay_errors); /// Ensures the internal buffers are sized for a given number of shots. void set_min_stripes(size_t min_stripes); /// Samples from the dem, writing results to files. /// /// Args: /// num_shots: The number of samples to take. /// det_out: Where to write detection event data. Set to nullptr to not write detection event data. /// det_out_format: The format to write detection event data in. /// obs_out: Where to write observable data. Set to nullptr to not write observable data. /// obs_out_format: The format to write observable data in. /// err_out: Where to write recorded error data. Set to nullptr to not write recorded error data. /// err_out_format: The format to write error data in. /// replay_err_in: If this argument is given a non-null file, error data will be read from that file /// and replayed (instead of generating new errors randomly). /// replay_err_in_format: The format to read recorded error data to replay in. void sample_write( size_t num_shots, FILE *det_out, SampleFormat det_out_format, FILE *obs_out, SampleFormat obs_out_format, FILE *err_out, SampleFormat err_out_format, FILE *replay_err_in, SampleFormat replay_err_in_format); }; } // namespace stim #include "stim/simulators/dem_sampler.inl" #endif ================================================ FILE: src/stim/simulators/dem_sampler.inl ================================================ /* * 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. */ #include #include "stim/io/measure_record_reader.h" #include "stim/io/measure_record_writer.h" #include "stim/simulators/dem_sampler.h" #include "stim/util_bot/probability_util.h" namespace stim { template DemSampler::DemSampler(DetectorErrorModel init_model, std::mt19937_64 &&rng, size_t min_stripes) : model(std::move(init_model)), num_detectors(model.count_detectors()), num_observables(model.count_observables()), num_errors(model.count_errors()), rng(rng), det_buffer((size_t)num_detectors, min_stripes), obs_buffer((size_t)num_observables, min_stripes), err_buffer((size_t)num_errors, min_stripes), num_stripes(det_buffer.num_minor_bits_padded()) { } template void DemSampler::set_min_stripes(size_t min_stripes) { size_t new_num_stripes = min_bits_to_num_bits_padded(min_stripes); if (new_num_stripes == num_stripes) { return; } det_buffer = simd_bit_table((size_t)num_detectors, min_stripes), obs_buffer = simd_bit_table((size_t)num_observables, min_stripes), err_buffer = simd_bit_table((size_t)num_errors, min_stripes), num_stripes = new_num_stripes; } template void DemSampler::resample(bool replay_errors) { det_buffer.clear(); obs_buffer.clear(); if (!replay_errors) { err_buffer.clear(); } size_t error_index = 0; model.iter_flatten_error_instructions([&](const DemInstruction &op) { simd_bits_range_ref err_row = err_buffer[error_index]; if (!replay_errors) { biased_randomize_bits((float)op.arg_data[0], err_row.u64, err_row.u64 + err_row.num_u64_padded(), rng); } for (const auto &t : op.target_data) { if (t.is_relative_detector_id()) { det_buffer[(size_t)t.raw_id()] ^= err_row; } else if (t.is_observable_id()) { obs_buffer[(size_t)t.raw_id()] ^= err_row; } } error_index++; }); } template void DemSampler::sample_write( size_t num_shots, FILE *det_out, SampleFormat det_out_format, FILE *obs_out, SampleFormat obs_out_format, FILE *err_out, SampleFormat err_out_format, FILE *err_in, SampleFormat err_in_format) { for (size_t k = 0; k < num_shots; k += num_stripes) { size_t shots_left = std::min(num_stripes, num_shots - k); if (err_in != nullptr) { size_t errors_read = read_file_data_into_shot_table( err_in, shots_left, (size_t)num_errors, err_in_format, 'M', err_buffer, false); if (errors_read != shots_left) { throw std::invalid_argument("Expected more error data for the requested number of shots."); } } resample(err_in != nullptr); if (err_out != nullptr) { write_table_data( err_out, shots_left, (size_t)num_errors, simd_bits(0), err_buffer, err_out_format, 'M', 'M', false); } if (obs_out != nullptr) { write_table_data( obs_out, shots_left, (size_t)num_observables, simd_bits(0), obs_buffer, obs_out_format, 'L', 'L', false); } if (det_out != nullptr) { write_table_data( det_out, shots_left, (size_t)num_detectors, simd_bits(0), det_buffer, det_out_format, 'D', 'D', false); } } } } // namespace stim ================================================ FILE: src/stim/simulators/dem_sampler.perf.cc ================================================ // 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. #include "dem_sampler.h" #include "stim/gen/gen_surface_code.h" #include "stim/perf.perf.h" #include "stim/simulators/error_analyzer.h" using namespace stim; BENCHMARK(DemSampler_surface_code_rotated_memory_z_distance11_100rounds_1024stripes) { auto params = CircuitGenParameters(100, 11, "rotated_memory_z"); params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.after_clifford_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, false, false, false); std::mt19937_64 rng(0); DemSampler sampler(dem, std::mt19937_64(0), 1024); size_t count = 0; benchmark_go([&]() { sampler.resample(false); count += sampler.det_buffer[0].popcnt(); count += sampler.obs_buffer[0].popcnt(); }).goal_millis(35); if (count == 0) { std::cerr << "Data dependence."; } } ================================================ FILE: src/stim/simulators/dem_sampler.pybind.cc ================================================ // 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. #include "stim/simulators/dem_sampler.pybind.h" #include "stim/io/raii_file.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" using namespace stim; using namespace stim_pybind; RaiiFile optional_py_path_to_raii_file(const pybind11::object &obj, const char *mode) { try { std::string_view path = pybind11::cast(obj); return RaiiFile(path, mode); } catch (pybind11::cast_error &ex) { } auto py_path = pybind11::module::import("pathlib").attr("Path"); if (pybind11::isinstance(obj, py_path)) { pybind11::object str_obj = pybind11::str(obj); std::string_view path = pybind11::cast(str_obj); return RaiiFile(path, mode); } return RaiiFile(nullptr); } pybind11::object dem_sampler_py_sample( DemSampler &self, size_t shots, bool bit_packed, bool return_errors, pybind11::object &recorded_errors_to_replay) { self.set_min_stripes(shots); bool replay = !recorded_errors_to_replay.is_none(); if (replay && min_bits_to_num_bits_padded(shots) != self.num_stripes) { DemSampler perfect_size(self.model, std::move(self.rng), shots); auto result = dem_sampler_py_sample(perfect_size, shots, bit_packed, return_errors, recorded_errors_to_replay); self.rng = std::move(perfect_size.rng); return result; } if (replay) { size_t out_shots; simd_bit_table converted = numpy_array_to_transposed_simd_table(recorded_errors_to_replay, self.num_errors, &out_shots); if (out_shots != shots) { throw std::invalid_argument("recorded_errors_to_replay.shape[0] != shots"); } assert(converted.num_minor_bits_padded() == self.err_buffer.num_minor_bits_padded()); assert(converted.num_major_bits_padded() == self.err_buffer.num_major_bits_padded()); self.err_buffer = std::move(converted); } self.resample(replay); pybind11::object err_out = pybind11::none(); if (return_errors) { err_out = simd_bit_table_to_numpy(self.err_buffer, self.num_errors, shots, bit_packed, true, pybind11::none()); } pybind11::object det_out = simd_bit_table_to_numpy(self.det_buffer, self.num_detectors, shots, bit_packed, true, pybind11::none()); pybind11::object obs_out = simd_bit_table_to_numpy(self.obs_buffer, self.num_observables, shots, bit_packed, true, pybind11::none()); return pybind11::make_tuple(det_out, obs_out, err_out); } pybind11::class_> stim_pybind::pybind_dem_sampler(pybind11::module &m) { return pybind11::class_>( m, "CompiledDemSampler", clean_doc_string(R"DOC( A helper class for efficiently sampler from a detector error model. Examples: >>> import stim >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) )DOC") .data()); } void stim_pybind::pybind_dem_sampler_methods(pybind11::module &m, pybind11::class_> &c) { c.def( "sample", &dem_sampler_py_sample, pybind11::arg("shots"), pybind11::kw_only(), pybind11::arg("bit_packed") = false, pybind11::arg("return_errors") = false, pybind11::arg("recorded_errors_to_replay") = pybind11::none(), clean_doc_string(R"DOC( @signature def sample(self, shots: int, *, bit_packed: bool = False, return_errors: bool = False, recorded_errors_to_replay: Optional[np.ndarray] = None) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray]]: Samples the detector error model's error mechanisms to produce sample data. Args: shots: The number of times to sample from the model. bit_packed: Defaults to false. False: the returned numpy arrays have dtype=np.bool_. True: the returned numpy arrays have dtype=np.uint8 and pack 8 bits into each byte. Setting this to True is equivalent to running `np.packbits(data, bitorder='little', axis=1)` on each output value, but has the performance benefit of the data never being expanded into an unpacked form. return_errors: Defaults to False. False: the third entry of the returned tuple is None. True: the third entry of the returned tuple is a numpy array recording which errors were sampled. recorded_errors_to_replay: Defaults to None, meaning sample errors randomly. If not None, this is expected to be a 2d numpy array specifying which errors to apply (e.g. one returned from a previous call to the sample method). The array must have dtype=np.bool_ and shape=(num_shots, num_errors) or dtype=np.uint8 and shape=(num_shots, math.ceil(num_errors / 8)). Returns: A tuple (detector_data, obs_data, error_data). Assuming bit_packed is False and return_errors is True: - If error_data[s, k] is True, then the error with index k fired in the shot with index s. - If detector_data[s, k] is True, then the detector with index k ended up flipped in the shot with index s. - If obs_data[s, k] is True, then the observable with index k ended up flipped in the shot with index s. The dtype and shape of the data depends on the arguments: if bit_packed: detector_data.shape == (num_shots, math.ceil(num_detectors / 8)) detector_data.dtype == np.uint8 obs_data.shape == (num_shots, math.ceil(num_observables / 8)) obs_data.dtype == np.uint8 if return_errors: error_data.shape = (num_shots, math.ceil(num_errors / 8)) error_data.dtype = np.uint8 else: error_data is None else: detector_data.shape == (num_shots, num_detectors) detector_data.dtype == np.bool_ obs_data.shape == (num_shots, num_observables) obs_data.dtype == np.bool_ if return_errors: error_data.shape = (num_shots, num_errors) error_data.dtype = np.bool_ else: error_data is None Note that bit packing is done using little endian order on the last axis (i.e. like `np.packbits(data, bitorder='little', axis=1)`). Examples: >>> import stim >>> import numpy as np >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(1) D1 D2 L0 ... ''') >>> sampler = dem.compile_sampler() >>> # Taking samples. >>> det_data, obs_data, err_data_not_requested = sampler.sample(shots=4) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data_not_requested is None True >>> # Recording errors. >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True) >>> det_data array([[False, True, True], [False, True, True], [False, True, True], [False, True, True]]) >>> obs_data array([[ True], [ True], [ True], [ True]]) >>> err_data array([[False, True], [False, True], [False, True], [False, True]]) >>> # Bit packing. >>> det_data, obs_data, err_data = sampler.sample( ... shots=4, ... return_errors=True, ... bit_packed=True) >>> det_data array([[6], [6], [6], [6]], dtype=uint8) >>> obs_data array([[1], [1], [1], [1]], dtype=uint8) >>> err_data array([[2], [2], [2], [2]], dtype=uint8) >>> # Recording and replaying errors. >>> noisy_dem = stim.DetectorErrorModel(''' ... error(0.125) D0 ... error(0.25) D1 ... ''') >>> noisy_sampler = noisy_dem.compile_sampler() >>> det_data, obs_data, err_data = noisy_sampler.sample( ... shots=100, ... return_errors=True) >>> replay_det_data, replay_obs_data, _ = noisy_sampler.sample( ... shots=100, ... recorded_errors_to_replay=err_data) >>> np.array_equal(det_data, replay_det_data) True >>> np.array_equal(obs_data, replay_obs_data) True )DOC") .data()); c.def( "sample_write", [](DemSampler &self, size_t shots, pybind11::object &det_out_file, std::string_view det_out_format, pybind11::object &obs_out_file, std::string_view obs_out_format, pybind11::object &err_out_file, std::string_view err_out_format, pybind11::object &replay_err_in_file, std::string_view replay_err_in_format) { RaiiFile fd = optional_py_path_to_raii_file(det_out_file, "wb"); RaiiFile fo = optional_py_path_to_raii_file(obs_out_file, "wb"); RaiiFile feo = optional_py_path_to_raii_file(err_out_file, "wb"); RaiiFile fei = optional_py_path_to_raii_file(replay_err_in_file, "rb"); self.sample_write( shots, fd.f, format_to_enum(det_out_format), fo.f, format_to_enum(obs_out_format), feo.f, format_to_enum(err_out_format), fei.f, format_to_enum(replay_err_in_format)); }, pybind11::arg("shots"), pybind11::kw_only(), pybind11::arg("det_out_file"), pybind11::arg("det_out_format") = "01", pybind11::arg("obs_out_file"), pybind11::arg("obs_out_format") = "01", pybind11::arg("err_out_file") = pybind11::none(), pybind11::arg("err_out_format") = "01", pybind11::arg("replay_err_in_file") = pybind11::none(), pybind11::arg("replay_err_in_format") = "01", clean_doc_string(R"DOC( @signature def sample_write(self, shots: int, *, det_out_file: Union[None, str, pathlib.Path], det_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', obs_out_file: Union[None, str, pathlib.Path], obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', err_out_file: Union[None, str, pathlib.Path] = None, err_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', replay_err_in_file: Union[None, str, pathlib.Path] = None, replay_err_in_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01') -> None: Samples the detector error model and writes the results to disk. Args: shots: The number of times to sample from the model. det_out_file: Where to write detection event data. If None: detection event data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase det_out_format: The format to write the detection event data in (e.g. "01" or "b8"). obs_out_file: Where to write observable flip data. If None: observable flip data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase obs_out_format: The format to write the observable flip data in (e.g. "01" or "b8"). err_out_file: Where to write errors-that-occurred data. If None: errors-that-occurred data is not written. If str or pathlib.Path: opens and overwrites the file at the given path. NOT IMPLEMENTED: io.IOBase err_out_format: The format to write the errors-that-occurred data in (e.g. "01" or "b8"). replay_err_in_file: If this is specified, errors are replayed from data instead of generated randomly. The following types are supported: - None: errors are generated randomly according to the probabilities in the detector error model. - str or pathlib.Path: the file at the given path is opened and errors-to-apply data is read from there. - io.IOBase: NOT IMPLEMENTED replay_err_in_format: The format to write the errors-that-occurred data in (e.g. "01" or "b8"). Returns: Nothing. Results are written to disk. Examples: >>> import stim >>> import tempfile >>> import pathlib >>> dem = stim.DetectorErrorModel(''' ... error(0) D0 ... error(0) D1 ... error(0) D0 ... error(1) D1 D2 L0 ... error(0) D0 ... ''') >>> sampler = dem.compile_sampler() >>> with tempfile.TemporaryDirectory() as d: ... d = pathlib.Path(d) ... sampler.sample_write( ... shots=1, ... det_out_file=d / 'dets.01', ... det_out_format='01', ... obs_out_file=d / 'obs.01', ... obs_out_format='01', ... err_out_file=d / 'err.hits', ... err_out_format='hits', ... ) ... with open(d / 'dets.01') as f: ... assert f.read() == "011\n" ... with open(d / 'obs.01') as f: ... assert f.read() == "1\n" ... with open(d / 'err.hits') as f: ... assert f.read() == "3\n" )DOC") .data()); } ================================================ FILE: src/stim/simulators/dem_sampler.pybind.h ================================================ // 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. #ifndef _STIM_SIMULATORS_DEM_SAMPLER_PYBIND_H #define _STIM_SIMULATORS_DEM_SAMPLER_PYBIND_H #include #include "stim/simulators/dem_sampler.h" namespace stim_pybind { pybind11::class_> pybind_dem_sampler(pybind11::module &m); void pybind_dem_sampler_methods(pybind11::module &m, pybind11::class_> &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/simulators/dem_sampler.test.cc ================================================ // 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. #include "stim/simulators/dem_sampler.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(DemSampler, basic_sizing, { DemSampler sampler(DetectorErrorModel(R"DEM()DEM"), std::mt19937_64(0), 700); ASSERT_EQ(sampler.det_buffer.num_major_bits_padded(), 0); ASSERT_EQ(sampler.obs_buffer.num_major_bits_padded(), 0); ASSERT_GE(sampler.det_buffer.num_minor_bits_padded(), 700); ASSERT_GE(sampler.obs_buffer.num_minor_bits_padded(), 700); sampler.resample(false); ASSERT_FALSE(sampler.obs_buffer.data.not_zero()); ASSERT_FALSE(sampler.det_buffer.data.not_zero()); sampler = DemSampler( DetectorErrorModel(R"DEM( logical_observable L2000 detector D1000 )DEM"), std::mt19937_64(0), 200); ASSERT_GE(sampler.det_buffer.num_major_bits_padded(), 1000); ASSERT_GE(sampler.obs_buffer.num_major_bits_padded(), 2000); ASSERT_GE(sampler.det_buffer.num_minor_bits_padded(), 200); ASSERT_GE(sampler.obs_buffer.num_minor_bits_padded(), 200); sampler.resample(false); ASSERT_FALSE(sampler.obs_buffer.data.not_zero()); ASSERT_FALSE(sampler.det_buffer.data.not_zero()); }) TEST_EACH_WORD_SIZE_W(DemSampler, resample_basic_probabilities, { DemSampler sampler( DetectorErrorModel(R"DEM( error(0) D0 error(0.25) D1 L0 error(0.5) D2 error(0.75) D3 error(1) D4 ^ D5 )DEM"), INDEPENDENT_TEST_RNG(), 1000); for (size_t k = 0; k < 2; k++) { sampler.resample(false); ASSERT_EQ(sampler.det_buffer[0].popcnt(), 0); ASSERT_GT(sampler.det_buffer[1].popcnt(), 0); ASSERT_LT(sampler.det_buffer[1].popcnt(), 500); ASSERT_GT(sampler.det_buffer[2].popcnt(), 250); ASSERT_LT(sampler.det_buffer[2].popcnt(), 750); ASSERT_GT(sampler.det_buffer[3].popcnt(), 500); ASSERT_LT(sampler.det_buffer[3].popcnt(), 1000); ASSERT_EQ(sampler.det_buffer[4].popcnt(), sampler.det_buffer[4].num_bits_padded()); ASSERT_EQ(sampler.det_buffer[1], sampler.obs_buffer[0]); ASSERT_EQ(sampler.det_buffer[4], sampler.det_buffer[5]); } }) TEST_EACH_WORD_SIZE_W(DemSampler, resample_combinations, { DemSampler sampler( DetectorErrorModel(R"DEM( error(0.1) D0 D1 error(0.2) D1 D2 error(0.3) D2 D0 )DEM"), INDEPENDENT_TEST_RNG(), 1000); for (size_t k = 0; k < 2; k++) { sampler.resample(false); ASSERT_GT(sampler.det_buffer[0].popcnt(), 340 - 100); ASSERT_LT(sampler.det_buffer[0].popcnt(), 340 + 100); ASSERT_GT(sampler.det_buffer[1].popcnt(), 260 - 100); ASSERT_LT(sampler.det_buffer[1].popcnt(), 260 + 100); ASSERT_GT(sampler.det_buffer[2].popcnt(), 380 - 100); ASSERT_LT(sampler.det_buffer[2].popcnt(), 380 + 100); simd_bits total = sampler.det_buffer[0]; total ^= sampler.det_buffer[1]; total ^= sampler.det_buffer[2]; ASSERT_FALSE(total.not_zero()); } }) ================================================ FILE: src/stim/simulators/dem_sampler_pybind_test.py ================================================ import numpy as np import pathlib import pytest import stim import tempfile @pytest.mark.parametrize("bit_packed", [False, True]) def test_dem_sampler_sample(bit_packed: bool): noisy_dem = stim.DetectorErrorModel(""" error(0.125) D0 error(0.25) D1 """) noisy_sampler = noisy_dem.compile_sampler() det_data, obs_data, err_data = noisy_sampler.sample(shots=100, return_errors=True, bit_packed=bit_packed) replay_det_data, replay_obs_data, _ = noisy_sampler.sample(shots=100, recorded_errors_to_replay=err_data, bit_packed=bit_packed) np.testing.assert_array_equal(det_data, replay_det_data) np.testing.assert_array_equal(obs_data, replay_obs_data) def test_dem_sampler_sampler_write(): dem = stim.DetectorErrorModel(''' error(0) D0 error(0) D1 error(0) D0 error(1) D1 D2 L0 error(0) D0 ''') sampler = dem.compile_sampler() with tempfile.TemporaryDirectory() as d: d = pathlib.Path(d) sampler.sample_write( shots=1, det_out_file=d / 'dets.01', det_out_format='01', obs_out_file=d / 'obs.01', obs_out_format='01', err_out_file=d / 'err.hits', err_out_format='hits', ) with open(d / 'dets.01') as f: assert f.read() == "011\n" with open(d / 'obs.01') as f: assert f.read() == "1\n" with open(d / 'err.hits') as f: assert f.read() == "3\n" sampler = stim.DetectorErrorModel(''' error(1) D0 # this should be overridden by the replay. error(1) D1 error(1) D0 error(1) D1 D2 L0 error(1) D0 ''').compile_sampler() sampler.sample_write( shots=1, det_out_file=d / 'dets.01', det_out_format='01', obs_out_file=d / 'obs.01', obs_out_format='01', err_out_file=d / 'err2.01', err_out_format='01', replay_err_in_file=d / 'err.hits', replay_err_in_format='hits', ) with open(d / 'dets.01') as f: assert f.read() == "011\n" with open(d / 'obs.01') as f: assert f.read() == "1\n" with open(d / 'err.hits') as f: assert f.read() == "3\n" with open(d / 'err2.01') as f: assert f.read() == "00010\n" def test_dem_sampler_actually_fills_obs_array(): dem = stim.DetectorErrorModel(''' error(1) L0 ''') sampler = dem.compile_sampler() _, obs_data, _ = sampler.sample(shots=10000) assert np.all(obs_data) ================================================ FILE: src/stim/simulators/error_analyzer.cc ================================================ // 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. #include "stim/simulators/error_analyzer.h" #include #include #include #include "stim/circuit/gate_decomposition.h" #include "stim/stabilizers/pauli_string.h" #include "stim/util_bot/error_decomp.h" using namespace stim; void ErrorAnalyzer::undo_gate(const CircuitInstruction &inst) { switch (inst.gate_type) { case GateType::DETECTOR: undo_DETECTOR(inst); break; case GateType::OBSERVABLE_INCLUDE: undo_OBSERVABLE_INCLUDE(inst); break; case GateType::TICK: undo_TICK(inst); break; case GateType::QUBIT_COORDS: undo_I(inst); break; case GateType::SHIFT_COORDS: undo_SHIFT_COORDS(inst); break; case GateType::REPEAT: undo_I(inst); break; case GateType::MX: undo_MX(inst); break; case GateType::MY: undo_MY(inst); break; case GateType::M: undo_MZ(inst); break; case GateType::MRX: undo_MRX(inst); break; case GateType::MRY: undo_MRY(inst); break; case GateType::MR: undo_MRZ(inst); break; case GateType::HERALDED_ERASE: undo_HERALDED_ERASE(inst); break; case GateType::HERALDED_PAULI_CHANNEL_1: undo_HERALDED_PAULI_CHANNEL_1(inst); break; case GateType::RX: undo_RX(inst); break; case GateType::RY: undo_RY(inst); break; case GateType::R: undo_RZ(inst); break; case GateType::MPP: undo_MPP(inst); break; case GateType::SPP: case GateType::SPP_DAG: undo_SPP(inst); break; case GateType::MPAD: undo_MPAD(inst); break; case GateType::MXX: undo_MXX(inst); break; case GateType::MYY: undo_MYY(inst); break; case GateType::MZZ: undo_MZZ(inst); break; case GateType::XCX: undo_XCX(inst); break; case GateType::XCY: undo_XCY(inst); break; case GateType::XCZ: undo_XCZ(inst); break; case GateType::YCX: undo_YCX(inst); break; case GateType::YCY: undo_YCY(inst); break; case GateType::YCZ: undo_YCZ(inst); break; case GateType::CX: undo_ZCX(inst); break; case GateType::CY: undo_ZCY(inst); break; case GateType::CZ: undo_ZCZ(inst); break; case GateType::DEPOLARIZE1: undo_DEPOLARIZE1(inst); break; case GateType::DEPOLARIZE2: undo_DEPOLARIZE2(inst); break; case GateType::X_ERROR: undo_X_ERROR(inst); break; case GateType::Y_ERROR: undo_Y_ERROR(inst); break; case GateType::Z_ERROR: undo_Z_ERROR(inst); break; case GateType::PAULI_CHANNEL_1: undo_PAULI_CHANNEL_1(inst); break; case GateType::PAULI_CHANNEL_2: undo_PAULI_CHANNEL_2(inst); break; case GateType::E: undo_CORRELATED_ERROR(inst); break; case GateType::ELSE_CORRELATED_ERROR: undo_ELSE_CORRELATED_ERROR(inst); break; case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: case GateType::X: case GateType::Y: case GateType::Z: undo_I(inst); break; case GateType::C_XYZ: case GateType::C_NXYZ: case GateType::C_XNYZ: case GateType::C_XYNZ: undo_C_XYZ(inst); break; case GateType::C_ZYX: case GateType::C_NZYX: case GateType::C_ZNYX: case GateType::C_ZYNX: undo_C_ZYX(inst); break; case GateType::H_YZ: case GateType::SQRT_X: case GateType::SQRT_X_DAG: case GateType::H_NYZ: undo_H_YZ(inst); break; case GateType::SQRT_Y: case GateType::SQRT_Y_DAG: case GateType::H: case GateType::H_NXZ: undo_H_XZ(inst); break; case GateType::S: case GateType::S_DAG: case GateType::H_XY: case GateType::H_NXY: undo_H_XY(inst); break; case GateType::SQRT_XX: case GateType::SQRT_XX_DAG: undo_SQRT_XX(inst); break; case GateType::SQRT_YY: case GateType::SQRT_YY_DAG: undo_SQRT_YY(inst); break; case GateType::SQRT_ZZ: case GateType::SQRT_ZZ_DAG: undo_SQRT_ZZ(inst); break; case GateType::SWAP: undo_SWAP(inst); break; case GateType::ISWAP: case GateType::ISWAP_DAG: undo_ISWAP(inst); break; case GateType::CXSWAP: undo_CXSWAP(inst); break; case GateType::CZSWAP: undo_CZSWAP(inst); break; case GateType::SWAPCX: undo_SWAPCX(inst); break; default: throw std::invalid_argument( "Not implemented by ErrorAnalyzer::undo_gate: " + std::string(GATE_DATA[inst.gate_type].name)); } } void ErrorAnalyzer::remove_gauge(SpanRef sorted) { if (sorted.empty()) { return; } const auto &max = sorted.back(); // HACK: linear overhead due to not keeping an index of which detectors used where. for (auto &x : tracker.xs) { if (x.contains(max)) { x.xor_sorted_items(sorted); } } for (auto &z : tracker.zs) { if (z.contains(max)) { z.xor_sorted_items(sorted); } } } void ErrorAnalyzer::undo_RX(const CircuitInstruction &dat) { undo_RX_with_context(dat, "an X-basis reset (RX)"); } void ErrorAnalyzer::undo_RY(const CircuitInstruction &dat) { undo_RY_with_context(dat, "an X-basis reset (RY)"); } void ErrorAnalyzer::undo_RZ(const CircuitInstruction &dat) { undo_RZ_with_context(dat, "a Z-basis reset (R)"); } void ErrorAnalyzer::undo_RX_with_context(const CircuitInstruction &dat, const char *context_op) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); check_for_gauge(tracker.zs[q], context_op, q, dat.tag); tracker.xs[q].clear(); tracker.zs[q].clear(); } } void ErrorAnalyzer::undo_RY_with_context(const CircuitInstruction &inst, const char *context_op) { for (size_t k = inst.targets.size(); k-- > 0;) { auto q = inst.targets[k].qubit_value(); check_for_gauge(tracker.xs[q], tracker.zs[q], context_op, q, inst.tag); tracker.xs[q].clear(); tracker.zs[q].clear(); } } void ErrorAnalyzer::undo_RZ_with_context(const CircuitInstruction &inst, const char *context_op) { for (size_t k = inst.targets.size(); k-- > 0;) { auto q = inst.targets[k].qubit_value(); check_for_gauge(tracker.xs[q], context_op, q, inst.tag); tracker.xs[q].clear(); tracker.zs[q].clear(); } } void ErrorAnalyzer::undo_MX_with_context(const CircuitInstruction &inst, const char *context_op) { for (size_t k = inst.targets.size(); k-- > 0;) { auto q = inst.targets[k].qubit_value(); tracker.num_measurements_in_past--; SparseXorVec &d = tracker.rec_bits[tracker.num_measurements_in_past]; xor_sorted_measurement_error(d.range(), inst); tracker.xs[q].xor_sorted_items(d.range()); check_for_gauge(tracker.zs[q], context_op, q, inst.tag); tracker.rec_bits.erase(tracker.num_measurements_in_past); } } void ErrorAnalyzer::undo_MY_with_context(const CircuitInstruction &inst, const char *context_op) { for (size_t k = inst.targets.size(); k-- > 0;) { auto q = inst.targets[k].qubit_value(); tracker.num_measurements_in_past--; SparseXorVec &d = tracker.rec_bits[tracker.num_measurements_in_past]; xor_sorted_measurement_error(d.range(), inst); tracker.xs[q].xor_sorted_items(d.range()); tracker.zs[q].xor_sorted_items(d.range()); check_for_gauge(tracker.xs[q], tracker.zs[q], context_op, q, inst.tag); tracker.rec_bits.erase(tracker.num_measurements_in_past); } } void ErrorAnalyzer::undo_MZ_with_context(const CircuitInstruction &inst, const char *context_op) { for (size_t k = inst.targets.size(); k-- > 0;) { auto q = inst.targets[k].qubit_value(); tracker.num_measurements_in_past--; SparseXorVec &d = tracker.rec_bits[tracker.num_measurements_in_past]; xor_sorted_measurement_error(d.range(), inst); tracker.zs[q].xor_sorted_items(d.range()); check_for_gauge(tracker.xs[q], context_op, q, inst.tag); tracker.rec_bits.erase(tracker.num_measurements_in_past); } } void ErrorAnalyzer::undo_HERALDED_ERASE(const CircuitInstruction &inst) { check_can_approximate_disjoint("HERALDED_ERASE", inst.args, false); double p = inst.args[0] * 0.25; double i = std::max(0.0, 1.0 - 4 * p); for (size_t k = inst.targets.size(); k-- > 0;) { auto q = inst.targets[k].qubit_value(); tracker.num_measurements_in_past--; SparseXorVec &herald_symptoms = tracker.rec_bits[tracker.num_measurements_in_past]; if (accumulate_errors) { add_error_combinations<3>( {i, 0, 0, 0, p, p, p, p}, {tracker.xs[q].range(), tracker.zs[q].range(), herald_symptoms.range()}, true, inst.tag); } tracker.rec_bits.erase(tracker.num_measurements_in_past); } } void ErrorAnalyzer::undo_HERALDED_PAULI_CHANNEL_1(const CircuitInstruction &inst) { check_can_approximate_disjoint("HERALDED_PAULI_CHANNEL_1", inst.args, true); double hi = inst.args[0]; double hx = inst.args[1]; double hy = inst.args[2]; double hz = inst.args[3]; double i = std::max(0.0, 1.0 - hi - hx - hy - hz); for (size_t k = inst.targets.size(); k-- > 0;) { auto q = inst.targets[k].qubit_value(); tracker.num_measurements_in_past--; SparseXorVec &herald_symptoms = tracker.rec_bits[tracker.num_measurements_in_past]; if (accumulate_errors) { add_error_combinations<3>( {i, 0, 0, 0, hi, hz, hx, hy}, {tracker.xs[q].range(), tracker.zs[q].range(), herald_symptoms.range()}, true, inst.tag); } tracker.rec_bits.erase(tracker.num_measurements_in_past); } } void ErrorAnalyzer::undo_MPAD(const CircuitInstruction &inst) { for (size_t k = inst.targets.size(); k-- > 0;) { tracker.num_measurements_in_past--; SparseXorVec &d = tracker.rec_bits[tracker.num_measurements_in_past]; xor_sorted_measurement_error(d.range(), inst); tracker.rec_bits.erase(tracker.num_measurements_in_past); } } void ErrorAnalyzer::check_for_gauge( SparseXorVec &potential_gauge_summand_1, const SparseXorVec &potential_gauge_summand_2, const char *context_op, uint64_t context_qubit, std::string_view tag) { if (potential_gauge_summand_1 == potential_gauge_summand_2) { return; } potential_gauge_summand_1 ^= potential_gauge_summand_2; check_for_gauge(potential_gauge_summand_1, context_op, context_qubit, tag); potential_gauge_summand_1 ^= potential_gauge_summand_2; } // This is redundant with comma_sep from str_util.h, but for reasons I can't figure out // (something to do with a dependency cycle involving templates) the compilation fails // if I use that one. template std::string comma_sep_workaround(const TIter &iterable) { std::stringstream out; bool first = true; for (const auto &t : iterable) { if (first) { first = false; } else { out << ", "; } out << t; } return out.str(); } void ErrorAnalyzer::check_for_gauge( const SparseXorVec &potential_gauge, const char *context_op, uint64_t context_qubit, std::string_view tag) { if (potential_gauge.empty()) { return; } bool has_observables = false; bool has_detectors = false; for (const auto &t : potential_gauge) { has_observables |= t.is_observable_id(); has_detectors |= t.is_relative_detector_id(); } if (allow_gauge_detectors && !has_observables) { remove_gauge(add_error(0.5, potential_gauge.range(), tag).targets); return; } // We are now in an error condition, and it's a bit hard to debug for the user. // The goal is to collect a *lot* of information that might be useful to them. std::stringstream error_msg; has_detectors &= !allow_gauge_detectors; if (has_observables) { error_msg << "The circuit contains non-deterministic observables.\n"; } if (has_detectors) { error_msg << "The circuit contains non-deterministic detectors.\n"; } size_t range_start = num_ticks_in_past - std::min((size_t)num_ticks_in_past, size_t{5}); size_t range_end = num_ticks_in_past + 5; error_msg << "\nTo make an SVG picture of the problem, you can use the python API like this:\n "; error_msg << "your_circuit.diagram('detslice-with-ops-svg'"; error_msg << ", tick=range(" << range_start << ", " << range_end << ")"; error_msg << ", filter_coords=["; for (auto d : potential_gauge) { error_msg << "'" << d << "', "; } error_msg << "])"; error_msg << "\nor the command line API like this:\n "; error_msg << "stim diagram --in your_circuit_file.stim"; error_msg << " --type detslice-with-ops-svg"; error_msg << " --tick " << range_start << ":" << range_end; error_msg << " --filter_coords "; for (size_t k = 0; k < potential_gauge.size(); k++) { if (k) { error_msg << ':'; } error_msg << potential_gauge.sorted_items[k]; } error_msg << " > output_image.svg\n"; std::map> qubit_coords_map; if (current_circuit_being_analyzed != nullptr) { qubit_coords_map = current_circuit_being_analyzed->get_final_qubit_coords(); } auto error_msg_qubit_with_coords = [&](uint64_t q, uint8_t p) { error_msg << "\n"; auto qubit_coords = qubit_coords_map[q]; if (p == 0) { error_msg << " qubit " << q; } else if (p == 1) { error_msg << " X" << q; } else if (p == 2) { error_msg << " Z" << q; } else if (p == 3) { error_msg << " Y" << q; } if (!qubit_coords.empty()) { error_msg << " [coords (" << comma_sep_workaround(qubit_coords) << ")]"; } }; error_msg << "\n"; error_msg << "This was discovered while analyzing " << context_op << " on:"; error_msg_qubit_with_coords(context_qubit, 0); error_msg << "\n\n"; error_msg << "The collapse anti-commuted with these detectors/observables:"; for (const auto &t : potential_gauge) { error_msg << "\n " << t; // Try to find recorded coordinate information for the detector. if (t.is_relative_detector_id() && current_circuit_being_analyzed != nullptr) { auto coords = current_circuit_being_analyzed->coords_of_detector(t.raw_id()); if (!coords.empty()) { error_msg << " [coords (" << comma_sep_workaround(coords) << ")]"; } } } for (const auto &t : potential_gauge) { if (t.is_relative_detector_id() && allow_gauge_detectors) { continue; } error_msg << "\n\n"; error_msg << "The backward-propagating error sensitivity for " << t << " was:"; auto sensitivity = current_error_sensitivity_for(t); sensitivity.ref().for_each_active_pauli([&](size_t q) { uint8_t p = sensitivity.xs[q] + sensitivity.zs[q] * 2; error_msg_qubit_with_coords(q, p); }); } throw std::invalid_argument(error_msg.str()); } PauliString ErrorAnalyzer::current_error_sensitivity_for(DemTarget t) const { PauliString result(tracker.xs.size()); for (size_t q = 0; q < tracker.xs.size(); q++) { result.xs[q] = std::ranges::find(tracker.xs[q], t) != tracker.xs[q].end(); result.zs[q] = std::ranges::find(tracker.zs[q], t) != tracker.zs[q].end(); } return result; } void ErrorAnalyzer::xor_sorted_measurement_error(SpanRef targets, const CircuitInstruction &inst) { // Measurement error. if (!inst.args.empty() && inst.args[0] > 0) { add_error(inst.args[0], targets, inst.tag); } } void ErrorAnalyzer::undo_MX(const CircuitInstruction &dat) { undo_MX_with_context(dat, "an X-basis measurement (MX)"); } void ErrorAnalyzer::undo_MY(const CircuitInstruction &dat) { undo_MY_with_context(dat, "a Y-basis measurement (MY)"); } void ErrorAnalyzer::undo_MZ(const CircuitInstruction &dat) { undo_MZ_with_context(dat, "a Z-basis measurement (M)"); } void ErrorAnalyzer::undo_MRX(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k]; undo_RX_with_context({GateType::RX, dat.args, &q, dat.tag}, "an X-basis demolition measurement (MRX)"); undo_MX_with_context({GateType::MX, dat.args, &q, dat.tag}, "an X-basis demolition measurement (MRX)"); } } void ErrorAnalyzer::undo_MRY(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k]; undo_RY_with_context({GateType::RY, dat.args, &q, dat.tag}, "a Y-basis demolition measurement (MRY)"); undo_MY_with_context({GateType::MY, dat.args, &q, dat.tag}, "a Y-basis demolition measurement (MRY)"); } } void ErrorAnalyzer::undo_MRZ(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k]; undo_RZ_with_context({GateType::R, dat.args, &q, dat.tag}, "a Z-basis demolition measurement (MR)"); undo_MZ_with_context({GateType::M, dat.args, &q, dat.tag}, "a Z-basis demolition measurement (MR)"); } } void ErrorAnalyzer::undo_H_XZ(const CircuitInstruction &dat) { tracker.undo_H_XZ(dat); } void ErrorAnalyzer::undo_H_XY(const CircuitInstruction &dat) { tracker.undo_H_XY(dat); } void ErrorAnalyzer::undo_H_YZ(const CircuitInstruction &dat) { tracker.undo_H_YZ(dat); } void ErrorAnalyzer::undo_C_XYZ(const CircuitInstruction &dat) { tracker.undo_C_XYZ(dat); } void ErrorAnalyzer::undo_C_ZYX(const CircuitInstruction &dat) { tracker.undo_C_ZYX(dat); } void ErrorAnalyzer::undo_XCX(const CircuitInstruction &dat) { tracker.undo_XCX(dat); } void ErrorAnalyzer::undo_XCY(const CircuitInstruction &dat) { tracker.undo_XCY(dat); } void ErrorAnalyzer::undo_YCX(const CircuitInstruction &dat) { tracker.undo_YCX(dat); } void ErrorAnalyzer::undo_ZCY(const CircuitInstruction &dat) { tracker.undo_ZCY(dat); } void ErrorAnalyzer::undo_YCZ(const CircuitInstruction &dat) { tracker.undo_YCZ(dat); } void ErrorAnalyzer::undo_YCY(const CircuitInstruction &dat) { tracker.undo_YCY(dat); } void ErrorAnalyzer::undo_ZCX(const CircuitInstruction &dat) { tracker.undo_ZCX(dat); } void ErrorAnalyzer::undo_XCZ(const CircuitInstruction &dat) { tracker.undo_XCZ(dat); } void ErrorAnalyzer::undo_ZCZ(const CircuitInstruction &dat) { tracker.undo_ZCZ(dat); } void ErrorAnalyzer::undo_TICK(const CircuitInstruction &dat) { num_ticks_in_past--; } void ErrorAnalyzer::undo_SQRT_XX(const CircuitInstruction &dat) { tracker.undo_SQRT_XX(dat); } void ErrorAnalyzer::undo_SQRT_YY(const CircuitInstruction &dat) { tracker.undo_SQRT_YY(dat); } void ErrorAnalyzer::undo_SQRT_ZZ(const CircuitInstruction &dat) { tracker.undo_SQRT_ZZ(dat); } void ErrorAnalyzer::undo_I(const CircuitInstruction &dat) { } void ErrorAnalyzer::undo_SWAP(const CircuitInstruction &dat) { tracker.undo_SWAP(dat); } void ErrorAnalyzer::undo_ISWAP(const CircuitInstruction &dat) { tracker.undo_ISWAP(dat); } void ErrorAnalyzer::undo_CXSWAP(const CircuitInstruction &dat) { tracker.undo_CXSWAP(dat); } void ErrorAnalyzer::undo_CZSWAP(const CircuitInstruction &dat) { tracker.undo_CZSWAP(dat); } void ErrorAnalyzer::undo_SWAPCX(const CircuitInstruction &dat) { tracker.undo_SWAPCX(dat); } void ErrorAnalyzer::undo_DETECTOR(const CircuitInstruction &dat) { tracker.undo_DETECTOR(dat); auto id = DemTarget::relative_detector_id(tracker.num_detectors_in_past); flushed_reversed_model.append_detector_instruction(dat.args, id, dat.tag); } void ErrorAnalyzer::undo_OBSERVABLE_INCLUDE(const CircuitInstruction &dat) { tracker.undo_OBSERVABLE_INCLUDE(dat); auto id = DemTarget::observable_id((int32_t)dat.args[0]); flushed_reversed_model.append_logical_observable_instruction(id, dat.tag); } ErrorAnalyzer::ErrorAnalyzer( uint64_t num_measurements, uint64_t num_detectors, size_t num_qubits, uint64_t num_ticks, bool decompose_errors, bool fold_loops, bool allow_gauge_detectors, double approximate_disjoint_errors_threshold, bool ignore_decomposition_failures, bool block_decomposition_from_introducing_remnant_edges) : tracker(num_qubits, num_measurements, num_detectors), decompose_errors(decompose_errors), accumulate_errors(true), fold_loops(fold_loops), allow_gauge_detectors(allow_gauge_detectors), approximate_disjoint_errors_threshold(approximate_disjoint_errors_threshold), ignore_decomposition_failures(ignore_decomposition_failures), block_decomposition_from_introducing_remnant_edges(block_decomposition_from_introducing_remnant_edges), num_ticks_in_past(num_ticks) { } void ErrorAnalyzer::undo_circuit(const Circuit &circuit) { std::vector stacked_else_correlated_errors; for (size_t k = circuit.operations.size(); k--;) { const auto &op = circuit.operations[k]; try { if (op.gate_type == GateType::ELSE_CORRELATED_ERROR) { stacked_else_correlated_errors.push_back(op); } else if (op.gate_type == GateType::E) { stacked_else_correlated_errors.push_back(op); correlated_error_block(stacked_else_correlated_errors); stacked_else_correlated_errors.clear(); } else if (!stacked_else_correlated_errors.empty()) { throw std::invalid_argument( "ELSE_CORRELATED_ERROR wasn't preceded by ELSE_CORRELATED_ERROR or CORRELATED_ERROR (E)"); } else if (op.gate_type == GateType::REPEAT) { const auto &loop_body = op.repeat_block_body(circuit); uint64_t repeats = op.repeat_block_rep_count(); run_loop(loop_body, repeats, op.tag); } else { undo_gate(op); } } catch (std::invalid_argument &ex) { std::stringstream error_msg; std::string body = ex.what(); const char *marker = "\n\nCircuit stack trace:\n at instruction"; size_t p = body.find(marker); if (p == std::string::npos) { error_msg << body; } else { error_msg << body.substr(0, p); } error_msg << "\n\nCircuit stack trace:"; if (&circuit == current_circuit_being_analyzed) { auto total_ticks = circuit.count_ticks(); if (total_ticks) { uint64_t current_tick = num_ticks_in_past; error_msg << "\n during TICK layer #" << (current_tick + 1) << " of " << (total_ticks + 1); } } error_msg << '\n' << circuit.describe_instruction_location(k); if (p != std::string::npos) { error_msg << "\n at block's instruction" << body.substr(p + strlen(marker)); } throw std::invalid_argument(error_msg.str()); } } if (!stacked_else_correlated_errors.empty()) { throw std::invalid_argument( "ELSE_CORRELATED_ERROR wasn't preceded by ELSE_CORRELATED_ERROR or CORRELATED_ERROR (E)"); } } void ErrorAnalyzer::post_check_initialization() { for (uint32_t q = 0; q < tracker.xs.size(); q++) { check_for_gauge(tracker.xs[q], "qubit initialization into |0> at the start of the circuit", q, ""); } } void ErrorAnalyzer::undo_X_ERROR(const CircuitInstruction &inst) { if (!accumulate_errors) { return; } for (auto q : inst.targets) { add_error(inst.args[0], tracker.zs[q.data].range(), inst.tag); } } void ErrorAnalyzer::undo_Y_ERROR(const CircuitInstruction &inst) { if (!accumulate_errors) { return; } for (auto q : inst.targets) { add_xored_error(inst.args[0], tracker.xs[q.data].range(), tracker.zs[q.data].range(), inst.tag); } } void ErrorAnalyzer::undo_Z_ERROR(const CircuitInstruction &inst) { if (!accumulate_errors) { return; } for (auto q : inst.targets) { add_error(inst.args[0], tracker.xs[q.data].range(), inst.tag); } } template inline void inplace_xor_tail(MonotonicBuffer &dst, const SparseXorVec &src) { SpanRef in1 = dst.tail; SpanRef in2 = src.range(); xor_merge_sort_temp_buffer_callback(in1, in2, [&](SpanRef result) { dst.discard_tail(); dst.append_tail(result); }); } void ErrorAnalyzer::add_composite_error(double probability, SpanRef targets, std::string_view tag) { if (!accumulate_errors) { return; } for (auto qp : targets) { auto q = qp.qubit_value(); if (qp.data & TARGET_PAULI_Z_BIT) { inplace_xor_tail(mono_buf, tracker.xs[q]); } if (qp.data & TARGET_PAULI_X_BIT) { inplace_xor_tail(mono_buf, tracker.zs[q]); } } add_error_in_sorted_jagged_tail(probability, tag); } void ErrorAnalyzer::correlated_error_block(const std::vector &dats) { assert(!dats.empty()); if (dats.size() == 1) { add_composite_error(dats[0].args[0], dats[0].targets, dats[0].tag); return; } check_can_approximate_disjoint("ELSE_CORRELATED_ERROR", {}, false); double remaining_p = 1; for (size_t k = dats.size(); k--;) { CircuitInstruction dat = dats[k]; double actual_p = dat.args[0] * remaining_p; remaining_p *= 1 - dat.args[0]; if (actual_p > approximate_disjoint_errors_threshold) { throw std::invalid_argument( "CORRELATED_ERROR/ELSE_CORRELATED_ERROR block has a component probability '" + std::to_string(actual_p) + "' larger than the " "`approximate_disjoint_errors` threshold of " "'" + std::to_string(approximate_disjoint_errors_threshold) + "'."); } add_composite_error(actual_p, dat.targets, dat.tag); } } void ErrorAnalyzer::undo_CORRELATED_ERROR(const CircuitInstruction &inst) { add_composite_error(inst.args[0], inst.targets, inst.tag); } void ErrorAnalyzer::undo_DEPOLARIZE1(const CircuitInstruction &inst) { if (!accumulate_errors) { return; } if (inst.args[0] > 0.75) { throw std::invalid_argument("Can't analyze over-mixing DEPOLARIZE1 errors (probability > 3/4)."); } double p = depolarize1_probability_to_independent_per_channel_probability(inst.args[0]); for (auto q : inst.targets) { add_error_combinations<2>( {0, p, p, p}, { tracker.xs[q.data].range(), tracker.zs[q.data].range(), }, false, inst.tag); } } void ErrorAnalyzer::undo_DEPOLARIZE2(const CircuitInstruction &inst) { if (!accumulate_errors) { return; } if (inst.args[0] > 15.0 / 16.0) { throw std::invalid_argument("Can't analyze over-mixing DEPOLARIZE2 errors (probability > 15/16)."); } double p = depolarize2_probability_to_independent_per_channel_probability(inst.args[0]); for (size_t i = 0; i < inst.targets.size(); i += 2) { auto a = inst.targets[i]; auto b = inst.targets[i + 1]; add_error_combinations<4>( {0, p, p, p, p, p, p, p, p, p, p, p, p, p, p, p}, { tracker.xs[a.data].range(), tracker.zs[a.data].range(), tracker.xs[b.data].range(), tracker.zs[b.data].range(), }, false, inst.tag); } } void ErrorAnalyzer::undo_ELSE_CORRELATED_ERROR(const CircuitInstruction &dat) { if (accumulate_errors) { throw std::invalid_argument("Failed to analyze ELSE_CORRELATED_ERROR: " + dat.str()); } } void ErrorAnalyzer::check_can_approximate_disjoint( const char *op_name, SpanRef probabilities, bool allow_single_component) const { if (allow_single_component) { size_t num_specified = 0; for (double p : probabilities) { num_specified += p > 0; } if (num_specified <= 1) { return; } } if (approximate_disjoint_errors_threshold == 0) { std::stringstream msg; msg << "Encountered the operation " << op_name << " during error analysis, but this operation requires the `approximate_disjoint_errors` option to be " "enabled."; msg << "\nIf you're calling from python, using stim.Circuit.detector_error_model, you need to add the " "argument approximate_disjoint_errors=True.\n"; msg << "\nIf you're calling from the command line, you need to specify --approximate_disjoint_errors."; throw std::invalid_argument(msg.str()); } for (double p : probabilities) { if (p > approximate_disjoint_errors_threshold) { std::stringstream msg; msg << op_name; msg << " has a probability argument ("; msg << p; msg << ") larger than the `approximate_disjoint_errors` threshold ("; msg << approximate_disjoint_errors_threshold; msg << +")."; throw std::invalid_argument(msg.str()); } } } void ErrorAnalyzer::undo_PAULI_CHANNEL_1(const CircuitInstruction &inst) { double dx = inst.args[0]; double dy = inst.args[1]; double dz = inst.args[2]; double ix; double iy; double iz; bool is_independent = try_disjoint_to_independent_xyz_errors_approx(dx, dy, dz, &ix, &iy, &iz); if (!is_independent) { check_can_approximate_disjoint("PAULI_CHANNEL_1", inst.args, true); ix = dx; iy = dy; iz = dz; } if (!accumulate_errors) { return; } for (auto q : inst.targets) { add_error_combinations<2>( {0, ix, iz, iy}, { tracker.zs[q.data].range(), tracker.xs[q.data].range(), }, !is_independent, inst.tag); } } void ErrorAnalyzer::undo_PAULI_CHANNEL_2(const CircuitInstruction &inst) { check_can_approximate_disjoint("PAULI_CHANNEL_2", inst.args, true); std::array probabilities; for (size_t k = 0; k < 15; k++) { size_t k2 = pauli_xyz_to_xz((k + 1) & 3) | (pauli_xyz_to_xz(((k + 1) >> 2) & 3) << 2); probabilities[k2] = inst.args[k]; } if (!accumulate_errors) { return; } for (size_t i = 0; i < inst.targets.size(); i += 2) { auto a = inst.targets[i]; auto b = inst.targets[i + 1]; add_error_combinations<4>( probabilities, { tracker.zs[b.data].range(), tracker.xs[b.data].range(), tracker.zs[a.data].range(), tracker.xs[a.data].range(), }, true, inst.tag); } } DetectorErrorModel unreversed(const DetectorErrorModel &rev, uint64_t &base_detector_id, std::set &seen) { DetectorErrorModel out; auto conv_append = [&](const DemInstruction &e) { auto stored_targets = out.target_buf.take_copy(e.target_data); auto stored_args = out.arg_buf.take_copy(e.arg_data); auto stored_tag = out.tag_buf.take_copy(e.tag); for (auto &t : stored_targets) { t.shift_if_detector_id(-(int64_t)base_detector_id); } out.instructions.push_back( DemInstruction{ .arg_data = stored_args, .target_data = stored_targets, .tag = stored_tag, .type = e.type, }); }; for (auto p = rev.instructions.crbegin(); p != rev.instructions.crend(); p++) { const auto &e = *p; switch (e.type) { case DemInstructionType::DEM_SHIFT_DETECTORS: base_detector_id += e.target_data[0].data; out.append_shift_detectors_instruction(e.arg_data, e.target_data[0].data, e.tag); break; case DemInstructionType::DEM_ERROR: for (auto &t : e.target_data) { seen.insert(t); } conv_append(e); break; case DemInstructionType::DEM_DETECTOR: case DemInstructionType::DEM_LOGICAL_OBSERVABLE: if (!e.arg_data.empty() || !e.tag.empty() || !seen.contains(e.target_data[0])) { conv_append(e); } break; case DemInstructionType::DEM_REPEAT_BLOCK: { uint64_t repetitions = e.repeat_block_rep_count(); if (repetitions) { uint64_t old_base_detector_id = base_detector_id; out.append_repeat_block( e.repeat_block_rep_count(), unreversed(e.repeat_block_body(rev), base_detector_id, seen), e.tag); uint64_t loop_shift = base_detector_id - old_base_detector_id; base_detector_id += loop_shift * (repetitions - 1); } } break; default: throw std::invalid_argument("Unknown instruction type in 'unreversed'."); } } return out; } DetectorErrorModel ErrorAnalyzer::circuit_to_detector_error_model( const Circuit &circuit, bool decompose_errors, bool fold_loops, bool allow_gauge_detectors, double approximate_disjoint_errors_threshold, bool ignore_decomposition_failures, bool block_decomposition_from_introducing_remnant_edges) { ErrorAnalyzer analyzer( circuit.count_measurements(), circuit.count_detectors(), circuit.count_qubits(), circuit.count_ticks(), decompose_errors, fold_loops, allow_gauge_detectors, approximate_disjoint_errors_threshold, ignore_decomposition_failures, block_decomposition_from_introducing_remnant_edges); analyzer.current_circuit_being_analyzed = &circuit; analyzer.undo_circuit(circuit); analyzer.post_check_initialization(); analyzer.flush(); uint64_t t = 0; std::set seen; return unreversed(analyzer.flushed_reversed_model, t, seen); } void ErrorAnalyzer::flush() { do_global_error_decomposition_pass(); for (auto kv = error_class_probabilities.crbegin(); kv != error_class_probabilities.crend(); kv++) { const ErrorEquivalenceClass &key = kv->first; const double &probability = kv->second; if (key.targets.empty() || probability == 0) { continue; } flushed_reversed_model.append_error_instruction(probability, key.targets, key.tag); } error_class_probabilities.clear(); } ErrorEquivalenceClass ErrorAnalyzer::add_xored_error( double probability, SpanRef flipped1, SpanRef flipped2, std::string_view tag) { mono_buf.ensure_available(flipped1.size() + flipped2.size()); mono_buf.tail.ptr_end = xor_merge_sort(flipped1, flipped2, mono_buf.tail.ptr_end); return add_error_in_sorted_jagged_tail(probability, tag); } ErrorEquivalenceClass ErrorAnalyzer::mono_dedupe_store_tail(std::string_view tag) { auto v = error_class_probabilities.find(ErrorEquivalenceClass{mono_buf.tail, tag}); if (v != error_class_probabilities.end()) { mono_buf.discard_tail(); return v->first; } auto result = ErrorEquivalenceClass{mono_buf.commit_tail(), tag}; error_class_probabilities.insert({result, 0}); return result; } ErrorEquivalenceClass ErrorAnalyzer::mono_dedupe_store(ErrorEquivalenceClass sorted) { auto v = error_class_probabilities.find(sorted); if (v != error_class_probabilities.end()) { return v->first; } mono_buf.append_tail(sorted.targets); auto result = ErrorEquivalenceClass{mono_buf.commit_tail(), sorted.tag}; error_class_probabilities.insert({result, 0}); return result; } ErrorEquivalenceClass ErrorAnalyzer::add_error( double probability, SpanRef flipped_sorted, std::string_view tag) { auto key = mono_dedupe_store(ErrorEquivalenceClass{flipped_sorted, tag}); auto &old_p = error_class_probabilities[key]; old_p = old_p * (1 - probability) + (1 - old_p) * probability; return key; } ErrorEquivalenceClass ErrorAnalyzer::add_error_in_sorted_jagged_tail(double probability, std::string_view tag) { auto key = mono_dedupe_store_tail(tag); auto &old_p = error_class_probabilities[key]; old_p = old_p * (1 - probability) + (1 - old_p) * probability; return key; } void ErrorAnalyzer::run_loop(const Circuit &loop, uint64_t iterations, std::string_view loop_tag) { if (!fold_loops) { // If loop folding is disabled, just manually run each iteration. for (size_t k = 0; k < iterations; k++) { undo_circuit(loop); } return; } uint64_t hare_iter = 0; uint64_t tortoise_iter = 0; ErrorAnalyzer hare( tracker.num_measurements_in_past, tracker.num_detectors_in_past, tracker.xs.size(), num_ticks_in_past, false, true, allow_gauge_detectors, approximate_disjoint_errors_threshold, false, false); hare.tracker = tracker; hare.accumulate_errors = false; // Perform tortoise-and-hare cycle finding. while (hare_iter < iterations) { try { hare.undo_circuit(loop); } catch (const std::invalid_argument &) { // Encountered an error. Abort loop folding so it can be re-triggered in a normal way. hare_iter = iterations; break; } hare_iter++; if (hare.tracker.is_shifted_copy(tracker)) { break; } if (hare_iter % 2 == 0) { undo_circuit(loop); tortoise_iter++; if (hare.tracker.is_shifted_copy(tracker)) { break; } } } if (hare_iter < iterations) { // Don't bother folding a single iteration into a repeated block. uint64_t period = hare_iter - tortoise_iter; uint64_t period_iterations = (iterations - tortoise_iter) / period; uint64_t ticks_per_period = num_ticks_in_past - hare.num_ticks_in_past; uint64_t detectors_per_period = tracker.num_detectors_in_past - hare.tracker.num_detectors_in_past; uint64_t measurements_per_period = tracker.num_measurements_in_past - hare.tracker.num_measurements_in_past; if (period_iterations > 1) { // Stash error model build up so far. flush(); DetectorErrorModel tmp = std::move(flushed_reversed_model); // Rewrite state to look like it would if loop had executed all but the last iteration. uint64_t skipped_periods = period_iterations - 1; tracker.shift( -(int64_t)(skipped_periods * measurements_per_period), -(int64_t)(skipped_periods * detectors_per_period)); num_ticks_in_past -= skipped_periods * ticks_per_period; tortoise_iter += skipped_periods * period; // Compute the loop's error model. for (size_t k = 0; k < period; k++) { undo_circuit(loop); tortoise_iter++; } flush(); DetectorErrorModel body = std::move(flushed_reversed_model); // The loop ends (well, starts because everything is reversed) by shifting the detector coordinates. uint64_t lower_level_shifts = body.total_detector_shift(); DemTarget remaining_shift = {detectors_per_period - lower_level_shifts}; if (remaining_shift.data > 0) { if (body.instructions.empty() || body.instructions.front().type != DemInstructionType::DEM_SHIFT_DETECTORS) { auto shift_targets = body.target_buf.take_copy(SpanRef(&remaining_shift)); body.instructions.insert( body.instructions.begin(), DemInstruction{ .arg_data = {}, .target_data = shift_targets, .tag = "", .type = DemInstructionType::DEM_SHIFT_DETECTORS, }); } else { remaining_shift.data += body.instructions[0].target_data[0].data; auto shift_targets = body.target_buf.take_copy(SpanRef(&remaining_shift)); body.instructions[0].target_data = shift_targets; } } // Append the loop to the growing error model and put the error model back in its proper place. tmp.append_repeat_block(period_iterations, std::move(body), loop_tag); flushed_reversed_model = std::move(tmp); } } // Perform remaining loop iterations leftover after jumping forward by multiples of the recurrence period. while (tortoise_iter < iterations) { undo_circuit(loop); tortoise_iter++; } } void ErrorAnalyzer::undo_SHIFT_COORDS(const CircuitInstruction &inst) { flushed_reversed_model.append_shift_detectors_instruction(inst.args, 0, inst.tag); } template void ErrorAnalyzer::decompose_helper_add_error_combinations( const std::array &detector_masks, std::array, 1 << s> &stored_ids, std::string_view tag) { // Count number of detectors affected by each error. std::array detector_counts{}; for (size_t k = 1; k < 1 << s; k++) { detector_counts[k] = std::popcount(detector_masks[k]); } // Find single-detector errors (and empty errors). uint64_t solved = 0; uint64_t single_detectors_union = 0; for (size_t k = 1; k < 1 << s; k++) { if (detector_counts[k] == 1) { single_detectors_union |= detector_masks[k]; solved |= 1 << k; } } // Find irreducible double-detector errors. FixedCapVector irreducible_pairs{}; for (size_t k = 1; k < 1 << s; k++) { if (detector_counts[k] == 2 && (detector_masks[k] & ~single_detectors_union)) { irreducible_pairs.push_back(k); solved |= 1 << k; } } auto append_involved_pairs_to_jag_tail = [&](size_t goal_k) -> uint64_t { uint64_t goal = detector_masks[goal_k]; // If single-detector excitations are sufficient, just use those. if ((goal & ~single_detectors_union) == 0) { return goal; } // Check if one double-detector excitation can get us into the single-detector region. for (auto k : irreducible_pairs) { auto m = detector_masks[k]; if ((goal & m) == m && (goal & ~(single_detectors_union | m)) == 0) { mono_buf.append_tail(stored_ids[k]); mono_buf.append_tail(DemTarget::separator()); return goal & ~m; } } // Check if two double-detector excitations can get us into the single-detector region. for (size_t i1 = 0; i1 < irreducible_pairs.size(); i1++) { auto k1 = irreducible_pairs[i1]; auto m1 = detector_masks[k1]; for (size_t i2 = i1 + 1; i2 < irreducible_pairs.size(); i2++) { auto k2 = irreducible_pairs[i2]; auto m2 = detector_masks[k2]; if ((m1 & m2) == 0 && (goal & ~(single_detectors_union | m1 | m2)) == 0) { if (stored_ids[k2] < stored_ids[k1]) { std::swap(k1, k2); } mono_buf.append_tail(stored_ids[k1]); mono_buf.append_tail(DemTarget::separator()); mono_buf.append_tail(stored_ids[k2]); mono_buf.append_tail(DemTarget::separator()); return goal & ~(m1 | m2); } } } // Failed to decompose into other components of the same composite Pauli channel. // Put it into the result undecomposed, to be worked on more later. mono_buf.append_tail(stored_ids[goal_k]); mono_buf.append_tail(DemTarget::separator()); return 0; }; // Solve the decomposition of each composite case. for (size_t k = 1; k < 1 << s; k++) { if (detector_counts[k] && ((solved >> k) & 1) == 0) { auto remnants = append_involved_pairs_to_jag_tail(k); // Finish off the solution using single-detector components. for (size_t k2 = 0; remnants && k2 < 1 << s; k2++) { if (detector_counts[k2] == 1 && (detector_masks[k2] & ~remnants) == 0) { remnants &= ~detector_masks[k2]; mono_buf.append_tail(stored_ids[k2]); mono_buf.append_tail(DemTarget::separator()); } } if (!mono_buf.tail.empty()) { mono_buf.tail.ptr_end -= 1; } stored_ids[k] = mono_dedupe_store_tail(tag).targets; } } } bool stim::is_graphlike(const SpanRef &components) { size_t symptom_count = 0; for (const auto &t : components) { if (t.is_separator()) { symptom_count = 0; } else if (t.is_relative_detector_id()) { symptom_count++; if (symptom_count > 2) { return false; } } } return true; } bool ErrorAnalyzer::has_unflushed_ungraphlike_errors() const { for (const auto &kv : error_class_probabilities) { const auto &component = kv.first; if (kv.second != 0 && !is_graphlike(component.targets)) { return true; } } return false; } bool ErrorAnalyzer::decompose_and_append_component_to_tail( SpanRef component, const std::map, SpanRef> &known_symptoms) { std::vector done(component.size(), false); size_t num_component_detectors = 0; for (size_t k = 0; k < component.size(); k++) { if (component[k].is_relative_detector_id()) { num_component_detectors++; } else { done[k] = true; } } if (num_component_detectors <= 2) { mono_buf.append_tail(component); mono_buf.append_tail(DemTarget::separator()); return true; } SparseXorVec sparse; sparse.xor_sorted_items(component); for (size_t k = 0; k < component.size(); k++) { if (!done[k]) { for (size_t k2 = k + 1; k2 < component.size(); k2++) { if (!done[k2]) { auto p = known_symptoms.find({component[k], component[k2]}); if (p != known_symptoms.end()) { done[k] = true; done[k2] = true; mono_buf.append_tail(p->second); mono_buf.append_tail(DemTarget::separator()); sparse.xor_sorted_items(p->second); break; } } } } } size_t missed = 0; for (size_t k = 0; k < component.size(); k++) { if (!done[k]) { auto p = known_symptoms.find({component[k]}); if (p != known_symptoms.end()) { done[k] = true; mono_buf.append_tail(p->second); mono_buf.append_tail(DemTarget::separator()); sparse.xor_sorted_items(p->second); } } missed += !done[k]; } if (missed <= 2) { if (!sparse.empty()) { mono_buf.append_tail({sparse.begin(), sparse.end()}); mono_buf.append_tail(DemTarget::separator()); } return true; } mono_buf.discard_tail(); return false; } std::pair obs_mask_of_targets(SpanRef targets) { uint64_t obs_mask = 0; uint64_t used_mask = 0; for (size_t k = 0; k < targets.size(); k++) { const auto &t = targets[k]; if (t.is_observable_id()) { if (t.val() >= 64) { throw std::invalid_argument("Not implemented: decomposing errors observable ids larger than 63."); } obs_mask |= uint64_t{1} << t.val(); used_mask |= uint64_t{1} << k; } } return {obs_mask, used_mask}; } bool brute_force_decomp_helper( size_t start, uint64_t used_term_mask, uint64_t remaining_obs_mask, SpanRef problem, const std::map, SpanRef> &known_symptoms, std::vector> &out_result) { while (true) { if (start >= problem.size()) { return remaining_obs_mask == 0; } if (((used_term_mask >> start) & 1) == 0) { break; } start++; } used_term_mask |= 1 << start; FixedCapVector key; key.push_back(problem[start]); for (size_t k = start + 1; k <= problem.size(); k++) { if (k < problem.size()) { if ((used_term_mask >> k) & 1) { continue; } key.push_back(problem[k]); used_term_mask ^= 1 << k; } auto match = known_symptoms.find(key); if (match != known_symptoms.end()) { uint64_t obs_change = obs_mask_of_targets(match->second).first; if (brute_force_decomp_helper( start + 1, used_term_mask, remaining_obs_mask ^ obs_change, problem, known_symptoms, out_result)) { out_result.push_back(match->second); return true; } } if (k < problem.size()) { key.pop_back(); used_term_mask ^= 1 << k; } } return false; } bool stim::brute_force_decomposition_into_known_graphlike_errors( SpanRef problem, const std::map, SpanRef> &known_graphlike_errors, MonotonicBuffer &output) { if (problem.size() >= 64) { throw std::invalid_argument("Not implemented: decomposing errors with more than 64 terms."); } std::vector> out; out.reserve(problem.size()); auto prob_masks = obs_mask_of_targets(problem); bool result = brute_force_decomp_helper(0, prob_masks.second, prob_masks.first, problem, known_graphlike_errors, out); if (result) { for (auto r = out.crbegin(); r != out.crend(); r++) { output.append_tail(*r); output.append_tail(DemTarget::separator()); } } return result; } void ErrorAnalyzer::do_global_error_decomposition_pass() { if (!decompose_errors || !has_unflushed_ungraphlike_errors()) { return; } std::vector component_symptoms; // Make a map from all known symptoms singlets and pairs to actual components including frame changes. std::map, SpanRef> known_symptoms; for (const auto &kv : error_class_probabilities) { if (kv.second == 0 || kv.first.targets.empty()) { continue; } const auto &targets = kv.first.targets; size_t start = 0; for (size_t k = 0; k <= targets.size(); k++) { if (k == targets.size() || targets[k].is_separator()) { if (component_symptoms.size() == 1) { known_symptoms[{component_symptoms[0]}] = {&targets[start], &targets[k]}; } else if (component_symptoms.size() == 2) { known_symptoms[{component_symptoms[0], component_symptoms[1]}] = {&targets[start], &targets[k]}; } component_symptoms.clear(); start = k + 1; } else if (targets[k].is_relative_detector_id()) { component_symptoms.push_back(targets[k]); } } } // Find how to rewrite hyper errors into graphlike errors. std::vector> rewrites; for (const auto &kv : error_class_probabilities) { if (kv.second == 0 || kv.first.targets.empty()) { continue; } const auto &targets = kv.first.targets; if (is_graphlike(targets)) { continue; } size_t start = 0; for (size_t k = 0; k <= targets.size(); k++) { if (k == targets.size() || targets[k].is_separator()) { SpanRef problem{&targets[start], &targets[k]}; if (brute_force_decomposition_into_known_graphlike_errors(problem, known_symptoms, mono_buf)) { // Solved using only existing edges. } else if ( !block_decomposition_from_introducing_remnant_edges && // We are now *really* desperate. // We need to start considering decomposing into errors that // don't exist, as long as they can be formed by xoring // together errors that do exist. This might impact the // graphlike code distance. decompose_and_append_component_to_tail({&targets[start], &targets[k]}, known_symptoms)) { // Solved using a remnant edge. } else if (ignore_decomposition_failures) { mono_buf.append_tail(problem); mono_buf.append_tail(DemTarget::separator()); } else { std::stringstream ss; ss << "Failed to decompose errors into graphlike components with at most two symptoms.\n"; ss << "The error component that failed to decompose is '" << comma_sep_workaround(problem) << "'.\n"; ss << "\n"; ss << "In Python, you can ignore this error by passing `ignore_decomposition_failures=True` to " "`stim.Circuit.detector_error_model(...)`.\n"; ss << "From the command line, you can ignore this error by passing the flag " "`--ignore_decomposition_failures` to `stim analyze_errors`."; if (block_decomposition_from_introducing_remnant_edges) { ss << "\n\nNote: `block_decomposition_from_introducing_remnant_edges` is ON.\n"; ss << "Turning it off may prevent this error."; } throw std::invalid_argument(ss.str()); } start = k + 1; } } if (!mono_buf.tail.empty()) { // Drop final separator. mono_buf.tail.ptr_end -= 1; } rewrites.push_back({kv.first, ErrorEquivalenceClass{mono_buf.commit_tail(), kv.first.tag}}); } for (const auto &rewrite : rewrites) { double p = error_class_probabilities[rewrite.first]; error_class_probabilities.erase(rewrite.first); add_error(p, rewrite.second.targets, rewrite.second.tag); } } template void ErrorAnalyzer::add_error_combinations( std::array probabilities, std::array, s> basis_errors, bool probabilities_are_disjoint, std::string_view tag) { std::array detector_masks{}; FixedCapVector involved_detectors{}; std::array, 1 << s> stored_ids; for (size_t k = 0; k < s; k++) { stored_ids[1 << k] = mono_dedupe_store(ErrorEquivalenceClass{basis_errors[k], tag}).targets; if (decompose_errors) { for (const auto &id : basis_errors[k]) { if (id.is_relative_detector_id()) { auto r = involved_detectors.find(id); if (r == involved_detectors.end()) { try { involved_detectors.push_back(id); } catch (const std::out_of_range &) { std::stringstream message; message << "An error case in a composite error exceeded the max supported number of symptoms " "(<=15)."; message << "\nThe " << std::to_string(s) << " basis error cases (e.g. X, Z) used to form the combined "; message << "error cases (e.g. Y = X*Z) are:\n"; for (size_t k2 = 0; k2 < s; k2++) { message << std::to_string(k2) << ":"; if (!basis_errors[k2].empty()) { message << ' '; } message << comma_sep_workaround(basis_errors[k2]) << "\n"; } throw std::invalid_argument(message.str()); } } detector_masks[1 << k] ^= 1 << (r - involved_detectors.begin()); } } } } // Fill in all 2**s - 1 possible combinations from the initial basis values. for (size_t k = 3; k < 1 << s; k++) { auto c1 = k & (k - 1); auto c2 = k ^ c1; if (c1) { mono_buf.ensure_available(stored_ids[c1].size() + stored_ids[c2].size()); mono_buf.tail.ptr_end = xor_merge_sort(stored_ids[c1], stored_ids[c2], mono_buf.tail.ptr_end); stored_ids[k] = mono_dedupe_store_tail(tag).targets; detector_masks[k] = detector_masks[c1] ^ detector_masks[c2]; } } // Determine involved detectors while creating basis masks and storing added data. if (decompose_errors) { decompose_helper_add_error_combinations(detector_masks, stored_ids, tag); } if (probabilities_are_disjoint) { // Merge indistinguishable cases. for (size_t k = 1; k < 1 << s; k++) { if (stored_ids[k].empty()) { // Since symptom k is empty, merge pairs A, B such that A^B = k. for (size_t k_dst = 0; k_dst < 1 << s; k_dst++) { size_t k_src = k_dst ^ k; if (k_src > k_dst) { probabilities[k_dst] += probabilities[k_src]; probabilities[k_src] = 0; } } } } } // Include errors in the record. for (size_t k = 1; k < 1 << s; k++) { add_error(probabilities[k], stored_ids[k], tag); } } void ErrorAnalyzer::undo_MPP(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_mpp_operation( CircuitInstruction{GateType::MPP, inst.args, reversed_targets, inst.tag}, tracker.xs.size(), [&](const CircuitInstruction &sub_inst) { if (sub_inst.gate_type == GateType::M) { reversed_measure_targets.clear(); for (size_t k = sub_inst.targets.size(); k--;) { reversed_measure_targets.push_back(sub_inst.targets[k]); } undo_MZ_with_context( CircuitInstruction{GateType::M, sub_inst.args, reversed_measure_targets, sub_inst.tag}, "a Pauli product measurement (MPP)"); } else { undo_gate(sub_inst); } }); } void ErrorAnalyzer::undo_SPP(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_spp_or_spp_dag_operation( CircuitInstruction{GateType::SPP, inst.args, reversed_targets, inst.tag}, tracker.xs.size(), false, [&](const CircuitInstruction &sub_inst) { undo_gate(sub_inst); }); } void ErrorAnalyzer::undo_MXX_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. undo_ZCX(CircuitInstruction{GateType::CX, {}, inst.targets, inst.tag}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { undo_MX_with_context( CircuitInstruction{GateType::MX, inst.args, SpanRef{&inst.targets[k]}, inst.tag}, "an X-basis pair measurement (MXX)"); } // Untransform from single qubit measurements back to 2 qubit measurements. undo_ZCX(CircuitInstruction{GateType::CX, {}, inst.targets, inst.tag}); } void ErrorAnalyzer::undo_MYY_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. undo_ZCY(CircuitInstruction{GateType::CY, {}, inst.targets, inst.tag}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { undo_MY_with_context( CircuitInstruction{GateType::MY, inst.args, SpanRef{&inst.targets[k]}, inst.tag}, "a Y-basis pair measurement (MYY)"); } // Untransform from single qubit measurements back to 2 qubit measurements. undo_ZCY(CircuitInstruction{GateType::CY, {}, inst.targets, inst.tag}); } void ErrorAnalyzer::undo_MZZ_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. undo_XCZ(CircuitInstruction{GateType::XCZ, {}, inst.targets, inst.tag}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { undo_MZ_with_context( CircuitInstruction{GateType::M, inst.args, SpanRef{&inst.targets[k]}, inst.tag}, "a Z-basis pair measurement (MZ)"); } // Untransform from single qubit measurements back to 2 qubit measurements. undo_XCZ(CircuitInstruction{GateType::XCZ, {}, inst.targets, inst.tag}); } void ErrorAnalyzer::undo_MXX(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_pair_instruction_into_disjoint_segments( CircuitInstruction{inst.gate_type, inst.args, reversed_targets, inst.tag}, tracker.xs.size(), [&](CircuitInstruction segment) { undo_MXX_disjoint_controls_segment(segment); }); } void ErrorAnalyzer::undo_MYY(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_pair_instruction_into_disjoint_segments( CircuitInstruction{inst.gate_type, inst.args, reversed_targets, inst.tag}, tracker.xs.size(), [&](CircuitInstruction segment) { undo_MYY_disjoint_controls_segment(segment); }); } void ErrorAnalyzer::undo_MZZ(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_pair_instruction_into_disjoint_segments( CircuitInstruction{inst.gate_type, inst.args, reversed_targets, inst.tag}, tracker.xs.size(), [&](CircuitInstruction segment) { undo_MZZ_disjoint_controls_segment(segment); }); } ================================================ FILE: src/stim/simulators/error_analyzer.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_ERROR_ANALYZER_H #define _STIM_SIMULATORS_ERROR_ANALYZER_H #include #include #include #include #include "sparse_rev_frame_tracker.h" #include "stim/circuit/circuit.h" #include "stim/dem/detector_error_model.h" #include "stim/mem/fixed_cap_vector.h" #include "stim/mem/monotonic_buffer.h" #include "stim/mem/sparse_xor_vec.h" #include "stim/stabilizers/pauli_string.h" namespace stim { struct ErrorEquivalenceClass { SpanRef targets; std::string_view tag; inline bool operator==(const ErrorEquivalenceClass &other) const { return targets == other.targets && tag == other.tag; } inline bool operator!=(const ErrorEquivalenceClass &other) const { return !(*this == other); } inline bool operator<(const ErrorEquivalenceClass &other) const { if (targets != other.targets) { return targets < other.targets; } if (tag != other.tag) { return tag < other.tag; } return false; } }; /// This class is responsible for iterating backwards over a circuit, tracking which detectors are currently /// sensitive to an X or Z error on each qubit. This is done by having a SparseXorVec for the X and Z /// sensitivities of each qubit, and transforming these collections in response to operations. /// For example, applying a CNOT gate from qubit A to qubit B will xor the X sensitivity of A into B and the /// Z sensitivity of B into A. /// /// Y error sensitivity is always implicit in the xor of the X and Z components. /// /// Note that the class iterates over the circuit *backward*, so that DETECTOR instructions are seen before /// the measurement operations that the detector depends on. When a DETECTOR is seen, it is noted which /// measurements it depends on and then when those measurements are seen the detector is added as one of the /// things sensitive to errors that anticommute with the measurement. /// /// Be wary that this class is definitely one of the more complex things in Stim. For example: /// - There is a monobuf that is often used as a temporary buffer to avoid allocations, meaning /// the state and meaning of the monobuf's tail is highly coupled to what method is currently /// executing. /// - The class recursively uses itself when performing period finding on loops. It is seriously /// hard to directly inspect and understand the current state when period finding is happening /// inside of period finding. /// - When period finding succeeds it flushes the recorded errors to avoid crosstalk between the /// in-loop errors and out-of-loop errors. The state of the buffers is coupled across features. /// - Error decomposition is done using heuristics that are not guaranteed to work, and prone to /// being tweaked, creating churn. Also the decomposition code itself is quite complex in order /// to make it fast (e.g. reducing the explicit detectors into bitmasks and then working with /// the bitmasks). /// - I guess what I'm saying is... have fun! struct ErrorAnalyzer { SparseUnsignedRevFrameTracker tracker; /// When false, no error decomposition is performed. /// When true, must decompose any non-graphlike error into graphlike components or fail. bool decompose_errors; /// When false, errors are skipped over instead of recorded. /// When true, errors are recorded into the error_class_probabilities dictionary. bool accumulate_errors; /// When false, loops are flattened and directly iterated over. /// When true, a tortoise-and-hare algorithm is used to notice periodicity in the errors. /// If periodicity is found, the rest of the loop becomes a loop in the output error model. bool fold_loops; /// When false, detectors with non-deterministic parities under noiseless execution cause failure. /// When true, the non-determinism is translated into a 50/50 random error mechanisms. bool allow_gauge_detectors; /// Determines how small the probabilities in disjoint error mechanisms like PAULI_CHANNEL_2 must be /// before they can be approximated as being independent. Any larger probabilities cause failure. double approximate_disjoint_errors_threshold; /// When true, errors that fail to decompose are inserted into the output /// undecomposed instead of raising an exception that terminates the /// conversion from circuit to detector error model. /// /// Only relevant when decompose_errors=True. bool ignore_decomposition_failures; /// When true, decomposition is permitted to split A B C D into A B ^ C D /// when only one of A B or C D exists elsewhere, instead of requiring both /// to exist. This can reduce the code distance of the decoding graph, but /// is sometimes necessary. /// /// Only relevant when decompose_errors=True. bool block_decomposition_from_introducing_remnant_edges; /// A buffer containing the growing output error model as the circuit is traversed. /// The buffer is in reverse order because the circuit is traversed back to front. /// Certain events during period solving of loops can cause the error probabilities /// to flush into this buffer. DetectorErrorModel flushed_reversed_model; /// Recorded errors. Independent probabilities of flipping various sets of detectors. std::map error_class_probabilities; /// Backing datastore for values in error_class_probabilities. MonotonicBuffer mono_buf; /// Counts the number of tick operations, for better debug messages. uint64_t num_ticks_in_past = 0; /// Used for producing debug information when errors occur. const Circuit *current_circuit_being_analyzed = nullptr; /// Creates an instance ready to start processing instructions from a circuit of known size. ErrorAnalyzer( uint64_t num_measurements, uint64_t num_detectors, size_t num_qubits, uint64_t num_ticks, bool decompose_errors, bool fold_loops, bool allow_gauge_detectors, double approximate_disjoint_errors_threshold, bool ignore_decomposition_failures, bool block_decomposition_from_introducing_remnant_edges); /// Returns the detector error model of the given circuit. /// /// Args: /// circuit: The circuit to analyze. /// decompose_errors: When true, complex errors must be split into graphlike components. /// fold_loops: When true, use a tortoise-and-hare algorithm to solve loops instead of flattening them. /// allow_gauge_detectors: When true, replace non-deterministic detectors with 50/50 error mechanisms instead /// of failing. /// approximate_disjoint_errors_threshold: When larger than 0, allows disjoint errors like PAULI_CHANNEL_2 to /// be present in the circuit, as long as their probabilities are not larger than this. /// ignore_decomposition_failures: Determines whether errors that that fail to decompose are inserted into the /// output, or cause the conversion to fail and raise an exception. /// block_decomposition_from_introducing_remnant_edges: When true, it is not permitted to decompose A B C D /// into A B ^ C D unless both A B and C D appear elsewhere in the error model. When false, only one has /// to appear elsewhere. /// /// Returns: /// The detector error model. static DetectorErrorModel circuit_to_detector_error_model( const Circuit &circuit, bool decompose_errors, bool fold_loops, bool allow_gauge_detectors, double approximate_disjoint_errors_threshold, bool ignore_decomposition_failures, bool block_decomposition_from_introducing_remnant_edges); /// Copying is unsafe because `error_class_probabilities` has overlapping pointers to `monobuf`'s internals. ErrorAnalyzer(const ErrorAnalyzer &analyzer) = delete; ErrorAnalyzer(ErrorAnalyzer &&analyzer) noexcept = delete; ErrorAnalyzer &operator=(ErrorAnalyzer &&analyzer) noexcept = delete; ErrorAnalyzer &operator=(const ErrorAnalyzer &analyzer) = delete; void undo_SHIFT_COORDS(const CircuitInstruction &inst); void undo_RX(const CircuitInstruction &inst); void undo_RY(const CircuitInstruction &inst); void undo_RZ(const CircuitInstruction &inst); void undo_MX(const CircuitInstruction &inst); void undo_MY(const CircuitInstruction &inst); void undo_MZ(const CircuitInstruction &inst); void undo_MPP(const CircuitInstruction &inst); void undo_SPP(const CircuitInstruction &inst); void undo_MXX(const CircuitInstruction &inst); void undo_MYY(const CircuitInstruction &inst); void undo_MZZ(const CircuitInstruction &inst); void undo_MPAD(const CircuitInstruction &inst); void undo_HERALDED_ERASE(const CircuitInstruction &inst); void undo_HERALDED_PAULI_CHANNEL_1(const CircuitInstruction &inst); void undo_MRX(const CircuitInstruction &inst); void undo_MRY(const CircuitInstruction &inst); void undo_MRZ(const CircuitInstruction &inst); void undo_H_XZ(const CircuitInstruction &inst); void undo_H_XY(const CircuitInstruction &inst); void undo_H_YZ(const CircuitInstruction &inst); void undo_C_XYZ(const CircuitInstruction &inst); void undo_C_ZYX(const CircuitInstruction &inst); void undo_XCX(const CircuitInstruction &inst); void undo_XCY(const CircuitInstruction &inst); void undo_XCZ(const CircuitInstruction &inst); void undo_YCX(const CircuitInstruction &inst); void undo_YCY(const CircuitInstruction &inst); void undo_YCZ(const CircuitInstruction &inst); void undo_ZCX(const CircuitInstruction &inst); void undo_ZCY(const CircuitInstruction &inst); void undo_ZCZ(const CircuitInstruction &inst); void undo_I(const CircuitInstruction &inst); void undo_TICK(const CircuitInstruction &inst); void undo_SQRT_XX(const CircuitInstruction &inst); void undo_SQRT_YY(const CircuitInstruction &inst); void undo_SQRT_ZZ(const CircuitInstruction &inst); void undo_SWAP(const CircuitInstruction &inst); void undo_DETECTOR(const CircuitInstruction &inst); void undo_OBSERVABLE_INCLUDE(const CircuitInstruction &inst); void undo_X_ERROR(const CircuitInstruction &inst); void undo_Y_ERROR(const CircuitInstruction &inst); void undo_Z_ERROR(const CircuitInstruction &inst); void undo_CORRELATED_ERROR(const CircuitInstruction &inst); void undo_DEPOLARIZE1(const CircuitInstruction &inst); void undo_DEPOLARIZE2(const CircuitInstruction &inst); void undo_ELSE_CORRELATED_ERROR(const CircuitInstruction &inst); void undo_PAULI_CHANNEL_1(const CircuitInstruction &inst); void undo_PAULI_CHANNEL_2(const CircuitInstruction &inst); void undo_ISWAP(const CircuitInstruction &inst); void undo_CXSWAP(const CircuitInstruction &inst); void undo_CZSWAP(const CircuitInstruction &inst); void undo_SWAPCX(const CircuitInstruction &inst); void undo_RX_with_context(const CircuitInstruction &inst, const char *context_op); void undo_RY_with_context(const CircuitInstruction &inst, const char *context_op); void undo_RZ_with_context(const CircuitInstruction &inst, const char *context_op); void undo_MX_with_context(const CircuitInstruction &inst, const char *context_op); void undo_MY_with_context(const CircuitInstruction &inst, const char *context_op); void undo_MZ_with_context(const CircuitInstruction &inst, const char *context_op); /// Processes each of the instructions in the circuit, in reverse order. void undo_circuit(const Circuit &circuit); /// This is used at the end of the analysis to check that any remaining sensitivities commute /// with the implicit Z basis initialization at the start of a circuit. void post_check_initialization(); void undo_gate(const CircuitInstruction &inst); /// Returns a PauliString indicating the current error sensitivity of a detector or observable. /// /// The observable or detector is sensitive to the Pauli error P at q if the Pauli sensitivity /// at q anti-commutes with P. PauliString current_error_sensitivity_for(DemTarget t) const; /// Processes the instructions in a circuit multiple times. /// If loop folding is enabled, also uses a tortoise-and-hare algorithm to attempt to solve the loop's period. void run_loop(const Circuit &loop, uint64_t iterations, std::string_view tag); private: /// When detectors anti-commute with a reset, that set of detectors becomes a degree of freedom. /// Use that degree of freedom to delete the largest detector in the set from the system. void remove_gauge(SpanRef sorted); /// Sorts the targets coming out of the measurement queue, then optionally inserts a measurement error. void xor_sorted_measurement_error(SpanRef targets, const CircuitInstruction &dat); /// Checks if the given sparse vector is empty. If it isn't, something that was supposed to be /// deterministic is actually random. Produces an error message with debug information that can be /// used to understand what went wrong. void check_for_gauge( const SparseXorVec &potential_gauge, const char *context_op, uint64_t context_qubit, std::string_view tag); /// Checks if the given sparse vectors are equal. If they aren't, something that was supposed to be /// deterministic is actually random. Produces an error message with debug information that can be /// used to understand what went wrong. void check_for_gauge( SparseXorVec &potential_gauge_summand_1, const SparseXorVec &potential_gauge_summand_2, const char *context_op, uint64_t context_qubit, std::string_view tag); /// Empties error_class_probabilities into flushed_reversed_model. void flush(); /// Adds (or folds) an error mechanism into error_class_probabilities. ErrorEquivalenceClass add_error(double probability, SpanRef flipped_sorted, std::string_view tag); /// Adds (or folds) an error mechanism equal into error_class_probabilities. /// The error is defined as the xor of two sparse vectors, because this is a common situation. /// Deals with the details of efficiently computing the xor of the vectors with minimal allocations. ErrorEquivalenceClass add_xored_error( double probability, SpanRef flipped1, SpanRef flipped2, std::string_view tag); /// Adds an error mechanism into error_class_probabilities. /// The error mechanism is not passed as an argument but is instead the current tail of `this->mono_buf`. ErrorEquivalenceClass add_error_in_sorted_jagged_tail(double probability, std::string_view tag); /// Saves the current tail of the monotonic buffer, deduping it to equal already stored data if possible. /// /// Returns: /// A range over the stored data. ErrorEquivalenceClass mono_dedupe_store_tail(std::string_view tag); /// Saves data to the monotonic buffer, deduping it to equal already stored data if possible. /// /// Args: /// data: A range of data to store. /// /// Returns: /// A range over the stored data. ErrorEquivalenceClass mono_dedupe_store(ErrorEquivalenceClass sorted); /// Adds each given error, and also each possible combination of the given errors, to the possible errors. /// /// Does analysis of which errors reduce to other errors (in the detector basis, not the given basis). /// /// Args: /// independent_probabilities: Probability of each error combination (including but ignoring the empty /// combination) occurring, independent of whether or not the others occurred. /// basis_errors: Building blocks for the error combinations. template void add_error_combinations( std::array probabilities, std::array, s> basis_errors, bool probabilities_are_disjoint, std::string_view tag); /// Handles local decomposition of errors. /// When an error has multiple channels, eg. a DEPOLARIZE2 error, this method attempts to express the more complex /// channels (the ones with more symptoms) in terms of the simpler ones that have just 1 or 2 symptoms. /// Works by rewriting the `stored_ids` argument. template void decompose_helper_add_error_combinations( const std::array &detector_masks, std::array, 1 << s> &stored_ids, std::string_view tag); /// Handles global decomposition of errors. /// When an error has more than two symptoms, this method attempts to find other known errors that can be used as /// components of this error, so that it is decomposed into graphlike components. bool decompose_and_append_component_to_tail( SpanRef component, const std::map, SpanRef> &known_symptoms); /// Performs a final check that all errors are decomposed. /// If any aren't, attempts to decompose them using other errors in the system. void do_global_error_decomposition_pass(); /// Checks whether there any errors that need decomposing. bool has_unflushed_ungraphlike_errors() const; private: void undo_MXX_disjoint_controls_segment(const CircuitInstruction &inst); void undo_MYY_disjoint_controls_segment(const CircuitInstruction &inst); void undo_MZZ_disjoint_controls_segment(const CircuitInstruction &inst); void check_can_approximate_disjoint( const char *op_name, SpanRef probabilities, bool allow_single_component) const; void add_composite_error(double probability, SpanRef targets, std::string_view tag); void correlated_error_block(const std::vector &dats); }; /// Determines if an error's targets are graphlike. /// /// An error is graphlike if it has at most two symptoms (two detectors) per component. /// For example, error(0.1) D0 D1 ^ D2 D3 L55 is graphlike but error(0.1) D0 D1 ^ D2 D3 D55 is not. bool is_graphlike(const SpanRef &components); bool brute_force_decomposition_into_known_graphlike_errors( SpanRef problem, const std::map, SpanRef> &known_graphlike_errors, MonotonicBuffer &output); } // namespace stim #endif ================================================ FILE: src/stim/simulators/error_analyzer.perf.cc ================================================ // 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. #include "stim/simulators/error_analyzer.h" #include "stim/gen/gen_surface_code.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(ErrorAnalyzer_surface_code_rotated_memory_z_d11_r100) { auto params = CircuitGenParameters(100, 11, "rotated_memory_z"); params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.after_clifford_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; benchmark_go([&]() { ErrorAnalyzer analyzer( circuit.count_measurements(), circuit.count_detectors(), circuit.count_qubits(), circuit.count_ticks(), false, false, false, 0.0, false, true); analyzer.undo_circuit(circuit); }).goal_millis(320); } BENCHMARK(ErrorAnalyzer_surface_code_rotated_memory_z_d11_r100_find_reducible_errors) { auto params = CircuitGenParameters(100, 11, "rotated_memory_z"); params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.after_clifford_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; benchmark_go([&]() { ErrorAnalyzer analyzer( circuit.count_measurements(), circuit.count_detectors(), circuit.count_qubits(), circuit.count_ticks(), true, false, false, 0.0, false, true); analyzer.undo_circuit(circuit); }).goal_millis(450); } BENCHMARK(ErrorAnalyzer_surface_code_rotated_memory_z_d11_r100000000_find_loops) { auto params = CircuitGenParameters(100000000, 11, "rotated_memory_z"); params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.after_clifford_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; benchmark_go([&]() { ErrorAnalyzer analyzer( circuit.count_measurements(), circuit.count_detectors(), circuit.count_qubits(), circuit.count_ticks(), false, true, false, 0.0, false, true); analyzer.undo_circuit(circuit); }).goal_millis(15); } ================================================ FILE: src/stim/simulators/error_analyzer.test.cc ================================================ #include "stim/simulators/error_analyzer.h" #include #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/gen/gen_rep_code.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/frame_simulator.h" #include "stim/util_bot/test_util.test.h" #include "stim/util_top/circuit_to_dem.h" using namespace stim; TEST(ErrorAnalyzer, circuit_to_detector_error_model) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 3 M 3 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 3 M 3 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 L0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( Y_ERROR(0.25) 3 M 3 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( Z_ERROR(0.25) 3 M 3 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( detector D0 )model")); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( DEPOLARIZE1(0.25) 3 M 3 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.166666) D0 )model"), 1e-4)); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 0 X_ERROR(0.125) 1 M 0 1 OBSERVABLE_INCLUDE(3) rec[-1] DETECTOR rec[-2] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 error(0.125) L3 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 0 X_ERROR(0.125) 1 M 0 1 OBSERVABLE_INCLUDE(3) rec[-1] DETECTOR rec[-2] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 error(0.125) L3 )model")); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( DEPOLARIZE2(0.25) 3 5 M 3 M 5 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, false, 0.0, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.0718255) D0 error(0.0718255) D0 D1 error(0.0718255) D1 )model"), 1e-5)); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( H 0 1 CNOT 0 2 1 3 DEPOLARIZE2(0.25) 0 1 CNOT 0 2 1 3 H 0 1 M 0 1 2 3 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] DETECTOR rec[-4] )circuit"), false, false, false, 0.0, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.019013) D0 error(0.019013) D0 D1 error(0.019013) D0 D1 D2 error(0.019013) D0 D1 D2 D3 error(0.019013) D0 D1 D3 error(0.019013) D0 D2 error(0.019013) D0 D2 D3 error(0.019013) D0 D3 error(0.019013) D1 error(0.019013) D1 D2 error(0.019013) D1 D2 D3 error(0.019013) D1 D3 error(0.019013) D2 error(0.019013) D2 D3 error(0.019013) D3 )model"), 1e-4)); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( H 0 1 CNOT 0 2 1 3 DEPOLARIZE2(0.25) 0 1 CNOT 0 2 1 3 H 0 1 M 0 1 2 3 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] DETECTOR rec[-4] )circuit"), true, false, false, 0.0, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.019013) D0 error(0.019013) D1 error(0.019013) D1 ^ D0 error(0.019013) D1 ^ D2 error(0.019013) D1 ^ D2 ^ D0 error(0.019013) D2 error(0.019013) D2 ^ D0 error(0.019013) D3 error(0.019013) D3 ^ D0 error(0.019013) D3 ^ D1 error(0.019013) D3 ^ D1 ^ D0 error(0.019013) D3 ^ D1 ^ D2 error(0.019013) D3 ^ D1 ^ D2 ^ D0 error(0.019013) D3 ^ D2 error(0.019013) D3 ^ D2 ^ D0 )model"), 1e-4)); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( H 0 1 CNOT 0 2 1 3 # Perform depolarizing error in a different basis. ZCX 0 10 ZCX 0 11 XCX 0 12 XCX 0 13 DEPOLARIZE2(0.25) 0 1 XCX 0 13 XCX 0 12 ZCX 0 11 ZCX 0 10 # Check where error is. M 10 11 12 13 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] DETECTOR rec[-4] )circuit"), true, false, true, 0.0, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.071825) D0 D1 error(0.071825) D0 D1 ^ D2 D3 error(0.071825) D2 D3 )model"), 1e-4)); } TEST_EACH_WORD_SIZE_W(ErrorAnalyzer, unitary_gates_match_frame_simulator, { CircuitStats stats; stats.num_qubits = 16; stats.num_measurements = 100; FrameSimulator f(stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 16, INDEPENDENT_TEST_RNG()); ErrorAnalyzer e(100, 1, 16, 100, false, false, false, 0.0, false, true); for (size_t q = 0; q < 16; q++) { if (q & 1) { e.tracker.xs[q].xor_item({0}); f.x_table[q][0] = true; } if (q & 2) { e.tracker.xs[q].xor_item({1}); f.x_table[q][1] = true; } if (q & 4) { e.tracker.zs[q].xor_item({0}); f.z_table[q][0] = true; } if (q & 8) { e.tracker.zs[q].xor_item({1}); f.z_table[q][1] = true; } } std::vector data; for (size_t k = 0; k < 16; k++) { data.push_back(GateTarget::qubit(k)); } for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { e.undo_gate(CircuitInstruction{gate.id, {}, data, ""}); f.do_gate(CircuitInstruction{gate.inverse().id, {}, data, ""}); for (size_t q = 0; q < 16; q++) { bool xs[2]{}; bool zs[2]{}; for (auto x : e.tracker.xs[q]) { ASSERT_TRUE(x.data < 2) << gate.name; xs[x.data] = true; } for (auto z : e.tracker.zs[q]) { ASSERT_TRUE(z.data < 2) << gate.name; zs[z.data] = true; } ASSERT_EQ(f.x_table[q][0], xs[0]) << gate.name; ASSERT_EQ(f.x_table[q][1], xs[1]) << gate.name; ASSERT_EQ(f.z_table[q][0], zs[0]) << gate.name; ASSERT_EQ(f.z_table[q][1], zs[1]) << gate.name; } } } }) TEST(ErrorAnalyzer, reversed_operation_order) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 0 CNOT 0 1 CNOT 1 0 M 0 1 DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D1 detector D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 0 CNOT 0 1 CNOT 1 0 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 detector D1 )model")); } TEST(ErrorAnalyzer, classical_error_propagation) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.125) 0 M 0 CNOT rec[-1] 1 M 1 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.125) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.125) 0 M 0 H 1 CZ rec[-1] 1 H 1 M 1 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.125) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.125) 0 M 0 H 1 CZ 1 rec[-1] H 1 M 1 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.125) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.125) 0 M 0 CY rec[-1] 1 M 1 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.125) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.125) 0 M 0 XCZ 1 rec[-1] M 1 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.125) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.125) 0 M 0 YCZ 1 rec[-1] M 1 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.125) D0 )model")); } TEST(ErrorAnalyzer, measure_reset_basis) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( RZ 0 1 2 X_ERROR(0.25) 0 Y_ERROR(0.25) 1 Z_ERROR(0.25) 2 MZ 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 error(0.25) D1 detector D2 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( RX 0 1 2 X_ERROR(0.25) 0 Y_ERROR(0.25) 1 Z_ERROR(0.25) 2 MX 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D1 error(0.25) D2 detector D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( RY 0 1 2 X_ERROR(0.25) 0 Y_ERROR(0.25) 1 Z_ERROR(0.25) 2 MY 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 error(0.25) D2 detector D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( MRZ 0 1 2 X_ERROR(0.25) 0 Y_ERROR(0.25) 1 Z_ERROR(0.25) 2 MRZ 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 error(0.25) D1 detector D2 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( MRX 0 1 2 X_ERROR(0.25) 0 Y_ERROR(0.25) 1 Z_ERROR(0.25) 2 MRX 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D1 error(0.25) D2 detector D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( MRY 0 1 2 X_ERROR(0.25) 0 Y_ERROR(0.25) 1 Z_ERROR(0.25) 2 MRY 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 error(0.25) D2 detector D1 )model")); } TEST(ErrorAnalyzer, repeated_measure_reset) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( MRZ 0 0 X_ERROR(0.25) 0 MRZ 0 0 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D2 detector D0 detector D1 detector D3 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( RY 0 0 MRY 0 0 X_ERROR(0.25) 0 MRY 0 0 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D2 detector D0 detector D1 detector D3 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( RX 0 0 MRX 0 0 Z_ERROR(0.25) 0 MRX 0 0 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D2 detector D0 detector D1 detector D3 )model")); } TEST(ErrorAnalyzer, period_3_gates) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( RY 0 1 2 X_ERROR(1) 0 Y_ERROR(1) 1 Z_ERROR(1) 2 C_XYZ 0 1 2 M 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(1) D0 error(1) D2 detector D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( R 0 1 2 C_XYZ 0 1 2 X_ERROR(1) 0 Y_ERROR(1) 1 Z_ERROR(1) 2 C_ZYX 0 1 2 M 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(1) D1 error(1) D2 detector D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( R 0 1 2 C_ZYX 0 1 2 X_ERROR(1) 0 Y_ERROR(1) 1 Z_ERROR(1) 2 C_XYZ 0 1 2 M 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(1) D0 error(1) D2 detector D1 )model")); } TEST(ErrorAnalyzer, detect_gauge_observables) { ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( R 0 H 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( R 0 H 0 M 0 OBSERVABLE_INCLUDE(0) rec[-1] )circuit"), false, false, true, 0.0, false, true); }); } TEST(ErrorAnalyzer, detect_gauge_detectors) { ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( R 0 H 0 M 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( M 0 H 0 M 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MZ 0 MX 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MY 0 MX 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MX 0 MZ 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( RX 0 MZ 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( RY 0 MX 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( RZ 0 MX 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MX 0 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true); }); } TEST(ErrorAnalyzer, gauge_detectors) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( R 0 H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( RX 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( RY 0 H_XY 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MR 0 H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MRX 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MRY 0 H_XY 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( M 0 H 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MX 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( R"circuit( MY 0 H_XY 0 CNOT 0 1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] )circuit"), false, false, true, 0.0, false, true), DetectorErrorModel(R"model( error(0.5) D0 D1 )model")); } TEST(ErrorAnalyzer, composite_error_analysis) { auto measure_stabilizers = Circuit(R"circuit( XCX 0 1 0 3 0 4 MR 0 XCZ 0 1 0 2 0 4 0 5 MR 0 XCX 0 2 0 5 0 6 MR 0 XCZ 0 3 0 4 0 7 MR 0 XCX 0 4 0 5 0 7 0 8 MR 0 XCZ 0 5 0 6 0 7 MR 0 )circuit"); auto detectors = Circuit(R"circuit( DETECTOR rec[-6] rec[-12] DETECTOR rec[-5] rec[-11] DETECTOR rec[-4] rec[-10] DETECTOR rec[-3] rec[-9] DETECTOR rec[-2] rec[-8] DETECTOR rec[-1] rec[-7] )circuit"); // . 1 2 . // X0 Z1 X2 // 3 4 5 6 // Z3 X4 Z5 // . 7 8 . auto encode = measure_stabilizers; auto decode = measure_stabilizers + detectors; ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(encode + Circuit("DEPOLARIZE1(0.01) 4") + decode), true, false, false, 0.0, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.0033445) D0 D4 error(0.0033445) D0 D4 ^ D1 D3 error(0.0033445) D1 D3 detector D2 detector D5 )model"), 1e-6)); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(encode + Circuit("DEPOLARIZE2(0.01) 4 5") + decode), true, false, false, 0.0, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.000669) D0 D2 error(0.000669) D0 D2 ^ D1 D3 error(0.000669) D0 D2 ^ D1 D5 error(0.000669) D0 D2 ^ D3 D5 error(0.000669) D0 D4 error(0.000669) D0 D4 ^ D1 D3 error(0.000669) D0 D4 ^ D1 D5 error(0.000669) D0 D4 ^ D3 D5 error(0.000669) D1 D3 error(0.000669) D1 D3 ^ D2 D4 error(0.000669) D1 D5 error(0.000669) D1 D5 ^ D2 D4 error(0.000669) D2 D4 error(0.000669) D2 D4 ^ D3 D5 error(0.000669) D3 D5 )model"), 1e-6)); auto expected = DetectorErrorModel(R"model( error(0.000669) D0 D1 D2 D3 error(0.000669) D0 D1 D2 D5 error(0.000669) D0 D1 D3 D4 error(0.000669) D0 D1 D4 D5 error(0.000669) D0 D2 error(0.000669) D0 D2 D3 D5 error(0.000669) D0 D3 D4 D5 error(0.000669) D0 D4 error(0.000669) D1 D2 D3 D4 error(0.000669) D1 D2 D4 D5 error(0.000669) D1 D3 error(0.000669) D1 D5 error(0.000669) D2 D3 D4 D5 error(0.000669) D2 D4 error(0.000669) D3 D5 )model"); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(encode + Circuit("DEPOLARIZE2(0.01) 4 5") + decode), false, false, false, 0.0, false, true) .approx_equals(expected, 1e-5)); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(encode + Circuit("CNOT 4 5\nDEPOLARIZE2(0.01) 4 5\nCNOT 4 5") + decode), false, false, false, 0.0, false, true) .approx_equals(expected, 1e-5)); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(encode + Circuit("H_XY 4\nCNOT 4 5\nDEPOLARIZE2(0.01) 4 5\nCNOT 4 5\nH_XY 4") + decode), false, false, false, 0.0, false, true) .approx_equals(expected, 1e-5)); } std::string declare_detectors(size_t min, size_t max) { std::stringstream result; for (size_t k = min; k <= max; k++) { result << "detector D" << k << "\n"; } return result.str(); } TEST(ErrorAnalyzer, loop_folding) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MR 1 REPEAT 12345678987654321 { X_ERROR(0.25) 0 CNOT 0 1 MR 1 DETECTOR rec[-2] rec[-1] } M 0 OBSERVABLE_INCLUDE(9) rec[-1] )CIRCUIT"), false, true, true, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.25) D0 L9 REPEAT 6172839493827159 { error(0.25) D1 L9 error(0.25) D2 L9 shift_detectors 2 } error(0.25) D1 L9 error(0.25) D2 L9 )MODEL")); // Solve period 8 logical observable oscillation. ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( R 0 1 2 3 4 REPEAT 12345678987654321 { CNOT 0 1 1 2 2 3 3 4 DETECTOR } M 4 OBSERVABLE_INCLUDE(9) rec[-1] )CIRCUIT"), false, true, true, 0.0, false, true), DetectorErrorModel(R"MODEL( detector D0 detector D1 detector D2 REPEAT 1543209873456789 { detector D3 detector D4 detector D5 detector D6 detector D7 detector D8 detector D9 detector D10 shift_detectors 8 } detector D3 detector D4 detector D5 detector D6 detector D7 detector D8 logical_observable L9 )MODEL")); // Solve period 127 logical observable oscillation. ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( R 0 1 2 3 4 5 6 REPEAT 12345678987654321 { CNOT 0 1 1 2 2 3 3 4 4 5 5 6 6 0 DETECTOR } M 6 OBSERVABLE_INCLUDE(9) rec[-1] R 7 X_ERROR(1) 7 M 7 DETECTOR rec[-1] )CIRCUIT"), false, true, true, 0.0, false, true), DetectorErrorModel((declare_detectors(0, 85) + R"MODEL( REPEAT 97210070768930 { )MODEL" + declare_detectors(86, 86 + 127 - 1) + R"MODEL( shift_detectors 127 } error(1) D211 )MODEL" + declare_detectors(86, 210) + R"MODEL( logical_observable L9 )MODEL") .data())); } TEST(ErrorAnalyzer, loop_folding_nested_loop) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MR 1 REPEAT 1000 { REPEAT 1000 { X_ERROR(0.25) 0 CNOT 0 1 MR 1 DETECTOR rec[-2] rec[-1] } } M 0 OBSERVABLE_INCLUDE(9) rec[-1] )CIRCUIT"), false, true, true, 0.0, false, true), DetectorErrorModel(R"MODEL( REPEAT 999 { REPEAT 1000 { error(0.25) D0 L9 shift_detectors 1 } } REPEAT 499 { error(0.25) D0 L9 error(0.25) D1 L9 shift_detectors 2 } error(0.25) D0 L9 error(0.25) D1 L9 )MODEL")); } TEST(ErrorAnalyzer, loop_folding_rep_code_circuit) { CircuitGenParameters params(100000, 4, "memory"); params.after_clifford_depolarization = 0.001; auto circuit = generate_rep_code_circuit(params).circuit; auto actual = ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, true, false, 0.0, false, true); auto expected = DetectorErrorModel(R"MODEL( error(0.000267) D0 error(0.000267) D0 D1 error(0.000267) D0 D3 error(0.000533) D0 D4 error(0.000267) D1 D2 error(0.000533) D1 D4 error(0.000533) D1 D5 error(0.000267) D2 D5 error(0.000267) D2 L0 error(0.000267) D3 error(0.000267) D3 D4 error(0.000267) D3 ^ D0 error(0.000267) D4 D5 error(0.000267) D5 L0 error(0.000267) D5 L0 ^ D2 L0 detector(1, 0) D0 detector(3, 0) D1 detector(5, 0) D2 repeat 99998 { error(0.000267) D3 error(0.000267) D3 D4 error(0.000267) D3 D6 error(0.000533) D3 D7 error(0.000267) D4 D5 error(0.000533) D4 D7 error(0.000533) D4 D8 error(0.000267) D5 D8 error(0.000267) D5 L0 error(0.000267) D6 error(0.000267) D6 D7 error(0.000267) D6 ^ D3 error(0.000267) D7 D8 error(0.000267) D8 L0 error(0.000267) D8 L0 ^ D5 L0 shift_detectors(0, 1) 0 detector(1, 0) D3 detector(3, 0) D4 detector(5, 0) D5 shift_detectors 3 } error(0.000267) D3 error(0.000267) D3 D4 error(0.000267) D3 D6 error(0.000533) D3 D7 error(0.000267) D4 D5 error(0.000533) D4 D7 error(0.000533) D4 D8 error(0.000267) D5 D8 error(0.000267) D5 L0 error(0.000267) D6 error(0.000267) D6 D7 error(0.000267) D6 ^ D3 error(0.000267) D7 D8 error(0.000267) D8 L0 error(0.000267) D8 L0 ^ D5 L0 shift_detectors(0, 1) 0 detector(1, 0) D3 detector(3, 0) D4 detector(5, 0) D5 detector(1, 1) D6 detector(3, 1) D7 detector(5, 1) D8 )MODEL"); ASSERT_TRUE(actual.approx_equals(expected, 0.00001)) << actual; } TEST(ErrorAnalyzer, multi_round_gauge_detectors_dont_grow) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( # Distance 2 Bacon-Shor. ZCX 0 10 1 10 ZCX 2 11 3 11 XCX 0 12 2 12 XCX 1 13 3 13 MR 10 11 12 13 REPEAT 5 { ZCX 0 10 1 10 ZCX 2 11 3 11 XCX 0 12 2 12 XCX 1 13 3 13 MR 10 11 12 13 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] } )CIRCUIT"), false, false, true, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.5) D0 D1 error(0.5) D2 D3 error(0.5) D4 D5 error(0.5) D6 D7 error(0.5) D8 D9 error(0.5) D10 D11 error(0.5) D12 D13 error(0.5) D14 D15 error(0.5) D16 D17 error(0.5) D18 D19 )MODEL")); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( # Distance 2 Bacon-Shor. ZCX 0 10 1 10 ZCX 2 11 3 11 XCX 0 12 2 12 XCX 1 13 3 13 MR 10 11 12 13 REPEAT 5 { DEPOLARIZE1(0.01) 0 1 2 3 ZCX 0 10 1 10 ZCX 2 11 3 11 XCX 0 12 2 12 XCX 1 13 3 13 MR 10 11 12 13 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] } )CIRCUIT"), false, false, true, 0.0, false, true) .approx_equals( DetectorErrorModel(R"MODEL( error(0.00667) D0 error(0.5) D0 D1 error(0.00334) D0 D2 error(0.00334) D0 D3 error(0.00667) D1 error(0.00334) D1 D2 error(0.00334) D1 D3 error(0.00667) D2 error(0.5) D2 D3 error(0.00667) D3 error(0.00667) D4 error(0.5) D4 D5 error(0.00334) D4 D6 error(0.00334) D4 D7 error(0.00667) D5 error(0.00334) D5 D6 error(0.00334) D5 D7 error(0.00667) D6 error(0.5) D6 D7 error(0.00667) D7 error(0.00667) D8 error(0.5) D8 D9 error(0.00334) D8 D10 error(0.00334) D8 D11 error(0.00667) D9 error(0.00334) D9 D10 error(0.00334) D9 D11 error(0.00667) D10 error(0.5) D10 D11 error(0.00667) D11 error(0.00667) D12 error(0.5) D12 D13 error(0.00334) D12 D14 error(0.00334) D12 D15 error(0.00667) D13 error(0.00334) D13 D14 error(0.00334) D13 D15 error(0.00667) D14 error(0.5) D14 D15 error(0.00667) D15 error(0.00667) D16 error(0.5) D16 D17 error(0.00334) D16 D18 error(0.00334) D16 D19 error(0.00667) D17 error(0.00334) D17 D18 error(0.00334) D17 D19 error(0.00667) D18 error(0.5) D18 D19 error(0.00667) D19 )MODEL"), 0.01)); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( # Distance 2 Bacon-Shor. ZCX 0 10 1 10 ZCX 2 11 3 11 XCX 0 12 2 12 XCX 1 13 3 13 MR 10 11 12 13 REPEAT 1000000000000000 { ZCX 0 10 1 10 ZCX 2 11 3 11 XCX 0 12 2 12 XCX 1 13 3 13 MR 10 11 12 13 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] } )CIRCUIT"), false, true, true, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.5) D0 D1 error(0.5) D2 D3 error(0.5) D6 D7 repeat 499999999999999 { error(0.5) D4 D5 error(0.5) D8 D9 error(0.5) D10 D11 error(0.5) D14 D15 shift_detectors 8 } error(0.5) D4 D5 detector D0 detector D1 detector D2 detector D3 detector D6 detector D7 )MODEL")); } TEST(ErrorAnalyzer, coordinate_tracking) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( DETECTOR(1, 2) SHIFT_COORDS(10, 20) DETECTOR(100, 200) )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( detector(1, 2) D0 shift_detectors(10, 20) 0 detector(100, 200) D1 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MR 1 REPEAT 1000 { REPEAT 1000 { X_ERROR(0.25) 0 CNOT 0 1 MR 1 DETECTOR(1,2,3) rec[-2] rec[-1] SHIFT_COORDS(4,5) } SHIFT_COORDS(6,7) } M 0 OBSERVABLE_INCLUDE(9) rec[-1] )CIRCUIT"), false, true, true, 0.0, false, true), DetectorErrorModel(R"MODEL( REPEAT 999 { REPEAT 1000 { error(0.25) D0 L9 detector(1, 2, 3) D0 shift_detectors(4, 5) 1 } shift_detectors(6, 7) 0 } REPEAT 499 { error(0.25) D0 L9 error(0.25) D1 L9 detector(1, 2, 3) D0 shift_detectors(4, 5) 0 detector(1, 2, 3) D1 shift_detectors(4, 5) 2 } error(0.25) D0 L9 error(0.25) D1 L9 detector(1, 2, 3) D0 shift_detectors(4, 5) 0 detector(1, 2, 3) D1 shift_detectors(4, 5) 0 shift_detectors(6, 7) 0 )MODEL")); } TEST(ErrorAnalyzer, omit_vacuous_detector_observable_instructions) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 3 M 3 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 3 M 3 DETECTOR(1, 0) rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) D0 detector(1, 0) D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( M 3 DETECTOR rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( detector D0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( X_ERROR(0.25) 3 M 3 OBSERVABLE_INCLUDE(0) rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.25) L0 )model")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"circuit( M 3 OBSERVABLE_INCLUDE(0) rec[-1] )circuit"), false, false, false, 0.0, false, true), DetectorErrorModel(R"model( logical_observable L0 )model")); } TEST(ErrorAnalyzer, exact_solved_pauli_channel_1_is_let_through) { auto c = Circuit(R"CIRCUIT( R 0 PAULI_CHANNEL_1(0.1, 0.2, 0.15) 0 M 0 DETECTOR rec[-1] )CIRCUIT"); auto actual_dem = ErrorAnalyzer::circuit_to_detector_error_model(c, false, false, false, 0, false, true); ASSERT_TRUE(actual_dem.approx_equals( DetectorErrorModel(R"MODEL( error(0.3) D0 )MODEL"), 1e-6)); } TEST(ErrorAnalyzer, pauli_channel_threshold) { auto c1 = Circuit(R"CIRCUIT( R 0 PAULI_CHANNEL_1(0.125, 0.25, 0.375) 0 M 0 DETECTOR rec[-1] )CIRCUIT"); auto c2 = Circuit(R"CIRCUIT( R 0 PAULI_CHANNEL_2(0.125, 0.25, 0.375, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 1 0 M 0 DETECTOR rec[-1] )CIRCUIT"); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model(c1, false, false, false, 0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model(c1, false, false, false, 0.3, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model(c2, false, false, false, 0, false, true); }); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model(c2, false, false, false, 0.3, false, true); }); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(c1, false, false, false, 0.38, false, true), DetectorErrorModel(R"MODEL( error(0.375) D0 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(c1, false, false, false, 1, false, true), DetectorErrorModel(R"MODEL( error(0.375) D0 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(c2, false, false, false, 0.38, false, true), DetectorErrorModel(R"MODEL( error(0.375) D0 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(c2, false, false, false, 1, false, true), DetectorErrorModel(R"MODEL( error(0.375) D0 )MODEL")); } TEST(ErrorAnalyzer, pauli_channel_composite_errors) { auto measure_stabilizers = Circuit(R"circuit( XCX 0 1 0 3 0 4 MR 0 XCZ 0 1 0 2 0 4 0 5 MR 0 XCX 0 2 0 5 0 6 MR 0 XCZ 0 3 0 4 0 7 MR 0 XCX 0 4 0 5 0 7 0 8 MR 0 XCZ 0 5 0 6 0 7 MR 0 )circuit"); auto detectors = Circuit(R"circuit( DETECTOR rec[-6] rec[-12] DETECTOR rec[-5] rec[-11] DETECTOR rec[-4] rec[-10] DETECTOR rec[-3] rec[-9] DETECTOR rec[-2] rec[-8] DETECTOR rec[-1] rec[-7] )circuit"); // . 1 2 . // X0 Z1 X2 // 3 4 5 6 // Z3 X4 Z5 // . 7 8 . auto encode = measure_stabilizers; auto decode = measure_stabilizers + detectors; ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(encode + Circuit("PAULI_CHANNEL_1(0.00001, 0.02, 0.03) 4") + decode), true, false, false, 0.1, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.03) D0 D4 error(0.02) D0 D4 ^ D1 D3 error(0.00001) D1 D3 detector D2 detector D5 )model"), 1e-6)); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(encode + Circuit("PAULI_CHANNEL_1(0.00001, 0.02, 0.03) 5") + decode), true, false, false, 0.1, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.00001) D1 D5 error(0.02) D1 D5 ^ D2 D4 error(0.03) D2 D4 detector D0 detector D3 )model"), 1e-6)); ASSERT_TRUE( ErrorAnalyzer::circuit_to_detector_error_model( Circuit( encode + Circuit( "PAULI_CHANNEL_2(0.001,0.002,0.003,0.004,0.005,0.006,0.007,0.008,0.009,0.010,0." "011,0." "012,0.013,0.014,0.015) 4 5") + decode), true, false, false, 0.02, false, true) .approx_equals( DetectorErrorModel(R"model( error(0.015) D0 D2 # ZZ error(0.011) D0 D2 ^ D1 D3 # YZ error(0.014) D0 D2 ^ D1 D5 # ZY error(0.010) D0 D2 ^ D3 D5 # YY error(0.012) D0 D4 # Z_ basis error(0.008) D0 D4 ^ D1 D3 # Y_ error(0.013) D0 D4 ^ D1 D5 # ZX error(0.009) D0 D4 ^ D3 D5 # YX error(0.004) D1 D3 # X_ basis error(0.007) D1 D3 ^ D2 D4 # XZ error(0.001) D1 D5 # _X basis error(0.002) D1 D5 ^ D2 D4 # _Y error(0.003) D2 D4 # _Z basis error(0.006) D2 D4 ^ D3 D5 # XY error(0.005) D3 D5 # XX )model"), 1e-6)); } TEST(ErrorAnalyzer, duplicate_records_in_detectors) { auto m0 = ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X_ERROR(0.25) 0 M 0 DETECTOR )CIRCUIT"), false, false, false, false, false, true); auto m1 = ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X_ERROR(0.25) 0 M 0 DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, true); auto m2 = ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X_ERROR(0.25) 0 M 0 DETECTOR rec[-1] rec[-1] )CIRCUIT"), false, false, false, false, false, true); auto m3 = ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X_ERROR(0.25) 0 M 0 DETECTOR rec[-1] rec[-1] rec[-1] )CIRCUIT"), false, false, false, false, false, true); ASSERT_EQ(m0, m2); ASSERT_EQ(m1, m3); } TEST(ErrorAnalyzer, noisy_measurement_mx) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RX 0 MX(0.125) 0 MX 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 detector D1 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RX 0 1 Y_ERROR(1) 0 1 MX(0.125) 0 1 MX 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 error(1) D0 D2 error(0.125) D1 error(1) D1 D3 )MODEL")); } TEST(ErrorAnalyzer, noisy_measurement_my) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RY 0 MY(0.125) 0 MY 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 detector D1 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RY 0 1 Z_ERROR(1) 0 1 MY(0.125) 0 1 MY 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 error(1) D0 D2 error(0.125) D1 error(1) D1 D3 )MODEL")); } TEST(ErrorAnalyzer, noisy_measurement_mz) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RZ 0 MZ(0.125) 0 MZ 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 detector D1 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RZ 0 1 X_ERROR(1) 0 1 MZ(0.125) 0 1 MZ 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 error(1) D0 D2 error(0.125) D1 error(1) D1 D3 )MODEL")); } TEST(ErrorAnalyzer, noisy_measurement_mrx) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RX 0 MRX(0.125) 0 MRX 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 detector D1 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RX 0 1 Z_ERROR(1) 0 1 MRX(0.125) 0 1 MRX 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.875) D0 error(0.875) D1 detector D2 detector D3 )MODEL")); } TEST(ErrorAnalyzer, noisy_measurement_mry) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RY 0 MRY(0.125) 0 MRY 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 detector D1 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RY 0 1 X_ERROR(1) 0 1 MRY(0.125) 0 1 MRY 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.875) D0 error(0.875) D1 detector D2 detector D3 )MODEL")); } TEST(ErrorAnalyzer, noisy_measurement_mrz) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RZ 0 MRZ(0.125) 0 MRZ 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 detector D1 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RZ 0 1 X_ERROR(1) 0 1 MRZ(0.125) 0 1 MRZ 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.875) D0 error(0.875) D1 detector D2 detector D3 )MODEL")); } template std::string expect_catch_message(std::function func) { try { func(); EXPECT_TRUE(false) << "Function didn't throw an exception."; return ""; } catch (const TEx &ex) { return ex.what(); } } TEST(ErrorAnalyzer, context_clues_for_errors) { ASSERT_EQ( expect_catch_message([&]() { ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X 0 DEPOLARIZE1(1) 0 )CIRCUIT"), false, false, false, 0.0, false, true); }), "Can't analyze over-mixing DEPOLARIZE1 errors (probability > 3/4).\n" "\n" "Circuit stack trace:\n" " at instruction #2 [which is DEPOLARIZE1(1) 0]"); ASSERT_EQ( expect_catch_message([&]() { circuit_to_dem( Circuit(R"CIRCUIT( X 0 Y 1 REPEAT 500 { DEPOLARIZE1(1) 0 } Z 3 )CIRCUIT"), {.block_decomposition_from_introducing_remnant_edges = true}); }), "Can't analyze over-mixing DEPOLARIZE1 errors (probability > 3/4).\n" "\n" "Circuit stack trace:\n" " at instruction #3 [which is a REPEAT 500 block]\n" " at block's instruction #1 [which is DEPOLARIZE1(1) 0]"); } TEST(ErrorAnalyzer, too_many_symptoms) { auto symptoms_20 = Circuit(R"CIRCUIT( DEPOLARIZE1(0.001) 0 M 0 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] )CIRCUIT"); ASSERT_EQ( expect_catch_message([&]() { circuit_to_dem( symptoms_20, {.decompose_errors = true, .block_decomposition_from_introducing_remnant_edges = true}); }), R"MSG(An error case in a composite error exceeded the max supported number of symptoms (<=15). The 2 basis error cases (e.g. X, Z) used to form the combined error cases (e.g. Y = X*Z) are: 0: 1: D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D14, D15, D16, D17, D18, D19 Circuit stack trace: at instruction #1 [which is DEPOLARIZE1(0.001) 0])MSG"); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(symptoms_20, false, false, false, 0.0, false, true), DetectorErrorModel(R"model( error(0.0006666666666666692465) D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 D11 D12 D13 D14 D15 D16 D17 D18 D19 )model")); } TEST(ErrorAnalyzer, decompose_error_failures) { ASSERT_EQ( expect_catch_message([&]() { circuit_to_dem( Circuit(R"CIRCUIT( DEPOLARIZE1(0.001) 0 M 0 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] )CIRCUIT"), {.decompose_errors = true, .block_decomposition_from_introducing_remnant_edges = true}); }), R"MSG(Failed to decompose errors into graphlike components with at most two symptoms. The error component that failed to decompose is 'D0, D1, D2'. In Python, you can ignore this error by passing `ignore_decomposition_failures=True` to `stim.Circuit.detector_error_model(...)`. From the command line, you can ignore this error by passing the flag `--ignore_decomposition_failures` to `stim analyze_errors`. Note: `block_decomposition_from_introducing_remnant_edges` is ON. Turning it off may prevent this error.)MSG"); ASSERT_EQ( expect_catch_message([&]() { circuit_to_dem( Circuit(R"CIRCUIT( X_ERROR(0.001) 0 M 0 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] )CIRCUIT"), {.decompose_errors = true, .block_decomposition_from_introducing_remnant_edges = true}); }), R"MSG(Failed to decompose errors into graphlike components with at most two symptoms. The error component that failed to decompose is 'D0, D1, D2'. In Python, you can ignore this error by passing `ignore_decomposition_failures=True` to `stim.Circuit.detector_error_model(...)`. From the command line, you can ignore this error by passing the flag `--ignore_decomposition_failures` to `stim analyze_errors`. Note: `block_decomposition_from_introducing_remnant_edges` is ON. Turning it off may prevent this error.)MSG"); ASSERT_EQ( expect_catch_message([&]() { circuit_to_dem( Circuit(R"CIRCUIT( X_ERROR(0.001) 0 M 0 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] OBSERVABLE_INCLUDE(5) rec[-1] )CIRCUIT"), {.decompose_errors = true, .block_decomposition_from_introducing_remnant_edges = true}); }), R"MSG(Failed to decompose errors into graphlike components with at most two symptoms. The error component that failed to decompose is 'D0, D1, D2, L5'. In Python, you can ignore this error by passing `ignore_decomposition_failures=True` to `stim.Circuit.detector_error_model(...)`. From the command line, you can ignore this error by passing the flag `--ignore_decomposition_failures` to `stim analyze_errors`. Note: `block_decomposition_from_introducing_remnant_edges` is ON. Turning it off may prevent this error.)MSG"); } TEST(ErrorAnalyzer, other_error_decomposition_fallback) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X_ERROR(0.125) 0 MR 0 X_ERROR(0.25) 0 MR 0 DETECTOR rec[-2] DETECTOR rec[-2] DETECTOR rec[-1] rec[-2] DETECTOR rec[-1] rec[-2] OBSERVABLE_INCLUDE(5) rec[-2] OBSERVABLE_INCLUDE(6) rec[-1] )CIRCUIT"), true, false, false, 0.0, false, false), DetectorErrorModel(R"MODEL( error(0.25) D2 D3 L6 error(0.125) D2 D3 L6 ^ D0 D1 L5 L6 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X_ERROR(0.125) 0 MR 0 X_ERROR(0.25) 0 MR 0 DETECTOR rec[-2] DETECTOR rec[-2] DETECTOR rec[-1] rec[-2] DETECTOR rec[-1] rec[-2] )CIRCUIT"), true, false, false, 0.0, false, false), DetectorErrorModel(R"MODEL( error(0.25) D2 D3 error(0.125) D2 D3 ^ D0 D1 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X_ERROR(0.125) 0 MR 0 X_ERROR(0.25) 0 MR 0 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] rec[-2] DETECTOR rec[-1] rec[-2] )CIRCUIT"), true, false, false, 0.0, false, false), DetectorErrorModel(R"MODEL( error(0.125) D2 D3 error(0.25) D2 D3 ^ D0 D1 )MODEL")); } TEST(ErrorAnalyzer, is_graph_like) { ASSERT_TRUE(is_graphlike(std::vector{})); ASSERT_TRUE(is_graphlike(std::vector{DemTarget::separator()})); ASSERT_TRUE(is_graphlike( std::vector{ DemTarget::observable_id(0), DemTarget::observable_id(1), DemTarget::observable_id(2), DemTarget::separator(), DemTarget::observable_id(1), })); ASSERT_TRUE(is_graphlike( std::vector{ DemTarget::observable_id(0), DemTarget::relative_detector_id(1), DemTarget::observable_id(2), DemTarget::separator(), DemTarget::observable_id(1), })); ASSERT_TRUE(is_graphlike( std::vector{ DemTarget::observable_id(0), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), DemTarget::separator(), DemTarget::observable_id(1), })); ASSERT_FALSE(is_graphlike( std::vector{ DemTarget::relative_detector_id(0), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), DemTarget::separator(), DemTarget::observable_id(1), })); ASSERT_FALSE(is_graphlike( std::vector{ DemTarget::relative_detector_id(0), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), })); ASSERT_FALSE(is_graphlike( std::vector{ DemTarget::separator(), DemTarget::separator(), DemTarget::relative_detector_id(0), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), DemTarget::separator(), DemTarget::separator(), })); ASSERT_TRUE(is_graphlike( std::vector{ DemTarget::separator(), DemTarget::relative_detector_id(0), DemTarget::separator(), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), DemTarget::separator(), DemTarget::separator(), })); } TEST(ErrorAnalyzer, honeycomb_code_decomposes) { ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( R 3 5 7 9 11 13 18 20 22 24 26 28 X_ERROR(0.001) 3 5 7 9 11 13 18 20 22 24 26 28 DEPOLARIZE1(0.001) 0 1 2 4 6 8 10 12 14 15 16 17 19 21 23 25 27 29 XCX 24 1 7 6 11 12 3 15 20 21 28 27 R 0 8 14 17 23 29 DEPOLARIZE2(0.001) 24 1 7 6 11 12 3 15 20 21 28 27 X_ERROR(0.001) 0 8 14 17 23 29 YCX 20 0 7 8 3 14 11 17 24 23 28 29 XCX 9 1 5 6 13 12 18 15 22 21 26 27 R 2 4 10 16 19 25 DEPOLARIZE2(0.001) 20 0 7 8 3 14 11 17 24 23 28 29 9 1 5 6 13 12 18 15 22 21 26 27 X_ERROR(0.001) 2 4 10 16 19 25 X_ERROR(0.001) 1 6 12 15 21 27 CX 28 2 3 4 11 10 7 16 20 19 24 25 YCX 5 0 9 8 13 14 26 17 22 23 18 29 MR 1 6 12 15 21 27 OBSERVABLE_INCLUDE(0) rec[-5] rec[-4] DEPOLARIZE2(0.001) 28 2 3 4 11 10 7 16 20 19 24 25 5 0 9 8 13 14 26 17 22 23 18 29 X_ERROR(0.001) 1 6 12 15 21 27 X_ERROR(0.001) 0 8 14 17 23 29 XCX 24 1 7 6 11 12 3 15 20 21 28 27 CX 13 2 5 4 9 10 22 16 18 19 26 25 MR 0 8 14 17 23 29 OBSERVABLE_INCLUDE(0) rec[-5] rec[-4] DETECTOR rec[-12] rec[-11] rec[-8] rec[-6] rec[-5] rec[-2] DETECTOR rec[-10] rec[-9] rec[-7] rec[-4] rec[-3] rec[-1] DEPOLARIZE2(0.001) 24 1 7 6 11 12 3 15 20 21 28 27 13 2 5 4 9 10 22 16 18 19 26 25 X_ERROR(0.001) 0 8 14 17 23 29 X_ERROR(0.001) 2 4 10 16 19 25 YCX 20 0 7 8 3 14 11 17 24 23 28 29 XCX 9 1 5 6 13 12 18 15 22 21 26 27 MR 2 4 10 16 19 25 OBSERVABLE_INCLUDE(0) rec[-5] rec[-4] DEPOLARIZE2(0.001) 20 0 7 8 3 14 11 17 24 23 28 29 9 1 5 6 13 12 18 15 22 21 26 27 X_ERROR(0.001) 2 4 10 16 19 25 X_ERROR(0.001) 1 6 12 15 21 27 YCX 5 0 9 8 13 14 26 17 22 23 18 29 MR 1 6 12 15 21 27 OBSERVABLE_INCLUDE(0) rec[-5] rec[-4] DETECTOR rec[-24] rec[-22] rec[-19] rec[-12] rec[-10] rec[-7] rec[-6] rec[-4] rec[-1] DETECTOR rec[-23] rec[-21] rec[-20] rec[-11] rec[-9] rec[-8] rec[-5] rec[-3] rec[-2] DEPOLARIZE2(0.001) 5 0 9 8 13 14 26 17 22 23 18 29 X_ERROR(0.001) 1 6 12 15 21 27 X_ERROR(0.001) 0 8 14 17 23 29 MR 0 8 14 17 23 29 OBSERVABLE_INCLUDE(0) rec[-5] rec[-4] DETECTOR rec[-30] rec[-29] rec[-26] rec[-24] rec[-23] rec[-20] rec[-12] rec[-11] rec[-8] rec[-6] rec[-5] rec[-2] DETECTOR rec[-28] rec[-27] rec[-25] rec[-22] rec[-21] rec[-19] rec[-10] rec[-9] rec[-7] rec[-4] rec[-3] rec[-1] X_ERROR(0.001) 0 8 14 17 23 29 X_ERROR(0.001) 3 5 7 9 11 13 18 20 22 24 26 28 M 3 5 7 9 11 13 18 20 22 24 26 28 DETECTOR rec[-36] rec[-34] rec[-31] rec[-30] rec[-29] rec[-26] rec[-18] rec[-16] rec[-13] rec[-12] rec[-11] rec[-7] rec[-6] rec[-5] rec[-1] DETECTOR rec[-35] rec[-33] rec[-32] rec[-28] rec[-27] rec[-25] rec[-17] rec[-15] rec[-14] rec[-10] rec[-9] rec[-8] rec[-4] rec[-3] rec[-2] DETECTOR rec[-24] rec[-23] rec[-20] rec[-18] rec[-17] rec[-14] rec[-11] rec[-10] rec[-9] rec[-5] rec[-4] rec[-3] DETECTOR rec[-22] rec[-21] rec[-19] rec[-16] rec[-15] rec[-13] rec[-12] rec[-8] rec[-7] rec[-6] rec[-2] rec[-1] OBSERVABLE_INCLUDE(0) rec[-12] rec[-10] rec[-9] rec[-7] )CIRCUIT"), true, false, false, 0.0, false, false); } TEST(ErrorAnalyzer, measure_pauli_product_4body) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RX 0 Z_ERROR(0.125) 0 MPP X0*Z1 DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.125) D0 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MPP(0.25) Z0*Z1 DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.25) D0 )MODEL")); } TEST(ErrorAnalyzer, ignores_sweep_controls) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( X_ERROR(0.25) 0 CNOT sweep[0] 0 M 0 DETECTOR rec[-1] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( error(0.25) D0 )MODEL")); } TEST(ErrorAnalyzer, mpp_ordering) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MPP X0*X1 X0 TICK MPP X0 DETECTOR rec[-1] rec[-2] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( detector D0 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MPP X0*X1 X0 X0 DETECTOR rec[-1] rec[-2] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( detector D0 )MODEL")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MPP X2*X1 X0 TICK MPP X0 DETECTOR rec[-1] rec[-2] )CIRCUIT"), false, false, false, 0.0, false, true), DetectorErrorModel(R"MODEL( detector D0 )MODEL")); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MPP X0 X0*X1 TICK MPP X0 DETECTOR rec[-1] rec[-2] )CIRCUIT"), false, false, false, 0.0, false, true); }, std::invalid_argument); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( MPP X0 X2*X1 TICK MPP X0 DETECTOR rec[-1] rec[-2] )CIRCUIT"), false, false, false, 0.0, false, true); }, std::invalid_argument); } TEST(ErrorAnalyzer, anticommuting_observable_error_message_help) { for (size_t folding = 0; folding < 2; folding++) { ASSERT_EQ( expect_catch_message([&]() { circuit_to_dem( Circuit(R"CIRCUIT( QUBIT_COORDS(1, 2, 3) 0 RX 2 REPEAT 10 { REPEAT 20 { C_XYZ 0 R 1 M 1 DETECTOR rec[-1] TICK } } M 0 2 OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] )CIRCUIT"), {.flatten_loops = folding != 1}); }), R"ERROR(The circuit contains non-deterministic observables. To make an SVG picture of the problem, you can use the python API like this: your_circuit.diagram('detslice-with-ops-svg', tick=range(0, 5), filter_coords=['L0', ]) or the command line API like this: stim diagram --in your_circuit_file.stim --type detslice-with-ops-svg --tick 0:5 --filter_coords L0 > output_image.svg This was discovered while analyzing an X-basis reset (RX) on: qubit 2 The collapse anti-commuted with these detectors/observables: L0 The backward-propagating error sensitivity for L0 was: X0 [coords (1, 2, 3)] Z2 Circuit stack trace: during TICK layer #1 of 201 at instruction #2 [which is RX 2])ERROR"); ASSERT_EQ( expect_catch_message([&]() { circuit_to_dem( Circuit(R"CIRCUIT( TICK SHIFT_COORDS(1000, 2000) M 0 1 REPEAT 100 { RX 0 DETECTOR rec[-1] TICK } REPEAT 200 { TICK } REPEAT 100 { M 0 1 SHIFT_COORDS(0, 100) DETECTOR(1, 2, 3) rec[-1] rec[-3] DETECTOR(4, 5, 6) rec[-2] rec[-4] OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] rec[-3] rec[-4] TICK } REPEAT 1000 { TICK } )CIRCUIT"), {.flatten_loops = folding != 1}); }), R"ERROR(The circuit contains non-deterministic observables. The circuit contains non-deterministic detectors. To make an SVG picture of the problem, you can use the python API like this: your_circuit.diagram('detslice-with-ops-svg', tick=range(95, 105), filter_coords=['D101', 'L0', ]) or the command line API like this: stim diagram --in your_circuit_file.stim --type detslice-with-ops-svg --tick 95:105 --filter_coords D101:L0 > output_image.svg This was discovered while analyzing an X-basis reset (RX) on: qubit 0 The collapse anti-commuted with these detectors/observables: D101 [coords (1004, 2105, 6)] L0 The backward-propagating error sensitivity for D101 was: Z0 The backward-propagating error sensitivity for L0 was: Z0 Z1 Circuit stack trace: during TICK layer #101 of 1402 at instruction #4 [which is a REPEAT 100 block] at block's instruction #1 [which is RX 0])ERROR"); } } TEST(ErrorAnalyzer, brute_force_decomp_simple) { MonotonicBuffer buf; std::map, SpanRef> known; bool actual; std::vector problem{ DemTarget::relative_detector_id(0), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), }; auto add = [&](uint32_t a, uint32_t b) { DemTarget a2 = DemTarget::relative_detector_id(a); DemTarget b2 = DemTarget::relative_detector_id(b); FixedCapVector v; v.push_back(a2); if (b != UINT32_MAX) { v.push_back(b2); } buf.append_tail({v.begin(), v.end()}); known[v] = buf.commit_tail(); }; actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_FALSE(actual); ASSERT_TRUE(buf.tail.empty()); add(0, 2); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_FALSE(actual); ASSERT_TRUE(buf.tail.empty()); add(1, UINT32_MAX); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_TRUE(actual); ASSERT_EQ(buf.tail.size(), 5); ASSERT_EQ(buf.tail[0], DemTarget::relative_detector_id(0)); ASSERT_EQ(buf.tail[1], DemTarget::relative_detector_id(2)); ASSERT_EQ(buf.tail[2], DemTarget::separator()); ASSERT_EQ(buf.tail[3], DemTarget::relative_detector_id(1)); ASSERT_EQ(buf.tail[4], DemTarget::separator()); } TEST(ErrorAnalyzer, brute_force_decomp_introducing_obs_pair) { MonotonicBuffer buf; std::map, SpanRef> known; bool actual; std::vector problem{ DemTarget::relative_detector_id(0), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), }; auto add = [&](uint32_t a, uint32_t b, bool obs) { DemTarget a2 = DemTarget::relative_detector_id(a); DemTarget b2 = DemTarget::relative_detector_id(b); FixedCapVector v; v.push_back(a2); if (b != UINT32_MAX) { v.push_back(b2); } buf.append_tail({v.begin(), v.end()}); if (obs) { buf.append_tail(DemTarget::observable_id(5)); } known[v] = buf.commit_tail(); }; actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_FALSE(actual); ASSERT_TRUE(buf.tail.empty()); add(0, 2, true); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_FALSE(actual); ASSERT_TRUE(buf.tail.empty()); add(1, UINT32_MAX, false); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_FALSE(actual); ASSERT_TRUE(buf.tail.empty()); add(1, UINT32_MAX, true); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_TRUE(actual); ASSERT_EQ(buf.tail.size(), 7); ASSERT_EQ(buf.tail[0], DemTarget::relative_detector_id(0)); ASSERT_EQ(buf.tail[1], DemTarget::relative_detector_id(2)); ASSERT_EQ(buf.tail[2], DemTarget::observable_id(5)); ASSERT_EQ(buf.tail[3], DemTarget::separator()); ASSERT_EQ(buf.tail[4], DemTarget::relative_detector_id(1)); ASSERT_EQ(buf.tail[5], DemTarget::observable_id(5)); ASSERT_EQ(buf.tail[6], DemTarget::separator()); } TEST(ErrorAnalyzer, brute_force_decomp_with_obs) { MonotonicBuffer buf; std::map, SpanRef> known; bool actual; std::vector problem{ DemTarget::relative_detector_id(0), DemTarget::relative_detector_id(1), DemTarget::relative_detector_id(2), DemTarget::observable_id(5), }; auto add = [&](uint32_t a, uint32_t b, bool obs) { DemTarget a2 = DemTarget::relative_detector_id(a); DemTarget b2 = DemTarget::relative_detector_id(b); FixedCapVector v; v.push_back(a2); if (b != UINT32_MAX) { v.push_back(b2); } buf.append_tail({v.begin(), v.end()}); if (obs) { buf.append_tail(DemTarget::observable_id(5)); } known[v] = buf.commit_tail(); }; actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_FALSE(actual); ASSERT_TRUE(buf.tail.empty()); add(0, 2, true); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_FALSE(actual); ASSERT_TRUE(buf.tail.empty()); add(1, UINT32_MAX, false); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_TRUE(actual); ASSERT_EQ(buf.tail.size(), 6); ASSERT_EQ(buf.tail[0], DemTarget::relative_detector_id(0)); ASSERT_EQ(buf.tail[1], DemTarget::relative_detector_id(2)); ASSERT_EQ(buf.tail[2], DemTarget::observable_id(5)); ASSERT_EQ(buf.tail[3], DemTarget::separator()); ASSERT_EQ(buf.tail[4], DemTarget::relative_detector_id(1)); ASSERT_EQ(buf.tail[5], DemTarget::separator()); buf.discard_tail(); add(0, 2, false); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_FALSE(actual); ASSERT_TRUE(buf.tail.empty()); add(1, UINT32_MAX, true); actual = brute_force_decomposition_into_known_graphlike_errors({problem}, known, buf); ASSERT_TRUE(actual); ASSERT_EQ(buf.tail.size(), 6); ASSERT_EQ(buf.tail[0], DemTarget::relative_detector_id(0)); ASSERT_EQ(buf.tail[1], DemTarget::relative_detector_id(2)); ASSERT_EQ(buf.tail[2], DemTarget::separator()); ASSERT_EQ(buf.tail[3], DemTarget::relative_detector_id(1)); ASSERT_EQ(buf.tail[4], DemTarget::observable_id(5)); ASSERT_EQ(buf.tail[5], DemTarget::separator()); } TEST(ErrorAnalyzer, ignore_failures) { stim::Circuit circuit(Circuit(R"CIRCUIT( X_ERROR(0.25) 0 MR 0 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] X_ERROR(0.125) 0 1 2 CORRELATED_ERROR(0.25) X0 X1 X2 M 0 1 2 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] )CIRCUIT")); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, false, false, 0.0, false, false); }, std::invalid_argument); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, false, false, 0.0, true, false), DetectorErrorModel(R"MODEL( error(0.25) D0 D1 D2 error(0.125) D3 error(0.25) D3 ^ D4 ^ D5 error(0.125) D4 error(0.125) D5 )MODEL")); } TEST(ErrorAnalyzer, block_remnant_edge) { stim::Circuit circuit(Circuit(R"CIRCUIT( X_ERROR(0.125) 0 CORRELATED_ERROR(0.25) X0 X1 M 0 1 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-2] )CIRCUIT")); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, false, false, 0.0, false, true); }, std::invalid_argument); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(circuit, true, false, false, 0.0, false, false), DetectorErrorModel(R"MODEL( error(0.125) D2 D3 error(0.25) D2 D3 ^ D0 D1 )MODEL")); } TEST(ErrorAnalyzer, dont_fold_when_observable_dependencies_cross_iterations) { Circuit c(R"CIRCUIT( RX 0 2 REPEAT 100 { R 1 CX 0 1 2 1 MRZ 1 MRX 2 } MX 0 # Doesn't include all elements from the loop. OBSERVABLE_INCLUDE(0) rec[-1] rec[-2] rec[-4] )CIRCUIT"); ASSERT_ANY_THROW({ ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 1, false, false); }); } TEST(ErrorAnalyzer, else_correlated_error_block) { Circuit c(R"CIRCUIT( CORRELATED_ERROR(0.25) X0 ELSE_CORRELATED_ERROR(0.25) X1 ELSE_CORRELATED_ERROR(0.25) X2 M 0 1 2 DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 1, false, false), DetectorErrorModel(R"DEM( error(0.25) D0 error(0.1875) D1 error(0.140625) D2 )DEM")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 0.25, false, false), DetectorErrorModel(R"DEM( error(0.25) D0 error(0.1875) D1 error(0.140625) D2 )DEM")); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 0, false, false); }, std::invalid_argument); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 0.1, false, false); }, std::invalid_argument); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 0.24, false, false); }, std::invalid_argument); c = Circuit(R"CIRCUIT( CORRELATED_ERROR(0.25) X0 ELSE_CORRELATED_ERROR(0.25) X1 ELSE_CORRELATED_ERROR(0.25) X2 CORRELATED_ERROR(0.25) X3 ELSE_CORRELATED_ERROR(0.25) X4 ELSE_CORRELATED_ERROR(0.25) X5 M 0 1 2 3 4 5 DETECTOR rec[-6] DETECTOR rec[-5] DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 1, false, false), DetectorErrorModel(R"DEM( error(0.25) D0 error(0.1875) D1 error(0.140625) D2 error(0.25) D3 error(0.1875) D4 error(0.140625) D5 )DEM")); c = Circuit(R"CIRCUIT( CORRELATED_ERROR(0.25) X0 ELSE_CORRELATED_ERROR(0.25) Z1 H 1 ELSE_CORRELATED_ERROR(0.25) X2 )CIRCUIT"); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 1, false, false); }, std::invalid_argument); c = Circuit(R"CIRCUIT( CORRELATED_ERROR(0.25) X0 REPEAT 1 { ELSE_CORRELATED_ERROR(0.25) Z1 } )CIRCUIT"); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 1, false, false); }, std::invalid_argument); c = Circuit(R"CIRCUIT( ELSE_CORRELATED_ERROR(0.25) Z1 )CIRCUIT"); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(c, true, true, false, 1, false, false); }, std::invalid_argument); } TEST(ErrorAnalyzer, measurement_before_beginning) { Circuit c(R"CIRCUIT( DETECTOR rec[-1] )CIRCUIT"); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(c, false, false, false, false, false, false); }, std::invalid_argument); c = Circuit(R"CIRCUIT( OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model(c, false, false, false, false, false, false); }, std::invalid_argument); } TEST(ErrorAnalyzer, mpad) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( M(0.125) 5 MPAD 0 1 DETECTOR rec[-1] rec[-2] DETECTOR rec[-3] )CIRCUIT"), false, false, false, false, false, false), DetectorErrorModel(R"DEM( error(0.125) D1 detector D0 )DEM")); } TEST(ErrorAnalyzer, mxx) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RX 0 1 MXX(0.125) 0 1 DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, false), DetectorErrorModel(R"DEM( error(0.125) D0 )DEM")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RX 0 1 2 3 X_ERROR(0.125) 0 Y_ERROR(0.25) 1 Z_ERROR(0.375) 2 MXX 0 1 !2 !3 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, false), DetectorErrorModel(R"DEM( error(0.25) D0 error(0.375) D1 )DEM")); } TEST(ErrorAnalyzer, myy) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RY 0 1 MYY(0.125) 0 1 DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, false), DetectorErrorModel(R"DEM( error(0.125) D0 )DEM")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RY 0 1 2 3 Y_ERROR(0.125) 0 X_ERROR(0.25) 1 Z_ERROR(0.375) 2 MYY 0 1 !2 !3 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, false), DetectorErrorModel(R"DEM( error(0.25) D0 error(0.375) D1 )DEM")); } TEST(ErrorAnalyzer, mzz) { ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RZ 0 1 MZZ(0.125) 0 1 DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, false), DetectorErrorModel(R"DEM( error(0.125) D0 )DEM")); ASSERT_EQ( ErrorAnalyzer::circuit_to_detector_error_model( Circuit(R"CIRCUIT( RZ 0 1 2 3 Z_ERROR(0.125) 0 Y_ERROR(0.25) 1 X_ERROR(0.375) 2 MZZ 0 1 !2 !3 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false, false, false, false, false), DetectorErrorModel(R"DEM( error(0.25) D0 error(0.375) D1 )DEM")); } TEST(ErrorAnalyzer, heralded_erase_conditional_division) { auto dem = [](bool h, bool x, bool z) { Circuit c(R"CIRCUIT( MPP X0*X1 Z0*Z1 HERALDED_ERASE(1.0) 0 MPP X0*X1 Z0*Z1 )CIRCUIT"); if (h) { c.append_from_text("DETECTOR rec[-3]"); } else { c.append_from_text("DETECTOR"); } if (x) { c.append_from_text("DETECTOR rec[-2] rec[-5]"); } else { c.append_from_text("DETECTOR"); } if (z) { c.append_from_text("DETECTOR rec[-1] rec[-4]"); } else { c.append_from_text("DETECTOR"); } return ErrorAnalyzer::circuit_to_detector_error_model(c, false, false, false, 1.0, false, false); }; EXPECT_EQ(dem(0, 0, 0), DetectorErrorModel(R"DEM( detector D0 detector D1 detector D2 )DEM")); EXPECT_EQ(dem(0, 0, 1), DetectorErrorModel(R"DEM( error(0.5) D2 detector D0 detector D1 )DEM")); EXPECT_EQ(dem(0, 1, 0), DetectorErrorModel(R"DEM( error(0.5) D1 detector D0 detector D2 )DEM")); EXPECT_EQ(dem(0, 1, 1), DetectorErrorModel(R"DEM( error(0.25) D1 error(0.25) D1 D2 error(0.25) D2 detector D0 )DEM")); EXPECT_EQ(dem(1, 0, 0), DetectorErrorModel(R"DEM( error(1.0) D0 detector D1 detector D2 )DEM")); EXPECT_EQ(dem(1, 0, 1), DetectorErrorModel(R"DEM( error(0.5) D0 error(0.5) D0 D2 detector D1 )DEM")); EXPECT_EQ(dem(1, 1, 0), DetectorErrorModel(R"DEM( error(0.5) D0 error(0.5) D0 D1 detector D2 )DEM")); EXPECT_EQ(dem(1, 1, 1), DetectorErrorModel(R"DEM( error(0.25) D0 error(0.25) D0 D1 error(0.25) D0 D1 D2 error(0.25) D0 D2 )DEM")); } TEST(ErrorAnalyzer, heralded_erase) { circuit_to_dem(Circuit("HERALDED_ERASE(0.25) 0"), {.approximate_disjoint_errors_threshold = 0.3}); ASSERT_THROW( { circuit_to_dem(Circuit("HERALDED_ERASE(0.25) 0"), {.approximate_disjoint_errors_threshold = 0.2}); }, std::invalid_argument); ASSERT_EQ( circuit_to_dem( Circuit(R"CIRCUIT( MZZ 0 1 MXX 0 1 HERALDED_ERASE(0.25) 0 MZZ 0 1 MXX 0 1 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] )CIRCUIT"), {.approximate_disjoint_errors_threshold = 1}), DetectorErrorModel(R"DEM( error(0.0625) D0 D1 D2 error(0.0625) D0 D2 error(0.0625) D1 D2 error(0.0625) D2 )DEM")); ASSERT_EQ( circuit_to_dem( Circuit(R"CIRCUIT( MPP X10*X11*X20*X21 MPP Z11*Z12*Z21*Z22 MPP Z20*Z21*Z30*Z31 MPP X21*X22*X31*X32 HERALDED_ERASE(0.25) 21 MPP X10*X11*X20*X21 MPP Z11*Z12*Z21*Z22 MPP Z20*Z21*Z30*Z31 MPP X21*X22*X31*X32 DETECTOR rec[-1] rec[-6] DETECTOR rec[-2] rec[-7] DETECTOR rec[-3] rec[-8] DETECTOR rec[-4] rec[-9] DETECTOR rec[-5] )CIRCUIT"), {.decompose_errors = true, .approximate_disjoint_errors_threshold = 1}), DetectorErrorModel(R"DEM( error(0.0625) D0 D3 ^ D1 D2 ^ D4 error(0.0625) D0 D3 ^ D4 error(0.0625) D1 D2 ^ D4 error(0.0625) D4 )DEM")); ASSERT_EQ( circuit_to_dem( Circuit(R"CIRCUIT( M 0 HERALDED_ERASE(0.25) 9 0 9 9 9 M 0 DETECTOR rec[-1] rec[-7] DETECTOR rec[-5] )CIRCUIT"), {.approximate_disjoint_errors_threshold = 1}), DetectorErrorModel(R"DEM( error(0.125) D0 D1 error(0.125) D1 )DEM")); ASSERT_EQ( circuit_to_dem( Circuit(R"CIRCUIT( MPAD 0 MPAD 0 MPP Z20*Z21*Z30*Z31 MPP X21*X22*X31*X32 HERALDED_ERASE(0.25) 21 MPAD 0 MPAD 0 MPP Z20*Z21*Z30*Z31 MPP X21*X22*X31*X32 DETECTOR rec[-1] rec[-6] DETECTOR rec[-2] rec[-7] DETECTOR rec[-3] rec[-8] DETECTOR rec[-4] rec[-9] DETECTOR rec[-5] )CIRCUIT"), {.decompose_errors = true, .approximate_disjoint_errors_threshold = 1}), DetectorErrorModel(R"DEM( error(0.0625) D0 ^ D1 ^ D4 error(0.0625) D0 ^ D4 error(0.0625) D1 ^ D4 error(0.0625) D4 detector D2 detector D3 )DEM")); } TEST(ErrorAnalyzer, runs_on_general_circuit) { auto circuit = generate_test_circuit_with_all_operations(); auto dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, false, false, false, true, false, false); ASSERT_GT(dem.instructions.size(), 0); } TEST(ErrorAnalyzer, heralded_pauli_channel_1) { ErrorAnalyzer::circuit_to_detector_error_model( Circuit("HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.25, 0.03) 0"), false, false, false, 0.3, false, false); ASSERT_THROW( { ErrorAnalyzer::circuit_to_detector_error_model( Circuit("HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.25, 0.03) 0"), false, false, false, 0.2, false, false); }, std::invalid_argument); ASSERT_TRUE(circuit_to_dem( Circuit(R"CIRCUIT( MZZ 0 1 MXX 0 1 HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.03, 0.04) 0 MZZ 0 1 MXX 0 1 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] )CIRCUIT"), {.approximate_disjoint_errors_threshold = 1}) .approx_equals( DetectorErrorModel(R"DEM( error(0.03) D0 D1 D2 error(0.04) D0 D2 error(0.02) D1 D2 error(0.01) D2 )DEM"), 1e-6)); ASSERT_TRUE(circuit_to_dem( Circuit(R"CIRCUIT( MZZ 0 1 MXX 0 1 HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.03, 0.1) 0 MZZ 0 1 MXX 0 1 DETECTOR DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] )CIRCUIT"), {.approximate_disjoint_errors_threshold = 1}) .approx_equals( DetectorErrorModel(R"DEM( error(0.05) D1 D2 error(0.11) D2 detector D0 )DEM"), 1e-6)); } TEST(ErrorAnalyzer, OBS_INCLUDE_PAULIS) { auto circuit = Circuit(R"CIRCUIT( OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 X_ERROR(0.125) 0 Y_ERROR(0.25) 0 Z_ERROR(0.375) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 )CIRCUIT"); ASSERT_EQ(circuit_to_dem(circuit), DetectorErrorModel(R"DEM( error(0.375) L0 L1 error(0.25) L0 L2 error(0.125) L1 L2 )DEM")); circuit = Circuit(R"CIRCUIT( DEPOLARIZE1(0.125) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 X_ERROR(0.25) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 DEPOLARIZE1(0.125) 0 )CIRCUIT"); ASSERT_EQ(circuit_to_dem(circuit), DetectorErrorModel(R"DEM( error(0.25) L1 L2 logical_observable L0 logical_observable L0 )DEM")); circuit = Circuit(R"CIRCUIT( DEPOLARIZE1(0.125) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 Y_ERROR(0.25) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 DEPOLARIZE1(0.125) 0 )CIRCUIT"); ASSERT_EQ(circuit_to_dem(circuit), DetectorErrorModel(R"DEM( error(0.25) L0 L2 logical_observable L1 logical_observable L1 )DEM")); circuit = Circuit(R"CIRCUIT( DEPOLARIZE1(0.125) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 Z_ERROR(0.25) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 DEPOLARIZE1(0.125) 0 )CIRCUIT"); ASSERT_EQ(circuit_to_dem(circuit), DetectorErrorModel(R"DEM( error(0.25) L0 L1 logical_observable L2 logical_observable L2 )DEM")); } TEST(ErrorAnalyzer, tagged_noise) { ASSERT_EQ( circuit_to_dem(Circuit(R"CIRCUIT( R[test-tag-0] 0 X_ERROR[test-tag-1](0.25) 0 M[test-tag-2] 0 DETECTOR[test-tag-3] rec[-1] OBSERVABLE_INCLUDE[test-tag-4](0) rec[-1] SHIFT_COORDS[test-tag-5](1) )CIRCUIT")), DetectorErrorModel(R"DEM( error[test-tag-1](0.25) D0 L0 detector[test-tag-3] D0 logical_observable[test-tag-4] L0 shift_detectors[test-tag-5](1.0) 0 )DEM")); ASSERT_EQ( circuit_to_dem(Circuit(R"CIRCUIT( OBSERVABLE_INCLUDE[test-tag-1](0) OBSERVABLE_INCLUDE[test-tag-2](0) )CIRCUIT")), DetectorErrorModel(R"DEM( logical_observable[test-tag-1] L0 logical_observable[test-tag-2] L0 )DEM")); ASSERT_EQ( circuit_to_dem( Circuit(R"CIRCUIT( R 0 X_ERROR[test-tag-0](0.25) 0 REPEAT[test-tag-1] 100 { X_ERROR[test-tag-2](0.125) 0 MR 0 DETECTOR[test-tag-3] rec[-1] OBSERVABLE_INCLUDE[test-tag-4](0) rec[-1] } )CIRCUIT"), {.decompose_errors = false, .flatten_loops = false}), DetectorErrorModel(R"DEM( error[test-tag-0](0.25) D0 L0 repeat[test-tag-1] 99 { error[test-tag-2](0.125) D0 L0 detector[test-tag-3] D0 logical_observable[test-tag-4] L0 shift_detectors 1 } error[test-tag-2](0.125) D0 L0 detector[test-tag-3] D0 logical_observable[test-tag-4] L0 )DEM")); } ================================================ FILE: src/stim/simulators/error_matcher.cc ================================================ // 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. #include "stim/simulators/error_matcher.h" #include #include using namespace stim; ErrorMatcher::ErrorMatcher( const Circuit &circuit, const DetectorErrorModel *init_filter, bool reduce_to_one_representative_error) : error_analyzer( circuit.count_measurements(), circuit.count_detectors(), circuit.count_qubits(), circuit.count_ticks(), false, false, true, 1, false, false), cur_loc(), output_map(), allow_adding_new_dem_errors_to_output_map(init_filter == nullptr), reduce_to_one_representative_error(reduce_to_one_representative_error), dem_coords_map(), qubit_coords_map(circuit.get_final_qubit_coords()), cur_coord_offset(circuit.final_coord_shift()), total_measurements_in_circuit(error_analyzer.tracker.num_measurements_in_past), total_ticks_in_circuit(error_analyzer.num_ticks_in_past) { // If filtering, get the filter errors into the output map immediately. if (!allow_adding_new_dem_errors_to_output_map) { SparseXorVec buf; init_filter->iter_flatten_error_instructions([&](const DemInstruction &instruction) { assert(instruction.type == DemInstructionType::DEM_ERROR); buf.clear(); // Note: quadratic overhead, but typical size is 4 and 100 would be crazy big. for (const auto &target : instruction.target_data) { if (!target.is_separator()) { buf.xor_item(target); } } dem_targets_buf.append_tail(buf.sorted_items); output_map.insert({dem_targets_buf.commit_tail(), {{}, {}}}); }); } } void ErrorMatcher::add_dem_error(ErrorEquivalenceClass dem_error) { auto entry = output_map.find(dem_error.targets); if (!dem_error.targets.empty() && (allow_adding_new_dem_errors_to_output_map || entry != output_map.end())) { // We have a desired match! Record it. CircuitErrorLocation new_loc = cur_loc; new_loc.noise_tag = dem_error.tag; if (cur_op != nullptr) { new_loc.instruction_targets.fill_args_and_targets_in_range(*cur_op, qubit_coords_map); } if (entry == output_map.end()) { dem_targets_buf.append_tail(dem_error.targets); auto stored_key = dem_targets_buf.commit_tail(); entry = output_map.insert({stored_key, {{}, {}}}).first; } auto &out = entry->second.circuit_error_locations; if (out.empty() || !reduce_to_one_representative_error) { out.push_back(std::move(new_loc)); } else if (new_loc.is_simpler_than(out.front())) { out[0] = std::move(new_loc); } } } void ErrorMatcher::err_atom(const CircuitInstruction &effect) { assert(error_analyzer.error_class_probabilities.empty()); error_analyzer.undo_gate(effect); if (error_analyzer.error_class_probabilities.empty()) { /// Maybe there were no detectors or observables nearby? Or the noise probability was zero? return; } assert(error_analyzer.error_class_probabilities.size() == 1); ErrorEquivalenceClass dem_error = error_analyzer.error_class_probabilities.begin()->first; add_dem_error(dem_error); // Restore the pristine state. error_analyzer.mono_buf.clear(); error_analyzer.error_class_probabilities.clear(); error_analyzer.flushed_reversed_model.clear(); } void ErrorMatcher::resolve_paulis_into( SpanRef targets, uint32_t target_flags, std::vector &out) { for (const auto &t : targets) { if (t.is_combiner()) { continue; } auto entry = qubit_coords_map.find(t.qubit_value()); if (entry != qubit_coords_map.end()) { out.push_back({t, entry->second}); } else { out.push_back({t, {}}); } out.back().gate_target.data |= target_flags; } } void ErrorMatcher::err_xyz(const CircuitInstruction &op, uint32_t target_flags) { const auto &a = op.args; const auto &t = op.targets; assert(a.size() == 1); if (a[0] == 0) { return; } for (size_t k = op.targets.size(); k--;) { cur_loc.instruction_targets.target_range_start = k; cur_loc.instruction_targets.target_range_end = k + 1; resolve_paulis_into(&op.targets[k], target_flags, cur_loc.flipped_pauli_product); err_atom(CircuitInstruction{op.gate_type, a, &t[k], op.tag}); cur_loc.flipped_pauli_product.clear(); } } void ErrorMatcher::err_heralded_pauli_channel_1(const CircuitInstruction &op) { assert(op.args.size() == 4); for (size_t k = op.targets.size(); k--;) { auto q = op.targets[k].qubit_value(); cur_loc.instruction_targets.target_range_start = k; cur_loc.instruction_targets.target_range_end = k + 1; cur_loc.flipped_measurement.measurement_record_index = error_analyzer.tracker.num_measurements_in_past - 1; SpanRef herald_symptoms = error_analyzer.tracker.rec_bits[error_analyzer.tracker.num_measurements_in_past - 1].range(); SpanRef x_symptoms = error_analyzer.tracker.zs[q].range(); SpanRef z_symptoms = error_analyzer.tracker.xs[q].range(); if (op.args[0] != 0) { add_dem_error(ErrorEquivalenceClass{herald_symptoms, op.tag}); } if (op.args[1] != 0) { error_analyzer.mono_buf.append_tail(herald_symptoms); error_analyzer.mono_buf.append_tail(x_symptoms); error_analyzer.mono_buf.tail = inplace_xor_sort(error_analyzer.mono_buf.tail); resolve_paulis_into(&op.targets[k], TARGET_PAULI_X_BIT, cur_loc.flipped_pauli_product); add_dem_error(ErrorEquivalenceClass{error_analyzer.mono_buf.tail, op.tag}); cur_loc.flipped_pauli_product.clear(); error_analyzer.mono_buf.discard_tail(); } if (op.args[2] != 0) { error_analyzer.mono_buf.append_tail(herald_symptoms); error_analyzer.mono_buf.append_tail(x_symptoms); error_analyzer.mono_buf.append_tail(z_symptoms); error_analyzer.mono_buf.tail = inplace_xor_sort(error_analyzer.mono_buf.tail); resolve_paulis_into(&op.targets[k], TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT, cur_loc.flipped_pauli_product); add_dem_error(ErrorEquivalenceClass{error_analyzer.mono_buf.tail, op.tag}); cur_loc.flipped_pauli_product.clear(); error_analyzer.mono_buf.discard_tail(); } if (op.args[3] != 0) { error_analyzer.mono_buf.append_tail(herald_symptoms); error_analyzer.mono_buf.append_tail(z_symptoms); error_analyzer.mono_buf.tail = inplace_xor_sort(error_analyzer.mono_buf.tail); resolve_paulis_into(&op.targets[k], TARGET_PAULI_Z_BIT, cur_loc.flipped_pauli_product); add_dem_error(ErrorEquivalenceClass{error_analyzer.mono_buf.tail, op.tag}); cur_loc.flipped_pauli_product.clear(); error_analyzer.mono_buf.discard_tail(); } cur_loc.flipped_measurement.measurement_record_index = UINT64_MAX; assert(error_analyzer.error_class_probabilities.empty()); error_analyzer.tracker.undo_gate( CircuitInstruction{ op.gate_type, op.args, op.targets.sub(k, k + 1), op.tag, }); error_analyzer.mono_buf.clear(); error_analyzer.error_class_probabilities.clear(); error_analyzer.flushed_reversed_model.clear(); } } void ErrorMatcher::err_pauli_channel_1(const CircuitInstruction &op) { const auto &a = op.args; const auto &t = op.targets; err_xyz(CircuitInstruction{GateType::X_ERROR, &a[0], t, op.tag}, TARGET_PAULI_X_BIT); err_xyz(CircuitInstruction{GateType::Y_ERROR, &a[1], t, op.tag}, TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT); err_xyz(CircuitInstruction{GateType::Z_ERROR, &a[2], t, op.tag}, TARGET_PAULI_Z_BIT); } void ErrorMatcher::err_pauli_channel_2(const CircuitInstruction &op) { const auto &t = op.targets; const auto &a = op.args; // Buffers and pointers into them. std::array pair; double p = 0; CircuitInstruction pair_effect = {GateType::E, &p, pair, op.tag}; CircuitInstruction first_effect = {GateType::E, &p, &pair[0], op.tag}; CircuitInstruction second_effect = {GateType::E, &p, &pair[1], op.tag}; for (size_t k = 0; k < t.size(); k += 2) { cur_loc.instruction_targets.target_range_start = k; cur_loc.instruction_targets.target_range_end = k + 2; for (uint8_t p0 = 0; p0 < 4; p0++) { for (uint8_t p1 = !p0; p1 < 4; p1++) { // Extract data for this term of the error. p = a[p0 * 4 + p1 - 1]; if (p == 0) { continue; } bool x0 = p0 & 1; bool z0 = p0 & 2; x0 ^= z0; bool x1 = p1 & 1; bool z1 = p1 & 2; x1 ^= z1; uint32_t m0 = x0 * TARGET_PAULI_X_BIT + z0 * TARGET_PAULI_Z_BIT; uint32_t m1 = x1 * TARGET_PAULI_X_BIT + z1 * TARGET_PAULI_Z_BIT; pair[0].data = t[k].data | m0; pair[1].data = t[k + 1].data | m1; // Handle the error term as if it were an isolated CORRELATED_ERROR. if (p0 == 0) { resolve_paulis_into(&pair[1], 0, cur_loc.flipped_pauli_product); err_atom(second_effect); } else if (p1 == 0) { resolve_paulis_into(&pair[0], 0, cur_loc.flipped_pauli_product); err_atom(first_effect); } else { resolve_paulis_into(pair, 0, cur_loc.flipped_pauli_product); err_atom(pair_effect); } cur_loc.flipped_pauli_product.clear(); } } } } void ErrorMatcher::err_m(const CircuitInstruction &op, uint32_t obs_mask) { const auto &t = op.targets; const auto &a = op.args; bool q2 = GATE_DATA[op.gate_type].flags & GATE_TARGETS_PAIRS; size_t end = t.size(); while (end > 0) { size_t start = end - 1; while (start > 0 && t[start - 1].is_combiner()) { start -= std::min(start, size_t{2}); } if (q2) { start--; } SpanRef slice{t.begin() + start, t.begin() + end}; cur_loc.instruction_targets.target_range_start = start; cur_loc.instruction_targets.target_range_end = end; cur_loc.flipped_measurement.measurement_record_index = error_analyzer.tracker.num_measurements_in_past - 1; resolve_paulis_into(slice, obs_mask, cur_loc.flipped_measurement.measured_observable); err_atom(CircuitInstruction{op.gate_type, a, slice, op.tag}); cur_loc.flipped_measurement.measurement_record_index = UINT64_MAX; cur_loc.flipped_measurement.measured_observable.clear(); end = start; } } void ErrorMatcher::rev_process_instruction(const CircuitInstruction &op) { cur_loc.instruction_targets.gate_type = op.gate_type; cur_loc.instruction_targets.gate_tag = op.tag; auto flags = GATE_DATA[op.gate_type].flags; cur_loc.tick_offset = error_analyzer.num_ticks_in_past; cur_op = &op; if (op.gate_type == GateType::DETECTOR) { error_analyzer.undo_DETECTOR(op); if (!op.args.empty()) { auto id = error_analyzer.tracker.num_detectors_in_past; auto entry = dem_coords_map.insert({id, {}}).first; for (size_t k = 0; k < op.args.size(); k++) { double d = op.args[k]; if (k < cur_coord_offset.size()) { d += cur_coord_offset[k]; } entry->second.push_back(d); } } return; } else if (op.gate_type == GateType::SHIFT_COORDS) { error_analyzer.undo_SHIFT_COORDS(op); for (size_t k = 0; k < op.args.size(); k++) { cur_coord_offset[k] -= op.args[k]; } return; } else if (!(flags & (GATE_IS_NOISY | GATE_PRODUCES_RESULTS))) { error_analyzer.undo_gate(op); return; } switch (op.gate_type) { case GateType::MPAD: error_analyzer.undo_gate(op); break; case GateType::E: case GateType::ELSE_CORRELATED_ERROR: { cur_loc.instruction_targets.target_range_start = 0; cur_loc.instruction_targets.target_range_end = op.targets.size(); resolve_paulis_into(op.targets, 0, cur_loc.flipped_pauli_product); CircuitInstruction op2 = op; op2.gate_type = GateType::E; err_atom(op2); cur_loc.flipped_pauli_product.clear(); break; } case GateType::I_ERROR: case GateType::II_ERROR: // No effect. break; case GateType::X_ERROR: err_xyz(op, TARGET_PAULI_X_BIT); break; case GateType::Y_ERROR: err_xyz(op, TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT); break; case GateType::Z_ERROR: err_xyz(op, TARGET_PAULI_Z_BIT); break; case GateType::PAULI_CHANNEL_1: err_pauli_channel_1(op); break; case GateType::HERALDED_PAULI_CHANNEL_1: err_heralded_pauli_channel_1(op); break; case GateType::HERALDED_ERASE: { float p = op.args[0] / 4; std::array spread{p, p, p, p}; err_heralded_pauli_channel_1(CircuitInstruction{op.gate_type, spread, op.targets, op.tag}); break; } case GateType::DEPOLARIZE1: { float p = op.args[0]; std::array spread{p, p, p}; err_pauli_channel_1(CircuitInstruction{op.gate_type, spread, op.targets, op.tag}); break; } case GateType::PAULI_CHANNEL_2: err_pauli_channel_2(op); break; case GateType::DEPOLARIZE2: { float p = op.args[0]; std::array spread{p, p, p, p, p, p, p, p, p, p, p, p, p, p, p}; err_pauli_channel_2(CircuitInstruction{op.gate_type, spread, op.targets, op.tag}); break; } case GateType::MPP: err_m(op, 0); break; case GateType::MX: case GateType::MRX: case GateType::MXX: err_m(op, TARGET_PAULI_X_BIT); break; case GateType::MY: case GateType::MRY: case GateType::MYY: err_m(op, TARGET_PAULI_X_BIT | TARGET_PAULI_Z_BIT); break; case GateType::M: case GateType::MR: case GateType::MZZ: err_m(op, TARGET_PAULI_Z_BIT); break; default: throw std::invalid_argument( "Not implemented in ErrorMatcher::rev_process_instruction: " + std::string(GATE_DATA[op.gate_type].name)); } } void ErrorMatcher::rev_process_circuit(uint64_t reps, const Circuit &block) { cur_loc.stack_frames.push_back({0, 0, 0}); cur_loc.flipped_measurement.measurement_record_index = UINT64_MAX; for (size_t rep = reps; rep--;) { cur_loc.stack_frames.back().iteration_index = rep; for (size_t k = block.operations.size(); k--;) { cur_loc.stack_frames.back().instruction_offset = k; const auto &op = block.operations[k]; if (op.gate_type == GateType::REPEAT) { auto rep_count = op.repeat_block_rep_count(); cur_loc.stack_frames.back().instruction_repetitions_arg = op.repeat_block_rep_count(); rev_process_circuit(rep_count, op.repeat_block_body(block)); cur_loc.stack_frames.back().instruction_repetitions_arg = 0; } else { rev_process_instruction(op); } } } cur_loc.stack_frames.pop_back(); } std::vector ErrorMatcher::explain_errors_from_circuit( const Circuit &circuit, const DetectorErrorModel *filter, bool reduce_to_one_representative_error) { // Find the matches. ErrorMatcher finder(circuit, filter, reduce_to_one_representative_error); finder.rev_process_circuit(1, circuit); // And list them out. std::vector result; for (auto &e : finder.output_map) { e.second.fill_in_dem_targets(e.first, finder.dem_coords_map); result.push_back(std::move(e.second)); } return result; } ================================================ FILE: src/stim/simulators/error_matcher.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_ERROR_MATCHER_H #define _STIM_SIMULATORS_ERROR_MATCHER_H #include "stim/simulators/error_analyzer.h" #include "stim/simulators/matched_error.h" namespace stim { /// This class handles matching circuit errors to detector-error-model errors. struct ErrorMatcher { /// The error analyzer handles most of converting circuit errors into detector errors. ErrorAnalyzer error_analyzer; // This value is tweaked and adjusted while iterating through the circuit, tracking // where we currently are. CircuitErrorLocation cur_loc; const CircuitInstruction *cur_op; // Tracks discovered pairings keyed by their detector-error-model error terms. // // Pointed-to key data is owned by `dem_targets_buf``. std::map, ExplainedError> output_map; bool allow_adding_new_dem_errors_to_output_map; bool reduce_to_one_representative_error; std::map> dem_coords_map; std::map> qubit_coords_map; std::vector cur_coord_offset; MonotonicBuffer dem_targets_buf; uint64_t total_measurements_in_circuit; uint64_t total_ticks_in_circuit; // This class has pointers into its own data. Can't just copy it around! ErrorMatcher(const ErrorMatcher &) = delete; /// Finds detector-error-model errors while matching them with their source circuit errors. /// /// Note that detector-error-model errors are not repeated. Each is matched with one /// representative circuit error, which is chosen arbitrarily from the available options. /// /// Args: /// circuit: The noisy circuit to search for detector-error-model+circuit matches. /// filter: Optional. When not empty, any detector-error-model errors that don't appear /// in this filter will not be included in the result. When empty, all /// detector-error-model errors are included. /// /// Returns: /// A list of detector-error-model-paired-with-explanatory-circuit-error items. static std::vector explain_errors_from_circuit( const Circuit &circuit, const DetectorErrorModel *filter, bool reduce_to_one_representative_error); /// Constructs an error candidate finder based on parameters that are given to /// `ErrorCandidateFinder::explain_errors_from_circuit`. ErrorMatcher(const Circuit &circuit, const DetectorErrorModel *filter, bool reduce_to_one_representative_error); /// Looks up the coordinates of qubit/pauli terms, and appends into an output vector. void resolve_paulis_into( SpanRef targets, uint32_t target_flags, std::vector &out); /// Base case for processing a single-term error mechanism. void err_atom(const CircuitInstruction &effect); /// Processes operations with X, Y, Z errors on each target. void err_pauli_channel_1(const CircuitInstruction &op); /// Processes operations with M, X, Y, Z errors on each target. void err_heralded_pauli_channel_1(const CircuitInstruction &op); /// Processes operations with 15 two-qubit Pauli product errors on each target pair. void err_pauli_channel_2(const CircuitInstruction &op); /// Processes measurement operations. void err_m(const CircuitInstruction &op, uint32_t obs_mask); void err_xyz(const CircuitInstruction &op, uint32_t target_flags); /// Processes arbitrary instructions. void rev_process_instruction(const CircuitInstruction &op); /// Processes entire circuits. void rev_process_circuit(uint64_t reps, const Circuit &block); void add_dem_error(ErrorEquivalenceClass dem_error); }; } // namespace stim #endif ================================================ FILE: src/stim/simulators/error_matcher.test.cc ================================================ // 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. #include "stim/simulators/error_matcher.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" using namespace stim; TEST(ErrorMatcher, X_ERROR) { auto actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( QUBIT_COORDS(5, 6) 0 X_ERROR(0.25) 0 M 0 DETECTOR(2, 3) rec[-1] )CIRCUIT"), nullptr, false); std::vector expected{ ExplainedError{ { {DemTarget::relative_detector_id(0), {2, 3}}, }, { CircuitErrorLocation{ .noise_tag = "", .tick_offset = 0, .flipped_pauli_product = { {GateTarget::x(0), {5, 6}}, }, .flipped_measurement = FlippedMeasurement{UINT64_MAX, {}}, .instruction_targets = CircuitTargetsInsideInstruction{ .gate_type = GateType::X_ERROR, .gate_tag = "", .args = {0.25}, .target_range_start = 0, .target_range_end = 1, .targets_in_range = { {GateTarget::qubit(0), {5, 6}}, }, }, .stack_frames = { CircuitErrorLocationStackFrame{1, 0, 0}, }, }, }}, }; ASSERT_EQ(actual, expected); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].str(), R"RESULT(ExplainedError { dem_error_terms: D0[coords 2,3] CircuitErrorLocation { flipped_pauli_product: X0[coords 5,6] Circuit location stack trace: (after 0 TICKs) at instruction #2 (X_ERROR) in the circuit at target #1 of the instruction resolving to X_ERROR(0.25) 0[coords 5,6] } })RESULT"); } TEST(ErrorMatcher, CORRELATED_ERROR) { auto actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( SHIFT_COORDS(10, 20) QUBIT_COORDS(5, 6) 0 SHIFT_COORDS(100, 200) CORRELATED_ERROR(0.25) X0 Y1 M 0 DETECTOR(2, 3) rec[-1] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].str(), R"RESULT(ExplainedError { dem_error_terms: D0[coords 112,223] CircuitErrorLocation { flipped_pauli_product: X0[coords 15,26]*Y1 Circuit location stack trace: (after 0 TICKs) at instruction #4 (E) in the circuit at targets #1 to #2 of the instruction resolving to E(0.25) X0[coords 15,26] Y1 } })RESULT"); } TEST(ErrorMatcher, MX_ERROR) { auto actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( QUBIT_COORDS(5, 6) 0 RX 0 REPEAT 10 { TICK } MX(0.125) 1 2 3 0 4 DETECTOR(2, 3) rec[-2] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].str(), R"RESULT(ExplainedError { dem_error_terms: D0[coords 2,3] CircuitErrorLocation { flipped_measurement.measurement_record_index: 3 flipped_measurement.measured_observable: X0[coords 5,6] Circuit location stack trace: (after 10 TICKs) at instruction #4 (MX) in the circuit at target #4 of the instruction resolving to MX(0.125) 0[coords 5,6] } })RESULT"); } TEST(ErrorMatcher, MPP_ERROR) { auto actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( QUBIT_COORDS(5, 6) 0 RY 0 MPP(0.125) Z1 Y0*Z3*Z4 Y5 DETECTOR(2, 3) rec[-2] DETECTOR(5, 6) rec[-2] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].str(), R"RESULT(ExplainedError { dem_error_terms: D0[coords 2,3] D1[coords 5,6] CircuitErrorLocation { flipped_measurement.measurement_record_index: 1 flipped_measurement.measured_observable: Y0[coords 5,6]*Z3*Z4 Circuit location stack trace: (after 0 TICKs) at instruction #3 (MPP) in the circuit at targets #2 to #6 of the instruction resolving to MPP(0.125) Y0[coords 5,6]*Z3*Z4 } })RESULT"); } TEST(ErrorMatcher, MXX_ERROR) { auto actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( QUBIT_COORDS(5, 6) 0 RX 0 CX 0 1 MXX(0.125) 0 1 DETECTOR(2, 3) rec[-1] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].str(), R"RESULT(ExplainedError { dem_error_terms: D0[coords 2,3] CircuitErrorLocation { flipped_measurement.measurement_record_index: 0 flipped_measurement.measured_observable: X0[coords 5,6]*X1 Circuit location stack trace: (after 0 TICKs) at instruction #4 (MXX) in the circuit at targets #1 to #2 of the instruction resolving to MXX(0.125) 0[coords 5,6] 1 } })RESULT"); } TEST(ErrorMatcher, ELSE_CORRELATED_ERROR) { auto actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( R 0 1 H 1 CORRELATED_ERROR(0.25) X0 ELSE_CORRELATED_ERROR(0.125) Z1 H 1 M 0 1 DETECTOR rec[-1] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].str(), R"RESULT(ExplainedError { dem_error_terms: D0 CircuitErrorLocation { flipped_pauli_product: Z1 Circuit location stack trace: (after 0 TICKs) at instruction #4 (ELSE_CORRELATED_ERROR) in the circuit at target #1 of the instruction resolving to ELSE_CORRELATED_ERROR(0.125) Z1 } })RESULT"); } TEST(ErrorMatcher, HERALDED_ERASE) { auto actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 1 MYY 0 1 MZZ 0 1 HERALDED_ERASE(0.125) 0 MXX 0 1 MYY 0 1 MZZ 0 1 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 4); ASSERT_EQ(actual[0].str(), R"RESULT(ExplainedError { dem_error_terms: D0 D1 D3 CircuitErrorLocation { flipped_pauli_product: X0 flipped_measurement.measurement_record_index: 3 Circuit location stack trace: (after 0 TICKs) at instruction #4 (HERALDED_ERASE) in the circuit at target #1 of the instruction resolving to HERALDED_ERASE(0.125) 0 } })RESULT"); ASSERT_EQ(actual[1].str(), R"RESULT(ExplainedError { dem_error_terms: D0 D2 D3 CircuitErrorLocation { flipped_pauli_product: Y0 flipped_measurement.measurement_record_index: 3 Circuit location stack trace: (after 0 TICKs) at instruction #4 (HERALDED_ERASE) in the circuit at target #1 of the instruction resolving to HERALDED_ERASE(0.125) 0 } })RESULT"); ASSERT_EQ(actual[2].str(), R"RESULT(ExplainedError { dem_error_terms: D1 D2 D3 CircuitErrorLocation { flipped_pauli_product: Z0 flipped_measurement.measurement_record_index: 3 Circuit location stack trace: (after 0 TICKs) at instruction #4 (HERALDED_ERASE) in the circuit at target #1 of the instruction resolving to HERALDED_ERASE(0.125) 0 } })RESULT"); ASSERT_EQ(actual[3].str(), R"RESULT(ExplainedError { dem_error_terms: D3 CircuitErrorLocation { flipped_measurement.measurement_record_index: 3 Circuit location stack trace: (after 0 TICKs) at instruction #4 (HERALDED_ERASE) in the circuit at target #1 of the instruction resolving to HERALDED_ERASE(0.125) 0 } })RESULT"); } TEST(ErrorMatcher, repetition_code_data_depolarization) { CircuitGenParameters params(2, 3, "memory"); params.before_round_data_depolarization = 0.001; auto circuit = generate_rep_code_circuit(params).circuit; auto actual = ErrorMatcher::explain_errors_from_circuit(circuit, nullptr, false); std::stringstream ss; for (const auto &match : actual) { ss << "\n" << match << "\n"; } ASSERT_EQ(ss.str(), R"RESULT( ExplainedError { dem_error_terms: D0[coords 1,0] CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } } ExplainedError { dem_error_terms: D0[coords 1,0] D1[coords 3,0] CircuitErrorLocation { flipped_pauli_product: X2 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #2 of the instruction resolving to DEPOLARIZE1(0.001) 2 } CircuitErrorLocation { flipped_pauli_product: Y2 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #2 of the instruction resolving to DEPOLARIZE1(0.001) 2 } } ExplainedError { dem_error_terms: D1[coords 3,0] L0 CircuitErrorLocation { flipped_pauli_product: X4 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #3 of the instruction resolving to DEPOLARIZE1(0.001) 4 } CircuitErrorLocation { flipped_pauli_product: Y4 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #3 of the instruction resolving to DEPOLARIZE1(0.001) 4 } } ExplainedError { dem_error_terms: D2[coords 1,1] CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } } ExplainedError { dem_error_terms: D2[coords 1,1] D3[coords 3,1] CircuitErrorLocation { flipped_pauli_product: X2 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #2 of the instruction resolving to DEPOLARIZE1(0.001) 2 } CircuitErrorLocation { flipped_pauli_product: Y2 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #2 of the instruction resolving to DEPOLARIZE1(0.001) 2 } } ExplainedError { dem_error_terms: D3[coords 3,1] L0 CircuitErrorLocation { flipped_pauli_product: X4 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #3 of the instruction resolving to DEPOLARIZE1(0.001) 4 } CircuitErrorLocation { flipped_pauli_product: Y4 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #3 of the instruction resolving to DEPOLARIZE1(0.001) 4 } } )RESULT"); } TEST(ErrorMatcher, repetition_code_data_depolarization_single_results) { CircuitGenParameters params(2, 3, "memory"); params.before_round_data_depolarization = 0.001; auto circuit = generate_rep_code_circuit(params).circuit; auto actual = ErrorMatcher::explain_errors_from_circuit(circuit, nullptr, true); std::stringstream ss; for (const auto &match : actual) { ss << "\n" << match << "\n"; } ASSERT_EQ(ss.str(), R"RESULT( ExplainedError { dem_error_terms: D0[coords 1,0] CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } } ExplainedError { dem_error_terms: D0[coords 1,0] D1[coords 3,0] CircuitErrorLocation { flipped_pauli_product: X2 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #2 of the instruction resolving to DEPOLARIZE1(0.001) 2 } } ExplainedError { dem_error_terms: D1[coords 3,0] L0 CircuitErrorLocation { flipped_pauli_product: X4 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #3 of the instruction resolving to DEPOLARIZE1(0.001) 4 } } ExplainedError { dem_error_terms: D2[coords 1,1] CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } } ExplainedError { dem_error_terms: D2[coords 1,1] D3[coords 3,1] CircuitErrorLocation { flipped_pauli_product: X2 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #2 of the instruction resolving to DEPOLARIZE1(0.001) 2 } } ExplainedError { dem_error_terms: D3[coords 3,1] L0 CircuitErrorLocation { flipped_pauli_product: X4 Circuit location stack trace: (after 4 TICKs) at instruction #12 (DEPOLARIZE1) in the circuit at target #3 of the instruction resolving to DEPOLARIZE1(0.001) 4 } } )RESULT"); } TEST(ErrorMatcher, surface_code_filter) { CircuitGenParameters params(5, 5, "rotated_memory_z"); params.before_round_data_depolarization = 0.001; params.after_clifford_depolarization = 0.001; params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; DetectorErrorModel filter(R"MODEL( error(1) D97 D98 D102 D103 )MODEL"); auto actual = ErrorMatcher::explain_errors_from_circuit(circuit, &filter, false); std::stringstream ss; for (const auto &match : actual) { ss << "\n" << match << "\n"; } ASSERT_EQ(ss.str(), R"RESULT( ExplainedError { dem_error_terms: D97[coords 4,6,4] D98[coords 6,6,4] D102[coords 2,8,4] D103[coords 4,8,4] CircuitErrorLocation { flipped_pauli_product: Y37[coords 4,6]*Y36[coords 3,7] Circuit location stack trace: (after 31 TICKs) at instruction #89 (a REPEAT 4 block) in the circuit after 3 completed iterations at instruction #10 (DEPOLARIZE2) in the REPEAT block at targets #9 to #10 of the instruction resolving to DEPOLARIZE2(0.001) 37[coords 4,6] 36[coords 3,7] } } )RESULT"); } TEST(ErrorMatcher, runs_on_all_gates_circuit) { DetectorErrorModel filter(R"MODEL( error(1) D0 )MODEL"); auto circuit = generate_test_circuit_with_all_operations(); auto actual = ErrorMatcher::explain_errors_from_circuit(circuit, &filter, false); std::stringstream ss; for (const auto &match : actual) { ss << "\n" << match << "\n"; } ASSERT_EQ(ss.str(), R"RESULT( ExplainedError { dem_error_terms: D0[coords 2,4,6] [no single circuit error had these exact symptoms] } )RESULT"); } TEST(ErrorMatcher, heralded_error) { Circuit circuit(R"CIRCUIT( HERALDED_ERASE(0.01) 0 DETECTOR rec[-1] HERALDED_ERASE(0.01) 1 2 )CIRCUIT"); DetectorErrorModel filter(R"MODEL( error(1) D0 )MODEL"); auto actual = ErrorMatcher::explain_errors_from_circuit(circuit, &filter, false); std::stringstream ss; for (const auto &match : actual) { ss << "\n" << match << "\n"; } ASSERT_EQ(ss.str(), R"RESULT( ExplainedError { dem_error_terms: D0 CircuitErrorLocation { flipped_measurement.measurement_record_index: 0 Circuit location stack trace: (after 0 TICKs) at instruction #1 (HERALDED_ERASE) in the circuit at target #1 of the instruction resolving to HERALDED_ERASE(0.01) 0 } CircuitErrorLocation { flipped_pauli_product: X0 flipped_measurement.measurement_record_index: 0 Circuit location stack trace: (after 0 TICKs) at instruction #1 (HERALDED_ERASE) in the circuit at target #1 of the instruction resolving to HERALDED_ERASE(0.01) 0 } CircuitErrorLocation { flipped_pauli_product: Y0 flipped_measurement.measurement_record_index: 0 Circuit location stack trace: (after 0 TICKs) at instruction #1 (HERALDED_ERASE) in the circuit at target #1 of the instruction resolving to HERALDED_ERASE(0.01) 0 } CircuitErrorLocation { flipped_pauli_product: Z0 flipped_measurement.measurement_record_index: 0 Circuit location stack trace: (after 0 TICKs) at instruction #1 (HERALDED_ERASE) in the circuit at target #1 of the instruction resolving to HERALDED_ERASE(0.01) 0 } } )RESULT"); } TEST(ErrorMatcher, PAULI_CHANNEL_2) { std::vector actual; actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{GateTargetWithCoords{GateTarget::x(1)}})); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{GateTargetWithCoords{GateTarget::y(1)}})); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{GateTargetWithCoords{GateTarget::z(1)}})); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::x(0)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::x(0)}, GateTargetWithCoords{GateTarget::x(1)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::x(0)}, GateTargetWithCoords{GateTarget::y(1)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0.1, 0, 0, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::x(0)}, GateTargetWithCoords{GateTarget::z(1)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0, 0.1, 0, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::y(0)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::y(0)}, GateTargetWithCoords{GateTarget::x(1)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::y(0)}, GateTargetWithCoords{GateTarget::y(1)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::y(0)}, GateTargetWithCoords{GateTarget::z(1)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::z(0)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::z(0)}, GateTargetWithCoords{GateTarget::x(1)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::z(0)}, GateTargetWithCoords{GateTarget::y(1)}, })); actual = ErrorMatcher::explain_errors_from_circuit( Circuit(R"CIRCUIT( MXX 0 2 1 3 MZZ 0 2 1 3 PAULI_CHANNEL_2(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.1) 0 1 MXX 0 2 1 3 MZZ 0 2 1 3 DETECTOR rec[-1] rec[-5] DETECTOR rec[-2] rec[-6] DETECTOR rec[-3] rec[-7] DETECTOR rec[-4] rec[-8] )CIRCUIT"), nullptr, false); ASSERT_EQ(actual.size(), 1); ASSERT_EQ(actual[0].circuit_error_locations.size(), 1); ASSERT_EQ( actual[0].circuit_error_locations[0].flipped_pauli_product, (std::vector{ GateTargetWithCoords{GateTarget::z(0)}, GateTargetWithCoords{GateTarget::z(1)}, })); } ================================================ FILE: src/stim/simulators/force_streaming.cc ================================================ #include "stim/simulators/force_streaming.h" #include namespace stim { static size_t force_stream_count = 0; DebugForceResultStreamingRaii::DebugForceResultStreamingRaii() { force_stream_count++; } DebugForceResultStreamingRaii::~DebugForceResultStreamingRaii() { force_stream_count--; } bool should_use_streaming_because_bit_count_is_too_large_to_store(uint64_t bit_count) { return force_stream_count > 0 || bit_count > (uint64_t{1} << 32); } } // namespace stim ================================================ FILE: src/stim/simulators/force_streaming.h ================================================ #ifndef _STIM_SIMULATORS_FORCE_STREAMING_H #define _STIM_SIMULATORS_FORCE_STREAMING_H #include namespace stim { /// Facilitates testing of simulators with streaming enabled without /// having to make enormous circuits. bool should_use_streaming_because_bit_count_is_too_large_to_store(uint64_t result_count); struct DebugForceResultStreamingRaii { DebugForceResultStreamingRaii(); ~DebugForceResultStreamingRaii(); }; } // namespace stim #endif ================================================ FILE: src/stim/simulators/frame_simulator.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_FRAME_SIMULATOR_H #define _STIM_SIMULATORS_FRAME_SIMULATOR_H #include #include "stim/circuit/circuit.h" #include "stim/io/measure_record_batch.h" #include "stim/mem/simd_bit_table.h" #include "stim/stabilizers/pauli_string.h" namespace stim { enum class FrameSimulatorMode { STORE_MEASUREMENTS_TO_MEMORY, // all measurements stored, detections not stored STREAM_MEASUREMENTS_TO_DISK, // measurements stored up to lookback, detections not stored STORE_DETECTIONS_TO_MEMORY, // measurements stored up to lookback, all detections stored STREAM_DETECTIONS_TO_DISK, // measurements stored up to lookback, detections stored until write STORE_EVERYTHING_TO_MEMORY, // all measurements stored and all detections stored }; /// A Pauli Frame simulator that computes many samples simultaneously. /// /// This simulator tracks, for each qubit, whether or not that qubit is bit flipped and/or phase flipped. /// Instead of reporting qubit measurements, it reports whether a measurement is inverted or not. /// This requires a set of reference measurements to diff against. /// /// The template parameter, W, represents the SIMD width template struct FrameSimulator { size_t num_qubits; // Number of qubits being tracked. uint64_t num_observables; // Number of observables being tracked. bool keeping_detection_data; // Whether or not to store dets and obs data. size_t batch_size; // Number of instances being tracked. simd_bit_table x_table; // x_table[q][k] is whether or not there's an X error on qubit q in instance k. simd_bit_table z_table; // z_table[q][k] is whether or not there's a Z error on qubit q in instance k. MeasureRecordBatch m_record; // The measurement record. MeasureRecordBatch det_record; // Detection event record. simd_bit_table obs_record; // Accumulating observable flip record. simd_bits rng_buffer; // Workspace used when sampling error processes. simd_bits tmp_storage; // Workspace used when sampling compound error processes. simd_bits last_correlated_error_occurred; // correlated error flag for each instance. simd_bit_table sweep_table; // Shot-to-shot configuration data. std::mt19937_64 rng; // Random number generator used for generating entropy. // Determines whether e.g. 50% Z errors are multiplied into the frame when measuring in the Z basis. // This is necessary for correct sampling. // It should only be disabled when e.g. using the frame simulator to understand how a fixed set of errors will // propagate, without interference from other effects. bool guarantee_anticommutation_via_frame_randomization = true; /// Constructs a FrameSimulator capable of simulating a circuit with the given size stats. /// /// Args: /// circuit_stats: Sizes that determine how large internal buffers must be. Get /// this from stim::Circuit::compute_stats. /// mode: Describes the intended usage of the simulator, which affects the sizing /// of buffers. /// batch_size: How many shots to simulate simultaneously. /// rng: The random number generator to pull noise from. FrameSimulator(CircuitStats circuit_stats, FrameSimulatorMode mode, size_t batch_size, std::mt19937_64 &&rng); FrameSimulator() = delete; PauliString get_frame(size_t sample_index) const; void set_frame(size_t sample_index, const PauliStringRef &new_frame); void configure_for(CircuitStats new_circuit_stats, FrameSimulatorMode new_mode, size_t new_batch_size); void ensure_safe_to_do_circuit_with_stats(const CircuitStats &stats); void safe_do_instruction(const CircuitInstruction &instruction); void safe_do_circuit(const Circuit &circuit, uint64_t repetitions = 1); void do_circuit(const Circuit &circuit); void reset_all(); void do_gate(const CircuitInstruction &inst); void do_MX(const CircuitInstruction &inst); void do_MY(const CircuitInstruction &inst); void do_MZ(const CircuitInstruction &inst); void do_RX(const CircuitInstruction &inst); void do_RY(const CircuitInstruction &inst); void do_RZ(const CircuitInstruction &inst); void do_MRX(const CircuitInstruction &inst); void do_MRY(const CircuitInstruction &inst); void do_MRZ(const CircuitInstruction &inst); void do_DETECTOR(const CircuitInstruction &inst); void do_OBSERVABLE_INCLUDE(const CircuitInstruction &inst); void do_I(const CircuitInstruction &inst); void do_H_XZ(const CircuitInstruction &inst); void do_H_XY(const CircuitInstruction &inst); void do_H_YZ(const CircuitInstruction &inst); void do_C_XYZ(const CircuitInstruction &inst); void do_C_ZYX(const CircuitInstruction &inst); void do_ZCX(const CircuitInstruction &inst); void do_ZCY(const CircuitInstruction &inst); void do_ZCZ(const CircuitInstruction &inst); void do_XCX(const CircuitInstruction &inst); void do_XCY(const CircuitInstruction &inst); void do_XCZ(const CircuitInstruction &inst); void do_YCX(const CircuitInstruction &inst); void do_YCY(const CircuitInstruction &inst); void do_YCZ(const CircuitInstruction &inst); void do_SWAP(const CircuitInstruction &inst); void do_ISWAP(const CircuitInstruction &inst); void do_CXSWAP(const CircuitInstruction &inst); void do_CZSWAP(const CircuitInstruction &inst); void do_SWAPCX(const CircuitInstruction &inst); void do_MPP(const CircuitInstruction &inst); void do_SPP(const CircuitInstruction &inst); void do_SPP_DAG(const CircuitInstruction &inst); void do_MXX(const CircuitInstruction &inst); void do_MYY(const CircuitInstruction &inst); void do_MZZ(const CircuitInstruction &inst); void do_MPAD(const CircuitInstruction &inst); void do_SQRT_XX(const CircuitInstruction &inst); void do_SQRT_YY(const CircuitInstruction &inst); void do_SQRT_ZZ(const CircuitInstruction &inst); void do_DEPOLARIZE1(const CircuitInstruction &inst); void do_DEPOLARIZE2(const CircuitInstruction &inst); void do_X_ERROR(const CircuitInstruction &inst); void do_Y_ERROR(const CircuitInstruction &inst); void do_Z_ERROR(const CircuitInstruction &inst); void do_PAULI_CHANNEL_1(const CircuitInstruction &inst); void do_PAULI_CHANNEL_2(const CircuitInstruction &inst); void do_CORRELATED_ERROR(const CircuitInstruction &inst); void do_ELSE_CORRELATED_ERROR(const CircuitInstruction &inst); void do_HERALDED_ERASE(const CircuitInstruction &inst); void do_HERALDED_PAULI_CHANNEL_1(const CircuitInstruction &inst); private: void do_MXX_disjoint_controls_segment(const CircuitInstruction &inst); void do_MYY_disjoint_controls_segment(const CircuitInstruction &inst); void do_MZZ_disjoint_controls_segment(const CircuitInstruction &inst); void xor_control_bit_into(uint32_t control, simd_bits_range_ref target); void single_cx(uint32_t c, uint32_t t); void single_cy(uint32_t c, uint32_t t); }; } // namespace stim #include "stim/simulators/frame_simulator.inl" #endif ================================================ FILE: src/stim/simulators/frame_simulator.inl ================================================ // 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. #include #include #include "stim/circuit/gate_decomposition.h" #include "stim/simulators/frame_simulator.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/probability_util.h" namespace stim { // Iterates over the X and Z frame components of a pair of qubits, applying a custom FUNC to each. // // HACK: Templating the body function type makes inlining significantly more likely. template inline void for_each_target_pair(FrameSimulator &sim, const CircuitInstruction &target_data, FUNC body) { const auto &targets = target_data.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { size_t q1 = targets[k].data; size_t q2 = targets[k + 1].data; sim.x_table[q1].for_each_word(sim.z_table[q1], sim.x_table[q2], sim.z_table[q2], body); } } template FrameSimulator::FrameSimulator( CircuitStats circuit_stats, FrameSimulatorMode mode, size_t batch_size, std::mt19937_64 &&rng) : num_qubits(0), num_observables(0), keeping_detection_data(false), batch_size(0), x_table(0, 0), z_table(0, 0), m_record(0, 0), det_record(0, 0), obs_record(0, 0), rng_buffer(0), tmp_storage(0), last_correlated_error_occurred(0), sweep_table(0, 0), rng(std::move(rng)) { configure_for(circuit_stats, mode, batch_size); } template void FrameSimulator::configure_for( CircuitStats new_circuit_stats, FrameSimulatorMode new_mode, size_t new_batch_size) { bool storing_all_measurements = new_mode == FrameSimulatorMode::STORE_MEASUREMENTS_TO_MEMORY || new_mode == FrameSimulatorMode::STORE_EVERYTHING_TO_MEMORY; bool storing_all_detections = new_mode == FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY || new_mode == FrameSimulatorMode::STORE_EVERYTHING_TO_MEMORY; bool storing_any_detections = new_mode == FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY || new_mode == FrameSimulatorMode::STORE_EVERYTHING_TO_MEMORY || new_mode == FrameSimulatorMode::STREAM_DETECTIONS_TO_DISK; batch_size = new_batch_size; num_qubits = new_circuit_stats.num_qubits; keeping_detection_data = storing_any_detections; x_table.destructive_resize(new_circuit_stats.num_qubits, batch_size); z_table.destructive_resize(new_circuit_stats.num_qubits, batch_size); rng_buffer.destructive_resize(batch_size); tmp_storage.destructive_resize(batch_size); last_correlated_error_occurred.destructive_resize(batch_size); sweep_table.destructive_resize(0, batch_size); uint64_t num_stored_measurements = new_circuit_stats.max_lookback; if (storing_all_measurements) { num_stored_measurements = std::max(new_circuit_stats.num_measurements, num_stored_measurements); } m_record.destructive_resize(batch_size, num_stored_measurements); num_observables = storing_any_detections ? new_circuit_stats.num_observables : 0; det_record.destructive_resize( batch_size, storing_all_detections ? new_circuit_stats.num_detectors : storing_any_detections ? 1 : 0), obs_record.destructive_resize(num_observables, batch_size); } template void FrameSimulator::ensure_safe_to_do_circuit_with_stats(const CircuitStats &stats) { if (x_table.num_major_bits_padded() < stats.num_qubits) { x_table.resize(stats.num_qubits * 2, batch_size); z_table.resize(stats.num_qubits * 2, batch_size); } while (num_qubits < stats.num_qubits) { if (guarantee_anticommutation_via_frame_randomization) { z_table[num_qubits].randomize(batch_size, rng); } num_qubits += 1; } size_t num_used_measurements = m_record.stored + stats.num_measurements; if (m_record.storage.num_major_bits_padded() < num_used_measurements) { m_record.storage.resize(num_used_measurements * 2, batch_size); } if (keeping_detection_data) { size_t num_detectors = det_record.stored + stats.num_detectors; if (det_record.storage.num_major_bits_padded() < num_detectors) { det_record.storage.resize(num_detectors * 2, batch_size); } if (obs_record.num_major_bits_padded() < stats.num_observables) { obs_record.resize(stats.num_observables * 2, batch_size); } num_observables = std::max(stats.num_observables, num_observables); } } template void FrameSimulator::safe_do_circuit(const Circuit &circuit, uint64_t repetitions) { ensure_safe_to_do_circuit_with_stats(circuit.compute_stats().repeated(repetitions)); for (size_t rep = 0; rep < repetitions; rep++) { do_circuit(circuit); } } template void FrameSimulator::safe_do_instruction(const CircuitInstruction &instruction) { ensure_safe_to_do_circuit_with_stats(instruction.compute_stats(nullptr)); do_gate(instruction); } template void FrameSimulator::xor_control_bit_into(uint32_t control, simd_bits_range_ref target) { uint32_t raw_control = control & ~(TARGET_RECORD_BIT | TARGET_SWEEP_BIT); assert(control != raw_control); if (control & TARGET_RECORD_BIT) { target ^= m_record.lookback(raw_control); } else { if (raw_control < sweep_table.num_major_bits_padded()) { target ^= sweep_table[raw_control]; } } } template void FrameSimulator::reset_all() { x_table.clear(); if (guarantee_anticommutation_via_frame_randomization) { z_table.data.randomize(z_table.data.num_bits_padded(), rng); } else { z_table.clear(); } m_record.clear(); det_record.clear(); obs_record.clear(); } template void FrameSimulator::do_circuit(const Circuit &circuit) { circuit.for_each_operation([&](const CircuitInstruction &op) { do_gate(op); }); } template void FrameSimulator::do_MX(const CircuitInstruction &inst) { m_record.reserve_noisy_space_for_results(inst, rng); for (auto t : inst.targets) { auto q = t.qubit_value(); // Flipping is ignored because it is accounted for in the reference sample. m_record.xor_record_reserved_result(z_table[q]); if (guarantee_anticommutation_via_frame_randomization) { x_table[q].randomize(x_table[q].num_bits_padded(), rng); } } } template void FrameSimulator::do_MY(const CircuitInstruction &inst) { m_record.reserve_noisy_space_for_results(inst, rng); for (auto t : inst.targets) { auto q = t.qubit_value(); // Flipping is ignored because it is accounted for in the reference sample. x_table[q] ^= z_table[q]; m_record.xor_record_reserved_result(x_table[q]); if (guarantee_anticommutation_via_frame_randomization) { z_table[q].randomize(z_table[q].num_bits_padded(), rng); } x_table[q] ^= z_table[q]; } } template void FrameSimulator::do_MZ(const CircuitInstruction &inst) { m_record.reserve_noisy_space_for_results(inst, rng); for (auto t : inst.targets) { auto q = t.qubit_value(); // Flipping is ignored because it is accounted for in the reference sample. m_record.xor_record_reserved_result(x_table[q]); if (guarantee_anticommutation_via_frame_randomization) { z_table[q].randomize(z_table[q].num_bits_padded(), rng); } } } template void FrameSimulator::do_RX(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; if (guarantee_anticommutation_via_frame_randomization) { x_table[q].randomize(z_table[q].num_bits_padded(), rng); } z_table[q].clear(); } } template void FrameSimulator::do_DETECTOR(const CircuitInstruction &inst) { if (keeping_detection_data) { auto r = det_record.record_zero_result_to_edit(); for (auto t : inst.targets) { uint32_t lookback = t.data & TARGET_VALUE_MASK; r ^= m_record.lookback(lookback); } } } template void FrameSimulator::do_OBSERVABLE_INCLUDE(const CircuitInstruction &inst) { if (keeping_detection_data) { auto r = obs_record[(size_t)inst.args[0]]; for (auto t : inst.targets) { if (t.is_measurement_record_target()) { uint32_t lookback = t.data & TARGET_VALUE_MASK; r ^= m_record.lookback(lookback); } else if (t.is_pauli_target()) { if (t.data & TARGET_PAULI_X_BIT) { r ^= z_table[t.qubit_value()]; } if (t.data & TARGET_PAULI_Z_BIT) { r ^= x_table[t.qubit_value()]; } } else { throw std::invalid_argument("Unexpected target for OBSERVABLE_INCLUDE: " + t.str()); } } } } template void FrameSimulator::do_RY(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; if (guarantee_anticommutation_via_frame_randomization) { z_table[q].randomize(z_table[q].num_bits_padded(), rng); } x_table[q] = z_table[q]; } } template void FrameSimulator::do_RZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; x_table[q].clear(); if (guarantee_anticommutation_via_frame_randomization) { z_table[q].randomize(z_table[q].num_bits_padded(), rng); } } } template void FrameSimulator::do_MRX(const CircuitInstruction &target_data) { // Note: Caution when implementing this. Can't group the resets. because the same qubit target may appear twice. m_record.reserve_noisy_space_for_results(target_data, rng); for (auto t : target_data.targets) { auto q = t.qubit_value(); // Flipping is ignored because it is accounted for in the reference sample. m_record.xor_record_reserved_result(z_table[q]); z_table[q].clear(); if (guarantee_anticommutation_via_frame_randomization) { x_table[q].randomize(x_table[q].num_bits_padded(), rng); } } } template void FrameSimulator::do_MRY(const CircuitInstruction &target_data) { // Note: Caution when implementing this. Can't group the resets. because the same qubit target may appear twice. m_record.reserve_noisy_space_for_results(target_data, rng); for (auto t : target_data.targets) { auto q = t.qubit_value(); // Flipping is ignored because it is accounted for in the reference sample. x_table[q] ^= z_table[q]; m_record.xor_record_reserved_result(x_table[q]); if (guarantee_anticommutation_via_frame_randomization) { z_table[q].randomize(z_table[q].num_bits_padded(), rng); } x_table[q] = z_table[q]; } } template void FrameSimulator::do_MRZ(const CircuitInstruction &target_data) { // Note: Caution when implementing this. Can't group the resets. because the same qubit target may appear twice. m_record.reserve_noisy_space_for_results(target_data, rng); for (auto t : target_data.targets) { auto q = t.qubit_value(); // Flipping is ignored because it is accounted for in the reference sample. m_record.xor_record_reserved_result(x_table[q]); x_table[q].clear(); if (guarantee_anticommutation_via_frame_randomization) { z_table[q].randomize(z_table[q].num_bits_padded(), rng); } } } template void FrameSimulator::do_I(const CircuitInstruction &target_data) { } template PauliString FrameSimulator::get_frame(size_t sample_index) const { assert(sample_index < batch_size); PauliString result(num_qubits); for (size_t q = 0; q < num_qubits; q++) { result.xs[q] = x_table[q][sample_index]; result.zs[q] = z_table[q][sample_index]; } return result; } template void FrameSimulator::set_frame(size_t sample_index, const PauliStringRef &new_frame) { assert(sample_index < batch_size); assert(new_frame.num_qubits == num_qubits); for (size_t q = 0; q < num_qubits; q++) { x_table[q][sample_index] = new_frame.xs[q]; z_table[q][sample_index] = new_frame.zs[q]; } } template void FrameSimulator::do_H_XZ(const CircuitInstruction &target_data) { for (auto t : target_data.targets) { auto q = t.data; x_table[q].swap_with(z_table[q]); } } template void FrameSimulator::do_H_XY(const CircuitInstruction &target_data) { for (auto t : target_data.targets) { auto q = t.data; z_table[q] ^= x_table[q]; } } template void FrameSimulator::do_H_YZ(const CircuitInstruction &target_data) { for (auto t : target_data.targets) { auto q = t.data; x_table[q] ^= z_table[q]; } } template void FrameSimulator::do_C_XYZ(const CircuitInstruction &target_data) { for (auto t : target_data.targets) { auto q = t.data; x_table[q] ^= z_table[q]; z_table[q] ^= x_table[q]; } } template void FrameSimulator::do_C_ZYX(const CircuitInstruction &target_data) { for (auto t : target_data.targets) { auto q = t.data; z_table[q] ^= x_table[q]; x_table[q] ^= z_table[q]; } } template void FrameSimulator::single_cx(uint32_t c, uint32_t t) { c &= ~TARGET_INVERTED_BIT; t &= ~TARGET_INVERTED_BIT; if (!((c | t) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { x_table[c].for_each_word( z_table[c], x_table[t], z_table[t], [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { z1 ^= z2; x2 ^= x1; }); } else if (t & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) { throw std::invalid_argument( "Controlled X had a bit (" + GateTarget{t}.str() + ") as its target, instead of its control."); } else { xor_control_bit_into(c, x_table[t]); } } template void FrameSimulator::single_cy(uint32_t c, uint32_t t) { c &= ~TARGET_INVERTED_BIT; t &= ~TARGET_INVERTED_BIT; if (!((c | t) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { x_table[c].for_each_word( z_table[c], x_table[t], z_table[t], [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { z1 ^= x2 ^ z2; z2 ^= x1; x2 ^= x1; }); } else if (t & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) { throw std::invalid_argument( "Controlled Y had a bit (" + GateTarget{t}.str() + ") as its target, instead of its control."); } else { xor_control_bit_into(c, x_table[t]); xor_control_bit_into(c, z_table[t]); } } template void FrameSimulator::do_ZCX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { single_cx(targets[k].data, targets[k + 1].data); } } template void FrameSimulator::do_ZCY(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { single_cy(targets[k].data, targets[k + 1].data); } } template void FrameSimulator::do_ZCZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { size_t c = targets[k].data; size_t t = targets[k + 1].data; c &= ~TARGET_INVERTED_BIT; t &= ~TARGET_INVERTED_BIT; if (!((c | t) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { x_table[c].for_each_word( z_table[c], x_table[t], z_table[t], [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { z1 ^= x2; z2 ^= x1; }); } else if (!(t & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { xor_control_bit_into(c, z_table[t]); } else if (!(c & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { xor_control_bit_into(t, z_table[c]); } else { // Both targets are bits. No effect. } } } template void FrameSimulator::do_SWAP(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { size_t q1 = targets[k].data; size_t q2 = targets[k + 1].data; x_table[q1].for_each_word( z_table[q1], x_table[q2], z_table[q2], [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { std::swap(z1, z2); std::swap(x1, x2); }); } } template void FrameSimulator::do_ISWAP(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { auto dx = x1 ^ x2; auto t1 = z1 ^ dx; auto t2 = z2 ^ dx; z1 = t2; z2 = t1; std::swap(x1, x2); }); } template void FrameSimulator::do_CXSWAP(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { z2 ^= z1; z1 ^= z2; x1 ^= x2; x2 ^= x1; }); } template void FrameSimulator::do_CZSWAP(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { std::swap(z1, z2); std::swap(x1, x2); z1 ^= x2; z2 ^= x1; }); } template void FrameSimulator::do_SWAPCX(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { z1 ^= z2; z2 ^= z1; x2 ^= x1; x1 ^= x2; }); } template void FrameSimulator::do_SQRT_XX(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { auto dz = z1 ^ z2; x1 ^= dz; x2 ^= dz; }); } template void FrameSimulator::do_SQRT_YY(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { auto d = x1 ^ z1 ^ x2 ^ z2; x1 ^= d; z1 ^= d; x2 ^= d; z2 ^= d; }); } template void FrameSimulator::do_SQRT_ZZ(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { auto dx = x1 ^ x2; z1 ^= dx; z2 ^= dx; }); } template void FrameSimulator::do_XCX(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { x1 ^= z2; x2 ^= z1; }); } template void FrameSimulator::do_XCY(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { x1 ^= x2 ^ z2; x2 ^= z1; z2 ^= z1; }); } template void FrameSimulator::do_XCZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { single_cx(targets[k + 1].data, targets[k].data); } } template void FrameSimulator::do_YCX(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { x2 ^= x1 ^ z1; x1 ^= z2; z1 ^= z2; }); } template void FrameSimulator::do_YCY(const CircuitInstruction &target_data) { for_each_target_pair( *this, target_data, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { auto y1 = x1 ^ z1; auto y2 = x2 ^ z2; x1 ^= y2; z1 ^= y2; x2 ^= y1; z2 ^= y1; }); } template void FrameSimulator::do_YCZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { single_cy(targets[k + 1].data, targets[k].data); } } template void FrameSimulator::do_DEPOLARIZE1(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; RareErrorIterator::for_samples(target_data.args[0], targets.size() * batch_size, rng, [&](size_t s) { auto p = 1 + (rng() % 3); auto target_index = s / batch_size; auto sample_index = s % batch_size; auto t = targets[target_index]; x_table[t.data][sample_index] ^= p & 1; z_table[t.data][sample_index] ^= p & 2; }); } template void FrameSimulator::do_DEPOLARIZE2(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); auto n = (targets.size() * batch_size) >> 1; RareErrorIterator::for_samples(target_data.args[0], n, rng, [&](size_t s) { auto p = 1 + (rng() % 15); auto target_index = (s / batch_size) << 1; auto sample_index = s % batch_size; size_t t1 = targets[target_index].data; size_t t2 = targets[target_index + 1].data; x_table[t1][sample_index] ^= (bool)(p & 1); z_table[t1][sample_index] ^= (bool)(p & 2); x_table[t2][sample_index] ^= (bool)(p & 4); z_table[t2][sample_index] ^= (bool)(p & 8); }); } template void FrameSimulator::do_X_ERROR(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; RareErrorIterator::for_samples(target_data.args[0], targets.size() * batch_size, rng, [&](size_t s) { auto target_index = s / batch_size; auto sample_index = s % batch_size; auto t = targets[target_index]; x_table[t.data][sample_index] ^= true; }); } template void FrameSimulator::do_Y_ERROR(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; RareErrorIterator::for_samples(target_data.args[0], targets.size() * batch_size, rng, [&](size_t s) { auto target_index = s / batch_size; auto sample_index = s % batch_size; auto t = targets[target_index]; x_table[t.data][sample_index] ^= true; z_table[t.data][sample_index] ^= true; }); } template void FrameSimulator::do_Z_ERROR(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; RareErrorIterator::for_samples(target_data.args[0], targets.size() * batch_size, rng, [&](size_t s) { auto target_index = s / batch_size; auto sample_index = s % batch_size; auto t = targets[target_index]; z_table[t.data][sample_index] ^= true; }); } template void FrameSimulator::do_MPP(const CircuitInstruction &target_data) { decompose_mpp_operation(target_data, num_qubits, [&](const CircuitInstruction &inst) { safe_do_instruction(inst); }); } template void FrameSimulator::do_SPP(const CircuitInstruction &target_data) { decompose_spp_or_spp_dag_operation(target_data, num_qubits, false, [&](const CircuitInstruction &inst) { safe_do_instruction(inst); }); } template void FrameSimulator::do_SPP_DAG(const CircuitInstruction &target_data) { decompose_spp_or_spp_dag_operation(target_data, num_qubits, false, [&](const CircuitInstruction &inst) { safe_do_instruction(inst); }); } template void FrameSimulator::do_PAULI_CHANNEL_1(const CircuitInstruction &target_data) { tmp_storage = last_correlated_error_occurred; perform_pauli_errors_via_correlated_errors<1>( target_data, [&]() { last_correlated_error_occurred.clear(); }, [&](const CircuitInstruction &d) { do_ELSE_CORRELATED_ERROR(d); }); last_correlated_error_occurred = tmp_storage; } template void FrameSimulator::do_PAULI_CHANNEL_2(const CircuitInstruction &target_data) { tmp_storage = last_correlated_error_occurred; perform_pauli_errors_via_correlated_errors<2>( target_data, [&]() { last_correlated_error_occurred.clear(); }, [&](const CircuitInstruction &d) { do_ELSE_CORRELATED_ERROR(d); }); last_correlated_error_occurred = tmp_storage; } template void FrameSimulator::do_CORRELATED_ERROR(const CircuitInstruction &target_data) { last_correlated_error_occurred.clear(); do_ELSE_CORRELATED_ERROR(target_data); } template void FrameSimulator::do_ELSE_CORRELATED_ERROR(const CircuitInstruction &target_data) { // Sample error locations. biased_randomize_bits(target_data.args[0], rng_buffer.u64, rng_buffer.u64 + ((batch_size + 63) >> 6), rng); if (batch_size & 63) { rng_buffer.u64[batch_size >> 6] &= (uint64_t{1} << (batch_size & 63)) - 1; } // Omit locations blocked by prev error, while updating prev error mask. simd_bits_range_ref{rng_buffer}.for_each_word( last_correlated_error_occurred, [](simd_word &buf, simd_word &prev) { buf = prev.andnot(buf); prev |= buf; }); // Apply error to only the indicated frames. for (auto qxz : target_data.targets) { auto q = qxz.qubit_value(); if (qxz.data & TARGET_PAULI_X_BIT) { x_table[q] ^= rng_buffer; } if (qxz.data & TARGET_PAULI_Z_BIT) { z_table[q] ^= rng_buffer; } } } template void FrameSimulator::do_HERALDED_PAULI_CHANNEL_1(const CircuitInstruction &inst) { auto nt = inst.targets.size(); m_record.reserve_space_for_results(nt); for (size_t k = 0; k < nt; k++) { m_record.storage[m_record.stored + k].clear(); } double hi = inst.args[0]; double hx = inst.args[1]; double hy = inst.args[2]; double hz = inst.args[3]; double t = hi + hx + hy + hz; std::uniform_real_distribution dist(0, 1); RareErrorIterator::for_samples(t, nt * batch_size, rng, [&](size_t s) { auto shot = s % batch_size; auto target = s / batch_size; auto qubit = inst.targets[target].qubit_value(); m_record.storage[m_record.stored + target][shot] = 1; double p = dist(rng) * t; if (p < hx) { x_table[qubit][shot] ^= 1; } else if (p < hx + hz) { z_table[qubit][shot] ^= 1; } else if (p < hx + hz + hy) { x_table[qubit][shot] ^= 1; z_table[qubit][shot] ^= 1; } }); m_record.stored += nt; m_record.unwritten += nt; } template void FrameSimulator::do_HERALDED_ERASE(const CircuitInstruction &inst) { auto nt = inst.targets.size(); m_record.reserve_space_for_results(nt); for (size_t k = 0; k < nt; k++) { m_record.storage[m_record.stored + k].clear(); } uint64_t rng_buf = 0; size_t buf_size = 0; RareErrorIterator::for_samples(inst.args[0], nt * batch_size, rng, [&](size_t s) { auto shot = s % batch_size; auto target = s / batch_size; auto qubit = inst.targets[target].qubit_value(); if (buf_size == 0) { rng_buf = rng(); buf_size = 64; } x_table[qubit][shot] ^= (bool)(rng_buf & 1); z_table[qubit][shot] ^= (bool)(rng_buf & 2); m_record.storage[m_record.stored + target][shot] = 1; rng_buf >>= 2; buf_size -= 2; }); m_record.stored += nt; m_record.unwritten += nt; } template void FrameSimulator::do_MXX_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. do_ZCX(CircuitInstruction{GateType::CX, {}, inst.targets, ""}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { do_MX(CircuitInstruction{GateType::MX, inst.args, SpanRef{&inst.targets[k]}, ""}); } // Untransform from single qubit measurements back to 2 qubit measurements. do_ZCX(CircuitInstruction{GateType::CX, {}, inst.targets, ""}); } template void FrameSimulator::do_MYY_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. do_ZCY(CircuitInstruction{GateType::CY, {}, inst.targets, ""}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { do_MY(CircuitInstruction{GateType::MY, inst.args, SpanRef{&inst.targets[k]}, ""}); } // Untransform from single qubit measurements back to 2 qubit measurements. do_ZCY(CircuitInstruction{GateType::CY, {}, inst.targets, ""}); } template void FrameSimulator::do_MZZ_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. do_XCZ(CircuitInstruction{GateType::XCZ, {}, inst.targets, ""}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { do_MZ(CircuitInstruction{GateType::M, inst.args, SpanRef{&inst.targets[k]}, ""}); } // Untransform from single qubit measurements back to 2 qubit measurements. do_XCZ(CircuitInstruction{GateType::XCZ, {}, inst.targets, ""}); } template void FrameSimulator::do_MXX(const CircuitInstruction &inst) { decompose_pair_instruction_into_disjoint_segments(inst, num_qubits, [&](CircuitInstruction segment) { do_MXX_disjoint_controls_segment(segment); }); } template void FrameSimulator::do_MYY(const CircuitInstruction &inst) { decompose_pair_instruction_into_disjoint_segments(inst, num_qubits, [&](CircuitInstruction segment) { do_MYY_disjoint_controls_segment(segment); }); } template void FrameSimulator::do_MZZ(const CircuitInstruction &inst) { decompose_pair_instruction_into_disjoint_segments(inst, num_qubits, [&](CircuitInstruction segment) { do_MZZ_disjoint_controls_segment(segment); }); } template void FrameSimulator::do_MPAD(const CircuitInstruction &inst) { m_record.reserve_noisy_space_for_results(inst, rng); simd_bits empty(batch_size); for (size_t k = 0; k < inst.targets.size(); k++) { // 0-vs-1 is ignored because it's accounted for in the reference sample. m_record.xor_record_reserved_result(empty); } } template void FrameSimulator::do_gate(const CircuitInstruction &inst) { switch (inst.gate_type) { case GateType::DETECTOR: do_DETECTOR(inst); break; case GateType::OBSERVABLE_INCLUDE: do_OBSERVABLE_INCLUDE(inst); break; case GateType::MX: do_MX(inst); break; case GateType::MY: do_MY(inst); break; case GateType::M: do_MZ(inst); break; case GateType::MRX: do_MRX(inst); break; case GateType::MRY: do_MRY(inst); break; case GateType::MR: do_MRZ(inst); break; case GateType::RX: do_RX(inst); break; case GateType::RY: do_RY(inst); break; case GateType::R: do_RZ(inst); break; case GateType::MPP: do_MPP(inst); break; case GateType::SPP: do_SPP(inst); break; case GateType::SPP_DAG: do_SPP_DAG(inst); break; case GateType::MPAD: do_MPAD(inst); break; case GateType::MXX: do_MXX(inst); break; case GateType::MYY: do_MYY(inst); break; case GateType::MZZ: do_MZZ(inst); break; case GateType::XCX: do_XCX(inst); break; case GateType::XCY: do_XCY(inst); break; case GateType::XCZ: do_XCZ(inst); break; case GateType::YCX: do_YCX(inst); break; case GateType::YCY: do_YCY(inst); break; case GateType::YCZ: do_YCZ(inst); break; case GateType::CX: do_ZCX(inst); break; case GateType::CY: do_ZCY(inst); break; case GateType::CZ: do_ZCZ(inst); break; case GateType::DEPOLARIZE1: do_DEPOLARIZE1(inst); break; case GateType::DEPOLARIZE2: do_DEPOLARIZE2(inst); break; case GateType::X_ERROR: do_X_ERROR(inst); break; case GateType::Y_ERROR: do_Y_ERROR(inst); break; case GateType::Z_ERROR: do_Z_ERROR(inst); break; case GateType::PAULI_CHANNEL_1: do_PAULI_CHANNEL_1(inst); break; case GateType::PAULI_CHANNEL_2: do_PAULI_CHANNEL_2(inst); break; case GateType::E: do_CORRELATED_ERROR(inst); break; case GateType::ELSE_CORRELATED_ERROR: do_ELSE_CORRELATED_ERROR(inst); break; case GateType::C_XYZ: case GateType::C_NXYZ: case GateType::C_XNYZ: case GateType::C_XYNZ: do_C_XYZ(inst); break; case GateType::C_ZYX: case GateType::C_NZYX: case GateType::C_ZNYX: case GateType::C_ZYNX: do_C_ZYX(inst); break; case GateType::SWAP: do_SWAP(inst); break; case GateType::CXSWAP: do_CXSWAP(inst); break; case GateType::CZSWAP: do_CZSWAP(inst); break; case GateType::SWAPCX: do_SWAPCX(inst); break; case GateType::HERALDED_ERASE: do_HERALDED_ERASE(inst); break; case GateType::HERALDED_PAULI_CHANNEL_1: do_HERALDED_PAULI_CHANNEL_1(inst); break; case GateType::SQRT_XX: case GateType::SQRT_XX_DAG: do_SQRT_XX(inst); break; case GateType::SQRT_YY: case GateType::SQRT_YY_DAG: do_SQRT_YY(inst); break; case GateType::SQRT_ZZ: case GateType::SQRT_ZZ_DAG: do_SQRT_ZZ(inst); break; case GateType::ISWAP: case GateType::ISWAP_DAG: do_ISWAP(inst); break; case GateType::SQRT_X: case GateType::SQRT_X_DAG: case GateType::H_YZ: case GateType::H_NYZ: do_H_YZ(inst); break; case GateType::SQRT_Y: case GateType::SQRT_Y_DAG: case GateType::H: case GateType::H_NXZ: do_H_XZ(inst); break; case GateType::S: case GateType::S_DAG: case GateType::H_XY: case GateType::H_NXY: do_H_XY(inst); break; case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::X: case GateType::Y: case GateType::Z: case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: do_I(inst); break; default: throw std::invalid_argument("Not implemented in FrameSimulator::do_gate: " + inst.str()); } } } // namespace stim ================================================ FILE: src/stim/simulators/frame_simulator.perf.cc ================================================ // 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. #include "stim/simulators/frame_simulator.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_surface_code.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(FrameSimulator_depolarize1_100Kqubits_1Ksamples_per1000) { CircuitStats stats; stats.num_qubits = 100 * 1000; size_t num_samples = 1000; double probability = 0.001; FrameSimulator sim( stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, num_samples, std::mt19937_64(0)); std::vector targets; for (uint32_t k = 0; k < stats.num_qubits; k++) { targets.push_back(GateTarget{k}); } CircuitInstruction op_data{GateType::DEPOLARIZE1, &probability, targets, ""}; benchmark_go([&]() { sim.do_DEPOLARIZE1(op_data); }) .goal_millis(5) .show_rate("OpQubits", targets.size() * num_samples); } BENCHMARK(FrameSimulator_depolarize2_100Kqubits_1Ksamples_per1000) { CircuitStats stats; stats.num_qubits = 100 * 1000; size_t num_samples = 1000; double probability = 0.001; FrameSimulator sim( stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, num_samples, std::mt19937_64(0)); std::vector targets; for (uint32_t k = 0; k < stats.num_qubits; k++) { targets.push_back({k}); } CircuitInstruction op_data{GateType::DEPOLARIZE2, &probability, targets, ""}; benchmark_go([&]() { sim.do_DEPOLARIZE2(op_data); }) .goal_millis(5) .show_rate("OpQubits", targets.size() * num_samples); } BENCHMARK(FrameSimulator_hadamard_100Kqubits_1Ksamples) { CircuitStats stats; stats.num_qubits = 100 * 1000; size_t num_samples = 1000; FrameSimulator sim( stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, num_samples, std::mt19937_64(0)); std::vector targets; for (uint32_t k = 0; k < stats.num_qubits; k++) { targets.push_back({k}); } CircuitInstruction op_data{GateType::H, {}, targets, ""}; benchmark_go([&]() { sim.do_H_XZ(op_data); }) .goal_millis(2) .show_rate("OpQubits", targets.size() * num_samples); } BENCHMARK(FrameSimulator_CX_100Kqubits_1Ksamples) { CircuitStats stats; stats.num_qubits = 100 * 1000; size_t num_samples = 1000; FrameSimulator sim( stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, num_samples, std::mt19937_64(0)); std::vector targets; for (uint32_t k = 0; k < stats.num_qubits; k++) { targets.push_back({k}); } CircuitInstruction op_data{GateType::CX, {}, targets, ""}; benchmark_go([&]() { sim.do_ZCX(op_data); }) .goal_millis(2) .show_rate("OpQubits", targets.size() * num_samples); } BENCHMARK(FrameSimulator_surface_code_rotated_memory_z_d11_r100_batch1024) { auto params = CircuitGenParameters(100, 11, "rotated_memory_z"); params.before_measure_flip_probability = 0.001; params.after_reset_flip_probability = 0.001; params.after_clifford_depolarization = 0.001; auto circuit = generate_surface_code_circuit(params).circuit; FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_MEASUREMENTS_TO_MEMORY, 1024, std::mt19937_64(0)); benchmark_go([&]() { sim.reset_all(); sim.do_circuit(circuit); }) .goal_millis(5.1) .show_rate("Shots", 1024) .show_rate("Dets", circuit.count_detectors() * 1024); sim.reset_all(); if (!sim.obs_record[0].not_zero()) { std::cerr << "data dependence"; } } ================================================ FILE: src/stim/simulators/frame_simulator.pybind.cc ================================================ #include "stim/simulators/frame_simulator.pybind.h" #include "stim/circuit/circuit_instruction.pybind.h" #include "stim/circuit/circuit_repeat_block.pybind.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" #include "stim/simulators/frame_simulator.h" #include "stim/stabilizers/pauli_string.pybind.h" using namespace stim; using namespace stim_pybind; std::optional py_index_to_optional_size_t( const pybind11::object &index, size_t length, const char *val_name, const char *len_name) { if (index.is_none()) { return {}; } int64_t i = pybind11::cast(index); if (i < -(int64_t)length || (i >= 0 && (uint64_t)i >= length)) { std::stringstream msg; msg << "not ("; msg << "-" << len_name << " <= "; msg << val_name << "=" << index; msg << " < "; msg << len_name << "=" << length; msg << ")"; throw std::out_of_range(msg.str()); } if (i < 0) { i += length; } assert(i >= 0); return (size_t)i; } static void generate_biased_samples_bit_packed_contiguous( uint8_t *out, size_t num_bytes, float p, std::mt19937_64 &rng) { uintptr_t start = (uintptr_t)out; uintptr_t end = start + num_bytes; uintptr_t aligned64_start = start & ~0b111ULL; uintptr_t aligned64_end = end & ~0b111ULL; if (aligned64_start != start) { aligned64_start += 8; } biased_randomize_bits(p, (uint64_t *)aligned64_start, (uint64_t *)aligned64_end, rng); if (start < aligned64_start) { uint64_t pad; biased_randomize_bits(p, &pad, &pad + 1, rng); for (size_t k = 0; k < aligned64_start - start; k++) { out[k] = (uint8_t)(pad & 0xFF); pad >>= 8; } } if (aligned64_end < end) { uint64_t pad; biased_randomize_bits(p, &pad, &pad + 1, rng); while (aligned64_end < end) { *(uint8_t *)aligned64_end = (uint8_t)(pad & 0xFF); aligned64_end++; pad >>= 8; } } } static void generate_biased_samples_bit_packed_with_stride( uint8_t *out, pybind11::ssize_t stride, size_t num_bytes, float p, std::mt19937_64 &rng) { uint64_t stack[64]; uint64_t *stack_ptr = &stack[0]; for (size_t k1 = 0; k1 < num_bytes; k1 += 64 * 8) { size_t n2 = std::min(num_bytes - k1, (size_t)(64 * 8)); biased_randomize_bits(p, stack_ptr, stack_ptr + (n2 + 7) / 8, rng); uint8_t *stack_data = (uint8_t *)(void *)stack_ptr; for (size_t k2 = 0; k2 < n2; k2++) { *out = *stack_data; stack_data += 1; out += stride; } } } static void generate_biased_samples_bool( uint8_t *out, pybind11::ssize_t stride, size_t num_samples, float p, std::mt19937_64 &rng) { uint64_t stack[64]; uint64_t *stack_ptr = &stack[0]; for (size_t k1 = 0; k1 < num_samples; k1 += 64 * 64) { size_t n2 = std::min(num_samples - k1, (size_t)(64 * 64)); biased_randomize_bits(p, stack_ptr, stack_ptr + (n2 + 63) / 64, rng); for (size_t k2 = 0; k2 < n2; k2++) { bool bit = (stack[k2 / 64] >> (k2 & 63)) & 1; *out++ = bit; } } } template pybind11::object generate_bernoulli_samples( FrameSimulator &self, size_t num_samples, float p, bool bit_packed, pybind11::object out) { if (bit_packed) { size_t num_bytes = (num_samples + 7) / 8; if (out.is_none()) { // Allocate u64 aligned memory. void *buffer = (void *)new uint64_t[(num_bytes + 7) / 8]; pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast(f); }); out = pybind11::array_t( {(pybind11::ssize_t)num_bytes}, {(pybind11::ssize_t)1}, (uint8_t *)buffer, free_when_done); } else if (!pybind11::isinstance>(out)) { throw std::invalid_argument("`out` wasn't `None` or a uint8 numpy array."); } auto buf = pybind11::cast>(out); if (buf.ndim() != 1) { throw std::invalid_argument("Output buffer wasn't one dimensional."); } if ((size_t)buf.shape(0) != num_bytes) { std::stringstream ss; ss << "Expected output buffer to have size " << num_bytes; ss << " but its size is " << buf.shape(0) << "."; throw std::invalid_argument(ss.str()); } auto stride = buf.strides(0); void *start_of_data = (void *)buf.mutable_data(); if (stride == 1) { generate_biased_samples_bit_packed_contiguous((uint8_t *)start_of_data, num_bytes, p, self.rng); } else { generate_biased_samples_bit_packed_with_stride((uint8_t *)start_of_data, stride, num_bytes, p, self.rng); } if (num_samples & 0b111) { uint8_t mask = (1 << (num_samples & 0b111)) - 1; buf.mutable_at(num_bytes - 1) &= mask; } } else { if (out.is_none()) { auto numpy = pybind11::module::import("numpy"); out = numpy.attr("empty")(num_samples, numpy.attr("bool_")); } else if (!pybind11::isinstance>(out)) { throw std::invalid_argument("`out` wasn't `None` or a bool_ numpy array."); } auto buf = pybind11::cast>(out); if (buf.ndim() != 1) { throw std::invalid_argument("Output buffer wasn't one dimensional."); } if ((size_t)buf.shape(0) != num_samples) { std::stringstream ss; ss << "Expected output buffer to have size " << num_samples; ss << " but its size is " << buf.shape(0) << "."; throw std::invalid_argument(ss.str()); } auto stride = buf.strides(0); void *start_of_data = (void *)buf.mutable_data(); generate_biased_samples_bool((uint8_t *)start_of_data, stride, num_samples, p, self.rng); } return out; } uint8_t pybind11_object_to_pauli_ixyz(const pybind11::object &obj) { if (pybind11::isinstance(obj)) { std::string_view s = pybind11::cast(obj); if (s == "X") { return 1; } else if (s == "Y") { return 2; } else if (s == "Z") { return 3; } else if (s == "I" || s == "_") { return 0; } } else if (pybind11::isinstance(obj)) { uint8_t v = 255; try { v = pybind11::cast(obj); } catch (const pybind11::cast_error &) { } if (v < 4) { return (uint8_t)v; } } throw std::invalid_argument("Need pauli in ['I', 'X', 'Y', 'Z', 0, 1, 2, 3, '_']."); } pybind11::class_> stim_pybind::pybind_frame_simulator(pybind11::module &m) { return pybind11::class_>( m, "FlipSimulator", clean_doc_string(R"DOC( A simulator that tracks whether things are flipped, instead of what they are. Tracking flips is significantly cheaper than tracking actual values, requiring O(1) work per gate (compared to O(n) for unitary operations and O(n^2) for collapsing operations in the tableau simulator, where n is the qubit count). Supports interactive usage, where gates and measurements are applied on demand. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) )DOC") .data()); } template pybind11::object peek_pauli_flips(const FrameSimulator &self, const pybind11::object &py_instance_index) { std::optional instance_index = py_index_to_optional_size_t(py_instance_index, self.batch_size, "instance_index", "batch_size"); if (instance_index.has_value()) { return pybind11::cast(FlexPauliString(self.get_frame(*instance_index))); } std::vector result; for (size_t k = 0; k < self.batch_size; k++) { result.push_back(FlexPauliString(self.get_frame(k))); } return pybind11::cast(std::move(result)); } pybind11::object pick_output_numpy_array( pybind11::object output_vs, bool bit_packed, bool transpose, size_t shape1, size_t shape2, const char *name) { auto numpy = pybind11::module::import("numpy"); auto dtype = bit_packed ? numpy.attr("uint8") : numpy.attr("bool_"); auto py_bool = pybind11::module::import("builtins").attr("bool"); if (transpose) { std::swap(shape1, shape2); } if (bit_packed) { shape2 = (shape2 + 7) >> 3; } auto shape = pybind11::make_tuple(shape1, shape2); if (pybind11::isinstance(output_vs) && pybind11::bool_(false).equal(output_vs)) { return pybind11::none(); } else if (pybind11::isinstance(output_vs) && pybind11::bool_(true).equal(output_vs)) { return numpy.attr("empty")(shape, dtype); } else if ( bit_packed && pybind11::isinstance>(output_vs) && shape.equal(output_vs.attr("shape"))) { return output_vs; } else if ( !bit_packed && pybind11::isinstance>(output_vs) && shape.equal(output_vs.attr("shape"))) { return output_vs; } else { std::stringstream ss; ss << name << " wasn't set to False, True, or a numpy array with dtype=" << pybind11::str(dtype) << " and shape=" << shape; throw std::invalid_argument(ss.str()); } } template pybind11::object to_numpy( const FrameSimulator &self, bool bit_packed, bool transpose, pybind11::object output_xs, pybind11::object output_zs, pybind11::object output_measure_flips, pybind11::object output_detector_flips, pybind11::object output_observable_flips) { output_xs = pick_output_numpy_array(output_xs, bit_packed, transpose, self.num_qubits, self.batch_size, "output_xs"); output_zs = pick_output_numpy_array(output_zs, bit_packed, transpose, self.num_qubits, self.batch_size, "output_zs"); output_measure_flips = pick_output_numpy_array( output_measure_flips, bit_packed, transpose, self.m_record.stored, self.batch_size, "output_measure_flips"); output_detector_flips = pick_output_numpy_array( output_detector_flips, bit_packed, transpose, self.det_record.stored, self.batch_size, "output_detector_flips"); output_observable_flips = pick_output_numpy_array( output_observable_flips, bit_packed, transpose, self.num_observables, self.batch_size, "output_observable_flips"); if (!output_xs.is_none()) { simd_bit_table_to_numpy(self.x_table, self.num_qubits, self.batch_size, bit_packed, transpose, output_xs); } if (!output_zs.is_none()) { simd_bit_table_to_numpy(self.z_table, self.num_qubits, self.batch_size, bit_packed, transpose, output_zs); } if (!output_measure_flips.is_none()) { simd_bit_table_to_numpy( self.m_record.storage, self.m_record.stored, self.batch_size, bit_packed, transpose, output_measure_flips); } if (!output_detector_flips.is_none()) { simd_bit_table_to_numpy( self.det_record.storage, self.det_record.stored, self.batch_size, bit_packed, transpose, output_detector_flips); } if (!output_observable_flips.is_none()) { simd_bit_table_to_numpy( self.obs_record, self.num_observables, self.batch_size, bit_packed, transpose, output_observable_flips); } if (output_xs.is_none() + output_zs.is_none() + output_measure_flips.is_none() + output_detector_flips.is_none() + output_observable_flips.is_none() == 5) { throw std::invalid_argument("No outputs requested! Specify at least one output_*= argument."); } return pybind11::make_tuple( output_xs, output_zs, output_measure_flips, output_detector_flips, output_observable_flips); } template FrameSimulator create_frame_simulator( size_t batch_size, bool disable_heisenberg_uncertainty, uint32_t num_qubits, const pybind11::object &seed) { FrameSimulator result( CircuitStats{ 0, // num_detectors 0, // num_observables 0, // num_measurements num_qubits, 0, // num_ticks (uint32_t)(1 << 24), // max_lookback 0, // num_sweep_bits }, FrameSimulatorMode::STORE_EVERYTHING_TO_MEMORY, batch_size, make_py_seeded_rng(seed)); result.guarantee_anticommutation_via_frame_randomization = !disable_heisenberg_uncertainty; result.reset_all(); return result; } template pybind11::object sliced_table_to_numpy( const simd_bit_table &table, size_t num_major_exact, size_t num_minor_exact, std::optional major_index, std::optional minor_index, bool bit_packed) { if (major_index.has_value()) { simd_bits_range_ref row = table[*major_index]; if (minor_index.has_value()) { bool b = row[*minor_index]; auto np = pybind11::module::import("numpy"); return np.attr("array")(b, bit_packed ? np.attr("uint8") : np.attr("bool_")); } else { return simd_bits_to_numpy(row, num_minor_exact, bit_packed); } } else { if (minor_index.has_value()) { auto data = table.read_across_majors_at_minor_index(0, num_major_exact, *minor_index); return simd_bits_to_numpy(data, num_major_exact, bit_packed); } else { return simd_bit_table_to_numpy( table, num_major_exact, num_minor_exact, bit_packed, false, pybind11::none()); } } } template pybind11::object get_measurement_flips( FrameSimulator &self, const pybind11::object &py_record_index, const pybind11::object &py_instance_index, bool bit_packed) { size_t num_measurements = self.m_record.stored; std::optional instance_index = py_index_to_optional_size_t(py_instance_index, self.batch_size, "instance_index", "batch_size"); std::optional record_index = py_index_to_optional_size_t(py_record_index, num_measurements, "record_index", "num_measurements"); return sliced_table_to_numpy( self.m_record.storage, num_measurements, self.batch_size, record_index, instance_index, bit_packed); } template pybind11::object get_detector_flips( FrameSimulator &self, const pybind11::object &py_detector_index, const pybind11::object &py_instance_index, bool bit_packed) { size_t num_detectors = self.det_record.stored; std::optional instance_index = py_index_to_optional_size_t(py_instance_index, self.batch_size, "instance_index", "batch_size"); std::optional detector_index = py_index_to_optional_size_t(py_detector_index, num_detectors, "detector_index", "num_detectors"); return sliced_table_to_numpy( self.det_record.storage, num_detectors, self.batch_size, detector_index, instance_index, bit_packed); } template pybind11::object get_obs_flips( FrameSimulator &self, const pybind11::object &py_observable_index, const pybind11::object &py_instance_index, bool bit_packed) { std::optional instance_index = py_index_to_optional_size_t(py_instance_index, self.batch_size, "instance_index", "batch_size"); std::optional observable_index = py_index_to_optional_size_t(py_observable_index, self.num_observables, "observable_index", "num_observables"); return sliced_table_to_numpy( self.obs_record, self.num_observables, self.batch_size, observable_index, instance_index, bit_packed); } void stim_pybind::pybind_frame_simulator_methods( pybind11::module &m, pybind11::class_> &c) { c.def( pybind11::init(&create_frame_simulator), pybind11::kw_only(), pybind11::arg("batch_size"), pybind11::arg("disable_stabilizer_randomization") = false, pybind11::arg("num_qubits") = 0, pybind11::arg("seed") = pybind11::none(), clean_doc_string(R"DOC( @signature def __init__(self, *, batch_size: int, disable_stabilizer_randomization: bool = False, num_qubits: int = 0, seed: Optional[int] = None) -> None: Initializes a stim.FlipSimulator. Args: batch_size: For speed, the flip simulator simulates many instances in parallel. This argument determines the number of parallel instances. It's recommended to use a multiple of 256, because internally the state of the instances is striped across SSE (128 bit) or AVX (256 bit) words with one bit in the word belonging to each instance. The result is that, even if you only ask for 1 instance, probably the same amount of work is being done as if you'd asked for 256 instances. The extra results just aren't being used, creating waste. disable_stabilizer_randomization: Determines whether or not the flip simulator uses stabilizer randomization. Defaults to False (stabilizer randomization used). Set to True to disable stabilizer randomization. Stabilizer randomization means that, when a qubit is initialized or measured in the Z basis, a Z error is added to the qubit with 50% probability. More generally, anytime a stabilizer is introduced into the system by any means, an error equal to that stabilizer is applied with 50% probability. This ensures that observables anticommuting with stabilizers of the system must be maximally uncertain. In other words, this feature enforces Heisenberg's uncertainty principle. This is a safety feature that you should not turn off unless you have a reason to do so. Stabilizer randomization is turned on by default because it catches mistakes. For example, suppose you are trying to create a stabilizer code but you accidentally have the code measure two anticommuting stabilizers. With stabilizer randomization turned off, it will look like this code works. With stabilizer randomization turned on, the two measurements will correctly randomize each other revealing that the code doesn't work. In some use cases, stabilizer randomization is a hindrance instead of helpful. For example, if you are using the flip simulator to understand how an error propagates through the system, the stabilizer randomization will be introducing error terms that you don't want. num_qubits: Sets the initial number of qubits tracked by the simulation. The simulator will still automatically resize as needed when qubits beyond this limit are touched. This parameter exists as a way to hint at the desired size of the simulator's state for performance, and to ensure methods that peek at the size have the expected size from the start instead of only after the relevant qubits have been touched. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: An initialized stim.FlipSimulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) )DOC") .data()); c.def_property_readonly( "batch_size", [](FrameSimulator &self) -> size_t { return self.batch_size; }, clean_doc_string(R"DOC( Returns the number of instances being simulated by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.batch_size 256 >>> sim = stim.FlipSimulator(batch_size=42) >>> sim.batch_size 42 )DOC") .data()); c.def_property_readonly( "num_qubits", [](FrameSimulator &self) -> size_t { return self.num_qubits; }, clean_doc_string(R"DOC( Returns the number of qubits currently tracked by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_qubits 0 >>> sim = stim.FlipSimulator(batch_size=256, num_qubits=4) >>> sim.num_qubits 4 >>> sim.do(stim.Circuit('H 5')) >>> sim.num_qubits 6 )DOC") .data()); c.def_property_readonly( "num_observables", [](FrameSimulator &self) -> size_t { return self.num_observables; }, clean_doc_string(R"DOC( Returns the number of observables currently tracked by the simulator. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_observables 0 >>> sim.do(stim.Circuit(''' ... M 0 ... OBSERVABLE_INCLUDE(4) rec[-1] ... ''')) >>> sim.num_observables 5 )DOC") .data()); c.def_property_readonly( "num_measurements", [](FrameSimulator &self) -> size_t { return self.m_record.stored; }, clean_doc_string(R"DOC( Returns the number of measurements that have been simulated and stored. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_measurements 0 >>> sim.do(stim.Circuit('M 3 5')) >>> sim.num_measurements 2 )DOC") .data()); c.def_property_readonly( "num_detectors", [](FrameSimulator &self) -> size_t { return self.det_record.stored; }, clean_doc_string(R"DOC( Returns the number of detectors that have been simulated and stored. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.num_detectors 0 >>> sim.do(stim.Circuit(''' ... M 0 0 ... DETECTOR rec[-1] rec[-2] ... ''')) >>> sim.num_detectors 1 )DOC") .data()); c.def( "set_pauli_flip", [](FrameSimulator &self, const pybind11::object &pauli, int64_t qubit_index, int64_t instance_index) { uint8_t p = pybind11_object_to_pauli_ixyz(pauli); if (instance_index < 0) { instance_index += self.batch_size; } if (qubit_index < 0) { throw std::out_of_range("qubit_index"); } if (instance_index < 0 || (uint64_t)instance_index >= self.batch_size) { throw std::out_of_range("instance_index"); } if ((uint64_t)qubit_index >= self.num_qubits) { CircuitStats stats; stats.num_qubits = qubit_index + 1; self.ensure_safe_to_do_circuit_with_stats(stats); } p ^= p >> 1; self.x_table[qubit_index][instance_index] = (p & 1) != 0; self.z_table[qubit_index][instance_index] = (p & 2) != 0; }, pybind11::arg("pauli"), pybind11::kw_only(), pybind11::arg("qubit_index"), pybind11::arg("instance_index"), clean_doc_string(R"DOC( @signature def set_pauli_flip(self, pauli: Union[str, int], *, qubit_index: int, instance_index: int) -> None: Sets the pauli flip on a given qubit in a given simulation instance. Args: pauli: The pauli, specified as an integer or string. Uses the convention 0=I, 1=X, 2=Y, 3=Z. Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. qubit_index: The qubit to put the error on. Must be non-negative. The state will automatically expand as needed to store the error. instance_index: The simulation index to put the error inside. Use negative indices to index from the end of the list. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=2, ... num_qubits=3, ... disable_stabilizer_randomization=True, ... ) >>> sim.set_pauli_flip('X', qubit_index=2, instance_index=1) >>> sim.peek_pauli_flips() [stim.PauliString("+___"), stim.PauliString("+__X")] )DOC") .data()); c.def( "peek_pauli_flips", &peek_pauli_flips, pybind11::kw_only(), pybind11::arg("instance_index") = pybind11::none(), clean_doc_string(R"DOC( @overload def peek_pauli_flips(self) -> List[stim.PauliString]: @overload def peek_pauli_flips(self, *, instance_index: int) -> stim.PauliString: @signature def peek_pauli_flips(self, *, instance_index: Optional[int] = None) -> Union[stim.PauliString, List[stim.PauliString]]: Returns the current pauli errors packed into stim.PauliString instances. Args: instance_index: Defaults to None. When set to None, the pauli errors from all instances are returned as a list of `stim.PauliString`. When set to an integer, a single `stim.PauliString` is returned containing the errors for the indexed instance. Returns: if instance_index is None: A list of stim.PauliString, with the k'th entry being the errors from the k'th simulation instance. else: A stim.PauliString with the errors from the k'th simulation instance. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=2, ... disable_stabilizer_randomization=True, ... num_qubits=10, ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+__________"), stim.PauliString("+__________")] >>> sim.peek_pauli_flips(instance_index=0) stim.PauliString("+__________") >>> sim.do(stim.Circuit(''' ... X_ERROR(1) 0 3 5 ... Z_ERROR(1) 3 6 ... ''')) >>> sim.peek_pauli_flips() [stim.PauliString("+X__Y_XZ___"), stim.PauliString("+X__Y_XZ___")] >>> sim = stim.FlipSimulator( ... batch_size=1, ... num_qubits=100, ... ) >>> flips: stim.PauliString = sim.peek_pauli_flips(instance_index=0) >>> sorted(set(str(flips))) # Should have Zs from stabilizer randomization ['+', 'Z', '_'] )DOC") .data()); c.def( "to_numpy", &to_numpy, pybind11::kw_only(), pybind11::arg("bit_packed") = false, pybind11::arg("transpose") = false, pybind11::arg("output_xs") = false, pybind11::arg("output_zs") = false, pybind11::arg("output_measure_flips") = false, pybind11::arg("output_detector_flips") = false, pybind11::arg("output_observable_flips") = false, clean_doc_string(R"DOC( @signature def to_numpy(self, *, bit_packed: bool = False, transpose: bool = False, output_xs: Union[bool, np.ndarray] = False, output_zs: Union[bool, np.ndarray] = False, output_measure_flips: Union[bool, np.ndarray] = False, output_detector_flips: Union[bool, np.ndarray] = False, output_observable_flips: Union[bool, np.ndarray] = False) -> Optional[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]]: Writes the simulator state into numpy arrays. Args: bit_packed: Whether or not the result is bit packed, storing 8 bits per byte instead of 1 bit per byte. Bit packing always applies to the second index of the result. Bits are packed in little endian order (as if by `np.packbits(X, axis=1, order='little')`). transpose: Defaults to False. When set to False, the second index of the returned array (the index affected by bit packing) is the shot index (meaning the first index is the qubit index or measurement index or etc). When set to True, results are transposed so that the first index is the shot index. output_xs: Defaults to False. When set to False, the X flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the X flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_zs: Defaults to False. When set to False, the Z flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the Z flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_measure_flips: Defaults to False. When set to False, the measure flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the measure flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_detector_flips: Defaults to False. When set to False, the detector flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the detector flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). output_observable_flips: Defaults to False. When set to False, the obs flip data is not generated and the corresponding array in the result tuple is set to None. When set to True, a new array is allocated to hold the obs flip data and this array is returned via the result tuple. When set to a numpy array, the results are written into that array (the shape and dtype of the array must be exactly correct). Returns: A tuple (xs, zs, ms, ds, os) of numpy arrays. The xs and zs arrays are the pauli flip data specified using XZ encoding (00=I, 10=X, 11=Y, 01=Z). The ms array is the measure flip data, the ds array is the detector flip data, and the os array is the obs flip data. The arrays default to `None` when the corresponding `output_*` argument was left False. The shape and dtype of the data depends on arguments given to the function. The following specifies each array's shape and dtype for each case: if not transpose and not bit_packed: xs.shape = (sim.batch_size, sim.num_qubits) zs.shape = (sim.batch_size, sim.num_qubits) ms.shape = (sim.batch_size, sim.num_measurements) ds.shape = (sim.batch_size, sim.num_detectors) os.shape = (sim.batch_size, sim.num_observables) xs.dtype = np.bool_ zs.dtype = np.bool_ ms.dtype = np.bool_ ds.dtype = np.bool_ os.dtype = np.bool_ elif not transpose and bit_packed: xs.shape = (sim.batch_size, math.ceil(sim.num_qubits / 8)) zs.shape = (sim.batch_size, math.ceil(sim.num_qubits / 8)) ms.shape = (sim.batch_size, math.ceil(sim.num_measurements / 8)) ds.shape = (sim.batch_size, math.ceil(sim.num_detectors / 8)) os.shape = (sim.batch_size, math.ceil(sim.num_observables / 8)) xs.dtype = np.uint8 zs.dtype = np.uint8 ms.dtype = np.uint8 ds.dtype = np.uint8 os.dtype = np.uint8 elif transpose and not bit_packed: xs.shape = (sim.num_qubits, sim.batch_size) zs.shape = (sim.num_qubits, sim.batch_size) ms.shape = (sim.num_measurements, sim.batch_size) ds.shape = (sim.num_detectors, sim.batch_size) os.shape = (sim.num_observables, sim.batch_size) xs.dtype = np.bool_ zs.dtype = np.bool_ ms.dtype = np.bool_ ds.dtype = np.bool_ os.dtype = np.bool_ elif transpose and bit_packed: xs.shape = (sim.num_qubits, math.ceil(sim.batch_size / 8)) zs.shape = (sim.num_qubits, math.ceil(sim.batch_size / 8)) ms.shape = (sim.num_measurements, math.ceil(sim.batch_size / 8)) ds.shape = (sim.num_detectors, math.ceil(sim.batch_size / 8)) os.shape = (sim.num_observables, math.ceil(sim.batch_size / 8)) xs.dtype = np.uint8 zs.dtype = np.uint8 ms.dtype = np.uint8 ds.dtype = np.uint8 os.dtype = np.uint8 Raises: ValueError: All the `output_*` arguments were False, or an `output_*` argument had a shape or dtype inconsistent with the requested data. Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit('M(1) 0 1 2')) >>> ms_buf = np.empty(shape=(9, 1), dtype=np.uint8) >>> xs, zs, ms, ds, os = sim.to_numpy( ... transpose=True, ... bit_packed=True, ... output_xs=True, ... output_measure_flips=ms_buf, ... ) >>> assert ms is ms_buf >>> xs array([[0], [0], [0], [0], [0], [0], [0], [0], [0]], dtype=uint8) >>> zs >>> ms array([[7], [7], [7], [7], [7], [7], [7], [7], [7]], dtype=uint8) >>> ds >>> os )DOC") .data()); c.def( "generate_bernoulli_samples", &generate_bernoulli_samples, pybind11::arg("num_samples"), pybind11::kw_only(), pybind11::arg("p"), pybind11::arg("bit_packed") = false, pybind11::arg("out") = pybind11::none(), clean_doc_string(R"DOC( @signature def generate_bernoulli_samples(self, num_samples: int, *, p: float, bit_packed: bool = False, out: Optional[np.ndarray] = None) -> np.ndarray: Uses the simulator's random number generator to produce biased coin flips. This method has best performance when specifying `bit_packed=True` and when specifying an `out=` parameter pointing to a numpy array that has contiguous data aligned to a 64 bit boundary. (If `out` isn't specified, the returned numpy array will have this property.) Args: num_samples: The number of samples to produce. p: The probability of each sample being True instead of False. bit_packed: Defaults to False (no bit packing). When True, the result has type np.uint8 instead of np.bool_ and 8 samples are packed into each byte as if by np.packbits(bitorder='little'). (The bit order is relevant when producing a number of samples that isn't a multiple of 8.) out: Defaults to None (allocate new). A numpy array to write the samples into. Must have the correct size and dtype. Returns: A numpy array containing the samples. The shape and dtype depends on the bit_packed argument: if not bit_packed: shape = (num_samples,) dtype = np.bool_ elif not transpose and bit_packed: shape = (math.ceil(num_samples / 8),) dtype = np.uint8 Raises: ValueError: The given `out` argument had a shape or dtype inconsistent with the requested data. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> r = sim.generate_bernoulli_samples(1001, p=0.25) >>> r.dtype dtype('bool') >>> r.shape (1001,) >>> r = sim.generate_bernoulli_samples(53, p=0.1, bit_packed=True) >>> r.dtype dtype('uint8') >>> r.shape (7,) >>> r[6] & 0b1110_0000 # zero'd padding bits np.uint8(0) >>> r2 = sim.generate_bernoulli_samples(53, p=0.2, bit_packed=True, out=r) >>> r is r2 # Check request to reuse r worked. True )DOC") .data()); c.def( "append_measurement_flips", [](FrameSimulator &self, const pybind11::object &measurement_flip_data) { if (pybind11::isinstance>(measurement_flip_data)) { const pybind11::array_t &arr = pybind11::cast>(measurement_flip_data); if (arr.ndim() == 1) { if ((size_t)arr.shape(0) != self.batch_size) { std::stringstream ss; ss << "dtype=np.bool_ and len(shape) == 1 but shape[0]="; ss << arr.shape(0) << " != batch_size=" << self.batch_size; throw std::invalid_argument(ss.str()); } simd_bits_range_ref r = self.m_record.record_zero_result_to_edit(); memcpy_bits_from_numpy_to_simd(self.batch_size, measurement_flip_data, r); return; } else if (arr.ndim() == 2) { if ((size_t)arr.shape(1) != self.batch_size) { std::stringstream ss; ss << "dtype=np.uint8 and len(shape) == 2 but shape[1]="; ss << arr.shape(1) << " != batch_size=" << self.batch_size; throw std::invalid_argument(ss.str()); } size_t num_measurements = (size_t)arr.shape(0); for (size_t k = 0; k < num_measurements; k++) { simd_bits_range_ref r = self.m_record.record_zero_result_to_edit(); memcpy_bits_from_numpy_to_simd(self.batch_size, measurement_flip_data[pybind11::cast(k)], r); } return; } } else if (pybind11::isinstance>(measurement_flip_data)) { const pybind11::array_t &arr = pybind11::cast>(measurement_flip_data); auto byte_size = (self.batch_size + 7) / 8; if (arr.ndim() == 1) { auto byte_size = (self.batch_size + 7) / 8; if ((size_t)arr.shape(0) != byte_size) { std::stringstream ss; ss << "dtype=np.uint8 and len(shape) == 1 but shape[0]="; ss << arr.shape(0) << " != (batch_size + 7) // 8=" << byte_size; throw std::invalid_argument(ss.str()); } simd_bits_range_ref r = self.m_record.record_zero_result_to_edit(); memcpy_bits_from_numpy_to_simd(self.batch_size, measurement_flip_data, r); return; } else if (arr.ndim() == 2) { if ((size_t)arr.shape(1) != byte_size) { std::stringstream ss; ss << "dtype=np.uint8 and len(shape) == 2 but shape[1]="; ss << arr.shape(1) << " != (batch_size + 7) // 8=" << byte_size; throw std::invalid_argument(ss.str()); } size_t num_measurements = (size_t)arr.shape(0); for (size_t k = 0; k < num_measurements; k++) { simd_bits_range_ref r = self.m_record.record_zero_result_to_edit(); memcpy_bits_from_numpy_to_simd(self.batch_size, measurement_flip_data[pybind11::cast(k)], r); } return; } } std::stringstream ss; ss << "Unsupported dtype/shape combination for append_measurement_flips.\n"; ss << "\nSupported combinations are:\n"; ss << " dtype=np.bool_, shape=(batch_size,)\n"; ss << " dtype=np.uint8, shape=(math.ceil(batch_size / 8),)\n"; ss << " dtype=np.bool_, shape=(num_measurements, batch_size)\n"; ss << " dtype=np.uint8, shape=(num_measurements, math.ceil(batch_size / 8))"; throw std::invalid_argument(ss.str()); }, pybind11::arg("measurement_flip_data"), clean_doc_string(R"DOC( @signature def append_measurement_flips(self, measurement_flip_data: np.ndarray) -> None: Appends measurement flip data to the simulator's measurement record. Args: measurement_flip_data: The flip data to append. The following shape/dtype combinations are supported. Single measurement without bit packing: shape=(self.batch_size,) dtype=np.bool_ Single measurement with bit packing: shape=(math.ceil(self.batch_size / 8),) dtype=np.uint8 Multiple measurements without bit packing: shape=(num_measurements, self.batch_size) dtype=np.bool_ Multiple measurements with bit packing: shape=(num_measurements, math.ceil(self.batch_size / 8)) dtype=np.uint8 Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.append_measurement_flips(np.array( ... [0, 1, 0, 0, 1, 0, 0, 1, 1], ... dtype=np.bool_, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True]]) >>> sim.append_measurement_flips(np.array( ... [0b11001001, 0], ... dtype=np.uint8, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False]]) >>> sim.append_measurement_flips(np.array( ... [[0b11111111, 0b1], [0b00000000, 0b0], [0b11111111, 0b1]], ... dtype=np.uint8, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False], [ True, True, True, True, True, True, True, True, True], [False, False, False, False, False, False, False, False, False], [ True, True, True, True, True, True, True, True, True]]) >>> sim.append_measurement_flips(np.array( ... [[1, 0, 1, 0, 1, 0, 1, 0, 1], [0, 1, 0, 1, 0, 1, 0, 1, 0]], ... dtype=np.bool_, ... )) >>> sim.get_measurement_flips() array([[False, True, False, False, True, False, False, True, True], [ True, False, False, True, False, False, True, True, False], [ True, True, True, True, True, True, True, True, True], [False, False, False, False, False, False, False, False, False], [ True, True, True, True, True, True, True, True, True], [ True, False, True, False, True, False, True, False, True], [False, True, False, True, False, True, False, True, False]]) )DOC") .data()); c.def( "get_measurement_flips", &get_measurement_flips, pybind11::kw_only(), pybind11::arg("record_index") = pybind11::none(), pybind11::arg("instance_index") = pybind11::none(), pybind11::arg("bit_packed") = false, clean_doc_string(R"DOC( @signature def get_measurement_flips(self, *, record_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False) -> np.ndarray: Retrieves measurement flip data from the simulator's measurement record. Args: record_index: Identifies a measurement to read results from. Setting this to None (default) returns results from all measurements. Setting this to a non-negative integer indexes measurements by the order they occurred. For example, record index 0 is the first measurement. Setting this to a negative integer indexes measurements by recency. For example, recording index -1 is the most recent measurement. instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be set to an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_measurements, self.batch_size), where the first index is the measurement_index and the second index is the instance_index and the dtype is np.bool_. Specifying record_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a measurement_index (or a 0d array, a boolean, if record_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit('M 0 1 2')) >>> sim.get_measurement_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_measurement_flips(bit_packed=True) array([[0, 0], [0, 0], [0, 0]], dtype=uint8) >>> sim.get_measurement_flips(instance_index=1) array([False, False, False]) >>> sim.get_measurement_flips(record_index=2) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_measurement_flips(instance_index=1, record_index=2) array(False) )DOC") .data()); c.def( "get_detector_flips", &get_detector_flips, pybind11::kw_only(), pybind11::arg("detector_index") = pybind11::none(), pybind11::arg("instance_index") = pybind11::none(), pybind11::arg("bit_packed") = false, clean_doc_string(R"DOC( @signature def get_detector_flips(self, *, detector_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False) -> np.ndarray: Retrieves detector flip data from the simulator's detection event record. Args: record_index: Identifies a detector to read results from. Setting this to None (default) returns results from all detectors. Otherwise this should be an integer in range(0, self.num_detectors). instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_detectors, self.batch_size), where the first index is the detector_index and the second index is the instance_index and the dtype is np.bool_. Specifying detector_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a detector_index (or a 0d array, a boolean, if detector_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit(''' ... M 0 0 0 ... DETECTOR rec[-2] rec[-3] ... DETECTOR rec[-1] rec[-2] ... ''')) >>> sim.get_detector_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_detector_flips(bit_packed=True) array([[0, 0], [0, 0]], dtype=uint8) >>> sim.get_detector_flips(instance_index=2) array([False, False]) >>> sim.get_detector_flips(detector_index=1) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_detector_flips(instance_index=2, detector_index=1) array(False) )DOC") .data()); c.def( "get_observable_flips", &get_obs_flips, pybind11::kw_only(), pybind11::arg("observable_index") = pybind11::none(), pybind11::arg("instance_index") = pybind11::none(), pybind11::arg("bit_packed") = false, clean_doc_string(R"DOC( @signature def get_observable_flips(self, *, observable_index: Optional[int] = None, instance_index: Optional[int] = None, bit_packed: bool = False) -> np.ndarray: Retrieves observable flip data from the simulator's detection event record. Args: record_index: Identifies a observable to read results from. Setting this to None (default) returns results from all observables. Otherwise this should be an integer in range(0, self.num_observables). instance_index: Identifies a simulation instance to read results from. Setting this to None (the default) returns results from all instances. Otherwise this should be an integer in range(0, self.batch_size). bit_packed: Defaults to False. Determines whether the result is bit packed. If this is set to true, the returned numpy array will be bit packed as if by applying out = np.packbits(out, axis=len(out.shape) - 1, bitorder='little') Behind the scenes the data is always bit packed, so setting this argument avoids ever unpacking in the first place. This substantially improves performance when there is a lot of data. Returns: A numpy array containing the requested data. By default this is a 2d array of shape (self.num_observables, self.batch_size), where the first index is the observable_index and the second index is the instance_index and the dtype is np.bool_. Specifying observable_index slices away the first index, leaving a 1d array with only an instance_index. Specifying instance_index slices away the last index, leaving a 1d array with only a observable_index (or a 0d array, a boolean, if observable_index was also specified). Specifying bit_packed=True bit packs the last remaining index, changing the dtype to np.uint8. Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=9) >>> sim.do(stim.Circuit(''' ... M 0 0 0 ... OBSERVABLE_INCLUDE(0) rec[-2] ... OBSERVABLE_INCLUDE(1) rec[-1] ... ''')) >>> sim.get_observable_flips() array([[False, False, False, False, False, False, False, False, False], [False, False, False, False, False, False, False, False, False]]) >>> sim.get_observable_flips(bit_packed=True) array([[0, 0], [0, 0]], dtype=uint8) >>> sim.get_observable_flips(instance_index=2) array([False, False]) >>> sim.get_observable_flips(observable_index=1) array([False, False, False, False, False, False, False, False, False]) >>> sim.get_observable_flips(instance_index=2, observable_index=1) array(False) )DOC") .data()); c.def( "do", [](FrameSimulator &self, const pybind11::object &obj) { if (pybind11::isinstance(obj)) { self.safe_do_circuit(pybind11::cast(obj)); } else if (pybind11::isinstance(obj)) { self.safe_do_instruction(pybind11::cast(obj)); } else if (pybind11::isinstance(obj)) { const CircuitRepeatBlock &block = pybind11::cast(obj); self.safe_do_circuit(block.body, block.repeat_count); } else { std::stringstream ss; ss << "Don't know how to do a '"; ss << pybind11::repr(obj); ss << "'."; throw std::invalid_argument(ss.str()); } }, pybind11::arg("obj"), clean_doc_string(R"DOC( @signature def do(self, obj: Union[stim.Circuit, stim.CircuitInstruction, stim.CircuitRepeatBlock]) -> None: Applies a circuit or circuit instruction to the simulator's state. The results of any measurements performed can be retrieved using the `get_measurement_flips` method. Args: obj: The circuit or instruction to apply to the simulator's state. Examples: >>> import stim >>> sim = stim.FlipSimulator( ... batch_size=1, ... disable_stabilizer_randomization=True, ... ) >>> circuit = stim.Circuit(''' ... X_ERROR(1) 0 1 3 ... REPEAT 5 { ... H 0 ... C_XYZ 1 ... } ... ''') >>> sim.do(circuit) >>> sim.peek_pauli_flips() [stim.PauliString("+ZZ_X")] >>> sim.do(circuit[0]) >>> sim.peek_pauli_flips() [stim.PauliString("+YY__")] >>> sim.do(circuit[1]) >>> sim.peek_pauli_flips() [stim.PauliString("+YX__")] )DOC") .data()); c.def( "broadcast_pauli_errors", [](FrameSimulator &self, const pybind11::object &pauli, const pybind11::object &mask, float p) { uint8_t pb = pybind11_object_to_pauli_ixyz(pauli); if (!pybind11::isinstance>(mask)) { throw std::invalid_argument("Need isinstance(mask, np.ndarray) and mask.dtype == np.bool_"); } const pybind11::array_t &arr = pybind11::cast>(mask); if (arr.ndim() != 2) { throw std::invalid_argument( "Need a 2d mask (first axis is qubits, second axis is simulation instances). Need len(mask.shape) " "== 2."); } pybind11::ssize_t s_mask_num_qubits = arr.shape(0); pybind11::ssize_t s_mask_batch_size = arr.shape(1); if ((uint64_t)s_mask_batch_size != self.batch_size) { throw std::invalid_argument("Need mask.shape[1] == flip_sim.batch_size"); } if (s_mask_num_qubits > UINT32_MAX) { throw std::invalid_argument("Mask exceeds maximum number of simulated qubits."); } uint32_t mask_num_qubits = (uint32_t)s_mask_num_qubits; uint32_t mask_batch_size = (uint32_t)s_mask_batch_size; self.ensure_safe_to_do_circuit_with_stats(CircuitStats{.num_qubits = mask_num_qubits}); auto u = arr.unchecked<2>(); bool p_x = (0b0110 >> pb) & 1; // parity of 2 bit number bool p_z = pb & 2; if (p != 1 && p != 0) { for (size_t i = 0; i < mask_num_qubits; i++) { biased_randomize_bits( p, self.rng_buffer.u64, self.rng_buffer.u64 + (mask_batch_size / 64), self.rng); for (size_t j = 0; j < mask_batch_size; j++) { bool b = *u.data(i, j); bool r = self.rng_buffer[j]; self.x_table[i][j] ^= b & p_x & r; self.z_table[i][j] ^= b & p_z & r; } } } else { for (size_t i = 0; i < mask_num_qubits; i++) { for (size_t j = 0; j < mask_batch_size; j++) { bool b = *u.data(i, j); self.x_table[i][j] ^= b & p_x; self.z_table[i][j] ^= b & p_z; } } } }, pybind11::kw_only(), pybind11::arg("pauli"), pybind11::arg("mask"), pybind11::arg("p") = 1, clean_doc_string(R"DOC( @signature def broadcast_pauli_errors(self, *, pauli: Union[str, int], mask: np.ndarray, p: float = 1) -> None: Applies a pauli error to all qubits in all instances, filtered by a mask. Args: pauli: The pauli, specified as an integer or string. Uses the convention 0=I, 1=X, 2=Y, 3=Z. Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. mask: A 2d numpy array specifying where to apply errors. The first axis is qubits, the second axis is simulation instances. The first axis can have a length less than the current number of qubits (or more, which adds qubits to the simulation). The length of the second axis must match the simulator's `batch_size`. The array must satisfy mask.dtype == np.bool_ len(mask.shape) == 2 mask.shape[1] == flip_sim.batch_size The error is only applied to qubit q in instance k when mask[q, k] == True. p: Defaults to 1 (no effect). When specified, the error is applied probabilistically instead of deterministically to each (instance, qubit) pair matching the mask. This argument specifies the probability. Examples: >>> import stim >>> import numpy as np >>> sim = stim.FlipSimulator( ... batch_size=2, ... num_qubits=3, ... disable_stabilizer_randomization=True, ... ) >>> sim.broadcast_pauli_errors( ... pauli='X', ... mask=np.asarray([[True, False],[False, False],[True, True]]), ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+X_X"), stim.PauliString("+__X")] >>> sim.broadcast_pauli_errors( ... pauli='Z', ... mask=np.asarray([[False, True],[False, False],[True, True]]), ... ) >>> sim.peek_pauli_flips() [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] )DOC") .data()); c.def( "copy", [](const FrameSimulator &self, bool copy_rng, pybind11::object &seed) { if (copy_rng && !seed.is_none()) { throw std::invalid_argument("seed and copy_rng are incompatible"); } FrameSimulator copy = self; if (!copy_rng || !seed.is_none()) { copy.rng = make_py_seeded_rng(seed); } return copy; }, pybind11::kw_only(), pybind11::arg("copy_rng") = false, pybind11::arg("seed") = pybind11::none(), clean_doc_string(R"DOC( @signature def copy(self, *, copy_rng: bool = False, seed: Optional[int] = None) -> stim.FlipSimulator: Returns a simulator with the same internal state, except perhaps its prng. Args: copy_rng: Defaults to False. When False, the copy's pseudo random number generator is reinitialized with a random seed instead of being a copy of the original simulator's pseudo random number generator. This causes the copy and the original to sample independent randomness, instead of identical randomness, for future random operations. When set to true, the copy will have the exact same pseudo random number generator state as the original, and so will produce identical results if told to do the same noisy operations. This argument is incompatible with the `seed` argument. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng state is either copied from the original simulator or reseeded from system entropy, depending on the copy_rng argument. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: The copy of the simulator. Examples: >>> import stim >>> import numpy as np >>> s1 = stim.FlipSimulator(batch_size=256) >>> s1.set_pauli_flip('X', qubit_index=2, instance_index=3) >>> s2 = s1.copy() >>> s2 is s1 False >>> s2.peek_pauli_flips() == s1.peek_pauli_flips() True >>> s1 = stim.FlipSimulator(batch_size=256) >>> s2 = s1.copy(copy_rng=True) >>> s1.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) >>> s2.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) >>> np.array_equal(s1.get_measurement_flips(), s2.get_measurement_flips()) True )DOC") .data()); c.def( "clear", [](FrameSimulator &self) { self.reset_all(); }, clean_doc_string(R"DOC( Clears the simulator's state, so it can be reused for another simulation. This clears the measurement flip history, clears the detector flip history, and zeroes the observable flip state. It also resets all qubits to |0>. If stabilizer randomization is disabled, this zeros all pauli flip data. Otherwise it randomizes all pauli flips to be I or Z with equal probability. Behind the scenes, this doesn't free memory or resize the simulator. So, repeating the same simulation with calls to `clear` in between will be faster than allocating a new simulator each time (by avoiding re-allocations). Examples: >>> import stim >>> sim = stim.FlipSimulator(batch_size=256) >>> sim.do(stim.Circuit("M(0.1) 9")) >>> sim.num_qubits 10 >>> sim.get_measurement_flips().shape (1, 256) >>> sim.clear() >>> sim.num_qubits 10 >>> sim.get_measurement_flips().shape (0, 256) )DOC") .data()); } ================================================ FILE: src/stim/simulators/frame_simulator.pybind.h ================================================ #ifndef _STIM_SIMULATORS_FRAME_SIMULATOR_PYBIND_H #define _STIM_SIMULATORS_FRAME_SIMULATOR_PYBIND_H #include #include "stim/simulators/frame_simulator.h" namespace stim_pybind { pybind11::class_> pybind_frame_simulator(pybind11::module &m); void pybind_frame_simulator_methods( pybind11::module &m, pybind11::class_> &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/simulators/frame_simulator.test.cc ================================================ // 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. #include "stim/simulators/frame_simulator.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(FrameSimulator, get_set_frame, { CircuitStats circuit_stats; circuit_stats.num_qubits = 6; circuit_stats.max_lookback = 999; FrameSimulator sim(circuit_stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 4, INDEPENDENT_TEST_RNG()); ASSERT_EQ(sim.get_frame(0), PauliString::from_str("______")); ASSERT_EQ(sim.get_frame(1), PauliString::from_str("______")); ASSERT_EQ(sim.get_frame(2), PauliString::from_str("______")); ASSERT_EQ(sim.get_frame(3), PauliString::from_str("______")); sim.set_frame(0, PauliString::from_str("_XYZ__")); ASSERT_EQ(sim.get_frame(0), PauliString::from_str("_XYZ__")); ASSERT_EQ(sim.get_frame(1), PauliString::from_str("______")); ASSERT_EQ(sim.get_frame(2), PauliString::from_str("______")); ASSERT_EQ(sim.get_frame(3), PauliString::from_str("______")); sim.set_frame(3, PauliString::from_str("ZZZZZZ")); ASSERT_EQ(sim.get_frame(0), PauliString::from_str("_XYZ__")); ASSERT_EQ(sim.get_frame(1), PauliString::from_str("______")); ASSERT_EQ(sim.get_frame(2), PauliString::from_str("______")); ASSERT_EQ(sim.get_frame(3), PauliString::from_str("ZZZZZZ")); circuit_stats.num_qubits = 501; circuit_stats.max_lookback = 999; FrameSimulator big_sim( circuit_stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1001, INDEPENDENT_TEST_RNG()); big_sim.set_frame(258, PauliString::from_func(false, 501, [](size_t k) { return "_X"[k == 303]; })); ASSERT_EQ(big_sim.get_frame(258).ref().sparse_str(), "+X303"); ASSERT_EQ(big_sim.get_frame(257).ref().sparse_str(), "+I"); }) template bool is_bulk_frame_operation_consistent_with_tableau(const Gate &gate) { auto tableau = gate.tableau(); CircuitStats circuit_stats; circuit_stats.num_qubits = 500; circuit_stats.max_lookback = 10; size_t num_samples = 1000; FrameSimulator sim(circuit_stats, FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1000, INDEPENDENT_TEST_RNG()); size_t num_targets = tableau.num_qubits; assert(num_targets == 1 || num_targets == 2); std::vector targets{{101}, {403}, {202}, {100}}; while (targets.size() > num_targets) { targets.pop_back(); } auto rng = INDEPENDENT_TEST_RNG(); for (size_t k = 7; k < num_samples; k += 101) { auto test_value = PauliString::random(circuit_stats.num_qubits, rng); PauliStringRef test_value_ref(test_value); sim.set_frame(k, test_value); sim.do_gate({gate.id, {}, targets, ""}); for (size_t k2 = 0; k2 < targets.size(); k2 += num_targets) { size_t target_buf[2]; if (num_targets == 1) { target_buf[0] = targets[k2].data; tableau.apply_within(test_value_ref, {&target_buf[0]}); } else { target_buf[0] = targets[k2].data; target_buf[1] = targets[k2 + 1].data; tableau.apply_within(test_value_ref, {&target_buf[0], &target_buf[2]}); } } test_value.sign = false; if (test_value != sim.get_frame(k)) { return false; } } return true; } TEST_EACH_WORD_SIZE_W(FrameSimulator, bulk_operations_consistent_with_tableau_data, { for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { EXPECT_TRUE(is_bulk_frame_operation_consistent_with_tableau(gate)) << gate.name; } } }) template bool is_output_possible_promising_no_bare_resets(const Circuit &circuit, const simd_bits_range_ref output) { auto tableau_sim = TableauSimulator(INDEPENDENT_TEST_RNG(), circuit.count_qubits()); size_t out_p = 0; bool pass = true; circuit.for_each_operation([&](const CircuitInstruction &op) { if (op.gate_type == GateType::M) { for (auto qf : op.targets) { tableau_sim.sign_bias = output[out_p] ? -1 : +1; tableau_sim.do_MZ(CircuitInstruction{GateType::M, {}, &qf, ""}); if (output[out_p] != tableau_sim.measurement_record.storage.back()) { pass = false; } out_p++; } } else { tableau_sim.do_gate(op); } }); return pass; } TEST_EACH_WORD_SIZE_W(FrameSimulator, test_util_is_output_possible, { auto circuit = Circuit( "H 0\n" "CNOT 0 1\n" "X 0\n" "M 0\n" "M 1\n"); auto data = simd_bits(2); data.u64[0] = 0; ASSERT_EQ(false, is_output_possible_promising_no_bare_resets(circuit, data)); data.u64[0] = 1; ASSERT_EQ(true, is_output_possible_promising_no_bare_resets(circuit, data)); data.u64[0] = 2; ASSERT_EQ(true, is_output_possible_promising_no_bare_resets(circuit, data)); data.u64[0] = 3; ASSERT_EQ(false, is_output_possible_promising_no_bare_resets(circuit, data)); }) template bool is_sim_frame_consistent_with_sim_tableau(const char *program_text) { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(program_text); auto reference_sample = TableauSimulator::reference_sample_circuit(circuit); auto samples = sample_batch_measurements(circuit, reference_sample, 10, rng, true); for (size_t k = 0; k < 10; k++) { simd_bits_range_ref sample = samples[k]; if (!is_output_possible_promising_no_bare_resets(circuit, sample)) { std::cerr << "Impossible output: "; for (size_t k2 = 0; k2 < circuit.count_measurements(); k2++) { std::cerr << '0' + sample[k2]; } std::cerr << "\n"; return false; } } return true; } #define EXPECT_SAMPLES_POSSIBLE(program) EXPECT_TRUE(is_sim_frame_consistent_with_sim_tableau(program)) << program TEST_EACH_WORD_SIZE_W(FrameSimulator, consistency, { EXPECT_SAMPLES_POSSIBLE( "H 0\n" "CNOT 0 1\n" "M 0\n" "M 1\n"); EXPECT_SAMPLES_POSSIBLE( "H 0\n" "H 1\n" "CNOT 0 2\n" "CNOT 1 3\n" "X 1\n" "M 0\n" "M 2\n" "M 3\n" "M 1\n"); EXPECT_SAMPLES_POSSIBLE( "H 0\n" "CNOT 0 1\n" "X 0\n" "M 0\n" "M 1\n"); EXPECT_SAMPLES_POSSIBLE( "H 0\n" "CNOT 0 1\n" "Z 0\n" "M 0\n" "M 1\n"); EXPECT_SAMPLES_POSSIBLE( "H 0\n" "M 0\n" "H 0\n" "M 0\n" "R 0\n" "H 0\n" "M 0\n"); EXPECT_SAMPLES_POSSIBLE( "H 0\n" "CNOT 0 1\n" "M 0\n" "R 0\n" "M 0\n" "M 1\n"); // Distance 2 surface code. EXPECT_SAMPLES_POSSIBLE( "R 0\n" "R 1\n" "R 5\n" "R 6\n" "tick\n" "tick\n" "R 2\n" "R 3\n" "R 4\n" "tick\n" "H 2\n" "H 3\n" "H 4\n" "tick\n" "CZ 3 0\n" "CNOT 4 1\n" "tick\n" "CZ 3 1\n" "CNOT 4 6\n" "tick\n" "CNOT 2 0\n" "CZ 3 5\n" "tick\n" "CNOT 2 5\n" "CZ 3 6\n" "tick\n" "H 2\n" "H 3\n" "H 4\n" "tick\n" "M 2\n" "M 3\n" "M 4\n" "tick\n" "R 2\n" "R 3\n" "R 4\n" "tick\n" "H 2\n" "H 3\n" "H 4\n" "tick\n" "CZ 3 0\n" "CNOT 4 1\n" "tick\n" "CZ 3 1\n" "CNOT 4 6\n" "tick\n" "CNOT 2 0\n" "CZ 3 5\n" "tick\n" "CNOT 2 5\n" "CZ 3 6\n" "tick\n" "H 2\n" "H 3\n" "H 4\n" "tick\n" "M 2\n" "M 3\n" "M 4\n" "tick\n" "tick\n" "M 0\n" "M 1\n" "M 5\n" "M 6"); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, sample_batch_measurements_writing_results_to_disk, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit( "X 0\n" "M 1\n" "M 0\n" "M 2\n" "M 3\n"); auto ref = TableauSimulator::reference_sample_circuit(circuit); auto r = sample_batch_measurements(circuit, ref, 10, rng, true); for (size_t k = 0; k < 10; k++) { ASSERT_EQ(r[k].u64[0], 2); } FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk(circuit, ref, 5, tmp, SampleFormat::SAMPLE_FORMAT_01, rng); ASSERT_EQ(rewind_read_close(tmp), "0100\n0100\n0100\n0100\n0100\n"); tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk(circuit, ref, 5, tmp, SampleFormat::SAMPLE_FORMAT_B8, rng); rewind(tmp); for (size_t k = 0; k < 5; k++) { ASSERT_EQ(getc(tmp), 2); } ASSERT_EQ(getc(tmp), EOF); tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk(circuit, ref, 64, tmp, SampleFormat::SAMPLE_FORMAT_PTB64, rng); rewind(tmp); for (size_t k = 0; k < 8; k++) { ASSERT_EQ(getc(tmp), 0); } for (size_t k = 0; k < 8; k++) { ASSERT_EQ(getc(tmp), 0xFF); } for (size_t k = 0; k < 8; k++) { ASSERT_EQ(getc(tmp), 0); ASSERT_EQ(getc(tmp), 0); } ASSERT_EQ(getc(tmp), EOF); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, big_circuit_measurements, { auto rng = INDEPENDENT_TEST_RNG(); Circuit circuit; for (uint32_t k = 0; k < 1250; k += 3) { circuit.safe_append_u("X", {k}); } for (uint32_t k = 0; k < 1250; k++) { circuit.safe_append_u("M", {k}); } auto ref = TableauSimulator::reference_sample_circuit(circuit); auto r = sample_batch_measurements(circuit, ref, 750, rng, true); for (size_t i = 0; i < 750; i++) { for (size_t k = 0; k < 1250; k++) { ASSERT_EQ(r[i][k], k % 3 == 0) << k; } } FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk(circuit, ref, 750, tmp, SampleFormat::SAMPLE_FORMAT_01, rng); rewind(tmp); for (size_t s = 0; s < 750; s++) { for (size_t k = 0; k < 1250; k++) { ASSERT_EQ(getc(tmp), "01"[k % 3 == 0]); } ASSERT_EQ(getc(tmp), '\n'); } ASSERT_EQ(getc(tmp), EOF); tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk(circuit, ref, 750, tmp, SampleFormat::SAMPLE_FORMAT_B8, rng); rewind(tmp); for (size_t s = 0; s < 750; s++) { for (size_t k = 0; k < 1250; k += 8) { char c = getc(tmp); for (size_t k2 = 0; k + k2 < 1250 && k2 < 8; k2++) { ASSERT_EQ((c >> k2) & 1, (k + k2) % 3 == 0); } } } ASSERT_EQ(getc(tmp), EOF); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, run_length_measurement_formats, { auto rng = INDEPENDENT_TEST_RNG(); Circuit circuit; circuit.safe_append_u("X", {100, 500, 501, 551, 1200}); for (uint32_t k = 0; k < 1250; k++) { circuit.safe_append_u("M", {k}); } auto ref = TableauSimulator::reference_sample_circuit(circuit); FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk(circuit, ref, 3, tmp, SampleFormat::SAMPLE_FORMAT_HITS, rng); ASSERT_EQ(rewind_read_close(tmp), "100,500,501,551,1200\n100,500,501,551,1200\n100,500,501,551,1200\n"); tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk(circuit, ref, 3, tmp, SampleFormat::SAMPLE_FORMAT_DETS, rng); ASSERT_EQ( rewind_read_close(tmp), "shot M100 M500 M501 M551 M1200\nshot M100 M500 M501 M551 M1200\nshot M100 M500 M501 M551 M1200\n"); tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk(circuit, ref, 3, tmp, SampleFormat::SAMPLE_FORMAT_R8, rng); rewind(tmp); for (size_t k = 0; k < 3; k++) { ASSERT_EQ(getc(tmp), 100); ASSERT_EQ(getc(tmp), 255); ASSERT_EQ(getc(tmp), 400 - 255 - 1); ASSERT_EQ(getc(tmp), 0); ASSERT_EQ(getc(tmp), 49); ASSERT_EQ(getc(tmp), 255); ASSERT_EQ(getc(tmp), 255); ASSERT_EQ(getc(tmp), 1200 - 551 - 255 * 2 - 1); ASSERT_EQ(getc(tmp), 1250 - 1200 - 1); } ASSERT_EQ(getc(tmp), EOF); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, big_circuit_random_measurements, { auto rng = INDEPENDENT_TEST_RNG(); Circuit circuit; for (uint32_t k = 0; k < 270; k++) { circuit.safe_append_u("H_XZ", {k}); } for (uint32_t k = 0; k < 270; k++) { circuit.safe_append_u("M", {k}); } auto ref = TableauSimulator::reference_sample_circuit(circuit); auto r = sample_batch_measurements(circuit, ref, 1000, rng, true); for (size_t k = 0; k < 1000; k++) { ASSERT_TRUE(r[k].not_zero()) << k; } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, correlated_error, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits ref(5); simd_bits expected(5); expected.clear(); ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( CORRELATED_ERROR(0) X0 X1 ELSE_CORRELATED_ERROR(0) X1 X2 ELSE_CORRELATED_ERROR(0) X2 X3 M 0 1 2 3 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( CORRELATED_ERROR(1) X0 X1 ELSE_CORRELATED_ERROR(0) X1 X2 ELSE_CORRELATED_ERROR(0) X2 X3 M 0 1 2 3 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[1] = true; expected[2] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( CORRELATED_ERROR(0) X0 X1 ELSE_CORRELATED_ERROR(1) X1 X2 ELSE_CORRELATED_ERROR(0) X2 X3 M 0 1 2 3 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[2] = true; expected[3] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( CORRELATED_ERROR(0) X0 X1 ELSE_CORRELATED_ERROR(0) X1 X2 ELSE_CORRELATED_ERROR(1) X2 X3 M 0 1 2 3 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( CORRELATED_ERROR(1) X0 X1 ELSE_CORRELATED_ERROR(1) X1 X2 ELSE_CORRELATED_ERROR(0) X2 X3 M 0 1 2 3 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( CORRELATED_ERROR(1) X0 X1 ELSE_CORRELATED_ERROR(1) X1 X2 ELSE_CORRELATED_ERROR(1) X2 X3 M 0 1 2 3 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[0] = true; expected[1] = true; expected[3] = true; expected[4] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( CORRELATED_ERROR(1) X0 X1 ELSE_CORRELATED_ERROR(1) X1 X2 ELSE_CORRELATED_ERROR(1) X2 X3 CORRELATED_ERROR(1) X3 X4 M 0 1 2 3 4 )circuit"), ref, 1, rng, true)[0], expected); int hits[3]{}; size_t n = 10000; auto samples = sample_batch_measurements( Circuit(R"circuit( CORRELATED_ERROR(0.5) X0 ELSE_CORRELATED_ERROR(0.25) X1 ELSE_CORRELATED_ERROR(0.75) X2 M 0 1 2 )circuit"), ref, n, rng, true); for (size_t k = 0; k < n; k++) { hits[0] += samples[k][0]; hits[1] += samples[k][1]; hits[2] += samples[k][2]; } ASSERT_TRUE(0.45 * n < hits[0] && hits[0] < 0.55 * n); ASSERT_TRUE((0.125 - 0.05) * n < hits[1] && hits[1] < (0.125 + 0.05) * n); ASSERT_TRUE((0.28125 - 0.05) * n < hits[2] && hits[2] < (0.28125 + 0.05) * n); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, quantum_cannot_control_classical, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits ref(128); // Quantum controlling classical operation is not allowed. ASSERT_THROW( { sample_batch_measurements( Circuit(R"circuit( M 0 CNOT 1 rec[-1] )circuit"), ref, 1, rng, true); }, std::invalid_argument); ASSERT_THROW( { sample_batch_measurements( Circuit(R"circuit( M 0 CY 1 rec[-1] )circuit"), ref, 1, rng, true); }, std::invalid_argument); ASSERT_THROW( { sample_batch_measurements( Circuit(R"circuit( M 0 YCZ rec[-1] 1 )circuit"), ref, 1, rng, true); }, std::invalid_argument); ASSERT_THROW( { sample_batch_measurements( Circuit(R"circuit( M 0 XCZ rec[-1] 1 )circuit"), ref, 1, rng, true); }, std::invalid_argument); ASSERT_THROW( { sample_batch_measurements( Circuit(R"circuit( M 0 SWAP 1 rec[-1] )circuit"), ref, 1, rng, true); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, classical_can_control_quantum, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits ref(128); simd_bits expected(5); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( X_ERROR(1) 0 M !0 CX rec[-1] 1 M 1 )circuit"), ref, 1, rng, true)[0], expected); ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( X_ERROR(1) 0 M !0 CY rec[-1] 1 M 1 )circuit"), ref, 1, rng, true)[0], expected); ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( X_ERROR(1) 0 M !0 XCZ 1 rec[-1] M 1 )circuit"), ref, 1, rng, true)[0], expected); ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( X_ERROR(1) 0 M !0 YCZ 1 rec[-1] M 1 )circuit"), ref, 1, rng, true)[0], expected); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, classical_controls, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits ref(128); simd_bits expected(5); expected.clear(); ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( M 0 CX rec[-1] 1 M 1 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( M !0 CX rec[-1] 1 M 1 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( X_ERROR(1) 0 M !0 CX rec[-1] 1 M 1 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( X_ERROR(1) 0 M 0 CX rec[-1] 1 M 1 )circuit"), ref, 1, rng, true)[0], expected); auto r = sample_batch_measurements( Circuit(R"circuit( X_ERROR(0.5) 0 M 0 CX rec[-1] 1 M 1 )circuit"), ref, 1000, rng, true); size_t hits = 0; for (size_t k = 0; k < 1000; k++) { ASSERT_EQ(r[k][0], r[k][1]); hits += r[k][0]; } ASSERT_TRUE(400 < hits && hits < 600); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( X_ERROR(1) 0 M 0 H 1 CZ rec[-1] 1 H 1 M 1 )circuit"), ref, 1, rng, true)[0], expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( sample_batch_measurements( Circuit(R"circuit( X_ERROR(1) 0 M 0 CY rec[-1] 1 M 1 )circuit"), ref, 1, rng, true)[0], expected); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, record_gets_trimmed, { Circuit c = Circuit("M 0 1 2 3 4 5 6 7 8 9"); FrameSimulator sim( c.compute_stats(), FrameSimulatorMode::STREAM_MEASUREMENTS_TO_DISK, 768, INDEPENDENT_TEST_RNG()); MeasureRecordBatchWriter b(tmpfile(), 768, SampleFormat::SAMPLE_FORMAT_B8); for (size_t k = 0; k < 1000; k++) { sim.do_MZ(c.operations[0]); sim.m_record.intermediate_write_unwritten_results_to(b, simd_bits(0)); ASSERT_LT(sim.m_record.storage.num_major_bits_padded(), 2500); } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, stream_huge_case, { auto rng = INDEPENDENT_TEST_RNG(); FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk( Circuit(R"CIRCUIT( X_ERROR(1) 2 REPEAT 100000 { M 0 1 2 3 } )CIRCUIT"), simd_bits(0), 256, tmp, SampleFormat::SAMPLE_FORMAT_B8, rng); rewind(tmp); for (size_t k = 0; k < 256 * 100000 * 4 / 8; k++) { ASSERT_EQ(getc(tmp), 0x44); } ASSERT_EQ(getc(tmp), EOF); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, block_results_single_shot, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"circuit( REPEAT 10000 { X_ERROR(1) 0 MR 0 M 0 0 } )circuit"); FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk( circuit, simd_bits(0), 3, tmp, SampleFormat::SAMPLE_FORMAT_01, rng); auto result = rewind_read_close(tmp); for (size_t k = 0; k < 30000; k += 3) { ASSERT_EQ(result[k], '1') << k; ASSERT_EQ(result[k + 1], '0') << (k + 1); ASSERT_EQ(result[k + 2], '0') << (k + 2); } ASSERT_EQ(result[30000], '\n'); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, block_results_triple_shot, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"circuit( REPEAT 10000 { X_ERROR(1) 0 MR 0 M 0 0 } )circuit"); FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk( circuit, simd_bits(0), 3, tmp, SampleFormat::SAMPLE_FORMAT_01, rng); auto result = rewind_read_close(tmp); for (size_t rep = 0; rep < 3; rep++) { size_t s = rep * 30001; for (size_t k = 0; k < 30000; k += 3) { ASSERT_EQ(result[s + k], '1') << (s + k); ASSERT_EQ(result[s + k + 1], '0') << (s + k + 1); ASSERT_EQ(result[s + k + 2], '0') << (s + k + 2); } ASSERT_EQ(result[s + 30000], '\n'); } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, stream_results, { auto rng = INDEPENDENT_TEST_RNG(); DebugForceResultStreamingRaii force_streaming; auto circuit = Circuit(R"circuit( REPEAT 10000 { X_ERROR(1) 0 MR 0 M 0 0 } )circuit"); FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk( circuit, simd_bits(0), 3, tmp, SampleFormat::SAMPLE_FORMAT_01, rng); auto result = rewind_read_close(tmp); for (size_t k = 0; k < 30000; k += 3) { ASSERT_EQ(result[k], '1') << k; ASSERT_EQ(result[k + 1], '0') << (k + 1); ASSERT_EQ(result[k + 2], '0') << (k + 2); } ASSERT_EQ(result[30000], '\n'); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, stream_many_shots, { auto rng = INDEPENDENT_TEST_RNG(); DebugForceResultStreamingRaii force_streaming; auto circuit = Circuit(R"circuit( X_ERROR(1) 1 M 0 1 2 )circuit"); FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk( circuit, simd_bits(0), 2049, tmp, SampleFormat::SAMPLE_FORMAT_01, rng); auto result = rewind_read_close(tmp); ASSERT_EQ(result.size(), 2049 * 4); for (size_t k = 0; k < 2049 * 4; k += 4) { ASSERT_EQ(result[k], '0') << k; ASSERT_EQ(result[k + 1], '1') << (k + 1); ASSERT_EQ(result[k + 2], '0') << (k + 2); ASSERT_EQ(result[k + 3], '\n') << (k + 3); } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, stream_results_triple_shot, { auto rng = INDEPENDENT_TEST_RNG(); DebugForceResultStreamingRaii force_streaming; auto circuit = Circuit(R"circuit( REPEAT 10000 { X_ERROR(1) 0 MR 0 M 0 0 } )circuit"); FILE *tmp = tmpfile(); sample_batch_measurements_writing_results_to_disk( circuit, simd_bits(0), 3, tmp, SampleFormat::SAMPLE_FORMAT_01, rng); auto result = rewind_read_close(tmp); for (size_t rep = 0; rep < 3; rep++) { size_t s = rep * 30001; for (size_t k = 0; k < 30000; k += 3) { ASSERT_EQ(result[s + k], '1') << (s + k); ASSERT_EQ(result[s + k + 1], '0') << (s + k + 1); ASSERT_EQ(result[s + k + 2], '0') << (s + k + 2); } ASSERT_EQ(result[s + 30000], '\n'); } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, measure_y_without_reset_doesnt_reset, { auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_batch_measurements( Circuit(R"CIRCUIT( RY 0 MY 0 MY 0 Z_ERROR(1) 0 MY 0 MY 0 Z_ERROR(1) 0 MY 0 MY 0 )CIRCUIT"), simd_bits(0), 10000, rng, false); ASSERT_EQ(r[0].popcnt(), 0); ASSERT_EQ(r[1].popcnt(), 0); ASSERT_EQ(r[2].popcnt(), 10000); ASSERT_EQ(r[3].popcnt(), 10000); ASSERT_EQ(r[4].popcnt(), 0); ASSERT_EQ(r[5].popcnt(), 0); r = sample_batch_measurements( Circuit(R"CIRCUIT( RY 0 MRY 0 MRY 0 Z_ERROR(1) 0 MRY 0 MRY 0 Z_ERROR(1) 0 MRY 0 MRY 0 )CIRCUIT"), simd_bits(0), 10000, rng, false); ASSERT_EQ(r[0].popcnt(), 0); ASSERT_EQ(r[1].popcnt(), 0); ASSERT_EQ(r[2].popcnt(), 10000); ASSERT_EQ(r[3].popcnt(), 0); ASSERT_EQ(r[4].popcnt(), 10000); ASSERT_EQ(r[5].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, resets_vs_measurements, { auto rng = INDEPENDENT_TEST_RNG(); auto check = [&](const char *circuit, std::vector results) { simd_bits ref(results.size()); for (size_t k = 0; k < results.size(); k++) { ref[k] = results[k]; } simd_bit_table t = sample_batch_measurements(Circuit(circuit), ref, 100, rng, true); return !t.data.not_zero(); }; ASSERT_TRUE(check( R"circuit( RX 0 RY 1 RZ 2 H_XZ 0 H_YZ 1 M 0 1 2 )circuit", { false, false, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 1 2 H_YZ 3 4 5 X_ERROR(1) 0 3 6 Y_ERROR(1) 1 4 7 Z_ERROR(1) 2 5 8 MX 0 1 2 MY 3 4 5 MZ 6 7 8 )circuit", { false, true, true, true, false, true, true, true, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 1 2 H_YZ 3 4 5 X_ERROR(1) 0 3 6 Y_ERROR(1) 1 4 7 Z_ERROR(1) 2 5 8 MX !0 !1 !2 MY !3 !4 !5 MZ !6 !7 !8 )circuit", { false, true, true, true, false, true, true, true, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 1 2 H_YZ 3 4 5 X_ERROR(1) 0 3 6 Y_ERROR(1) 1 4 7 Z_ERROR(1) 2 5 8 MRX 0 1 2 MRY 3 4 5 MRZ 6 7 8 H_XZ 0 H_YZ 3 M 0 3 6 )circuit", { false, true, true, true, false, true, true, true, false, false, false, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 1 2 H_YZ 3 4 5 X_ERROR(1) 0 3 6 Y_ERROR(1) 1 4 7 Z_ERROR(1) 2 5 8 MRX !0 !1 !2 MRY !3 !4 !5 MRZ !6 !7 !8 H_XZ 0 H_YZ 3 M 0 3 6 )circuit", { false, true, true, true, false, true, true, true, false, false, false, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 H_YZ 1 Z_ERROR(1) 0 1 X_ERROR(1) 2 MRX 0 0 MRY 1 1 MRZ 2 2 )circuit", { true, false, true, false, true, false, })); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, noisy_measurement_x, { auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_batch_measurements( Circuit(R"CIRCUIT( RX 0 MX(0.05) 0 MX 0 )CIRCUIT"), simd_bits(0), 10000, rng, false); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_batch_measurements( Circuit(R"CIRCUIT( RX 0 1 Z_ERROR(1) 0 1 MX(0.05) 0 1 MX 0 1 )CIRCUIT"), simd_bits(0), 5000, rng, false); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 5000); ASSERT_EQ(r[3].popcnt(), 5000); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, noisy_measurement_y, { auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_batch_measurements( Circuit(R"CIRCUIT( RY 0 MY(0.05) 0 MY 0 )CIRCUIT"), simd_bits(0), 10000, rng, false); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_batch_measurements( Circuit(R"CIRCUIT( RY 0 1 Z_ERROR(1) 0 1 MY(0.05) 0 1 MY 0 1 )CIRCUIT"), simd_bits(0), 5000, rng, false); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 5000); ASSERT_EQ(r[3].popcnt(), 5000); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, noisy_measurement_z, { auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_batch_measurements( Circuit(R"CIRCUIT( RZ 0 MZ(0.05) 0 MZ 0 )CIRCUIT"), simd_bits(0), 10000, rng, false); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_batch_measurements( Circuit(R"CIRCUIT( RZ 0 1 X_ERROR(1) 0 1 MZ(0.05) 0 1 MZ 0 1 )CIRCUIT"), simd_bits(0), 5000, rng, false); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 5000); ASSERT_EQ(r[3].popcnt(), 5000); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, noisy_measurement_reset_x, { auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_batch_measurements( Circuit(R"CIRCUIT( RX 0 MRX(0.05) 0 MRX 0 )CIRCUIT"), simd_bits(0), 10000, rng, false); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_batch_measurements( Circuit(R"CIRCUIT( RX 0 1 Z_ERROR(1) 0 1 MRX(0.05) 0 1 MRX 0 1 )CIRCUIT"), simd_bits(0), 5000, rng, false); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 0); ASSERT_EQ(r[3].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, noisy_measurement_reset_y, { auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_batch_measurements( Circuit(R"CIRCUIT( RY 0 MRY(0.05) 0 MRY 0 )CIRCUIT"), simd_bits(0), 10000, rng, false); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_batch_measurements( Circuit(R"CIRCUIT( RY 0 1 Z_ERROR(1) 0 1 MRY(0.05) 0 1 MRY 0 1 )CIRCUIT"), simd_bits(0), 5000, rng, false); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 0); ASSERT_EQ(r[3].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, noisy_measurement_reset_z, { auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_batch_measurements( Circuit(R"CIRCUIT( RZ 0 MRZ(0.05) 0 MRZ 0 )CIRCUIT"), simd_bits(0), 10000, rng, false); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_batch_measurements( Circuit(R"CIRCUIT( RZ 0 1 X_ERROR(1) 0 1 MRZ(0.05) 0 1 MRZ 0 1 )CIRCUIT"), simd_bits(0), 5000, rng, false); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 0); ASSERT_EQ(r[3].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, measure_pauli_product_4body, { auto rng = INDEPENDENT_TEST_RNG(); auto r = sample_batch_measurements( Circuit(R"CIRCUIT( X_ERROR(0.5) 0 1 2 3 Z_ERROR(0.5) 0 1 2 3 MPP X0*X1*X2*X3 MX 0 1 2 3 4 5 MPP X2*X3*X4*X5 MPP Z0*Z1*Z4*Z5 !Y0*Y1*Y4*Y5 )CIRCUIT"), simd_bits(0), 10, rng, false); for (size_t k = 0; k < 10; k++) { auto x0123 = r[0][k]; auto x0 = r[1][k]; auto x1 = r[2][k]; auto x2 = r[3][k]; auto x3 = r[4][k]; auto x4 = r[5][k]; auto x5 = r[6][k]; auto x2345 = r[7][k]; auto mz0145 = r[8][k]; auto y0145 = r[9][k]; ASSERT_EQ(x0123, x0 ^ x1 ^ x2 ^ x3); ASSERT_EQ(x2345, x2 ^ x3 ^ x4 ^ x5); ASSERT_EQ(y0145 ^ mz0145, x0123 ^ x2345); } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, non_deterministic_pauli_product_detectors, { auto rng = INDEPENDENT_TEST_RNG(); auto n = sample_batch_measurements( Circuit(R"CIRCUIT( MPP Z8*X9 DETECTOR rec[-1] )CIRCUIT"), simd_bits(0), 1000, rng, false)[0] .popcnt(); ASSERT_TRUE(400 < n && n < 600); n = sample_batch_measurements( Circuit(R"CIRCUIT( MPP X9 DETECTOR rec[-1] )CIRCUIT"), simd_bits(0), 1000, rng, false)[0] .popcnt(); ASSERT_TRUE(400 < n && n < 600); n = sample_batch_measurements( Circuit(R"CIRCUIT( MX 9 DETECTOR rec[-1] )CIRCUIT"), simd_bits(0), 1000, rng, false)[0] .popcnt(); ASSERT_TRUE(400 < n && n < 600); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, ignores_sweep_controls_when_given_no_sweep_data, { auto rng = INDEPENDENT_TEST_RNG(); auto n = sample_batch_measurements( Circuit(R"CIRCUIT( CNOT sweep[0] 0 M 0 DETECTOR rec[-1] )CIRCUIT"), simd_bits(0), 1000, rng, false)[0] .popcnt(); ASSERT_EQ(n, 0); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, reconfigure_for, { auto circuit = Circuit(R"CIRCUIT( X_ERROR(1) 0 M 0 DETECTOR rec[-1] )CIRCUIT"); FrameSimulator frame_sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 0, INDEPENDENT_TEST_RNG()); frame_sim.configure_for(circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 256); frame_sim.reset_all(); frame_sim.do_circuit(circuit); ASSERT_EQ(frame_sim.det_record.storage[0].popcnt(), 256); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, mpad, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"CIRCUIT( MPAD 0 1 )CIRCUIT"); auto sample = sample_batch_measurements(circuit, TableauSimulator::reference_sample_circuit(circuit), 100, rng, false); for (size_t k = 0; k < 100; k++) { ASSERT_EQ(sample[0][k], false); ASSERT_EQ(sample[1][k], true); } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, mxxyyzz_basis, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"CIRCUIT( RX 0 1 MXX 0 1 RY 0 1 MYY 0 1 RZ 0 1 MZZ 0 1 )CIRCUIT"); auto sample = sample_batch_measurements(circuit, TableauSimulator::reference_sample_circuit(circuit), 100, rng, false); for (size_t k = 0; k < 100; k++) { ASSERT_EQ(sample[0][k], false); ASSERT_EQ(sample[1][k], false); ASSERT_EQ(sample[2][k], false); } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, mxxyyzz_inversion, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"CIRCUIT( MXX 0 1 0 !1 !0 1 !0 !1 MYY 0 1 0 !1 !0 1 !0 !1 MZZ 0 1 0 !1 !0 1 !0 !1 )CIRCUIT"); auto sample = sample_batch_measurements(circuit, TableauSimulator::reference_sample_circuit(circuit), 100, rng, false); for (size_t k = 0; k < 100; k++) { ASSERT_EQ(sample[1][k], !sample[0][k]); ASSERT_EQ(sample[2][k], !sample[0][k]); ASSERT_EQ(sample[3][k], sample[0][k]); ASSERT_EQ(sample[5][k], !sample[4][k]); ASSERT_EQ(sample[6][k], !sample[4][k]); ASSERT_EQ(sample[7][k], sample[4][k]); ASSERT_EQ(sample[8][k], 1 ^ sample[0][k] ^ sample[4][k]); ASSERT_EQ(sample[9][k], !sample[8][k]); ASSERT_EQ(sample[10][k], !sample[8][k]); ASSERT_EQ(sample[11][k], sample[8][k]); } }) TEST_EACH_WORD_SIZE_W(FrameSimulator, runs_on_general_circuit, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = generate_test_circuit_with_all_operations(); auto sample = sample_batch_measurements(circuit, TableauSimulator::reference_sample_circuit(circuit), 100, rng, false); ASSERT_GT(sample.num_simd_words_minor, 0); ASSERT_GT(sample.num_simd_words_major, 0); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, heralded_erase_detect_statistics, { auto circuit = Circuit(R"CIRCUIT( MXX 0 1 MZZ 0 1 HERALDED_ERASE(0.1) 0 MXX 0 1 MZZ 0 1 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] )CIRCUIT"); size_t n; std::array bins{}; FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1024, INDEPENDENT_TEST_RNG()); for (n = 0; n < 1024 * 256; n += 1024) { sim.reset_all(); sim.do_circuit(circuit); auto sample = sim.det_record.storage.transposed(); for (size_t k = 0; k < 1024; k++) { bins[sample[k].u8[0]]++; } } EXPECT_NEAR(bins[0] / (double)n, 0.9, 0.05); EXPECT_EQ(bins[1], 0); EXPECT_EQ(bins[2], 0); EXPECT_EQ(bins[3], 0); EXPECT_NEAR(bins[4] / (double)n, 0.025, 0.02); EXPECT_NEAR(bins[5] / (double)n, 0.025, 0.02); EXPECT_NEAR(bins[6] / (double)n, 0.025, 0.02); EXPECT_NEAR(bins[7] / (double)n, 0.025, 0.02); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, heralded_pauli_channel_1_statistics, { auto circuit = Circuit(R"CIRCUIT( MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0.05, 0.10, 0.15, 0.25) 0 MXX 0 1 MZZ 0 1 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] )CIRCUIT"); size_t n; std::array bins{}; FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1024, INDEPENDENT_TEST_RNG()); for (n = 0; n < 1024 * 256; n += 1024) { sim.reset_all(); sim.do_circuit(circuit); auto sample = sim.det_record.storage.transposed(); for (size_t k = 0; k < 1024; k++) { bins[sample[k].u8[0]]++; } } EXPECT_NEAR(bins[0] / (double)n, 0.45, 0.05); EXPECT_EQ(bins[1], 0); EXPECT_EQ(bins[2], 0); EXPECT_EQ(bins[3], 0); EXPECT_NEAR(bins[4] / (double)n, 0.05, 0.04); EXPECT_NEAR(bins[5] / (double)n, 0.10, 0.04); EXPECT_NEAR(bins[6] / (double)n, 0.25, 0.04); EXPECT_NEAR(bins[7] / (double)n, 0.15, 0.04); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, heralded_erase_statistics_offset_by_2, { auto circuit = Circuit(R"CIRCUIT( MXX 2 3 MZZ 2 3 HERALDED_ERASE(0.1) 2 MXX 2 3 MZZ 2 3 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] )CIRCUIT"); size_t n; std::array bins{}; FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1024, INDEPENDENT_TEST_RNG()); for (n = 0; n < 1024 * 256; n += 1024) { sim.reset_all(); sim.do_circuit(circuit); auto sample = sim.det_record.storage.transposed(); for (size_t k = 0; k < 1024; k++) { bins[sample[k].u8[0]]++; } } EXPECT_NEAR(bins[0] / (double)n, 0.9, 0.05); EXPECT_EQ(bins[1], 0); EXPECT_EQ(bins[2], 0); EXPECT_EQ(bins[3], 0); EXPECT_NEAR(bins[4] / (double)n, 0.025, 0.02); EXPECT_NEAR(bins[5] / (double)n, 0.025, 0.02); EXPECT_NEAR(bins[6] / (double)n, 0.025, 0.02); EXPECT_NEAR(bins[7] / (double)n, 0.025, 0.02); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, heralded_pauli_channel_1_statistics_offset_by_2, { auto circuit = Circuit(R"CIRCUIT( MXX 2 3 MZZ 2 3 HERALDED_PAULI_CHANNEL_1(0.05, 0.10, 0.15, 0.25) 2 MXX 2 3 MZZ 2 3 DETECTOR rec[-1] rec[-4] DETECTOR rec[-2] rec[-5] DETECTOR rec[-3] )CIRCUIT"); size_t n; std::array bins{}; FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1024, INDEPENDENT_TEST_RNG()); for (n = 0; n < 1024 * 256; n += 1024) { sim.reset_all(); sim.do_circuit(circuit); auto sample = sim.det_record.storage.transposed(); for (size_t k = 0; k < 1024; k++) { bins[sample[k].u8[0]]++; } } EXPECT_NEAR(bins[0] / (double)n, 0.45, 0.05); EXPECT_EQ(bins[1], 0); EXPECT_EQ(bins[2], 0); EXPECT_EQ(bins[3], 0); EXPECT_NEAR(bins[4] / (double)n, 0.05, 0.04); EXPECT_NEAR(bins[5] / (double)n, 0.10, 0.04); EXPECT_NEAR(bins[6] / (double)n, 0.25, 0.04); EXPECT_NEAR(bins[7] / (double)n, 0.15, 0.04); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, observable_include_paulis_rz, { auto circuit = Circuit(R"CIRCUIT( RZ 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 )CIRCUIT"); FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1024, INDEPENDENT_TEST_RNG()); sim.reset_all(); sim.do_circuit(circuit); auto x0 = sim.obs_record[0].popcnt(); auto y0 = sim.obs_record[1].popcnt(); auto z0 = sim.obs_record[2].popcnt(); ASSERT_EQ(x0, y0); ASSERT_GT(x0, 300); ASSERT_LT(x0, 700); ASSERT_EQ(z0, 0); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, observable_include_paulis_ry, { auto circuit = Circuit(R"CIRCUIT( RY 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 )CIRCUIT"); FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1024, INDEPENDENT_TEST_RNG()); sim.reset_all(); sim.do_circuit(circuit); auto x0 = sim.obs_record[0].popcnt(); auto y0 = sim.obs_record[1].popcnt(); auto z0 = sim.obs_record[2].popcnt(); ASSERT_EQ(x0, z0); ASSERT_GT(x0, 300); ASSERT_LT(x0, 700); ASSERT_EQ(y0, 0); }) TEST_EACH_WORD_SIZE_W(FrameSimulator, observable_include_paulis_rx, { auto circuit = Circuit(R"CIRCUIT( RX 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 )CIRCUIT"); FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, 1024, INDEPENDENT_TEST_RNG()); sim.reset_all(); sim.do_circuit(circuit); auto x0 = sim.obs_record[0].popcnt(); auto y0 = sim.obs_record[1].popcnt(); auto z0 = sim.obs_record[2].popcnt(); ASSERT_EQ(y0, z0); ASSERT_GT(y0, 300); ASSERT_LT(y0, 700); ASSERT_EQ(x0, 0); }) ================================================ FILE: src/stim/simulators/frame_simulator_pybind_test.py ================================================ import collections import pytest import stim import numpy as np def test_get_measurement_flips(): s = stim.FlipSimulator(batch_size=11) assert s.num_measurements == 0 assert s.num_qubits == 0 assert s.batch_size == 11 s.do(stim.Circuit(""" X_ERROR(1) 100 M 100 """)) assert s.num_measurements == 1 assert s.num_qubits == 101 assert s.batch_size == 11 m = s.get_measurement_flips(record_index=0) np.testing.assert_array_equal(m, [True] * 11) assert s.num_measurements == 1 assert s.num_qubits == 101 assert s.batch_size == 11 def test_stabilizer_randomization(): s = stim.FlipSimulator( batch_size=256, num_qubits=10, disable_stabilizer_randomization=True, ) assert s.peek_pauli_flips() == [stim.PauliString(10)] * 256 s.do(stim.Circuit("R 19")) assert s.peek_pauli_flips() == [stim.PauliString(20)] * 256 s = stim.FlipSimulator( batch_size=256, num_qubits=10, ) v = np.array([list(p) for p in s.peek_pauli_flips()], dtype=np.uint8) assert v.shape == (256, 10) assert np.all((v == 0) | (v == 3)) assert 0.2 < np.count_nonzero(v == 3) / (256 * 10) < 0.8 s = stim.FlipSimulator( batch_size=256, num_qubits=10, disable_stabilizer_randomization=False, ) v = np.array([list(p) for p in s.peek_pauli_flips()], dtype=np.uint8) assert v.shape == (256, 10) assert np.all((v == 0) | (v == 3)) assert 0.2 < np.count_nonzero(v == 3) / (256 * 10) < 0.8 s.do(stim.Circuit("R 19")) v = np.array([list(p) for p in s.peek_pauli_flips()], dtype=np.uint8) assert v.shape == (256, 20) assert np.all((v == 0) | (v == 3)) assert 0.2 < np.count_nonzero(v == 3) / (256 * 20) < 0.8 def test_get_detector_flips(): s = stim.FlipSimulator(batch_size=11) assert s.num_measurements == 0 assert s.num_qubits == 0 assert s.batch_size == 11 s.do(stim.Circuit(""" X_ERROR(1) 25 M 24 25 DETECTOR rec[-1] DETECTOR rec[-1] DETECTOR rec[-1] """)) assert s.num_measurements == 2 assert s.num_detectors == 3 assert s.num_qubits == 26 assert s.batch_size == 11 np.testing.assert_array_equal( s.get_detector_flips(), np.ones(shape=(3, 11), dtype=np.bool_)) np.testing.assert_array_equal( s.get_detector_flips(detector_index=1), np.ones(shape=(11,), dtype=np.bool_)) np.testing.assert_array_equal( s.get_detector_flips(instance_index=1), np.ones(shape=(3,), dtype=np.bool_)) assert s.get_detector_flips(detector_index=1, instance_index=1) def test_get_observable_flips(): s = stim.FlipSimulator(batch_size=11) assert s.num_measurements == 0 assert s.num_qubits == 0 assert s.batch_size == 11 assert s.num_observables == 0 s.do(stim.Circuit(""" X_ERROR(1) 25 M 24 25 OBSERVABLE_INCLUDE(2) rec[-1] """)) assert s.num_measurements == 2 assert s.num_detectors == 0 assert s.num_observables == 3 assert s.num_qubits == 26 assert s.batch_size == 11 np.testing.assert_array_equal( s.get_observable_flips(observable_index=1), np.zeros(shape=(11,), dtype=np.bool_)) np.testing.assert_array_equal( s.get_observable_flips(observable_index=2), np.ones(shape=(11,), dtype=np.bool_)) np.testing.assert_array_equal( s.get_observable_flips(instance_index=1), [False, False, True]) assert not s.get_observable_flips(observable_index=1, instance_index=1) assert s.get_observable_flips(observable_index=2, instance_index=1) def test_peek_pauli_flips(): sim = stim.FlipSimulator(batch_size=500, disable_stabilizer_randomization=True) sim.do(stim.Circuit(""" X_ERROR(0.3) 1 Y_ERROR(0.3) 2 Z_ERROR(0.3) 3 DEPOLARIZE1(0.3) 4 """)) assert sim.num_qubits == 5 assert sim.batch_size == 500 flips = sim.peek_pauli_flips() assert len(flips) == 500 assert len(flips[0]) == 5 v0 = collections.Counter([p[0] for p in flips]) v1 = collections.Counter([p[1] for p in flips]) v2 = collections.Counter([p[2] for p in flips]) v3 = collections.Counter([p[3] for p in flips]) v4 = collections.Counter([p[4] for p in flips]) assert v0.keys() == {0} assert v1.keys() == {0, 1} assert v2.keys() == {0, 2} assert v3.keys() == {0, 3} assert v4.keys() == {0, 1, 2, 3} assert v0[0] == 500 assert 250 < v1[0] < 450 assert 250 < v2[0] < 450 assert 250 < v3[0] < 450 assert 250 < v4[0] < 450 def test_set_pauli_flip(): sim = stim.FlipSimulator( batch_size=2, disable_stabilizer_randomization=True, num_qubits=3, ) assert sim.peek_pauli_flips() == [ stim.PauliString('___'), stim.PauliString('___'), ] sim.set_pauli_flip('X', qubit_index=2, instance_index=0) assert sim.peek_pauli_flips() == [ stim.PauliString('__X'), stim.PauliString('___'), ] sim.set_pauli_flip(3, qubit_index=1, instance_index=1) assert sim.peek_pauli_flips() == [ stim.PauliString('__X'), stim.PauliString('_Z_'), ] sim.set_pauli_flip(2, qubit_index=0, instance_index=1) assert sim.peek_pauli_flips() == [ stim.PauliString('__X'), stim.PauliString('YZ_'), ] sim.set_pauli_flip(1, qubit_index=0, instance_index=-1) assert sim.peek_pauli_flips() == [ stim.PauliString('__X'), stim.PauliString('XZ_'), ] sim.set_pauli_flip(0, qubit_index=2, instance_index=-2) assert sim.peek_pauli_flips() == [ stim.PauliString('___'), stim.PauliString('XZ_'), ] with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip(-1, qubit_index=0, instance_index=0) with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip(4, qubit_index=0, instance_index=0) with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip('R', qubit_index=0, instance_index=0) with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip('XY', qubit_index=0, instance_index=0) with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip(object(), qubit_index=0, instance_index=0) with pytest.raises(IndexError, match='instance_index'): sim.set_pauli_flip('X', qubit_index=0, instance_index=-3) with pytest.raises(IndexError, match='instance_index'): sim.set_pauli_flip('X', qubit_index=0, instance_index=3) with pytest.raises(IndexError, match='qubit_index'): sim.set_pauli_flip('X', qubit_index=-1, instance_index=0) sim.set_pauli_flip('X', qubit_index=4, instance_index=0) assert sim.peek_pauli_flips() == [ stim.PauliString('____X'), stim.PauliString('XZ___'), ] def test_broadcast_pauli_errors(): sim = stim.FlipSimulator( batch_size=2, num_qubits=3, disable_stabilizer_randomization=True, ) sim.broadcast_pauli_errors( pauli='X', mask=np.asarray([ [True, False], [False, False], [True, True]] ), ) peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+X_X"), stim.PauliString("+__X") ] sim.broadcast_pauli_errors( pauli='Z', mask=np.asarray([ [True, True], [True, False], [False, False]] ), ) peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+YZX"), stim.PauliString("+Z_X") ] sim.broadcast_pauli_errors( pauli='Y', mask=np.asarray([ [True, False], [False, True], [False, True]] ), ) peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+_ZX"), stim.PauliString("+ZYZ") ] sim.broadcast_pauli_errors( pauli='I', mask=np.asarray([ [True, True], [False, True], [True, True]] ), ) peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+_ZX"), stim.PauliString("+ZYZ") ] # do it again with ints sim = stim.FlipSimulator( batch_size=2, num_qubits=3, disable_stabilizer_randomization=True, ) sim.broadcast_pauli_errors( pauli=1, mask=np.asarray([ [True, False], [False, False], [True, True]] ), ) peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+X_X"), stim.PauliString("+__X") ] sim.broadcast_pauli_errors( pauli=3, mask=np.asarray([ [True, True], [True, False], [False, False]] ), ) peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+YZX"), stim.PauliString("+Z_X") ] sim.broadcast_pauli_errors( pauli=2, mask=np.asarray([ [True, False], [False, True], [False, True]] ), ) peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+_ZX"), stim.PauliString("+ZYZ") ] sim.broadcast_pauli_errors( pauli=0, mask=np.asarray([ [True, True], [False, True], [True, True]] ), ) peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+_ZX"), stim.PauliString("+ZYZ") ] with pytest.raises(ValueError, match='pauli'): sim.broadcast_pauli_errors( pauli='whoops', mask=np.asarray([ [True, True], [False, True], [True, True]] ), ) with pytest.raises(ValueError, match='pauli'): sim.broadcast_pauli_errors( pauli=4, mask=np.asarray([ [True, True], [False, True], [True, True]] ), ) with pytest.raises(ValueError, match='batch_size'): sim.broadcast_pauli_errors( pauli='X', mask=np.asarray([ [True, True,True], [False, True, True], [True, True, True]] ), ) with pytest.raises(ValueError, match='batch_size'): sim.broadcast_pauli_errors( pauli='X', mask=np.asarray([ [True], [False], [True]] ), ) sim = stim.FlipSimulator( batch_size=2, num_qubits=3, disable_stabilizer_randomization=True, ) sim.broadcast_pauli_errors( pauli='X', mask=np.asarray([ [True, False], [False, False], [True, True], [True, True]] ), ) # automatically expands the qubit basis peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+X_XX"), stim.PauliString("+__XX") ] sim.broadcast_pauli_errors( pauli='X', mask=np.asarray([ [True, False], [False, False], ] ), ) # tolerates fewer qubits in mask than in simulator peek = sim.peek_pauli_flips() assert peek == [ stim.PauliString("+__XX"), stim.PauliString("+__XX") ] def test_repro_heralded_pauli_channel_1_bug(): circuit = stim.Circuit(""" R 0 1 HERALDED_PAULI_CHANNEL_1(0.2, 0.2, 0, 0) 1 M 0 """) result = circuit.compile_sampler().sample(1024) assert np.sum(result[:, 0]) > 0 assert np.sum(result[:, 1]) == 0 def test_to_numpy(): sim = stim.FlipSimulator(batch_size=50) sim.do(stim.Circuit.generated( "surface_code:rotated_memory_x", distance=5, rounds=3, after_clifford_depolarization=0.1, )) xs0, zs0, ms0, ds0, os0 = sim.to_numpy( output_xs=True, output_zs=True, output_measure_flips=True, output_detector_flips=True, output_observable_flips=True, ) for k in range(50): np.testing.assert_array_equal(xs0[:, k], sim.peek_pauli_flips()[k].to_numpy()[0]) np.testing.assert_array_equal(zs0[:, k], sim.peek_pauli_flips()[k].to_numpy()[1]) np.testing.assert_array_equal(ms0[:, k], sim.get_measurement_flips(instance_index=k)) np.testing.assert_array_equal(ds0[:, k], sim.get_detector_flips(instance_index=k)) np.testing.assert_array_equal(os0[:, k], sim.get_observable_flips(instance_index=k)) xs, zs, ms, ds, os = sim.to_numpy(output_xs=True) np.testing.assert_array_equal(xs, xs0) assert zs is None assert ms is None assert ds is None assert os is None xs, zs, ms, ds, os = sim.to_numpy(output_zs=True) assert xs is None np.testing.assert_array_equal(zs, zs0) assert ms is None assert ds is None assert os is None xs, zs, ms, ds, os = sim.to_numpy(output_measure_flips=True) assert xs is None assert zs is None np.testing.assert_array_equal(ms, ms0) assert ds is None assert os is None xs, zs, ms, ds, os = sim.to_numpy(output_detector_flips=True) assert xs is None assert zs is None assert ms is None np.testing.assert_array_equal(ds, ds0) assert os is None xs, zs, ms, ds, os = sim.to_numpy(output_observable_flips=True) assert xs is None assert zs is None assert ms is None assert ds is None np.testing.assert_array_equal(os, os0) xs1 = np.empty_like(xs0) zs1 = np.empty_like(zs0) ms1 = np.empty_like(ms0) ds1 = np.empty_like(ds0) os1 = np.empty_like(os0) xs2, zs2, ms2, ds2, os2 = sim.to_numpy( output_xs=xs1, output_zs=zs1, output_measure_flips=ms1, output_detector_flips=ds1, output_observable_flips=os1, ) assert xs1 is xs2 assert zs1 is zs2 assert ms1 is ms2 assert ds1 is ds2 assert os1 is os2 np.testing.assert_array_equal(xs1, xs0) np.testing.assert_array_equal(zs1, zs0) np.testing.assert_array_equal(ms1, ms0) np.testing.assert_array_equal(ds1, ds0) np.testing.assert_array_equal(os1, os0) xs2, zs2, ms2, ds2, os2 = sim.to_numpy( transpose=True, output_xs=True, output_zs=True, output_measure_flips=True, output_detector_flips=True, output_observable_flips=True, ) np.testing.assert_array_equal(xs2, np.transpose(xs0)) np.testing.assert_array_equal(zs2, np.transpose(zs0)) np.testing.assert_array_equal(ms2, np.transpose(ms0)) np.testing.assert_array_equal(ds2, np.transpose(ds0)) np.testing.assert_array_equal(os2, np.transpose(os0)) xs2, zs2, ms2, ds2, os2 = sim.to_numpy( bit_packed=True, output_xs=True, output_zs=True, output_measure_flips=True, output_detector_flips=True, output_observable_flips=True, ) np.testing.assert_array_equal(xs2, np.packbits(xs0, axis=1, bitorder='little')) np.testing.assert_array_equal(zs2, np.packbits(zs0, axis=1, bitorder='little')) np.testing.assert_array_equal(ms2, np.packbits(ms0, axis=1, bitorder='little')) np.testing.assert_array_equal(ds2, np.packbits(ds0, axis=1, bitorder='little')) np.testing.assert_array_equal(os2, np.packbits(os0, axis=1, bitorder='little')) xs2, zs2, ms2, ds2, os2 = sim.to_numpy( transpose=True, bit_packed=True, output_xs=True, output_zs=True, output_measure_flips=True, output_detector_flips=True, output_observable_flips=True, ) np.testing.assert_array_equal(xs2, np.packbits(np.transpose(xs0), axis=1, bitorder='little')) np.testing.assert_array_equal(zs2, np.packbits(np.transpose(zs0), axis=1, bitorder='little')) np.testing.assert_array_equal(ms2, np.packbits(np.transpose(ms0), axis=1, bitorder='little')) np.testing.assert_array_equal(ds2, np.packbits(np.transpose(ds0), axis=1, bitorder='little')) np.testing.assert_array_equal(os2, np.packbits(np.transpose(os0), axis=1, bitorder='little')) with pytest.raises(ValueError, match="at least one output"): sim.to_numpy() with pytest.raises(ValueError, match="shape="): sim.to_numpy(output_xs=np.empty(shape=(0, 0), dtype=np.uint64)) with pytest.raises(ValueError, match="shape="): sim.to_numpy(output_zs=np.empty(shape=(0, 0), dtype=np.uint64)) with pytest.raises(ValueError, match="shape="): sim.to_numpy(output_measure_flips=np.empty(shape=(0, 0), dtype=np.uint64)) with pytest.raises(ValueError, match="shape="): sim.to_numpy(output_detector_flips=np.empty(shape=(0, 0), dtype=np.uint64)) with pytest.raises(ValueError, match="shape="): sim.to_numpy(output_observable_flips=np.empty(shape=(0, 0), dtype=np.uint64)) def test_generate_bernoulli_samples(): sim = stim.FlipSimulator(batch_size=10) v = sim.generate_bernoulli_samples(1001, p=0, bit_packed=False) assert v.shape == (1001,) assert v.dtype == np.bool_ assert np.sum(v) == 0 v2 = sim.generate_bernoulli_samples(1001, p=1, bit_packed=False, out=v) assert v is v2 assert v.shape == (1001,) assert v.dtype == np.bool_ assert np.sum(v) == 1001 v = sim.generate_bernoulli_samples(2**16, p=0.25, bit_packed=False) assert abs(np.sum(v) - 2**16*0.25) < 2**12 v = sim.generate_bernoulli_samples(1001, p=0, bit_packed=True) assert v.shape == (126,) assert v.dtype == np.uint8 assert np.sum(np.unpackbits(v, count=1001, bitorder='little')) == 0 assert np.sum(np.unpackbits(v, count=1008, bitorder='little')) == 0 v2 = sim.generate_bernoulli_samples(1001, p=1, bit_packed=True, out=v) assert v is v2 assert v.shape == (126,) assert v.dtype == np.uint8 assert np.sum(np.unpackbits(v, count=1001, bitorder='little')) == 1001 assert np.sum(np.unpackbits(v, count=1008, bitorder='little')) == 1001 v = sim.generate_bernoulli_samples(256, p=0, bit_packed=True) assert np.all(v == 0) sim.generate_bernoulli_samples(256 - 101, p=1, bit_packed=True, out=v[1:-11]) for k in v: print(k) assert np.all(v[1:-12] == 0xFF) assert v[-12] == 7 assert np.all(v[-11:] == 0) assert np.all(v[:1] == 0) v = sim.generate_bernoulli_samples(2**16, p=0.25, bit_packed=True) assert abs(np.sum(np.unpackbits(v, count=2**16)) - 2**16*0.25) < 2**12 v[:] = 0 sim.generate_bernoulli_samples(2**16 - 1, p=1, bit_packed=True, out=v) assert np.all(v[:-1] == 0xFF) assert v[-1] == 0x7F v[:] = 0 sim.generate_bernoulli_samples(2**15, p=1, bit_packed=True, out=v[::2]) assert np.all(v[0::2] == 0xFF) assert np.all(v[1::2] == 0) v[:] = 0 sim.generate_bernoulli_samples(2**15 - 1, p=1, bit_packed=True, out=v[::2]) assert np.all(v[0::2][:-1] == 0xFF) assert v[0::2][-1] == 0x7F assert np.all(v[1::2] == 0) def test_get_measurement_flips_negative_index(): sim = stim.FlipSimulator(batch_size=8, disable_stabilizer_randomization=True) sim.do(stim.Circuit(""" X_ERROR(1) 1 M 0 1 """)) np.testing.assert_array_equal(sim.get_measurement_flips(record_index=-2), [False] * 8) np.testing.assert_array_equal(sim.get_measurement_flips(record_index=-1), [True] * 8) np.testing.assert_array_equal(sim.get_measurement_flips(record_index=0), [False] * 8) np.testing.assert_array_equal(sim.get_measurement_flips(record_index=1), [True] * 8) ================================================ FILE: src/stim/simulators/frame_simulator_util.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_DETECTION_SIMULATOR_H #define _STIM_SIMULATORS_DETECTION_SIMULATOR_H #include #include "stim/circuit/circuit.h" #include "stim/io/stim_data_formats.h" #include "stim/mem/simd_bit_table.h" namespace stim { /// A convenience method for batch sampling detection events from a circuit. /// /// Uses the frame simulator. /// /// This method is intentionally inefficient to make it easier to use. In particular, it /// assumes the circuit is small enough that it's reasonable to simply store the /// detection event data of all samples in memory simultaneously. It also assumes that it's /// acceptable to recreate a fresh FrameSimulator with all its various buffers each time the /// method is called. /// /// Args: /// circuit: The circuit to sample. /// num_shots: The number of samples to take. /// rng: Random number generator to use. /// /// Returns: /// A pair of simd_bit_tables. The first is the detection event data. The second is the /// observable data. Each table is arranged as follows: /// major axis (first index): detector index (or observable index) /// minor axis (second index): shot index template std::pair, simd_bit_table> sample_batch_detection_events( const Circuit &circuit, size_t num_shots, std::mt19937_64 &rng); /// Samples detection events from a circuit and writes them to a file. /// /// Uses the frame simulator. /// /// Args: /// circuit: The circuit to sample. /// num_shots: The number of samples to take. /// prepend_observables: Include the observables in the output, before the detectors. /// append_observables: Include the observables in the output, after the detectors. /// out: The file to write the result data to. /// format: The format to use when encoding the data into the file. /// rng: Random number generator to use. /// obs_out: An optional secondary file to write observable data to. Set to nullptr to /// not use. /// obs_out_format: The format to use when writing to the secondary file. template void sample_batch_detection_events_writing_results_to_disk( const Circuit &circuit, size_t num_shots, bool prepend_observables, bool append_observables, FILE *out, SampleFormat format, std::mt19937_64 &rng, FILE *obs_out, SampleFormat obs_out_format); /// A convenience method for batch sampling measurements from a circuit. /// /// Uses the frame simulator. /// /// This method is intentionally inefficient to make it easier to use. In particular, it /// assumes the circuit is small enough that it's reasonable to simply store the /// measurement of all samples in memory simultaneously. It also assumes that it's /// acceptable to recreate a fresh FrameSimulator with all its various buffers each time the /// method is called. /// /// Args: /// circuit: The circuit to sample. /// num_shots: The number of samples to take. /// rng: Random number generator to use. /// transposed: Whether or not to exchange the axes of the resulting table. /// /// Returns: /// A simd_bit_table containing the sampled measurement data. /// If not transposed: /// major axis (first index): measurement index /// minor axis (second index): shot index /// If transposed: /// major axis (first index): shot index /// minor axis (second index): measurement index template simd_bit_table sample_batch_measurements( const Circuit &circuit, const simd_bits &reference_sample, size_t num_samples, std::mt19937_64 &rng, bool transposed); /// Samples measurements from a circuit and writes them to a file. /// /// Uses the frame simulator. /// /// Args: /// circuit: The circuit to sample. /// num_shots: The number of samples to take. /// reference_sample: A noiseless sample from the circuit, acquired via other means /// (for example, via TableauSimulator::reference_sample_circuit). /// out: The file to write the result data to. /// format: The format to use when encoding the data into the file. /// rng: Random number generator to use. template void sample_batch_measurements_writing_results_to_disk( const Circuit &circuit, const simd_bits &reference_sample, uint64_t num_shots, FILE *out, SampleFormat format, std::mt19937_64 &rng); } // namespace stim #include "stim/simulators/frame_simulator_util.inl" #endif ================================================ FILE: src/stim/simulators/frame_simulator_util.inl ================================================ // 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. #include "stim/simulators/force_streaming.h" #include "stim/simulators/frame_simulator.h" #include "stim/simulators/frame_simulator_util.h" namespace stim { template std::pair, simd_bit_table> sample_batch_detection_events( const Circuit &circuit, size_t num_shots, std::mt19937_64 &rng) { FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, num_shots, std::move(rng)); sim.reset_all(); sim.do_circuit(circuit); rng = std::move( sim.rng); // Update input rng as if it was used directly, by moving the updated state out of the simulator. return std::pair, simd_bit_table>{ std::move(sim.det_record.storage), std::move(sim.obs_record), }; } template void rerun_frame_sim_while_streaming_dets_to_disk( const Circuit &circuit, CircuitStats circuit_stats, FrameSimulator &sim, size_t num_shots, bool prepend_observables, bool append_observables, FILE *out, SampleFormat format, FILE *obs_out, SampleFormat obs_out_format) { if (prepend_observables) { throw std::invalid_argument( "--prepend_observables isn't supported when sampling circuits so large that they require streaming the " "results"); } MeasureRecordBatchWriter writer(out, num_shots, format); std::vector> observables; sim.reset_all(); writer.begin_result_type('D'); circuit.for_each_operation([&](const CircuitInstruction &op) { sim.do_gate(op); sim.m_record.mark_all_as_written(); if (op.gate_type == GateType::DETECTOR) { constexpr size_t WRITE_SIZE = 256; if (sim.det_record.unwritten >= WRITE_SIZE) { assert(sim.det_record.stored == WRITE_SIZE); assert(sim.det_record.unwritten == WRITE_SIZE); writer.batch_write_bytes(sim.det_record.storage, WRITE_SIZE >> 6); sim.det_record.clear(); } } }); for (size_t k = sim.det_record.stored - sim.det_record.unwritten; k < sim.det_record.stored; k++) { writer.batch_write_bit(sim.det_record.storage[k]); } if (append_observables) { writer.begin_result_type('L'); for (size_t k = 0; k < circuit_stats.num_observables; k++) { writer.batch_write_bit(sim.obs_record[k]); } } writer.write_end(); if (obs_out != nullptr) { write_table_data( obs_out, num_shots, circuit_stats.num_observables, simd_bits(0), sim.obs_record, obs_out_format, 'L', 'L', circuit_stats.num_observables); } } template void rerun_frame_sim_while_streaming_measurements_to_disk( const Circuit &circuit, FrameSimulator &sim, const simd_bits &reference_sample, size_t num_shots, FILE *out, SampleFormat format) { MeasureRecordBatchWriter writer(out, num_shots, format); sim.reset_all(); circuit.for_each_operation([&](const CircuitInstruction &op) { sim.do_gate(op); sim.m_record.intermediate_write_unwritten_results_to(writer, reference_sample); }); sim.m_record.final_write_unwritten_results_to(writer, reference_sample); } template void rerun_frame_sim_in_memory_and_write_dets_to_disk( const Circuit &circuit, const CircuitStats &circuit_stats, FrameSimulator &frame_sim, simd_bit_table &out_concat_buf, size_t num_shots, bool prepend_observables, bool append_observables, FILE *out, SampleFormat format, FILE *obs_out, SampleFormat obs_out_format) { if (prepend_observables + append_observables + (obs_out != nullptr) > 1) { throw std::out_of_range("Can't combine --prepend_observables, --append_observables, or --obs_out"); } frame_sim.reset_all(); frame_sim.do_circuit(circuit); const auto &obs_data = frame_sim.obs_record; const auto &det_data = frame_sim.det_record.storage; if (obs_out != nullptr) { write_table_data( obs_out, num_shots, circuit_stats.num_observables, simd_bits(0), frame_sim.obs_record, obs_out_format, 'L', 'L', circuit_stats.num_observables); } if (prepend_observables || append_observables) { if (prepend_observables) { assert(!append_observables); out_concat_buf.overwrite_major_range_with( circuit_stats.num_observables, det_data, 0, circuit_stats.num_detectors); out_concat_buf.overwrite_major_range_with(0, obs_data, 0, circuit_stats.num_observables); } else { assert(append_observables); out_concat_buf.overwrite_major_range_with(0, det_data, 0, circuit_stats.num_detectors); out_concat_buf.overwrite_major_range_with( circuit_stats.num_detectors, obs_data, 0, circuit_stats.num_observables); } char c1 = append_observables ? 'D' : 'L'; char c2 = append_observables ? 'L' : 'D'; size_t ct = append_observables ? circuit_stats.num_detectors : circuit_stats.num_observables; write_table_data( out, num_shots, circuit_stats.num_observables + circuit_stats.num_detectors, simd_bits(0), out_concat_buf, format, c1, c2, ct); } else { write_table_data( out, num_shots, circuit_stats.num_detectors, simd_bits(0), det_data, format, 'D', 'L', circuit_stats.num_detectors); } } template void rerun_frame_sim_in_memory_and_write_measurements_to_disk( const Circuit &circuit, CircuitStats circuit_stats, FrameSimulator &frame_sim, const simd_bits &reference_sample, size_t num_shots, FILE *out, SampleFormat format) { frame_sim.reset_all(); frame_sim.do_circuit(circuit); const auto &measure_data = frame_sim.m_record.storage; write_table_data( out, num_shots, circuit_stats.num_measurements, reference_sample, measure_data, format, 'M', 'M', 0); } template void sample_batch_detection_events_writing_results_to_disk( const Circuit &circuit, size_t num_shots, bool prepend_observables, bool append_observables, FILE *out, SampleFormat format, std::mt19937_64 &rng, FILE *obs_out, SampleFormat obs_out_format) { if (num_shots == 0) { // Vacuously complete. return; } auto stats = circuit.compute_stats(); // Pick a batch size that's not so large that it would cause memory issues. size_t batch_size = 0; while (batch_size < 1024 && batch_size < num_shots) { batch_size += W; } uint64_t memory_per_full_shot = 2 * stats.num_qubits + 2 * stats.max_lookback + stats.num_observables + stats.num_detectors; while (batch_size > 0 && should_use_streaming_because_bit_count_is_too_large_to_store(memory_per_full_shot * batch_size)) { batch_size -= W; } // If the batch size ended up at 0, the results won't fit in memory. Need to stream. bool streaming = batch_size == 0; if (streaming) { batch_size = W; } // Create a correctly sized frame simulator. FrameSimulator frame_sim( stats, streaming ? FrameSimulatorMode::STREAM_DETECTIONS_TO_DISK : FrameSimulatorMode::STORE_DETECTIONS_TO_MEMORY, batch_size, std::move(rng)); // Will copy rng state back out later. // Run the frame simulator until as many shots as requested have been written. simd_bit_table out_concat_buf(0, 0); if (append_observables || prepend_observables) { out_concat_buf = simd_bit_table(stats.num_detectors + stats.num_observables, batch_size); } size_t shots_left = num_shots; while (shots_left) { size_t shots_performed = std::min(shots_left, batch_size); if (streaming) { rerun_frame_sim_while_streaming_dets_to_disk( circuit, stats, frame_sim, shots_performed, prepend_observables, append_observables, out, format, obs_out, obs_out_format); } else { rerun_frame_sim_in_memory_and_write_dets_to_disk( circuit, stats, frame_sim, out_concat_buf, shots_performed, prepend_observables, append_observables, out, format, obs_out, obs_out_format); } shots_left -= shots_performed; } // Update input rng as if it was used directly, by moving the updated state out of the simulator. rng = std::move(frame_sim.rng); } template simd_bit_table sample_batch_measurements( const Circuit &circuit, const simd_bits &reference_sample, size_t num_samples, std::mt19937_64 &rng, bool transposed) { FrameSimulator sim( circuit.compute_stats(), FrameSimulatorMode::STORE_MEASUREMENTS_TO_MEMORY, num_samples, std::move(rng)); sim.reset_all(); sim.do_circuit(circuit); simd_bit_table result = std::move(sim.m_record.storage); rng = std::move( sim.rng); // Update input rng as if it was used directly, by moving the updated state out of the simulator. if (reference_sample.not_zero()) { result = transposed_vs_ref(num_samples, result, reference_sample); transposed = !transposed; } if (transposed) { result = result.transposed(); } return result; } template void sample_batch_measurements_writing_results_to_disk( const Circuit &circuit, const simd_bits &reference_sample, uint64_t num_shots, FILE *out, SampleFormat format, std::mt19937_64 &rng) { if (num_shots == 0) { // Vacuously complete. return; } auto stats = circuit.compute_stats(); // Pick a batch size that's not so large that it would cause memory issues. size_t batch_size = 0; while (batch_size < 1024 && batch_size < num_shots) { batch_size += W; } uint64_t memory_per_full_shot = 2 * stats.num_qubits + stats.num_measurements; while (batch_size > 0 && should_use_streaming_because_bit_count_is_too_large_to_store(memory_per_full_shot * batch_size)) { batch_size -= W; } // If the batch size ended up at 0, the results won't fit in memory. Need to stream. bool streaming = batch_size == 0; if (streaming) { batch_size = W; } // Create a correctly sized frame simulator. FrameSimulator frame_sim( circuit.compute_stats(), streaming ? FrameSimulatorMode::STREAM_MEASUREMENTS_TO_DISK : FrameSimulatorMode::STORE_MEASUREMENTS_TO_MEMORY, batch_size, std::move(rng)); // Temporarily move rng into simulator. // Run the frame simulator until as many shots as requested have been written. size_t shots_left = num_shots; while (shots_left) { size_t shots_performed = std::min(shots_left, batch_size); if (streaming) { rerun_frame_sim_while_streaming_measurements_to_disk( circuit, frame_sim, reference_sample, shots_performed, out, format); } else { rerun_frame_sim_in_memory_and_write_measurements_to_disk( circuit, stats, frame_sim, reference_sample, shots_performed, out, format); } shots_left -= shots_performed; } // Update input rng as if it was used directly, by moving the updated state out of the simulator. rng = std::move(frame_sim.rng); } } // namespace stim ================================================ FILE: src/stim/simulators/frame_simulator_util.test.cc ================================================ // 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. #include "stim/simulators/frame_simulator_util.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/frame_simulator.h" #include "stim/util_bot/test_util.test.h" using namespace stim; template simd_bit_table sample_test_detection_events(const Circuit &circuit, size_t num_shots) { auto rng = INDEPENDENT_TEST_RNG(); return sample_batch_detection_events(circuit, num_shots, rng).first; } TEST_EACH_WORD_SIZE_W(DetectionSimulator, sample_test_detection_events, { auto circuit = Circuit( "X_ERROR(1) 0\n" "M 0\n" "DETECTOR rec[-1]\n"); auto samples = sample_test_detection_events(circuit, 5); ASSERT_EQ( samples.str(5), "11111\n" ".....\n" ".....\n" ".....\n" "....."); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, bad_detector, { ASSERT_THROW({ sample_test_detection_events(Circuit("rec[-1]"), 5); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, sample_batch_detection_events_writing_results_to_disk, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"circuit( X_ERROR(1) 0 M 0 1 DETECTOR rec[-1] DETECTOR rec[-2] OBSERVABLE_INCLUDE(4) rec[-2] )circuit"); FILE *tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 2, false, false, tmp, SampleFormat::SAMPLE_FORMAT_DETS, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); ASSERT_EQ(rewind_read_close(tmp), "shot D1\nshot D1\n"); tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 2, true, false, tmp, SampleFormat::SAMPLE_FORMAT_DETS, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); ASSERT_EQ(rewind_read_close(tmp), "shot L4 D1\nshot L4 D1\n"); tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 2, false, true, tmp, SampleFormat::SAMPLE_FORMAT_DETS, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); ASSERT_EQ(rewind_read_close(tmp), "shot D1 L4\nshot D1 L4\n"); tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 2, false, true, tmp, SampleFormat::SAMPLE_FORMAT_HITS, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); ASSERT_EQ(rewind_read_close(tmp), "1,6\n1,6\n"); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, stream_many_shots, { auto rng = INDEPENDENT_TEST_RNG(); DebugForceResultStreamingRaii force_streaming; auto circuit = Circuit(R"circuit( X_ERROR(1) 1 M 0 1 2 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] )circuit"); FILE *tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 2048, false, false, tmp, SampleFormat::SAMPLE_FORMAT_01, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); auto result = rewind_read_close(tmp); for (size_t k = 0; k < 2048 * 4; k += 4) { ASSERT_EQ(result[k], '0') << k; ASSERT_EQ(result[k + 1], '1') << (k + 1); ASSERT_EQ(result[k + 2], '0') << (k + 2); ASSERT_EQ(result[k + 3], '\n') << (k + 3); } }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, block_results_single_shot, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"circuit( REPEAT 10000 { M 0 X_ERROR(1) 0 M 0 R 0 DETECTOR rec[-1] rec[-2] M 0 DETECTOR rec[-1] DETECTOR rec[-1] } )circuit"); FILE *tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 1, false, true, tmp, SampleFormat::SAMPLE_FORMAT_01, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); auto result = rewind_read_close(tmp); for (size_t k = 0; k < 30000; k += 3) { ASSERT_EQ(result[k], '1') << k; ASSERT_EQ(result[k + 1], '0') << (k + 1); ASSERT_EQ(result[k + 2], '0') << (k + 2); } ASSERT_EQ(result[30000], '\n'); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, block_results_triple_shot, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"circuit( REPEAT 10000 { M 0 X_ERROR(1) 0 M 0 R 0 DETECTOR rec[-1] rec[-2] M 0 DETECTOR rec[-1] DETECTOR rec[-1] } )circuit"); FILE *tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 3, false, true, tmp, SampleFormat::SAMPLE_FORMAT_01, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); auto result = rewind_read_close(tmp); for (size_t rep = 0; rep < 3; rep++) { size_t s = rep * 30001; for (size_t k = 0; k < 30000; k += 3) { ASSERT_EQ(result[s + k], '1') << (s + k); ASSERT_EQ(result[s + k + 1], '0') << (s + k + 1); ASSERT_EQ(result[s + k + 2], '0') << (s + k + 2); } ASSERT_EQ(result[s + 30000], '\n'); } }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, stream_results, { auto rng = INDEPENDENT_TEST_RNG(); DebugForceResultStreamingRaii force_streaming; auto circuit = Circuit(R"circuit( REPEAT 10000 { M 0 X_ERROR(1) 0 M 0 R 0 DETECTOR rec[-1] rec[-2] M 0 DETECTOR rec[-1] DETECTOR rec[-1] } )circuit"); RaiiTempNamedFile tmp; FILE *f = fopen(tmp.path.c_str(), "w"); sample_batch_detection_events_writing_results_to_disk( circuit, 1, false, true, f, SampleFormat::SAMPLE_FORMAT_01, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); fclose(f); auto result = tmp.read_contents(); for (size_t k = 0; k < 30000; k += 3) { ASSERT_EQ(result[k], '1') << k; ASSERT_EQ(result[k + 1], '0') << (k + 1); ASSERT_EQ(result[k + 2], '0') << (k + 2); } ASSERT_EQ(result[30000], '\n'); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, stream_results_triple_shot, { auto rng = INDEPENDENT_TEST_RNG(); DebugForceResultStreamingRaii force_streaming; auto circuit = Circuit(R"circuit( REPEAT 10000 { M 0 X_ERROR(1) 0 M 0 R 0 DETECTOR rec[-1] rec[-2] M 0 DETECTOR rec[-1] DETECTOR rec[-1] } )circuit"); FILE *tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 3, false, true, tmp, SampleFormat::SAMPLE_FORMAT_01, rng, nullptr, SampleFormat::SAMPLE_FORMAT_01); auto result = rewind_read_close(tmp); for (size_t rep = 0; rep < 3; rep++) { size_t s = rep * 30001; for (size_t k = 0; k < 30000; k += 3) { ASSERT_EQ(result[s + k], '1') << (s + k); ASSERT_EQ(result[s + k + 1], '0') << (s + k + 1); ASSERT_EQ(result[s + k + 2], '0') << (s + k + 2); } ASSERT_EQ(result[s + 30000], '\n'); } }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, noisy_measurement_x, { auto r = sample_test_detection_events( Circuit(R"CIRCUIT( RX 0 MX(0.05) 0 MX 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 10000); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_test_detection_events( Circuit(R"CIRCUIT( RX 0 1 Z_ERROR(1) 0 1 MX(0.05) 0 1 MX 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 5000); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 5000); ASSERT_EQ(r[3].popcnt(), 5000); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, noisy_measurement_y, { auto r = sample_test_detection_events( Circuit(R"CIRCUIT( RY 0 MY(0.05) 0 MY 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 10000); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_test_detection_events( Circuit(R"CIRCUIT( RY 0 1 Z_ERROR(1) 0 1 MY(0.05) 0 1 MY 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 5000); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 5000); ASSERT_EQ(r[3].popcnt(), 5000); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, noisy_measurement_z, { auto r = sample_test_detection_events( Circuit(R"CIRCUIT( RZ 0 MZ(0.05) 0 MZ 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 10000); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_test_detection_events( Circuit(R"CIRCUIT( RZ 0 1 X_ERROR(1) 0 1 MZ(0.05) 0 1 MZ 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 5000); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 5000); ASSERT_EQ(r[3].popcnt(), 5000); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, noisy_measure_reset_x, { auto r = sample_test_detection_events( Circuit(R"CIRCUIT( RX 0 MRX(0.05) 0 MRX 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 10000); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_test_detection_events( Circuit(R"CIRCUIT( RX 0 1 Z_ERROR(1) 0 1 MRX(0.05) 0 1 MRX 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 5000); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 0); ASSERT_EQ(r[3].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, noisy_measure_reset_y, { auto r = sample_test_detection_events( Circuit(R"CIRCUIT( RY 0 MRY(0.05) 0 MRY 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 10000); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_test_detection_events( Circuit(R"CIRCUIT( RY 0 1 Z_ERROR(1) 0 1 MRY(0.05) 0 1 MRY 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 5000); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 0); ASSERT_EQ(r[3].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, noisy_measure_reset_z, { auto r = sample_test_detection_events( Circuit(R"CIRCUIT( RZ 0 MRZ(0.05) 0 MRZ 0 DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 10000); ASSERT_FALSE(r[1].not_zero()); auto m1 = r[0].popcnt(); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); r = sample_test_detection_events( Circuit(R"CIRCUIT( RZ 0 1 X_ERROR(1) 0 1 MRZ(0.05) 0 1 MRZ 0 1 DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), 5000); auto m2 = r[0].popcnt() + r[1].popcnt(); ASSERT_LT(m2, 10000 - 300); ASSERT_GT(m2, 10000 - 700); ASSERT_EQ(r[2].popcnt(), 0); ASSERT_EQ(r[3].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(DetectionSimulator, obs_data, { auto rng = INDEPENDENT_TEST_RNG(); auto circuit = Circuit(R"circuit( REPEAT 399 { X_ERROR(1) 0 MR 0 DETECTOR rec[-1] } REPEAT 600 { MR 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] } X_ERROR(1) 0 MR 0 OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(1) rec[-1] MR 0 OBSERVABLE_INCLUDE(2) rec[-1] )circuit"); FILE *det_tmp = tmpfile(); FILE *obs_tmp = tmpfile(); sample_batch_detection_events_writing_results_to_disk( circuit, 1001, false, false, det_tmp, SampleFormat::SAMPLE_FORMAT_B8, rng, obs_tmp, SampleFormat::SAMPLE_FORMAT_B8); auto det_saved = rewind_read_close(det_tmp); auto obs_saved = rewind_read_close(obs_tmp); ASSERT_EQ(det_saved.size(), 125 * 1001); ASSERT_EQ(obs_saved.size(), 1 * 1001); for (size_t k = 0; k < det_saved.size(); k++) { for (size_t b = 0; b < 8; b++) { size_t det_index = (k % 125) * 8 + b; bool bit = (((uint8_t)det_saved[k]) >> b) & 1; ASSERT_EQ(bit, det_index < 399) << k; } } for (size_t k = 0; k < obs_saved.size(); k++) { ASSERT_EQ(obs_saved[k], 0x3); } }) ================================================ FILE: src/stim/simulators/graph_simulator.cc ================================================ #include "stim/simulators/graph_simulator.h" using namespace stim; GraphSimulator::GraphSimulator(size_t num_qubits) : num_qubits(num_qubits), adj(num_qubits, num_qubits), paulis(num_qubits), x2outs(num_qubits), z2outs(num_qubits) { for (size_t k = 0; k < num_qubits; k++) { x2outs.zs[k] = 1; z2outs.xs[k] = 1; } } void GraphSimulator::do_1q_gate(GateType gate, size_t qubit) { GateTarget t = GateTarget::qubit(qubit); x2outs.ref().do_instruction(CircuitInstruction{gate, {}, &t, ""}); z2outs.ref().do_instruction(CircuitInstruction{gate, {}, &t, ""}); paulis.xs[qubit] ^= z2outs.sign; paulis.zs[qubit] ^= x2outs.sign; x2outs.sign = 0; z2outs.sign = 0; } std::tuple GraphSimulator::after2inside_basis_transform(size_t qubit, bool x, bool z) { bool xx = x2outs.xs[qubit]; bool xz = x2outs.zs[qubit]; bool zx = z2outs.xs[qubit]; bool zz = z2outs.zs[qubit]; bool out_x = (x & zz) ^ (z & zx); bool out_z = (x & xz) ^ (z & xx); bool sign = false; sign ^= paulis.xs[qubit] & out_z; sign ^= paulis.zs[qubit] & out_x; sign ^= out_x == out_z && !(xx ^ zz) && !(xx ^ xz ^ zx); return {out_x, out_z, sign}; } void GraphSimulator::inside_do_cz(size_t a, size_t b) { adj[a][b] ^= 1; adj[b][a] ^= 1; } void GraphSimulator::inside_do_cx(size_t c, size_t t) { adj[c] ^= adj[t]; for (size_t k = 0; k < num_qubits; k++) { adj[k][c] = adj[c][k]; } paulis.zs[c] ^= adj[c][c]; adj[c][c] = 0; } void GraphSimulator::inside_do_sqrt_z(size_t q) { bool x2x = x2outs.xs[q]; bool x2z = x2outs.zs[q]; bool z2x = z2outs.xs[q]; bool z2z = z2outs.zs[q]; paulis.zs[q] ^= paulis.xs[q]; paulis.zs[q] ^= !(x2x ^ z2z) && !(x2x ^ x2z ^ z2x); x2outs.xs[q] ^= z2x; x2outs.zs[q] ^= z2z; } void GraphSimulator::inside_do_sqrt_x_dag(size_t q) { bool x2x = x2outs.xs[q]; bool x2z = x2outs.zs[q]; bool z2x = z2outs.xs[q]; bool z2z = z2outs.zs[q]; paulis.xs[q] ^= paulis.zs[q]; paulis.xs[q] ^= !(x2x ^ z2z) && !(x2x ^ x2z ^ z2x); z2outs.xs[q] ^= x2x; z2outs.zs[q] ^= x2z; } void GraphSimulator::inside_do_cy(size_t c, size_t t) { inside_do_cz(c, t); inside_do_cx(c, t); inside_do_sqrt_z(c); } void GraphSimulator::verify_invariants() const { // No self-adjacency. for (size_t q = 0; q < num_qubits; q++) { assert(!adj[q][q]); } // Undirected adjacency. for (size_t q1 = 0; q1 < num_qubits; q1++) { for (size_t q2 = q1 + 1; q2 < num_qubits; q2++) { assert(adj[q1][q2] == adj[q2][q1]); } } // Single qubits gates are clifford. for (size_t q = 0; q < num_qubits; q++) { assert(x2outs.xs[q] || x2outs.zs[q]); assert(z2outs.xs[q] || z2outs.zs[q]); assert((x2outs.xs[q] != z2outs.xs[q]) || (x2outs.zs[q] != z2outs.zs[q])); } } void GraphSimulator::do_complementation(size_t q) { buffer.clear(); for (size_t neighbor = 0; neighbor < num_qubits; neighbor++) { if (adj[q][neighbor]) { buffer.push_back(neighbor); inside_do_sqrt_z(neighbor); } } for (size_t k1 = 0; k1 < buffer.size(); k1++) { for (size_t k2 = k1 + 1; k2 < buffer.size(); k2++) { inside_do_cz(buffer[k1], buffer[k2]); } } inside_do_sqrt_x_dag(q); } void GraphSimulator::inside_do_ycx(size_t q1, size_t q2) { if (adj[q1][q2]) { // Y:X -> SQRT_X(Y):SQRT_Z(X) = Z:Y do_complementation(q1); inside_do_cy(q1, q2); paulis.zs[q1] ^= 1; } else { // Y:X -> SQRT_X(Y):Y = Z:X do_complementation(q1); inside_do_cx(q1, q2); } } void GraphSimulator::inside_do_ycy(size_t q1, size_t q2) { if (adj[q1][q2]) { // Y:Y -> SQRT_X(Y):SQRT_Z(Y) = Z:X do_complementation(q1); inside_do_cx(q1, q2); } else { // Y:Y -> SQRT_X(Y):Y = Z:Y do_complementation(q1); inside_do_cy(q1, q2); } } void GraphSimulator::inside_do_xcx(size_t q1, size_t q2) { if (adj[q1][q2]) { // X:X -> S(X):SQRT_X_DAG(X) = (-Y):X do_complementation(q2); // (-Y):X -> SQRT_X_DAG(-Y):S(X) = (-Z):(-Y) do_complementation(q1); inside_do_cy(q1, q2); paulis.zs[q1] ^= 1; paulis.xs[q2] ^= 1; paulis.zs[q2] ^= 1; } else { // Need an S gate. // Get it by finding a neighbor to do local complementation on. for (size_t q3 = 0; q3 < num_qubits; q3++) { if (adj[q1][q3]) { do_complementation(q3); if (adj[q2][q3]) { // X:X -> S(X):S(X) = (-Y):(-Y) paulis.xs[q1] ^= 1; paulis.zs[q1] ^= 1; paulis.xs[q2] ^= 1; paulis.zs[q2] ^= 1; inside_do_ycy(q1, q2); } else { // X:X -> S(X):X = (-Y):X paulis.xs[q2] ^= 1; inside_do_ycx(q1, q2); } return; } } // q1 has no CZ gates applied to it. // Therefore, inside the single qubit gates, q1 is in the |+> state. // Therefore the XCX gate's control isn't satisfied, and it does nothing. // <-- look at all the doing-nothing right here. --> } } void GraphSimulator::inside_do_pauli_interaction(bool x1, bool z1, bool x2, bool z2, size_t q1, size_t q2) { int p1 = x1 + z1 * 2 - 1; int p2 = x2 + z2 * 2 - 1; switch (p1 + p2 * 3) { case 0: // X:X inside_do_xcx(q1, q2); break; case 1: // Z:X inside_do_cx(q1, q2); break; case 2: // Y:X inside_do_ycx(q1, q2); break; case 3: // X:Z inside_do_cx(q2, q1); break; case 4: // Z:Z inside_do_cz(q1, q2); break; case 5: // Y:Z inside_do_cy(q2, q1); break; case 6: // X:Y inside_do_ycx(q2, q1); break; case 7: // Z:Y inside_do_cy(q1, q2); break; case 8: // Y:Y inside_do_ycy(q1, q2); break; default: throw std::invalid_argument("Unknown pauli interaction."); } } std::ostream &stim::operator<<(std::ostream &out, const GraphSimulator &sim) { out << "stim::GraphSimulator{\n"; out << " .num_qubits=" << sim.num_qubits << ",\n"; out << " .paulis=" << sim.paulis << ",\n"; out << " .x2outs=" << sim.x2outs << ",\n"; out << " .z2outs=" << sim.z2outs << ",\n"; out << " .adj=stim::simd_bit_table<64>::from_text(R\"TAB(\n" << sim.adj.str(sim.num_qubits) << "\n)TAB\"),\n"; out << "}"; return out; } std::string GraphSimulator::str() const { std::stringstream ss; ss << *this; return ss.str(); } GraphSimulator GraphSimulator::random_state(size_t n, std::mt19937_64 &rng) { GraphSimulator sim(n); sim.adj = simd_bit_table<64>::random(n, n, rng); for (size_t q1 = 0; q1 < n; q1++) { sim.adj[q1][q1] = 0; for (size_t q2 = q1 + 1; q2 < n; q2++) { sim.adj[q1][q2] = sim.adj[q2][q1]; } } sim.paulis = PauliString<64>::random(n, rng); sim.paulis.sign = 0; std::array gate_xz_data{ 0b1001, // I 0b0110, // H 0b1011, // S 0b1101, // SQRT_X_DAG 0b0111, // C_XYZ 0b1110, // C_ZYX }; for (size_t q = 0; q < n; q++) { auto r = gate_xz_data[rng() % 6]; sim.x2outs.xs[q] = r & 1; sim.x2outs.zs[q] = r & 2; sim.z2outs.xs[q] = r & 4; sim.z2outs.zs[q] = r & 8; } return sim; } void GraphSimulator::do_pauli_interaction(bool x1, bool z1, bool x2, bool z2, size_t qubit1, size_t qubit2) { // Propagate the interaction across the single qubit gate layer. auto [x1_in, z1_in, sign1] = after2inside_basis_transform(qubit1, x1, z1); auto [x2_in, z2_in, sign2] = after2inside_basis_transform(qubit2, x2, z2); // Kickback minus signs into Pauli gates on the other side. if (sign1) { paulis.xs[qubit2] ^= x2_in; paulis.zs[qubit2] ^= z2_in; } if (sign2) { paulis.xs[qubit1] ^= x1_in; paulis.zs[qubit1] ^= z1_in; } // Do the positive-sign interaction between the single-qubit layer and the CZ layer. inside_do_pauli_interaction(x1_in, z1_in, x2_in, z2_in, qubit1, qubit2); } void GraphSimulator::do_gate_by_decomposition(const CircuitInstruction &inst) { const Gate &d = GATE_DATA[inst.gate_type]; bool is_all_qubits = true; for (auto t : inst.targets) { is_all_qubits &= t.is_qubit_target(); } if (!is_all_qubits || d.h_s_cx_m_r_decomposition == nullptr || !(d.flags & GATE_TARGETS_PAIRS)) { throw std::invalid_argument("Not supported: " + inst.str()); } Circuit circuit(d.h_s_cx_m_r_decomposition); for (size_t k = 0; k < inst.targets.size(); k += 2) { auto a = inst.targets[k]; auto b = inst.targets[k + 1]; for (const auto &inst2 : circuit.operations) { assert( inst2.gate_type == GateType::CX || inst2.gate_type == GateType::H || inst2.gate_type == GateType::S || inst2.gate_type == GateType::R || inst2.gate_type == GateType::M); if (inst2.gate_type == GateType::CX) { for (size_t k2 = 0; k2 < inst2.targets.size(); k2 += 2) { auto a2 = inst2.targets[k2]; auto b2 = inst2.targets[k2 + 1]; assert(a2.is_qubit_target()); assert(b2.is_qubit_target()); assert(a2.qubit_value() == 0 || a2.qubit_value() == 1); assert(b2.qubit_value() == 0 || b2.qubit_value() == 1); auto a3 = a2.qubit_value() == 0 ? a : b; auto b3 = b2.qubit_value() == 0 ? a : b; do_pauli_interaction(false, true, true, false, a3.qubit_value(), b3.qubit_value()); } } else { for (GateTarget a2 : inst2.targets) { assert(a2.is_qubit_target()); assert(a2.qubit_value() == 0 || a2.qubit_value() == 1); auto a3 = a2.qubit_value() == 0 ? a : b; do_1q_gate(inst2.gate_type, a3.qubit_value()); } } } } } void GraphSimulator::do_2q_unitary_instruction(const CircuitInstruction &inst) { uint8_t p1; uint8_t p2; constexpr uint8_t X = 0b01; constexpr uint8_t Y = 0b11; constexpr uint8_t Z = 0b10; switch (inst.gate_type) { case GateType::XCX: p1 = X; p2 = X; break; case GateType::XCY: p1 = X; p2 = Y; break; case GateType::XCZ: p1 = X; p2 = Z; break; case GateType::YCX: p1 = Y; p2 = X; break; case GateType::YCY: p1 = Y; p2 = Y; break; case GateType::YCZ: p1 = Y; p2 = Z; break; case GateType::CX: p1 = Z; p2 = X; break; case GateType::CY: p1 = Z; p2 = Y; break; case GateType::CZ: p1 = Z; p2 = Z; break; default: do_gate_by_decomposition(inst); return; } bool x1 = p1 & 1; bool z1 = p1 & 2; bool x2 = p2 & 1; bool z2 = p2 & 2; for (size_t k = 0; k < inst.targets.size(); k += 2) { auto t1 = inst.targets[k]; auto t2 = inst.targets[k + 1]; if (!t1.is_qubit_target() || !t2.is_qubit_target()) { throw std::invalid_argument("Unsupported operation: " + inst.str()); } do_pauli_interaction(x1, z1, x2, z2, t1.qubit_value(), t2.qubit_value()); } } void GraphSimulator::output_pauli_layer(Circuit &out, bool to_hs_xyz) const { std::array, 4> groups; for (size_t q = 0; q < paulis.num_qubits; q++) { bool x = paulis.xs[q]; bool z = paulis.zs[q]; if (to_hs_xyz) { bool xx = x2outs.xs[q]; bool xz = x2outs.zs[q]; bool zx = z2outs.xs[q]; bool zz = z2outs.zs[q]; z ^= xx == 1 && xz == 1 && zx == 1 && zz == 0; } groups[x + 2 * z].push_back(GateTarget::qubit(q)); } auto f = [&](GateType g, int k) { if (!groups[k].empty()) { out.safe_append(CircuitInstruction(g, {}, groups[k], "")); } }; f(GateType::X, 0b01); f(GateType::Y, 0b11); f(GateType::Z, 0b10); } void GraphSimulator::output_clifford_layer(Circuit &out, bool to_hs_xyz) const { output_pauli_layer(out, to_hs_xyz); std::array, 16> groups; for (size_t q = 0; q < x2outs.num_qubits; q++) { bool xx = x2outs.xs[q]; bool xz = x2outs.zs[q]; bool zx = z2outs.xs[q]; bool zz = z2outs.zs[q]; groups[xx + 2 * xz + 4 * zx + 8 * zz].push_back(GateTarget::qubit(q)); } std::array, 3> shs; auto f = [&](GateType g, int k, std::array use_shs) { if (to_hs_xyz) { for (size_t j = 0; j < 3; j++) { if (use_shs[j]) { shs[j].insert(shs[j].end(), groups[k].begin(), groups[k].end()); } } } else { if (!groups[k].empty()) { out.safe_append(CircuitInstruction(g, {}, groups[k], "")); } } }; f(GateType::C_XYZ, 0b0111, {1, 1, 0}); f(GateType::C_ZYX, 0b1110, {0, 1, 1}); f(GateType::H, 0b0110, {0, 1, 0}); f(GateType::S, 0b1011, {1, 0, 0}); f(GateType::SQRT_X_DAG, 0b1101, {1, 1, 1}); for (size_t k = 0; k < 3; k++) { if (!shs[k].empty()) { std::sort(shs[k].begin(), shs[k].end()); out.safe_append(CircuitInstruction(k == 1 ? GateType::H : GateType::S, {}, shs[k], "")); } } } Circuit GraphSimulator::to_circuit(bool to_hs_xyz) const { std::vector targets; targets.reserve(2 * num_qubits); Circuit out; for (size_t q = 0; q < num_qubits; q++) { targets.push_back(GateTarget::qubit(q)); } if (!targets.empty()) { out.safe_append(CircuitInstruction(GateType::RX, {}, targets, "")); } out.safe_append(CircuitInstruction(GateType::TICK, {}, {}, "")); bool has_cz = false; for (size_t q = 0; q < num_qubits; q++) { targets.clear(); for (size_t q2 = q + 1; q2 < num_qubits; q2++) { if (adj[q][q2]) { targets.push_back(GateTarget::qubit(q)); targets.push_back(GateTarget::qubit(q2)); } } if (!targets.empty()) { out.safe_append(CircuitInstruction(GateType::CZ, {}, targets, "")); } has_cz |= !targets.empty(); } if (has_cz) { out.safe_append(CircuitInstruction(GateType::TICK, {}, {}, "")); } output_clifford_layer(out, to_hs_xyz); return out; } void GraphSimulator::do_instruction(const CircuitInstruction &instruction) { auto f = GATE_DATA[instruction.gate_type].flags; if (f & GATE_IS_UNITARY) { if (f & GATE_IS_SINGLE_QUBIT_GATE) { for (auto t : instruction.targets) { do_1q_gate(instruction.gate_type, t.qubit_value()); } return; } if (f & GATE_TARGETS_PAIRS) { do_2q_unitary_instruction(instruction); return; } } switch (instruction.gate_type) { case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: return; // No effect. default: throw std::invalid_argument("Unsupported operation: " + instruction.str()); } } void GraphSimulator::do_circuit(const Circuit &circuit) { circuit.for_each_operation([&](const CircuitInstruction &inst) { do_instruction(inst); }); } ================================================ FILE: src/stim/simulators/graph_simulator.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_GRAPH_SIMULATOR_H #define _STIM_SIMULATORS_GRAPH_SIMULATOR_H #include "stim/circuit/circuit.h" #include "stim/mem/simd_bit_table.h" #include "stim/mem/simd_bits.h" #include "stim/stabilizers/pauli_string.h" namespace stim { struct GraphSimulator { // RX applied to each qubit. size_t num_qubits; // Then CZs applied according to this adjacency matrix. simd_bit_table<64> adj; // Then Paulis adding sign data. PauliString<64> paulis; // Then an unsigned Clifford mapping. PauliString<64> x2outs; PauliString<64> z2outs; // Used as temporary workspace. std::vector buffer; explicit GraphSimulator(size_t num_qubits); static GraphSimulator random_state(size_t n, std::mt19937_64 &rng); Circuit to_circuit(bool to_hs_xyz = false) const; void do_circuit(const Circuit &circuit); void do_instruction(const CircuitInstruction &instruction); void do_complementation(size_t c); void verify_invariants() const; void inside_do_sqrt_z(size_t q); void inside_do_sqrt_x_dag(size_t q); std::tuple after2inside_basis_transform(size_t qubit, bool x, bool z); std::string str() const; private: void output_pauli_layer(Circuit &out, bool to_hs_xyz) const; void output_clifford_layer(Circuit &out, bool to_hs_xyz) const; void do_1q_gate(GateType gate, size_t qubit); void do_2q_unitary_instruction(const CircuitInstruction &inst); void do_pauli_interaction(bool x1, bool z1, bool x2, bool z2, size_t qubit1, size_t qubit2); void do_gate_by_decomposition(const CircuitInstruction &inst); // These operations apply to the state inside of the single qubit gates. void inside_do_cz(size_t a, size_t b); void inside_do_cx(size_t c, size_t t); void inside_do_cy(size_t c, size_t t); void inside_do_ycx(size_t q1, size_t q2); void inside_do_ycy(size_t q1, size_t q2); void inside_do_xcx(size_t q1, size_t q2); void inside_do_pauli_interaction(bool x1, bool z1, bool x2, bool z2, size_t q1, size_t q2); }; std::ostream &operator<<(std::ostream &out, const GraphSimulator &sim); } // namespace stim #endif ================================================ FILE: src/stim/simulators/graph_simulator.test.cc ================================================ #include "stim/simulators/graph_simulator.h" #include "gtest/gtest.h" #include "stim/diagram/timeline/timeline_ascii_drawer.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/test_util.test.h" using namespace stim; void expect_graph_circuit_is_equivalent(const Circuit &circuit) { auto n = circuit.count_qubits(); GraphSimulator sim(n); sim.do_circuit(circuit); sim.verify_invariants(); Circuit converted = sim.to_circuit(); TableauSimulator<64> sim1(std::mt19937_64{}, n); TableauSimulator<64> sim2(std::mt19937_64{}, n); sim1.safe_do_circuit(circuit); sim2.safe_do_circuit(converted); auto s1 = sim1.canonical_stabilizers(); auto s2 = sim2.canonical_stabilizers(); if (s1 != s2) { EXPECT_EQ(s1, s2) << "\nACTUAL:\n" << stim_draw_internal::DiagramTimelineAsciiDrawer::make_diagram(circuit) << "\nCONVERTED:\n" << stim_draw_internal::DiagramTimelineAsciiDrawer::make_diagram(converted); } } void expect_graph_sim_effect_matches_tableau_sim(const GraphSimulator &state, const Circuit &effect) { GraphSimulator state_after_effect = state; auto n = state_after_effect.num_qubits; state_after_effect.do_circuit(effect); state_after_effect.verify_invariants(); TableauSimulator<64> tableau_sim1(std::mt19937_64{}, n); TableauSimulator<64> tableau_sim2(std::mt19937_64{}, n); tableau_sim1.safe_do_circuit(state.to_circuit()); tableau_sim1.safe_do_circuit(effect); tableau_sim2.safe_do_circuit(state_after_effect.to_circuit()); auto s1 = tableau_sim1.canonical_stabilizers(); auto s2 = tableau_sim2.canonical_stabilizers(); if (s1 != s2) { EXPECT_EQ(s1, s2) << "EFFECT:\nstim::Circuit(R\"CIRCUIT(\n" << effect << "\n)CIRCUIT\")" << "\n" << "STATE:\n" << state << "\n"; } } TEST(graph_simulator, converts_circuits) { expect_graph_circuit_is_equivalent(Circuit(R"CIRCUIT( H 2 3 5 S 0 1 2 C_XYZ 1 2 3 S_DAG 1 H_YZ 5 4 SQRT_Y 2 4 X 1 )CIRCUIT")); expect_graph_circuit_is_equivalent(Circuit(R"CIRCUIT( H 0 1 CZ 0 1 )CIRCUIT")); expect_graph_circuit_is_equivalent(Circuit(R"CIRCUIT( H 0 1 2 3 CZ 0 1 0 2 0 3 )CIRCUIT")); expect_graph_circuit_is_equivalent(Circuit(R"CIRCUIT( H 0 1 2 3 CZ 0 1 CX 2 1 )CIRCUIT")); } TEST(graph_simulator, after2inside_basis_transform) { GraphSimulator sim(6); // VI+SH- sim.x2outs = PauliString<64>("XXYYZZ"); sim.z2outs = PauliString<64>("YZXZXY"); ASSERT_EQ(sim.after2inside_basis_transform(0u, 1, 0), (std::make_tuple(1, 0, false))); ASSERT_EQ(sim.after2inside_basis_transform(0u, 1, 1), (std::make_tuple(0, 1, false))); ASSERT_EQ(sim.after2inside_basis_transform(0u, 0, 1), (std::make_tuple(1, 1, true))); ASSERT_EQ(sim.after2inside_basis_transform(1u, 1, 0), (std::make_tuple(1, 0, false))); ASSERT_EQ(sim.after2inside_basis_transform(1u, 0, 1), (std::make_tuple(0, 1, false))); ASSERT_EQ(sim.after2inside_basis_transform(1u, 1, 1), (std::make_tuple(1, 1, false))); ASSERT_EQ(sim.after2inside_basis_transform(2u, 1, 1), (std::make_tuple(1, 0, false))); ASSERT_EQ(sim.after2inside_basis_transform(2u, 1, 0), (std::make_tuple(0, 1, false))); ASSERT_EQ(sim.after2inside_basis_transform(2u, 0, 1), (std::make_tuple(1, 1, false))); ASSERT_EQ(sim.after2inside_basis_transform(3u, 1, 1), (std::make_tuple(1, 0, false))); ASSERT_EQ(sim.after2inside_basis_transform(3u, 0, 1), (std::make_tuple(0, 1, false))); ASSERT_EQ(sim.after2inside_basis_transform(3u, 1, 0), (std::make_tuple(1, 1, true))); ASSERT_EQ(sim.after2inside_basis_transform(4u, 0, 1), (std::make_tuple(1, 0, false))); ASSERT_EQ(sim.after2inside_basis_transform(4u, 1, 0), (std::make_tuple(0, 1, false))); ASSERT_EQ(sim.after2inside_basis_transform(4u, 1, 1), (std::make_tuple(1, 1, true))); ASSERT_EQ(sim.after2inside_basis_transform(5u, 0, 1), (std::make_tuple(1, 0, false))); ASSERT_EQ(sim.after2inside_basis_transform(5u, 1, 1), (std::make_tuple(0, 1, false))); ASSERT_EQ(sim.after2inside_basis_transform(5u, 1, 0), (std::make_tuple(1, 1, false))); } TEST(graph_simulator, to_hs_xyz) { GraphSimulator sim(10); sim.do_circuit(Circuit(R"CIRCUIT( H 0 1 2 3 4 5 6 7 8 9 I 0 H 1 S 2 SQRT_X_DAG 3 C_XYZ 4 C_ZYX 5 X 6 Y 7 Z 8 H 9 Z 9 )CIRCUIT")); ASSERT_EQ(sim.to_circuit(true), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 6 7 8 9 TICK X 6 9 Y 7 Z 4 8 S 2 3 4 H 1 3 4 5 9 S 3 5 )CIRCUIT")); } TEST(graph_simulator, inside_do_sqrt_z) { GraphSimulator sim(10); sim.do_circuit(Circuit(R"CIRCUIT( H 0 1 2 3 4 5 6 7 8 9 I 0 H 1 S 2 SQRT_X_DAG 3 C_XYZ 4 C_ZYX 5 X 6 Y 7 Z 8 H 9 Z 9 )CIRCUIT")); ASSERT_EQ(sim.to_circuit(), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 6 7 8 9 TICK X 6 9 Y 7 Z 8 C_XYZ 4 C_ZYX 5 H 1 9 S 2 SQRT_X_DAG 3 )CIRCUIT")); for (size_t q = 0; q < sim.num_qubits; q++) { sim.inside_do_sqrt_z(q); } sim.verify_invariants(); ASSERT_EQ(sim.to_circuit(), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 6 7 8 9 TICK X 7 9 Y 6 Z 1 2 3 8 C_XYZ 1 9 C_ZYX 3 H 4 S 0 6 7 8 SQRT_X_DAG 5 )CIRCUIT")); } TEST(graph_simulator, inside_do_sqrt_x_dag) { GraphSimulator sim(10); sim.do_circuit(Circuit(R"CIRCUIT( H 0 1 2 3 4 5 6 7 8 9 I 0 H 1 S 2 SQRT_X_DAG 3 C_XYZ 4 C_ZYX 5 X 6 Y 7 Z 8 H 9 Z 9 )CIRCUIT")); ASSERT_EQ(sim.to_circuit(), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 6 7 8 9 TICK X 6 9 Y 7 Z 8 C_XYZ 4 C_ZYX 5 H 1 9 S 2 SQRT_X_DAG 3 )CIRCUIT")); for (size_t q = 0; q < sim.num_qubits; q++) { sim.inside_do_sqrt_x_dag(q); } sim.verify_invariants(); ASSERT_EQ(sim.to_circuit(), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 6 7 8 9 TICK X 1 2 3 6 Y 8 Z 7 C_XYZ 2 C_ZYX 1 9 H 5 S 4 SQRT_X_DAG 0 6 7 8 )CIRCUIT")); } TEST(graph_simulator, do_complementation) { for (size_t k = 0; k < 50; k++) { auto rng = INDEPENDENT_TEST_RNG(); GraphSimulator sim = GraphSimulator::random_state(8, rng); GraphSimulator sim2 = sim; sim2.do_complementation(3); sim.verify_invariants(); sim2.verify_invariants(); TableauSimulator<64> tableau_sim1(std::mt19937_64{}, 8); TableauSimulator<64> tableau_sim2(std::mt19937_64{}, 8); tableau_sim1.safe_do_circuit(sim.to_circuit()); tableau_sim2.safe_do_circuit(sim2.to_circuit()); auto s1 = tableau_sim1.canonical_stabilizers(); auto s2 = tableau_sim2.canonical_stabilizers(); if (s1 != s2) { ASSERT_EQ(s1, s2) << sim; } } } TEST(graph_simulator, all_unitary_gates_work) { auto rng = INDEPENDENT_TEST_RNG(); std::vector targets{GateTarget::qubit(2), GateTarget::qubit(5)}; SpanRef t2 = targets; SpanRef t1 = t2; t1.ptr_end--; for (const auto &gate : GATE_DATA.items) { if (!gate.has_known_unitary_matrix()) { continue; } Circuit circuit; circuit.safe_append(CircuitInstruction(gate.id, {}, (gate.flags & GATE_TARGETS_PAIRS) ? t2 : t1, "")); for (size_t k = 0; k < 20; k++) { expect_graph_sim_effect_matches_tableau_sim(GraphSimulator::random_state(8, rng), circuit); } } } TEST(graph_simulator, to_circuit_single_qubit_gates) { GraphSimulator sim(6); ASSERT_EQ(sim.to_circuit(), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 TICK H 0 1 2 3 4 5 )CIRCUIT")); sim.do_circuit(Circuit(R"CIRCUIT( H 0 1 2 3 4 5 )CIRCUIT")); ASSERT_EQ(sim.to_circuit(), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 TICK )CIRCUIT")); sim.do_circuit(Circuit(R"CIRCUIT( H 0 S 1 C_XYZ 2 3 3 SQRT_X_DAG 4 )CIRCUIT")); ASSERT_EQ(sim.to_circuit(), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 TICK C_XYZ 2 C_ZYX 3 H 0 S 1 SQRT_X_DAG 4 )CIRCUIT")); sim.do_circuit(Circuit(R"CIRCUIT( X 0 S 1 Y 2 Z 3 )CIRCUIT")); ASSERT_EQ(sim.to_circuit(), Circuit(R"CIRCUIT( RX 0 1 2 3 4 5 TICK X 2 3 Z 0 1 C_XYZ 2 C_ZYX 3 H 0 SQRT_X_DAG 4 )CIRCUIT")); } ================================================ FILE: src/stim/simulators/matched_error.cc ================================================ // 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. #include "stim/simulators/matched_error.h" #include #include #include #include "stim/util_bot/str_util.h" using namespace stim; void print_pauli_product(std::ostream &out, const std::vector &pauli_terms) { for (size_t k = 0; k < pauli_terms.size(); k++) { const auto &p = pauli_terms[k]; if (k) { out << "*"; } out << p; } } void print_circuit_error_loc_indent(std::ostream &out, const CircuitErrorLocation &e, const char *indent) { out << indent << "CircuitErrorLocation {\n"; if (!e.noise_tag.empty()) { out << indent << " noise_tag: " << e.noise_tag << "\n"; } if (!e.flipped_pauli_product.empty()) { out << indent << " flipped_pauli_product: "; print_pauli_product(out, e.flipped_pauli_product); out << "\n"; } if (e.flipped_measurement.measurement_record_index != UINT64_MAX) { out << indent << " flipped_measurement.measurement_record_index: " << e.flipped_measurement.measurement_record_index << "\n"; } if (!e.flipped_measurement.measured_observable.empty()) { out << indent << " flipped_measurement.measured_observable: "; print_pauli_product(out, e.flipped_measurement.measured_observable); out << "\n"; } out << indent << " Circuit location stack trace:\n"; out << indent << " (after " << e.tick_offset << " TICKs)\n"; for (size_t k = 0; k < e.stack_frames.size(); k++) { const auto &frame = e.stack_frames[k]; if (k) { out << indent << " after " << frame.iteration_index << " completed iterations\n"; } out << indent << " "; out << "at instruction #" << (frame.instruction_offset + 1); const auto &gate_data = GATE_DATA[e.instruction_targets.gate_type]; if (k < e.stack_frames.size() - 1) { out << " (a REPEAT " << frame.instruction_repetitions_arg << " block)"; } else { out << " (" << gate_data.name << ")"; } if (k) { out << " in the REPEAT block"; } else { out << " in the circuit"; } out << "\n"; } if (e.instruction_targets.target_range_start + 1 == e.instruction_targets.target_range_end) { out << indent << " at target #" << (e.instruction_targets.target_range_start + 1); } else { out << indent << " at targets #" << (e.instruction_targets.target_range_start + 1); out << " to #" << e.instruction_targets.target_range_end; } out << " of the instruction\n"; out << indent << " resolving to " << e.instruction_targets << "\n"; out << indent << "}"; } void CircuitTargetsInsideInstruction::fill_args_and_targets_in_range( const CircuitInstruction &actual_op, const std::map> &qubit_coords) { targets_in_range.clear(); for (size_t k = target_range_start; k < target_range_end; k++) { const auto &t = actual_op.targets[k]; bool is_non_coord_target = t.data & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT | TARGET_COMBINER); auto entry = qubit_coords.find(t.qubit_value()); if (entry == qubit_coords.end() || is_non_coord_target) { targets_in_range.push_back({t, {}}); } else { targets_in_range.push_back({t, entry->second}); } } args.clear(); args.insert(args.begin(), actual_op.args.begin(), actual_op.args.end()); } void ExplainedError::fill_in_dem_targets( SpanRef targets, const std::map> &dem_coords) { dem_error_terms.clear(); for (const auto &t : targets) { auto entry = dem_coords.find(t.raw_id()); if (t.is_relative_detector_id() && entry != dem_coords.end()) { dem_error_terms.push_back({t, entry->second}); } else { dem_error_terms.push_back({t, {}}); } } } std::ostream &stim::operator<<(std::ostream &out, const FlippedMeasurement &e) { out << "FlippedMeasurement{"; if (e.measurement_record_index == UINT64_MAX) { return out << "none}"; } out << e.measurement_record_index; out << ", "; print_pauli_product(out, e.measured_observable); out << "}"; return out; } std::ostream &stim::operator<<(std::ostream &out, const CircuitErrorLocation &e) { print_circuit_error_loc_indent(out, e, ""); return out; } std::ostream &stim::operator<<(std::ostream &out, const CircuitErrorLocationStackFrame &e) { out << "CircuitErrorLocationStackFrame"; out << "{instruction_offset=" << e.instruction_offset; out << ", iteration_index=" << e.iteration_index; out << ", instruction_repetitions_arg=" << e.instruction_repetitions_arg << "}"; return out; } std::ostream &stim::operator<<(std::ostream &out, const ExplainedError &e) { out << "ExplainedError {\n"; out << " dem_error_terms: " << comma_sep(e.dem_error_terms, " "); if (e.circuit_error_locations.empty()) { out << "\n [no single circuit error had these exact symptoms]"; } for (const auto &loc : e.circuit_error_locations) { out << "\n"; print_circuit_error_loc_indent(out, loc, " "); } out << "\n}"; return out; } std::ostream &stim::operator<<(std::ostream &out, const CircuitTargetsInsideInstruction &e) { const auto &gate_data = GATE_DATA[e.gate_type]; if (gate_data.flags == GateFlags::NO_GATE_FLAG) { out << "null"; } else { out << gate_data.name; } if (!e.gate_tag.empty()) { out << '['; write_tag_escaped_string_to(e.gate_tag, out); out << ']'; } if (!e.args.empty()) { out << '(' << comma_sep(e.args) << ')'; } bool was_combiner = false; for (const auto &t : e.targets_in_range) { bool is_combiner = t.gate_target.is_combiner(); if (!is_combiner && !was_combiner) { out << ' '; } was_combiner = is_combiner; out << t; } return out; } std::ostream &stim::operator<<(std::ostream &out, const GateTargetWithCoords &e) { e.gate_target.write_succinct(out); if (!e.coords.empty()) { out << "[coords " << comma_sep(e.coords, ",") << "]"; } return out; } std::ostream &stim::operator<<(std::ostream &out, const DemTargetWithCoords &e) { out << e.dem_target; if (!e.coords.empty()) { out << "[coords " << comma_sep(e.coords, ",") << "]"; } return out; } bool ExplainedError::operator==(const ExplainedError &other) const { return dem_error_terms == other.dem_error_terms && circuit_error_locations == other.circuit_error_locations; } bool CircuitErrorLocationStackFrame::operator==(const CircuitErrorLocationStackFrame &other) const { return iteration_index == other.iteration_index && instruction_offset == other.instruction_offset && instruction_repetitions_arg == other.instruction_repetitions_arg; } bool CircuitErrorLocation::operator==(const CircuitErrorLocation &other) const { return flipped_measurement == other.flipped_measurement && tick_offset == other.tick_offset && flipped_pauli_product == other.flipped_pauli_product && instruction_targets == other.instruction_targets && stack_frames == other.stack_frames; } bool CircuitTargetsInsideInstruction::operator==(const CircuitTargetsInsideInstruction &other) const { return gate_type == other.gate_type && gate_tag == other.gate_tag && target_range_start == other.target_range_start && target_range_end == other.target_range_end && targets_in_range == other.targets_in_range && args == other.args; } bool DemTargetWithCoords::operator==(const DemTargetWithCoords &other) const { return coords == other.coords && dem_target == other.dem_target; } bool FlippedMeasurement::operator==(const FlippedMeasurement &other) const { return measured_observable == other.measured_observable && measurement_record_index == other.measurement_record_index; } bool GateTargetWithCoords::operator==(const GateTargetWithCoords &other) const { return coords == other.coords && gate_target == other.gate_target; } bool CircuitErrorLocation::operator!=(const CircuitErrorLocation &other) const { return !(*this == other); } bool CircuitErrorLocationStackFrame::operator!=(const CircuitErrorLocationStackFrame &other) const { return !(*this == other); } bool CircuitTargetsInsideInstruction::operator!=(const CircuitTargetsInsideInstruction &other) const { return !(*this == other); } bool DemTargetWithCoords::operator!=(const DemTargetWithCoords &other) const { return !(*this == other); } bool FlippedMeasurement::operator!=(const FlippedMeasurement &other) const { return !(*this == other); } bool GateTargetWithCoords::operator!=(const GateTargetWithCoords &other) const { return !(*this == other); } bool ExplainedError::operator!=(const ExplainedError &other) const { return !(*this == other); } std::string CircuitErrorLocation::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::string CircuitErrorLocationStackFrame::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::string CircuitTargetsInsideInstruction::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::string DemTargetWithCoords::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::string FlippedMeasurement::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::string GateTargetWithCoords::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::string ExplainedError::str() const { std::stringstream ss; ss << *this; return ss.str(); } void ExplainedError::canonicalize() { for (auto &c : circuit_error_locations) { c.canonicalize(); } std::sort(dem_error_terms.begin(), dem_error_terms.end()); std::sort(circuit_error_locations.begin(), circuit_error_locations.end()); } void CircuitErrorLocation::canonicalize() { std::sort(flipped_pauli_product.begin(), flipped_pauli_product.end()); std::sort(flipped_measurement.measured_observable.begin(), flipped_measurement.measured_observable.end()); } template bool vec_less_than(const std::vector &vec1, const std::vector &vec2) { SpanRef c1 = vec1; SpanRef c2 = vec2; return c1 < c2; } bool GateTargetWithCoords::operator<(const GateTargetWithCoords &other) const { if (gate_target != other.gate_target) { return gate_target < other.gate_target; } if (coords != other.coords) { return vec_less_than(coords, other.coords); } return false; } bool DemTargetWithCoords::operator<(const DemTargetWithCoords &other) const { if (dem_target != other.dem_target) { return dem_target < other.dem_target; } if (coords != other.coords) { return vec_less_than(coords, other.coords); } return false; } bool CircuitErrorLocation::operator<(const CircuitErrorLocation &other) const { if (tick_offset != other.tick_offset) { return tick_offset < other.tick_offset; } if (flipped_pauli_product != other.flipped_pauli_product) { return vec_less_than(flipped_pauli_product, other.flipped_pauli_product); } if (flipped_measurement != other.flipped_measurement) { return flipped_measurement < other.flipped_measurement; } if (instruction_targets != other.instruction_targets) { return instruction_targets < other.instruction_targets; } if (stack_frames != other.stack_frames) { return vec_less_than(stack_frames, other.stack_frames); } return false; } bool FlippedMeasurement::operator<(const FlippedMeasurement &other) const { if (measurement_record_index != other.measurement_record_index) { return measurement_record_index < other.measurement_record_index; } if (measured_observable != other.measured_observable) { return vec_less_than(measured_observable, other.measured_observable); } return false; } bool CircuitErrorLocationStackFrame::operator<(const CircuitErrorLocationStackFrame &other) const { if (instruction_offset != other.instruction_offset) { return instruction_offset < other.instruction_offset; } if (iteration_index != other.iteration_index) { return iteration_index < other.iteration_index; } if (instruction_repetitions_arg != other.instruction_repetitions_arg) { return instruction_repetitions_arg < other.instruction_repetitions_arg; } return false; } bool CircuitTargetsInsideInstruction::operator<(const CircuitTargetsInsideInstruction &other) const { if (target_range_start != other.target_range_start) { return target_range_start < other.target_range_start; } if (target_range_end != other.target_range_end) { return target_range_end < other.target_range_end; } if (targets_in_range != other.targets_in_range) { return vec_less_than(targets_in_range, other.targets_in_range); } if (args != other.args) { return vec_less_than(args, other.args); } if (gate_type == GateType::NOT_A_GATE || other.gate_type == GateType::NOT_A_GATE) { return gate_type < other.gate_type; } return GATE_DATA[gate_type].name < GATE_DATA[other.gate_type].name; } bool CircuitErrorLocation::is_simpler_than(const CircuitErrorLocation &other) const { if (flipped_measurement.measured_observable.size() != other.flipped_measurement.measured_observable.size()) { return other.flipped_measurement.measured_observable.size() < other.flipped_measurement.measured_observable.size(); } if (flipped_pauli_product.size() != other.flipped_pauli_product.size()) { return flipped_pauli_product.size() < other.flipped_pauli_product.size(); } return *this < other; } ================================================ FILE: src/stim/simulators/matched_error.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_MATCHED_ERROR_H #define _STIM_SIMULATORS_MATCHED_ERROR_H #include "stim/circuit/gate_target.h" #include "stim/dem/detector_error_model.h" namespace stim { /// Describes the location of an instruction being executed within a /// circuit or loop, distinguishing between separate loop iterations. /// /// The full location is a stack of these frames, drilling down from /// the top level circuit to the inner-most loop that the instruction /// is within. struct CircuitErrorLocationStackFrame { /// The index of the instruction in the circuit (or block). uint64_t instruction_offset; /// If inside a loop, the number of iterations of the loop that completed /// before the moment that we are trying to refer to. /// If not in a loop, set to 0. uint64_t iteration_index; /// If the instruction is a REPEAT block, this is how many times it repeats. /// If not, set to 0. uint64_t instruction_repetitions_arg; bool operator<(const CircuitErrorLocationStackFrame &other) const; /// Standard methods for easy testing. bool operator==(const CircuitErrorLocationStackFrame &other) const; bool operator!=(const CircuitErrorLocationStackFrame &other) const; std::string str() const; }; /// A gate target with qubit coordinate metadata attached to it. struct GateTargetWithCoords { GateTarget gate_target; std::vector coords; bool operator<(const GateTargetWithCoords &other) const; /// Standard methods for easy testing. bool operator==(const GateTargetWithCoords &other) const; bool operator!=(const GateTargetWithCoords &other) const; std::string str() const; }; /// A dem target with detector coordinate metadata attached to it. struct DemTargetWithCoords { DemTarget dem_target; std::vector coords; bool operator<(const DemTargetWithCoords &other) const; /// Standard methods for easy testing. bool operator==(const DemTargetWithCoords &other) const; bool operator!=(const DemTargetWithCoords &other) const; std::string str() const; }; /// Stores additional details about measurement errors. struct FlippedMeasurement { /// Which output bit this measurement corresponds to. /// UINT64_MAX means "no measurement error occurred". uint64_t measurement_record_index; /// Which observable this measurement was responsible for measuring. std::vector measured_observable; bool operator<(const FlippedMeasurement &other) const; /// Standard methods for easy testing. bool operator==(const FlippedMeasurement &other) const; bool operator!=(const FlippedMeasurement &other) const; std::string str() const; }; /// Describes a specific range of targets within a parameterized instruction. struct CircuitTargetsInsideInstruction { /// The instruction type. GateType gate_type; std::string gate_tag; /// The parens arguments for the instruction. std::vector args; /// The range of targets within the instruction that were executing. size_t target_range_start; size_t target_range_end; std::vector targets_in_range; void fill_args_and_targets_in_range( const CircuitInstruction &actual_op, const std::map> &qubit_coords); bool operator<(const CircuitTargetsInsideInstruction &other) const; /// Standard methods for easy testing. bool operator==(const CircuitTargetsInsideInstruction &other) const; bool operator!=(const CircuitTargetsInsideInstruction &other) const; std::string str() const; }; /// Describes the location of an error within a circuit, with as much extra information /// as possible in order to make it easier for users to grok the location. struct CircuitErrorLocation { /// The tag on the noise instruction. std::string noise_tag; /// The number of ticks that have been executed by this point. uint64_t tick_offset; /// The pauli terms corresponding to the circuit error. /// For non-measurement errors, this is the actual pauli error that triggers the problem. /// For measurement errors, this is the observable that was being measured. std::vector flipped_pauli_product; /// Determines if the circuit error was a measurement error. /// UINT64_MAX means NOT a measurement error. /// Other values refer to a specific measurement index in the measurement record. FlippedMeasurement flipped_measurement; /// These two values identify a specific range of targets within the executing instruction. CircuitTargetsInsideInstruction instruction_targets; // Stack trace within the circuit and nested loop blocks. std::vector stack_frames; void canonicalize(); bool operator<(const CircuitErrorLocation &other) const; bool is_simpler_than(const CircuitErrorLocation &other) const; /// Standard methods for easy testing. bool operator==(const CircuitErrorLocation &other) const; bool operator!=(const CircuitErrorLocation &other) const; std::string str() const; }; /// Explains how an error from a detector error model matches error(s) from a circuit. struct ExplainedError { /// A sorted list of detector and observable targets flipped by the error. /// This list should never contain target separators. std::vector dem_error_terms; /// Locations of matching errors in the circuit. std::vector circuit_error_locations; void fill_in_dem_targets( SpanRef targets, const std::map> &dem_coords); void canonicalize(); /// Standard methods for easy testing. std::string str() const; bool operator==(const ExplainedError &other) const; bool operator!=(const ExplainedError &other) const; }; std::ostream &operator<<(std::ostream &out, const CircuitErrorLocation &e); std::ostream &operator<<(std::ostream &out, const CircuitErrorLocationStackFrame &e); std::ostream &operator<<(std::ostream &out, const CircuitTargetsInsideInstruction &e); std::ostream &operator<<(std::ostream &out, const DemTargetWithCoords &e); std::ostream &operator<<(std::ostream &out, const FlippedMeasurement &e); std::ostream &operator<<(std::ostream &out, const GateTargetWithCoords &e); std::ostream &operator<<(std::ostream &out, const ExplainedError &e); } // namespace stim #endif ================================================ FILE: src/stim/simulators/matched_error.pybind.cc ================================================ // 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. #include "stim/simulators/matched_error.pybind.h" #include "stim/circuit/gate_target.h" #include "stim/circuit/gate_target.pybind.h" #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/gates/gates.h" #include "stim/py/base.pybind.h" #include "stim/simulators/matched_error.h" #include "stim/util_bot/str_util.h" using namespace stim; using namespace stim_pybind; std::string CircuitErrorLocationStackFrame_repr(const CircuitErrorLocationStackFrame &self) { std::stringstream out; out << "stim.CircuitErrorLocationStackFrame("; out << "\n instruction_offset=" << self.instruction_offset << ","; out << "\n iteration_index=" << self.iteration_index << ","; out << "\n instruction_repetitions_arg=" << self.instruction_repetitions_arg << ","; out << "\n)"; return out.str(); } std::string DemTargetWithCoords_repr(const DemTargetWithCoords &self) { std::stringstream out; out << "stim.DemTargetWithCoords"; out << "(dem_target=" << ExposedDemTarget(self.dem_target).repr(); out << ", coords=[" << comma_sep(self.coords) << "]"; out << ")"; return out.str(); } pybind11::ssize_t CircuitTargetsInsideInstruction_hash(const CircuitTargetsInsideInstruction &self) { return pybind11::hash( pybind11::make_tuple( "CircuitTargetsInsideInstruction", self.gate_type == GateType::NOT_A_GATE ? std::string_view("") : GATE_DATA[self.gate_type].name, self.target_range_start, self.target_range_end, tuple_tree(self.targets_in_range), tuple_tree(self.args))); } std::string GateTargetWithCoords_repr(const GateTargetWithCoords &self) { std::stringstream out; out << "stim.GateTargetWithCoords"; out << "(" << self.gate_target; out << ", [" << comma_sep(self.coords) << "]"; out << ")"; return out.str(); } std::string FlippedMeasurement_repr(const FlippedMeasurement &self) { std::stringstream out; out << "stim.FlippedMeasurement("; out << "\n record_index="; if (self.measurement_record_index == UINT64_MAX) { out << "None"; } else { out << self.measurement_record_index; } out << ",\n observable=("; for (const auto &e : self.measured_observable) { out << GateTargetWithCoords_repr(e) << ","; } out << "),\n)"; return out.str(); } std::string CircuitTargetsInsideInstruction_repr(const CircuitTargetsInsideInstruction &self) { std::stringstream out; out << "stim.CircuitTargetsInsideInstruction"; out << "(gate='" << (self.gate_type == GateType::NOT_A_GATE ? "NULL" : GATE_DATA[self.gate_type].name) << "'"; out << ", args=[" << comma_sep(self.args) << "]"; out << ", target_range_start=" << self.target_range_start; out << ", target_range_end=" << self.target_range_end; out << ", targets_in_range=("; for (const auto &e : self.targets_in_range) { out << GateTargetWithCoords_repr(e) << ","; } out << "))"; return out.str(); } std::string CircuitErrorLocation_repr(const CircuitErrorLocation &self) { std::stringstream out; out << "stim.CircuitErrorLocation"; out << "(tick_offset=" << self.tick_offset; out << ", flipped_pauli_product=("; for (const auto &e : self.flipped_pauli_product) { out << GateTargetWithCoords_repr(e) << ","; } out << ")"; out << ", flipped_measurement=" << FlippedMeasurement_repr(self.flipped_measurement); out << ", instruction_targets=" << CircuitTargetsInsideInstruction_repr(self.instruction_targets); out << ", stack_frames=("; for (const auto &e : self.stack_frames) { out << CircuitErrorLocationStackFrame_repr(e) << ","; } out << ")"; out << ")"; return out.str(); } std::string MatchedError_repr(const ExplainedError &self) { std::stringstream out; out << "stim.ExplainedError"; out << "(dem_error_terms=("; for (const auto &e : self.dem_error_terms) { out << DemTargetWithCoords_repr(e) << ","; } out << ")"; out << ", circuit_error_locations=("; for (const auto &e : self.circuit_error_locations) { out << CircuitErrorLocation_repr(e) << ","; } out << ")"; out << ")"; return out.str(); } pybind11::class_ stim_pybind::pybind_circuit_error_location_stack_frame( pybind11::module &m) { return pybind11::class_( m, "CircuitErrorLocationStackFrame", clean_doc_string(R"DOC( Describes the location of an instruction being executed within a circuit or loop, distinguishing between separate loop iterations. The full location of an instruction is a list of these frames, drilling down from the top level circuit to the inner-most loop that the instruction is within. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames[0] stim.CircuitErrorLocationStackFrame( instruction_offset=0, iteration_index=0, instruction_repetitions_arg=5, ) >>> err[0].circuit_error_locations[0].stack_frames[1] stim.CircuitErrorLocationStackFrame( instruction_offset=1, iteration_index=4, instruction_repetitions_arg=0, ) )DOC") .data()); } void stim_pybind::pybind_circuit_error_location_stack_frame_methods( pybind11::module &m, pybind11::class_ &c) { c.def_readonly( "instruction_offset", &CircuitErrorLocationStackFrame::instruction_offset, clean_doc_string(R"DOC( The index of the instruction within the circuit, or within the instruction's parent REPEAT block. This is slightly different from the line number, because blank lines and commented lines don't count and also because the offset of the first instruction is 0 instead of 1. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames[0].instruction_offset 2 )DOC") .data()); c.def_readonly( "iteration_index", &CircuitErrorLocationStackFrame::iteration_index, clean_doc_string(R"DOC( Disambiguates which iteration of the loop containing this instruction is being referred to. If the instruction isn't in a REPEAT block, this field defaults to 0. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> full = err[0].circuit_error_locations[0].stack_frames[0] >>> loop = err[0].circuit_error_locations[0].stack_frames[1] >>> full.iteration_index 0 >>> loop.iteration_index 4 )DOC") .data()); c.def_readonly( "instruction_repetitions_arg", &CircuitErrorLocationStackFrame::instruction_repetitions_arg, clean_doc_string(R"DOC( If the instruction being referred to is a REPEAT block, this is the repetition count of that REPEAT block. Otherwise this field defaults to 0. Examples: >>> import stim >>> err = stim.Circuit(''' ... REPEAT 5 { ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... } ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> full = err[0].circuit_error_locations[0].stack_frames[0] >>> loop = err[0].circuit_error_locations[0].stack_frames[1] >>> full.instruction_repetitions_arg 5 >>> loop.instruction_repetitions_arg 0 )DOC") .data()); c.def(pybind11::self == pybind11::self); c.def(pybind11::self != pybind11::self); c.def("__hash__", [](const CircuitErrorLocationStackFrame &self) { return pybind11::hash( pybind11::make_tuple( "CircuitErrorLocationStackFrame", self.instruction_offset, self.iteration_index, self.instruction_repetitions_arg)); }); c.def( pybind11::init( [](uint64_t instruction_offset, uint64_t iteration_index, uint64_t instruction_repetitions_arg) -> CircuitErrorLocationStackFrame { return CircuitErrorLocationStackFrame{instruction_offset, iteration_index, instruction_repetitions_arg}; }), pybind11::kw_only(), pybind11::arg("instruction_offset"), pybind11::arg("iteration_index"), pybind11::arg("instruction_repetitions_arg"), clean_doc_string(R"DOC( Creates a stim.CircuitErrorLocationStackFrame. Examples: >>> import stim >>> frame = stim.CircuitErrorLocationStackFrame( ... instruction_offset=1, ... iteration_index=2, ... instruction_repetitions_arg=3, ... ) )DOC") .data()); c.def("__str__", &CircuitErrorLocationStackFrame_repr); c.def("__repr__", &CircuitErrorLocationStackFrame_repr); } pybind11::class_ stim_pybind::pybind_gate_target_with_coords(pybind11::module &m) { return pybind11::class_( m, "GateTargetWithCoords", clean_doc_string(R"DOC( A gate target with associated coordinate information. For example, if the gate target is a qubit from a circuit with QUBIT_COORDS instructions, the coords field will contain the coordinate data from the QUBIT_COORDS instruction for the qubit. This is helpful information to have available when debugging a problem in a circuit, instead of having to constantly manually look up the coordinates of a qubit index in order to understand what is happening. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) >>> t.coords [1.5, 2.0] )DOC") .data()); } void stim_pybind::pybind_gate_target_with_coords_methods( pybind11::module &m, pybind11::class_ &c) { c.def_readonly( "gate_target", &GateTargetWithCoords::gate_target, clean_doc_string(R"DOC( Returns the actual gate target as a `stim.GateTarget`. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) )DOC") .data()); c.def_readonly( "coords", &GateTargetWithCoords::coords, clean_doc_string(R"DOC( Returns the associated coordinate information as a list of floats. If there is no coordinate information, returns an empty list. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.coords [1.5, 2.0] )DOC") .data()); c.def(pybind11::self == pybind11::self); c.def(pybind11::self != pybind11::self); c.def("__hash__", [](const GateTargetWithCoords &self) { return pybind11::hash(pybind11::make_tuple("GateTargetWithCoords", self.gate_target, tuple_tree(self.coords))); }); c.def("__str__", &GateTargetWithCoords::str); c.def( pybind11::init( [](const pybind11::object &gate_target, const std::vector &coords) -> GateTargetWithCoords { return GateTargetWithCoords{obj_to_gate_target(gate_target), coords}; }), pybind11::arg("gate_target"), pybind11::arg("coords"), clean_doc_string(R"DOC( Creates a stim.GateTargetWithCoords. Examples: >>> import stim >>> t = stim.GateTargetWithCoords(0, [1.5, 2.0]) >>> t.gate_target stim.GateTarget(0) >>> t.coords [1.5, 2.0] )DOC") .data()); c.def("__repr__", &GateTargetWithCoords_repr); } pybind11::class_ stim_pybind::pybind_dem_target_with_coords(pybind11::module &m) { return pybind11::class_( m, "DemTargetWithCoords", clean_doc_string(R"DOC( A detector error model instruction target with associated coords. It is also guaranteed that, if the type of the DEM target is a relative detector id, it is actually absolute (i.e. relative to 0). For example, if the DEM target is a detector from a circuit with coordinate arguments given to detectors, the coords field will contain the coordinate data for the detector. This is helpful information to have available when debugging a problem in a circuit, instead of having to constantly manually look up the coordinates of a detector index in order to understand what is happening. Examples: >>> import stim >>> t = stim.DemTargetWithCoords(stim.DemTarget("D1"), [1.5, 2.0]) >>> t.dem_target stim.DemTarget('D1') >>> t.coords [1.5, 2.0] )DOC") .data()); } void stim_pybind::pybind_dem_target_with_coords_methods( pybind11::module &m, pybind11::class_ &c) { c.def_property_readonly( "dem_target", [](const DemTargetWithCoords &self) -> ExposedDemTarget { return ExposedDemTarget(self.dem_target); }, clean_doc_string(R"DOC( Returns the actual DEM target as a `stim.DemTarget`. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0].dem_target stim.DemTarget('D0') )DOC") .data()); c.def_readonly( "coords", &DemTargetWithCoords::coords, clean_doc_string(R"DOC( Returns the associated coordinate information as a list of floats. If there is no coordinate information, returns an empty list. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0].coords [2.0, 3.0] )DOC") .data()); c.def(pybind11::self == pybind11::self); c.def(pybind11::self != pybind11::self); c.def("__hash__", [](const DemTargetWithCoords &self) { return pybind11::hash( pybind11::make_tuple("DemTargetWithCoords", self.dem_target.data, tuple_tree(self.coords))); }); c.def("__str__", &DemTargetWithCoords::str); c.def( pybind11::init( [](const ExposedDemTarget &dem_target, const std::vector &coords) -> DemTargetWithCoords { return DemTargetWithCoords{dem_target, coords}; }), pybind11::arg("dem_target"), pybind11::arg("coords"), clean_doc_string(R"DOC( Creates a stim.DemTargetWithCoords. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].dem_error_terms[0] stim.DemTargetWithCoords(dem_target=stim.DemTarget('D0'), coords=[2, 3]) )DOC") .data()); c.def("__repr__", DemTargetWithCoords_repr); } pybind11::class_ stim_pybind::pybind_flipped_measurement(pybind11::module &m) { return pybind11::class_( m, "FlippedMeasurement", clean_doc_string(R"DOC( Describes a measurement that was flipped. Gives the measurement's index in the measurement record, and also the observable of the measurement. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement stim.FlippedMeasurement( record_index=1, observable=(stim.GateTargetWithCoords(stim.target_z(10), []),), ) )DOC") .data()); } void stim_pybind::pybind_flipped_measurement_methods( pybind11::module &m, pybind11::class_ &c) { c.def_readonly( "record_index", &FlippedMeasurement::measurement_record_index, clean_doc_string(R"DOC( The measurement record index of the flipped measurement. For example, the fifth measurement in a circuit has a measurement record index of 4. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement.record_index 1 )DOC") .data()); c.def_readonly( "observable", &FlippedMeasurement::measured_observable, clean_doc_string(R"DOC( Returns the observable of the flipped measurement. For example, an `MX 5` measurement will have the observable X5. Examples: >>> import stim >>> err = stim.Circuit(''' ... M(0.25) 1 10 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement.observable [stim.GateTargetWithCoords(stim.target_z(10), [])] )DOC") .data()); c.def(pybind11::self == pybind11::self); c.def(pybind11::self != pybind11::self); c.def("__hash__", [](const FlippedMeasurement &self) { return pybind11::hash( pybind11::make_tuple( "FlippedMeasurement", self.measurement_record_index, tuple_tree(self.measured_observable))); }); c.def( pybind11::init( [](const pybind11::object &measurement_record_index, const pybind11::object &measured_observable) -> FlippedMeasurement { uint64_t u; if (measurement_record_index.is_none()) { u = UINT64_MAX; } else { u = pybind11::cast(measurement_record_index); } FlippedMeasurement result{u, {}}; for (const auto &e : measured_observable) { result.measured_observable.push_back(pybind11::cast(e)); } return result; }), pybind11::kw_only(), pybind11::arg("record_index"), pybind11::arg("observable"), clean_doc_string(R"DOC( @signature def __init__(self, measurement_record_index: Optional[int], measured_observable: Iterable[stim.GateTargetWithCoords]): Creates a stim.FlippedMeasurement. Examples: >>> import stim >>> print(stim.FlippedMeasurement( ... record_index=5, ... observable=[], ... )) stim.FlippedMeasurement( record_index=5, observable=(), ) )DOC") .data()); c.def("__repr__", &FlippedMeasurement_repr); c.def("__str__", &FlippedMeasurement_repr); } pybind11::class_ stim_pybind::pybind_circuit_targets_inside_instruction( pybind11::module &m) { return pybind11::class_( m, "CircuitTargetsInsideInstruction", clean_doc_string(R"DOC( Describes a range of targets within a circuit instruction. )DOC") .data()); } void stim_pybind::pybind_circuit_targets_inside_instruction_methods( pybind11::module &m, pybind11::class_ &c) { c.def_property_readonly( "gate", [](const CircuitTargetsInsideInstruction &self) -> pybind11::object { if (self.gate_type == GateType::NOT_A_GATE) { return pybind11::none(); } return pybind11::str(GATE_DATA[self.gate_type].name); }, clean_doc_string(R"DOC( Returns the name of the gate / instruction that was being executed. @signature def gate(self) -> Optional[str]: Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.gate 'X_ERROR' )DOC") .data()); c.def_property_readonly( "tag", [](const CircuitTargetsInsideInstruction &self) { return self.gate_tag; }, clean_doc_string(R"DOC( Returns the tag of the gate / instruction that was being executed. @signature def tag(self) -> str: Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR[look-at-me-imma-tag](0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.tag 'look-at-me-imma-tag' )DOC") .data()); c.def_readonly( "target_range_start", &CircuitTargetsInsideInstruction::target_range_start, clean_doc_string(R"DOC( Returns the inclusive start of the range of targets that were executing within the gate / instruction. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.target_range_start 0 >>> loc.instruction_targets.target_range_end 1 )DOC") .data()); c.def_readonly( "target_range_end", &CircuitTargetsInsideInstruction::target_range_end, clean_doc_string(R"DOC( Returns the exclusive end of the range of targets that were executing within the gate / instruction. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.target_range_start 0 >>> loc.instruction_targets.target_range_end 1 )DOC") .data()); c.def_readonly( "args", &CircuitTargetsInsideInstruction::args, clean_doc_string(R"DOC( Returns parens arguments of the gate / instruction that was being executed. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.args [0.25] )DOC") .data()); c.def_readonly( "targets_in_range", &CircuitTargetsInsideInstruction::targets_in_range, clean_doc_string(R"DOC( Returns the subset of targets of the gate/instruction that were being executed. Includes coordinate data with the targets. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 1 ... X_ERROR(0.25) 0 1 ... M 0 1 ... DETECTOR(2, 3) rec[-1] rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> loc: stim.CircuitErrorLocation = err[0].circuit_error_locations[0] >>> loc.instruction_targets.targets_in_range [stim.GateTargetWithCoords(0, [])] )DOC") .data()); c.def(pybind11::self == pybind11::self); c.def(pybind11::self != pybind11::self); c.def("__hash__", &CircuitTargetsInsideInstruction_hash); c.def( pybind11::init( [](std::string_view gate, std::string_view tag, const std::vector &args, size_t target_range_start, size_t target_range_end, const std::vector &targets_in_range) -> CircuitTargetsInsideInstruction { CircuitTargetsInsideInstruction result{ GATE_DATA.at(gate).id, std::string(tag), args, target_range_start, target_range_end, targets_in_range}; return result; }), pybind11::kw_only(), pybind11::arg("gate"), pybind11::arg("tag") = "", pybind11::arg("args"), pybind11::arg("target_range_start"), pybind11::arg("target_range_end"), pybind11::arg("targets_in_range"), clean_doc_string(R"DOC( Creates a stim.CircuitTargetsInsideInstruction. Examples: >>> import stim >>> val = stim.CircuitTargetsInsideInstruction( ... gate='X_ERROR', ... tag='', ... args=[0.25], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=[stim.GateTargetWithCoords(0, [])], ... ) )DOC") .data()); c.def("__repr__", &CircuitTargetsInsideInstruction_repr); c.def("__str__", &CircuitTargetsInsideInstruction::str); } pybind11::class_ stim_pybind::pybind_circuit_error_location(pybind11::module &m) { return pybind11::class_( m, "CircuitErrorLocation", clean_doc_string(R"DOC( Describes the location of an error mechanism from a stim circuit. Examples: >>> import stim >>> circuit = stim.Circuit.generated( ... "repetition_code:memory", ... distance=5, ... rounds=5, ... before_round_data_depolarization=1e-3, ... ) >>> logical_error = circuit.shortest_graphlike_error() >>> error_location = logical_error[0].circuit_error_locations[0] >>> print(error_location) CircuitErrorLocation { flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } )DOC") .data()); } void stim_pybind::pybind_circuit_error_location_methods( pybind11::module &m, pybind11::class_ &c) { c.def_readonly( "tick_offset", &CircuitErrorLocation::tick_offset, clean_doc_string(R"DOC( The number of TICKs that executed before the error happened. This counts TICKs occurring multiple times during loops. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... TICK ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].tick_offset 3 )DOC") .data()); c.def_readonly( "noise_tag", &CircuitErrorLocation::noise_tag, clean_doc_string(R"DOC( The tag on the noise instruction that caused the error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... Y_ERROR[test-tag](0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].noise_tag 'test-tag' )DOC") .data()); c.def_readonly( "flipped_pauli_product", &CircuitErrorLocation::flipped_pauli_product, clean_doc_string(R"DOC( The Pauli errors that the error mechanism applied to qubits. When the error is a measurement error, this will be an empty list. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_pauli_product [stim.GateTargetWithCoords(stim.target_y(0), [])] )DOC") .data()); c.def_property_readonly( "flipped_measurement", [](const CircuitErrorLocation &self) -> pybind11::object { if (self.flipped_measurement.measured_observable.empty()) { return pybind11::none(); } return pybind11::cast(self.flipped_measurement); }, clean_doc_string(R"DOC( @signature def flipped_measurement(self) -> Optional[stim.FlippedMeasurement]: The measurement that was flipped by the error mechanism. If the error isn't a measurement error, this will be None. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... M(0.125) 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].flipped_measurement stim.FlippedMeasurement( record_index=0, observable=(stim.GateTargetWithCoords(stim.target_z(0), []),), ) )DOC") .data()); c.def_readonly( "instruction_targets", &CircuitErrorLocation::instruction_targets, clean_doc_string(R"DOC( Within the error instruction, which may have hundreds of targets, which specific targets were being executed to produce the error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> targets = err[0].circuit_error_locations[0].instruction_targets >>> targets == stim.CircuitTargetsInsideInstruction( ... gate='Y_ERROR', ... args=[0.125], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=(stim.GateTargetWithCoords(0, []),), ... ) True )DOC") .data()); c.def_readonly( "stack_frames", &CircuitErrorLocation::stack_frames, clean_doc_string(R"DOC( Describes where in the circuit's execution the error happened. Multiple frames are needed because the error may occur within a loop, or a loop nested inside a loop, or etc. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> err[0].circuit_error_locations[0].stack_frames [stim.CircuitErrorLocationStackFrame( instruction_offset=2, iteration_index=0, instruction_repetitions_arg=0, )] )DOC") .data()); c.def(pybind11::self == pybind11::self); c.def(pybind11::self != pybind11::self); c.def("__hash__", [](const CircuitErrorLocation &self) { return pybind11::hash( pybind11::make_tuple( "CircuitErrorLocation", self.tick_offset, tuple_tree(self.flipped_pauli_product), self.flipped_measurement, self.instruction_targets, tuple_tree(self.stack_frames))); }); c.def( pybind11::init( [](uint64_t tick_offset, const std::vector &flipped_pauli_product, const pybind11::object &flipped_measurement, const CircuitTargetsInsideInstruction &instruction_targets, const std::vector &stack_frames, std::string_view noise_tag) -> CircuitErrorLocation { FlippedMeasurement m{0, {}}; if (!flipped_measurement.is_none()) { m = pybind11::cast(flipped_measurement); } CircuitErrorLocation result{ std::string(noise_tag), tick_offset, flipped_pauli_product, m, instruction_targets, stack_frames}; return result; }), pybind11::kw_only(), pybind11::arg("tick_offset"), pybind11::arg("flipped_pauli_product"), pybind11::arg("flipped_measurement"), pybind11::arg("instruction_targets"), pybind11::arg("stack_frames"), pybind11::arg("noise_tag") = "", clean_doc_string(R"DOC( Creates a stim.CircuitErrorLocation. Examples: >>> import stim >>> err = stim.CircuitErrorLocation( ... tick_offset=1, ... flipped_pauli_product=( ... stim.GateTargetWithCoords( ... gate_target=stim.target_x(0), ... coords=[], ... ), ... ), ... flipped_measurement=stim.FlippedMeasurement( ... record_index=None, ... observable=(), ... ), ... instruction_targets=stim.CircuitTargetsInsideInstruction( ... gate='DEPOLARIZE1', ... args=[0.001], ... target_range_start=0, ... target_range_end=1, ... targets_in_range=(stim.GateTargetWithCoords( ... gate_target=0, ... coords=[], ... ),) ... ), ... stack_frames=( ... stim.CircuitErrorLocationStackFrame( ... instruction_offset=2, ... iteration_index=0, ... instruction_repetitions_arg=0, ... ), ... ), ... noise_tag='test-tag', ... ) >>> print(err) CircuitErrorLocation { noise_tag: test-tag flipped_pauli_product: X0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (DEPOLARIZE1) in the circuit at target #1 of the instruction resolving to DEPOLARIZE1(0.001) 0 } )DOC") .data()); c.def("__repr__", &CircuitErrorLocation_repr); c.def("__str__", &CircuitErrorLocation::str); } pybind11::class_ stim_pybind::pybind_explained_error(pybind11::module &m) { return pybind11::class_( m, "ExplainedError", clean_doc_string(R"DOC( Describes the location of an error mechanism from a stim circuit. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0]) ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } } )DOC") .data()); } void stim_pybind::pybind_explained_error_methods(pybind11::module &m, pybind11::class_ &c) { c.def_readonly( "dem_error_terms", &ExplainedError::dem_error_terms, clean_doc_string(R"DOC( The detectors and observables flipped by this error mechanism. )DOC") .data()); c.def_readonly( "circuit_error_locations", &ExplainedError::circuit_error_locations, clean_doc_string(R"DOC( The locations of circuit errors that produce the symptoms in dem_error_terms. Note: if this list contains a single entry, it may be because a result with a single representative error was requested (as opposed to all possible errors). Note: if this list is empty, it may be because there was a DEM error decomposed into parts where one of the parts is impossible to make on its own from a single circuit error. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0].circuit_error_locations[0]) CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } )DOC") .data()); c.def(pybind11::self == pybind11::self); c.def(pybind11::self != pybind11::self); c.def("__hash__", [](const ExplainedError &self) { return pybind11::hash( pybind11::make_tuple( "ExplainedError", tuple_tree(self.dem_error_terms), tuple_tree(self.circuit_error_locations))); }); c.def( pybind11::init( [](const std::vector dem_error_terms, const std::vector &circuit_error_locations) -> ExplainedError { ExplainedError result{ dem_error_terms, circuit_error_locations, }; return result; }), pybind11::kw_only(), pybind11::arg("dem_error_terms"), pybind11::arg("circuit_error_locations"), clean_doc_string(R"DOC( Creates a stim.ExplainedError. Examples: >>> import stim >>> err = stim.Circuit(''' ... R 0 ... TICK ... Y_ERROR(0.125) 0 ... M 0 ... OBSERVABLE_INCLUDE(0) rec[-1] ... ''').shortest_graphlike_error() >>> print(err[0]) ExplainedError { dem_error_terms: L0 CircuitErrorLocation { flipped_pauli_product: Y0 Circuit location stack trace: (after 1 TICKs) at instruction #3 (Y_ERROR) in the circuit at target #1 of the instruction resolving to Y_ERROR(0.125) 0 } } )DOC") .data()); c.def("__repr__", &MatchedError_repr); c.def("__str__", &ExplainedError::str); } ================================================ FILE: src/stim/simulators/matched_error.pybind.h ================================================ // 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. #ifndef _STIM_SIMULATORS_MATCHED_ERROR_PYBIND_H #define _STIM_SIMULATORS_MATCHED_ERROR_PYBIND_H #include #include "stim/simulators/matched_error.h" namespace stim_pybind { pybind11::class_ pybind_circuit_error_location_stack_frame(pybind11::module &m); pybind11::class_ pybind_gate_target_with_coords(pybind11::module &m); pybind11::class_ pybind_dem_target_with_coords(pybind11::module &m); pybind11::class_ pybind_flipped_measurement(pybind11::module &m); pybind11::class_ pybind_circuit_targets_inside_instruction(pybind11::module &m); pybind11::class_ pybind_circuit_error_location(pybind11::module &m); pybind11::class_ pybind_explained_error(pybind11::module &m); void pybind_circuit_error_location_stack_frame_methods( pybind11::module &m, pybind11::class_ &c); void pybind_gate_target_with_coords_methods(pybind11::module &m, pybind11::class_ &c); void pybind_dem_target_with_coords_methods(pybind11::module &m, pybind11::class_ &c); void pybind_flipped_measurement_methods(pybind11::module &m, pybind11::class_ &c); void pybind_circuit_targets_inside_instruction_methods( pybind11::module &m, pybind11::class_ &c); void pybind_circuit_error_location_methods(pybind11::module &m, pybind11::class_ &c); void pybind_explained_error_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/simulators/matched_error.test.cc ================================================ // 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. #include "gtest/gtest.h" #include "stim/simulators/error_matcher.h" using namespace stim; TEST(matched_error, FlippedMeasurement_basics) { } TEST(matched_error, DemTargetWithCoords_basics) { DemTargetWithCoords c{DemTarget::relative_detector_id(5), {1, 2, 3}}; DemTargetWithCoords c2{DemTarget::relative_detector_id(5)}; ASSERT_EQ(c.str(), "D5[coords 1,2,3]"); ASSERT_EQ(c2.str(), "D5"); ASSERT_TRUE(c2 == (DemTargetWithCoords{DemTarget::relative_detector_id(5), {}})); ASSERT_FALSE(c2 != (DemTargetWithCoords{DemTarget::relative_detector_id(5), {}})); ASSERT_TRUE(c != c2); ASSERT_FALSE(c == c2); ASSERT_NE(c, c2); ASSERT_EQ(c, (DemTargetWithCoords{DemTarget::relative_detector_id(5), {1, 2, 3}})); ASSERT_NE(c, (DemTargetWithCoords{DemTarget::relative_detector_id(5), {1, 2, 4}})); ASSERT_NE(c, (DemTargetWithCoords{DemTarget::relative_detector_id(6), {1, 2, 3}})); } TEST(matched_error, GateTargetWithCoords_basics) { GateTargetWithCoords c{GateTarget::qubit(5), {1, 2, 3}}; GateTargetWithCoords c2{GateTarget::qubit(5)}; GateTargetWithCoords c3{GateTarget::x(5), {1, 2, 3}}; ASSERT_EQ(c.str(), "5[coords 1,2,3]"); ASSERT_EQ(c2.str(), "5"); ASSERT_EQ(c3.str(), "X5[coords 1,2,3]"); ASSERT_TRUE(c2 == (GateTargetWithCoords{GateTarget::qubit(5), {}})); ASSERT_FALSE(c2 != (GateTargetWithCoords{GateTarget::qubit(5), {}})); ASSERT_TRUE(c != c2); ASSERT_FALSE(c == c2); ASSERT_NE(c, c2); ASSERT_EQ(c, (GateTargetWithCoords{GateTarget::qubit(5), {1, 2, 3}})); ASSERT_NE(c, (GateTargetWithCoords{GateTarget::qubit(5), {1, 2, 4}})); ASSERT_NE(c, (GateTargetWithCoords{GateTarget::qubit(6), {1, 2, 3}})); } TEST(matched_error, CircuitErrorLocationStackFrame_basics) { CircuitErrorLocationStackFrame c{1, 2, 3}; ASSERT_EQ( c.str(), "CircuitErrorLocationStackFrame{" "instruction_offset=1, " "iteration_index=2, " "instruction_repetitions_arg=3}"); ASSERT_TRUE(c == (CircuitErrorLocationStackFrame{1, 2, 3})); ASSERT_FALSE(c != (CircuitErrorLocationStackFrame{1, 2, 3})); ASSERT_TRUE(c != (CircuitErrorLocationStackFrame{2, 2, 3})); ASSERT_FALSE(c == (CircuitErrorLocationStackFrame{2, 2, 3})); ASSERT_EQ(c, (CircuitErrorLocationStackFrame{1, 2, 3})); ASSERT_NE(c, (CircuitErrorLocationStackFrame{9, 2, 3})); ASSERT_NE(c, (CircuitErrorLocationStackFrame{1, 9, 3})); ASSERT_NE(c, (CircuitErrorLocationStackFrame{1, 2, 9})); } TEST(matched_error, CircuitTargetsInsideInstruction_basics) { CircuitTargetsInsideInstruction targets{ GateType::X_ERROR, "", {0.125}, 11, 17, { {GateTarget::qubit(5), {1, 2, 3}}, {GateTarget::x(6), {}}, {GateTarget::combiner(), {}}, {GateTarget::y(9), {3, 4}}, {GateTarget::rec(-5), {}}, }, }; ASSERT_EQ(targets.str(), "X_ERROR(0.125) 5[coords 1,2,3] X6*Y9[coords 3,4] rec[-5]"); ASSERT_TRUE(targets == targets); ASSERT_FALSE(targets != targets); auto targets2 = targets; targets2.target_range_start++; ASSERT_TRUE(targets != targets2); ASSERT_FALSE(targets == targets2); } TEST(matched_error, CircuitTargetsInsideInstruction_fill) { CircuitTargetsInsideInstruction not_filled{ GateType::X_ERROR, "", {0.125}, 2, 5, {}, }; not_filled.fill_args_and_targets_in_range( Circuit("X_ERROR(0.125) 0 1 2 3 4 5 6 7 8 9").operations[0], {{4, {11, 13}}}); ASSERT_EQ(not_filled.str(), "X_ERROR(0.125) 2 3 4[coords 11,13]"); } TEST(matched_error, CircuitErrorLocation_basics) { CircuitErrorLocation loc{ "", 6, { {GateTarget::x(3), {11, 12}}, {GateTarget::z(5), {}}, }, FlippedMeasurement{ 5, { {GateTarget::x(3), {}}, {GateTarget::y(4), {14, 15}}, }, }, CircuitTargetsInsideInstruction{ GateType::X_ERROR, "", {0.125}, 11, 17, { {GateTarget::qubit(5), {1, 2, 3}}, {GateTarget::x(6), {}}, {GateTarget::combiner(), {}}, {GateTarget::y(9), {3, 4}}, {GateTarget::rec(-5), {}}, }, }, { CircuitErrorLocationStackFrame{9, 0, 100}, CircuitErrorLocationStackFrame{13, 15, 0}, }, }; ASSERT_EQ(loc.str(), R"RESULT(CircuitErrorLocation { flipped_pauli_product: X3[coords 11,12]*Z5 flipped_measurement.measurement_record_index: 5 flipped_measurement.measured_observable: X3*Y4[coords 14,15] Circuit location stack trace: (after 6 TICKs) at instruction #10 (a REPEAT 100 block) in the circuit after 15 completed iterations at instruction #14 (X_ERROR) in the REPEAT block at targets #12 to #17 of the instruction resolving to X_ERROR(0.125) 5[coords 1,2,3] X6*Y9[coords 3,4] rec[-5] })RESULT"); ASSERT_TRUE(loc == loc); ASSERT_FALSE(loc != loc); auto loc2 = loc; loc2.tick_offset++; ASSERT_TRUE(loc != loc2); ASSERT_FALSE(loc == loc2); } TEST(matched_error, MatchedError_basics) { CircuitErrorLocation loc{ "", 6, { {GateTarget::x(3), {11, 12}}, {GateTarget::z(5), {}}, }, FlippedMeasurement{ 5, { {GateTarget::x(3), {}}, {GateTarget::y(4), {14, 15}}, }, }, CircuitTargetsInsideInstruction{ GateType::X_ERROR, "", {0.125}, 11, 17, { {GateTarget::qubit(5), {1, 2, 3}}, {GateTarget::x(6), {}}, {GateTarget::combiner(), {}}, {GateTarget::y(9), {3, 4}}, {GateTarget::rec(-5), {}}, }, }, { CircuitErrorLocationStackFrame{9, 0, 100}, CircuitErrorLocationStackFrame{13, 15, 0}, }, }; CircuitErrorLocation loc2 = loc; loc2.tick_offset++; ExplainedError err{ { {DemTarget::relative_detector_id(5), {1, 2}}, {DemTarget::observable_id(5), {}}, }, {loc, loc2}, }; ExplainedError err2{ { {DemTarget::relative_detector_id(5), {1, 2}}, }, {loc2, loc}, }; ASSERT_TRUE(err == err); ASSERT_FALSE(err != err); ASSERT_TRUE(err != err2); ASSERT_FALSE(err == err2); ASSERT_EQ(err.str(), R"RESULT(ExplainedError { dem_error_terms: D5[coords 1,2] L5 CircuitErrorLocation { flipped_pauli_product: X3[coords 11,12]*Z5 flipped_measurement.measurement_record_index: 5 flipped_measurement.measured_observable: X3*Y4[coords 14,15] Circuit location stack trace: (after 6 TICKs) at instruction #10 (a REPEAT 100 block) in the circuit after 15 completed iterations at instruction #14 (X_ERROR) in the REPEAT block at targets #12 to #17 of the instruction resolving to X_ERROR(0.125) 5[coords 1,2,3] X6*Y9[coords 3,4] rec[-5] } CircuitErrorLocation { flipped_pauli_product: X3[coords 11,12]*Z5 flipped_measurement.measurement_record_index: 5 flipped_measurement.measured_observable: X3*Y4[coords 14,15] Circuit location stack trace: (after 7 TICKs) at instruction #10 (a REPEAT 100 block) in the circuit after 15 completed iterations at instruction #14 (X_ERROR) in the REPEAT block at targets #12 to #17 of the instruction resolving to X_ERROR(0.125) 5[coords 1,2,3] X6*Y9[coords 3,4] rec[-5] } })RESULT"); } TEST(matched_error, MatchedError_fill) { ExplainedError err{{}, {}}; err.fill_in_dem_targets( std::vector{DemTarget::relative_detector_id(5), DemTarget::relative_detector_id(6)}, {{5, {11, 13}}}); ASSERT_EQ(err.str(), R"RESULT(ExplainedError { dem_error_terms: D5[coords 11,13] D6 [no single circuit error had these exact symptoms] })RESULT"); } ================================================ FILE: src/stim/simulators/matched_error_pybind_test.py ================================================ import stim def test_CircuitErrorLocationStackFrame(): v1 = stim.CircuitErrorLocationStackFrame( instruction_offset=1, iteration_index=2, instruction_repetitions_arg=3, ) assert v1.instruction_offset == 1 assert v1.iteration_index == 2 assert v1.instruction_repetitions_arg == 3 v2 = stim.CircuitErrorLocationStackFrame( instruction_offset=2, iteration_index=3, instruction_repetitions_arg=5, ) assert v1 != v2 assert v1 == v1 assert len({v1, v1, v2}) == 2 # Check hashable. assert eval(repr(v1), {"stim": stim}) == v1 assert eval(repr(v2), {"stim": stim}) == v2 assert str(v1) == repr(v1) def test_GateTargetWithCoords(): v1 = stim.GateTargetWithCoords( gate_target=stim.target_x(5), coords=[1, 2, 3], ) assert v1.gate_target == stim.GateTarget(stim.target_x(5)) assert v1.coords == [1, 2, 3] v2 = stim.GateTargetWithCoords( gate_target=stim.GateTarget(4), coords=[1, 2], ) assert v1 != v2 assert v1 == v1 assert len({v1, v1, v2}) == 2 # Check hashable. assert eval(repr(v1), {"stim": stim}) == v1 assert eval(repr(v2), {"stim": stim}) == v2 assert str(v1) == 'X5[coords 1,2,3]' def test_DemTargetWithCoords(): v1 = stim.DemTargetWithCoords( dem_target=stim.DemTarget.relative_detector_id(5), coords=[1, 2, 3], ) assert v1.dem_target == stim.DemTarget.relative_detector_id(5) assert v1.coords == [1, 2, 3] v2 = stim.DemTargetWithCoords( dem_target=stim.DemTarget.logical_observable_id(3), coords=(), ) assert v1 != v2 assert v1 == v1 assert len({v1, v1, v2}) == 2 # Check hashable. assert eval(repr(v1), {"stim": stim}) == v1 assert eval(repr(v2), {"stim": stim}) == v2 assert str(v1) == 'D5[coords 1,2,3]' def test_FlippedMeasurement(): v1 = stim.FlippedMeasurement( record_index=5, observable=[ stim.GateTargetWithCoords( gate_target=stim.target_x(5), coords=[1, 2, 3]), ], ) assert v1.record_index == 5 assert v1.observable == [ stim.GateTargetWithCoords( gate_target=stim.target_x(5), coords=[1, 2, 3]), ] v2 = stim.FlippedMeasurement( record_index=5, observable=[], ) assert v1 != v2 assert v1 == v1 assert len({v1, v1, v2}) == 2 # Check hashable. assert eval(repr(v1), {"stim": stim}) == v1 assert eval(repr(v2), {"stim": stim}) == v2 assert str(v1) == repr(v1) def test_CircuitTargetsInsideInstruction(): v1 = stim.CircuitTargetsInsideInstruction( gate="X_ERROR", args=[0.25], target_range_start=2, target_range_end=5, targets_in_range=[ stim.GateTargetWithCoords(gate_target=5, coords=[1, 2]), stim.GateTargetWithCoords(gate_target=6, coords=[1, 3]), stim.GateTargetWithCoords(gate_target=7, coords=[]), ], ) assert v1.gate == "X_ERROR" assert v1.args == [0.25] assert v1.target_range_start == 2 assert v1.target_range_end == 5 assert v1.targets_in_range == [ stim.GateTargetWithCoords(gate_target=5, coords=[1, 2]), stim.GateTargetWithCoords(gate_target=6, coords=[1, 3]), stim.GateTargetWithCoords(gate_target=7, coords=[]), ] v2 = stim.CircuitTargetsInsideInstruction( gate="Z_ERROR", args=[0.125], target_range_start=3, target_range_end=3, targets_in_range=[], ) assert v1 != v2 assert v1 == v1 assert len({v1, v1, v2}) == 2 # Check hashable. assert eval(repr(v1), {"stim": stim}) == v1 assert eval(repr(v2), {"stim": stim}) == v2 assert str(v1) == "X_ERROR(0.25) 5[coords 1,2] 6[coords 1,3] 7" def test_CircuitErrorLocation(): m = stim.FlippedMeasurement( record_index=5, observable=[ stim.GateTargetWithCoords( gate_target=stim.target_x(5), coords=[1, 2, 3]), ], ) p = [ stim.GateTargetWithCoords( gate_target=stim.target_y(6), coords=[1, 2, 3]), ] t = stim.CircuitTargetsInsideInstruction( gate="X_ERROR", args=[0.25], target_range_start=2, target_range_end=5, targets_in_range=[ stim.GateTargetWithCoords(gate_target=5, coords=[1, 2]), stim.GateTargetWithCoords(gate_target=6, coords=[1, 3]), stim.GateTargetWithCoords(gate_target=7, coords=[]), ], ) s = [ stim.CircuitErrorLocationStackFrame( instruction_offset=1, iteration_index=2, instruction_repetitions_arg=3, ) ] * 2 v1 = stim.CircuitErrorLocation( tick_offset=5, flipped_pauli_product=p, flipped_measurement=m, instruction_targets=t, stack_frames=s, ) assert v1.tick_offset == 5 assert v1.flipped_pauli_product == p assert v1.flipped_measurement == m assert v1.instruction_targets == t assert v1.stack_frames == s v2 = stim.CircuitErrorLocation( tick_offset=5, flipped_pauli_product=[], flipped_measurement=None, instruction_targets=t, stack_frames=[], ) assert v2.flipped_measurement is None assert v1 != v2 assert v1 == v1 assert len({v1, v1, v2}) == 2 # Check hashable. assert eval(repr(v1), {"stim": stim}) == v1 assert eval(repr(v2), {"stim": stim}) == v2 assert str(v1) == """CircuitErrorLocation { flipped_pauli_product: Y6[coords 1,2,3] flipped_measurement.measurement_record_index: 5 flipped_measurement.measured_observable: X5[coords 1,2,3] Circuit location stack trace: (after 5 TICKs) at instruction #2 (a REPEAT 3 block) in the circuit after 2 completed iterations at instruction #2 (X_ERROR) in the REPEAT block at targets #3 to #5 of the instruction resolving to X_ERROR(0.25) 5[coords 1,2] 6[coords 1,3] 7 }""" def test_MatchedError(): m = stim.FlippedMeasurement( record_index=5, observable=[ stim.GateTargetWithCoords( gate_target=stim.target_x(5), coords=[1, 2, 3]), ], ) p = [ stim.GateTargetWithCoords( gate_target=stim.target_y(6), coords=[1, 2, 3]), ] t = stim.CircuitTargetsInsideInstruction( gate="X_ERROR", args=[0.25], target_range_start=2, target_range_end=5, targets_in_range=[ stim.GateTargetWithCoords(gate_target=5, coords=[1, 2]), stim.GateTargetWithCoords(gate_target=6, coords=[1, 3]), stim.GateTargetWithCoords(gate_target=7, coords=[]), ], ) s = [ stim.CircuitErrorLocationStackFrame( instruction_offset=1, iteration_index=2, instruction_repetitions_arg=3, ) ] * 2 e = stim.CircuitErrorLocation( tick_offset=5, flipped_pauli_product=p, flipped_measurement=m, instruction_targets=t, stack_frames=s, ) v1 = stim.ExplainedError( dem_error_terms=[stim.DemTargetWithCoords( dem_target=stim.DemTarget.relative_detector_id(5), coords=[1, 2, 3], )], circuit_error_locations=[e], ) assert v1.dem_error_terms == [stim.DemTargetWithCoords( dem_target=stim.DemTarget.relative_detector_id(5), coords=[1, 2, 3], )] assert v1.circuit_error_locations == [e] v2 = stim.ExplainedError( dem_error_terms=[], circuit_error_locations=[], ) assert v1 != v2 assert v1 == v1 assert len({v1, v1, v2}) == 2 # Check hashable. assert eval(repr(v1), {"stim": stim}) == v1 assert eval(repr(v2), {"stim": stim}) == v2 assert str(v1) == """ExplainedError { dem_error_terms: D5[coords 1,2,3] CircuitErrorLocation { flipped_pauli_product: Y6[coords 1,2,3] flipped_measurement.measurement_record_index: 5 flipped_measurement.measured_observable: X5[coords 1,2,3] Circuit location stack trace: (after 5 TICKs) at instruction #2 (a REPEAT 3 block) in the circuit after 2 completed iterations at instruction #2 (X_ERROR) in the REPEAT block at targets #3 to #5 of the instruction resolving to X_ERROR(0.25) 5[coords 1,2] 6[coords 1,3] 7 } }""" ================================================ FILE: src/stim/simulators/measurements_to_detection_events.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_MEASUREMENTS_TO_DETECTION_EVENTS_H #define _STIM_SIMULATORS_MEASUREMENTS_TO_DETECTION_EVENTS_H #include #include #include #include #include #include #include "stim/circuit/circuit.h" #include "stim/io/measure_record.h" #include "stim/stabilizers/tableau.h" #include "stim/stabilizers/tableau_transposed_raii.h" namespace stim { /// Reads measurement data from a file, converts it to detection event data, and writes that out to another file. /// /// Args: /// measurements_in: The file to read measurement data from. /// input_format: The format of the measurement data in the file. /// optional_sweep_bits_in: An optional file containing sweep data for each shot in the measurements file. /// sweep_bits_in_format: The format of the sweep data file. Ignored when optional_sweep_bits_in == nullptr. /// results_out: The file to write detection event data to. /// output_format: The format to use when writing the detection event data. /// circuit: The circuit that the measurement data corresponds to, with DETECTOR and OBSERVABLE_INCLUDE annotations /// indicating how to perform the conversion. /// append_observables: Whether or not to include observable flip data in the detection event data. /// skip_reference_sample: When set to True, the reference sample used by the conversion is initialized to /// all-zeroes instead of being collected from the circuit. This should probably only be done if you know the /// all-zero sample is a valid sample, or if you know that the measurements were generated by a frame simulator /// that was also incorrectly assuming an all-zero reference sample. template void stream_measurements_to_detection_events( FILE *measurements_in, SampleFormat measurements_in_format, FILE *optional_sweep_bits_in, SampleFormat sweep_bits_in_format, FILE *results_out, SampleFormat results_out_format, const Circuit &circuit, bool append_observables, bool skip_reference_sample, FILE *obs_out, SampleFormat obs_out_format); /// A variant of `stim::stream_measurements_to_detection_events` with derived values passed in, not recomputed. template void stream_measurements_to_detection_events_helper( FILE *measurements_in, SampleFormat measurements_in_format, FILE *optional_sweep_bits_in, SampleFormat sweep_bits_in_format, FILE *results_out, SampleFormat results_out_format, const Circuit &circuit, CircuitStats circuit_stats, bool append_observables, simd_bits_range_ref reference_sample, FILE *obs_out, SampleFormat obs_out_format); /// Converts measurement data into detection event data based on a circuit. /// /// Args: /// measurements__minor_shot_index: Recorded measurement data. /// Major axis: measurement bit index. /// Minor axis: shot index. /// sweep_bits__minor_shot_index: Per-shot configuration data controlling operations like `CNOT sweep[0] 1`. /// Major axis: sweep bit index. /// Minor axis: shot index. /// /// To not specify sweep data, set the major axis length of sweep_bits__minor_shot_index to 0 (the minor axis /// length must still match the number of shots). The major axis can also be partially truncated. Sweep bits /// beyond that length default to False. /// circuit: The circuit that the measurement data corresponds to, with DETECTOR and OBSERVABLE_INCLUDE annotations /// indicating how to perform the conversion. /// append_observables: Whether or not to include observable flip data in the detection event data. /// skip_reference_sample: When set to True, the reference sample used by the conversion is initialized to /// all-zeroes instead of being collected from the circuit. This should probably only be done if you know the /// all-zero sample is a valid sample, or if you know that the measurements were generated by a frame simulator /// that was also incorrectly assuming an all-zero reference sample. /// /// Returns: /// Detection event data. Major axis is detector index (+ observable index). Minor axis is shot index. template simd_bit_table measurements_to_detection_events( const simd_bit_table &measurements__minor_shot_index, const simd_bit_table &sweep_bits__minor_shot_index, const Circuit &circuit, bool append_observables, bool skip_reference_sample); /// A variant of `stim::measurements_to_detection_events` with derived values passed in, not recomputed. template void measurements_to_detection_events_helper( const simd_bit_table &measurements__minor_shot_index, const simd_bit_table &sweep_bits__minor_shot_index, simd_bit_table &out_detection_results__minor_shot_index, const Circuit &noiseless_circuit, CircuitStats circuit_stats, const simd_bits &reference_sample, bool append_observables); } // namespace stim #include "stim/simulators/measurements_to_detection_events.inl" #endif ================================================ FILE: src/stim/simulators/measurements_to_detection_events.inl ================================================ // 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. #include #include "stim/gates/gates.h" #include "stim/io/measure_record_batch_writer.h" #include "stim/io/measure_record_reader.h" #include "stim/io/stim_data_formats.h" #include "stim/mem/simd_util.h" #include "stim/simulators/frame_simulator.h" #include "stim/simulators/measurements_to_detection_events.h" #include "stim/simulators/tableau_simulator.h" #include "stim/stabilizers/pauli_string.h" namespace stim { template void measurements_to_detection_events_helper( const simd_bit_table &measurements__minor_shot_index, const simd_bit_table &sweep_bits__minor_shot_index, simd_bit_table &out_detection_results__minor_shot_index, const Circuit &noiseless_circuit, CircuitStats circuit_stats, const simd_bits &reference_sample, bool append_observables) { // Tables should agree on the batch size. size_t batch_size = out_detection_results__minor_shot_index.num_minor_bits_padded(); if (measurements__minor_shot_index.num_minor_bits_padded() != batch_size) { throw std::invalid_argument("measurements__minor_shot_index.num_minor_bits_padded() != batch_size"); } if (sweep_bits__minor_shot_index.num_minor_bits_padded() != batch_size) { throw std::invalid_argument("sweep_bits__minor_shot_index.num_minor_bits_padded() != batch_size"); } // Tables should have the right number of bits per shot. if (out_detection_results__minor_shot_index.num_major_bits_padded() < circuit_stats.num_detectors + circuit_stats.num_observables * append_observables) { throw std::invalid_argument( "out_detection_results__minor_shot_index.num_major_bits_padded() < num_detectors + num_observables * " "append_observables"); } if (measurements__minor_shot_index.num_major_bits_padded() < circuit_stats.num_measurements) { throw std::invalid_argument("measurements__minor_shot_index.num_major_bits_padded() < num_measurements"); } // The frame simulator is used to account for flips in the measurement results that originate from the sweep data. // Eg. a `CNOT sweep[5] 0` can bit flip qubit 0, which can invert later measurement results, which will invert the // expected parity of detectors involving that measurement. This can vary from shot to shot. FrameSimulator frame_sim( circuit_stats, FrameSimulatorMode::STREAM_DETECTIONS_TO_DISK, batch_size, std::mt19937_64(0)); frame_sim.sweep_table = sweep_bits__minor_shot_index; frame_sim.guarantee_anticommutation_via_frame_randomization = false; uint64_t detector_offset = 0; uint64_t measure_count_so_far = 0; noiseless_circuit.for_each_operation([&](const CircuitInstruction &op) { frame_sim.do_gate(op); switch (op.gate_type) { case GateType::DETECTOR: { simd_bits_range_ref out_row = out_detection_results__minor_shot_index[detector_offset]; detector_offset++; // Include dependence from gates controlled by sweep bits. out_row ^= frame_sim.det_record.lookback(1); bool expectation = false; for (const auto &t : op.targets) { uint32_t lookback = t.data & TARGET_VALUE_MASK; // Include dependence from physical measurement results. out_row ^= measurements__minor_shot_index[measure_count_so_far - lookback]; // Include dependence from reference sample expectation. expectation ^= reference_sample[measure_count_so_far - lookback]; } if (expectation) { out_row.invert_bits(); } frame_sim.det_record.clear(); break; } case GateType::OBSERVABLE_INCLUDE: { simd_bits_range_ref obs_row = frame_sim.obs_record[(uint64_t)op.args[0]]; bool expectation = false; for (const auto &t : op.targets) { if (t.is_classical_bit_target()) { uint32_t lookback = t.data & TARGET_VALUE_MASK; // Include dependence from physical measurement results. obs_row ^= measurements__minor_shot_index[measure_count_so_far - lookback]; // Include dependence from reference sample expectation. expectation ^= reference_sample[measure_count_so_far - lookback]; } else if (t.is_pauli_target()) { // Ignored. } else { throw std::invalid_argument("Unexpected target for OBSERVABLE_INCLUDE: " + t.str()); } } if (expectation) { obs_row.invert_bits(); } break; } default: measure_count_so_far += op.count_measurement_results(); } }); if (append_observables) { for (size_t k = 0; k < circuit_stats.num_observables; k++) { // Include dependence from gates controlled by sweep bits. out_detection_results__minor_shot_index[circuit_stats.num_detectors + k] ^= frame_sim.obs_record[k]; } } // Safety check verifying no randomness was used by the frame simulator. std::mt19937_64 fresh_rng(0); if (frame_sim.rng() != fresh_rng() || frame_sim.rng() != fresh_rng() || frame_sim.rng() != fresh_rng()) { throw std::invalid_argument("Something is wrong. Converting measurements consumed entropy, but it shouldn't."); } } template simd_bit_table measurements_to_detection_events( const simd_bit_table &measurements__minor_shot_index, const simd_bit_table &sweep_bits__minor_shot_index, const Circuit &circuit, bool append_observables, bool skip_reference_sample) { CircuitStats circuit_stats = circuit.compute_stats(); simd_bits reference_sample(circuit_stats.num_measurements); if (!skip_reference_sample) { reference_sample = TableauSimulator::reference_sample_circuit(circuit); } simd_bit_table out( circuit_stats.num_detectors + circuit_stats.num_observables * append_observables, measurements__minor_shot_index.num_minor_bits_padded()); measurements_to_detection_events_helper( measurements__minor_shot_index, sweep_bits__minor_shot_index, out, circuit.aliased_noiseless_circuit(), circuit_stats, reference_sample, append_observables); return out; } template void stream_measurements_to_detection_events( FILE *measurements_in, SampleFormat measurements_in_format, FILE *optional_sweep_bits_in, SampleFormat sweep_bits_format, FILE *results_out, SampleFormat results_out_format, const Circuit &circuit, bool append_observables, bool skip_reference_sample, FILE *obs_out, SampleFormat obs_out_format) { // Circuit metadata. CircuitStats circuit_stats = circuit.compute_stats(); simd_bits reference_sample(circuit_stats.num_measurements); Circuit noiseless_circuit = circuit.aliased_noiseless_circuit(); if (!skip_reference_sample) { reference_sample = TableauSimulator::reference_sample_circuit(circuit); } stream_measurements_to_detection_events_helper( measurements_in, measurements_in_format, optional_sweep_bits_in, sweep_bits_format, results_out, results_out_format, noiseless_circuit, circuit_stats, append_observables, reference_sample, obs_out, obs_out_format); } template void stream_measurements_to_detection_events_helper( FILE *measurements_in, SampleFormat measurements_in_format, FILE *optional_sweep_bits_in, SampleFormat sweep_bits_in_format, FILE *results_out, SampleFormat results_out_format, const Circuit &noiseless_circuit, CircuitStats circuit_stats, bool append_observables, simd_bits_range_ref reference_sample, FILE *obs_out, SampleFormat obs_out_format) { bool internally_append_observables = append_observables || obs_out != nullptr; size_t num_out_bits_including_any_obs = circuit_stats.num_detectors + circuit_stats.num_observables * internally_append_observables; size_t num_sweep_bits_available = optional_sweep_bits_in == nullptr ? 0 : circuit_stats.num_sweep_bits; size_t num_buffered_shots = 1024; // Readers / writers. auto reader = MeasureRecordReader::make(measurements_in, measurements_in_format, circuit_stats.num_measurements); std::unique_ptr> sweep_data_reader; std::unique_ptr obs_writer; if (obs_out != nullptr) { obs_writer = MeasureRecordWriter::make(obs_out, obs_out_format); } auto writer = MeasureRecordWriter::make(results_out, results_out_format); if (optional_sweep_bits_in != nullptr) { sweep_data_reader = MeasureRecordReader::make(optional_sweep_bits_in, sweep_bits_in_format, circuit_stats.num_sweep_bits); } // Buffers and transposed buffers. simd_bit_table measurements__minor_shot_index(circuit_stats.num_measurements, num_buffered_shots); simd_bit_table out__minor_shot_index(num_out_bits_including_any_obs, num_buffered_shots); simd_bit_table out__major_shot_index(num_buffered_shots, num_out_bits_including_any_obs); simd_bit_table sweep_bits__minor_shot_index(num_sweep_bits_available, num_buffered_shots); if (reader->expects_empty_serialized_data_for_each_shot()) { throw std::invalid_argument( "Can't tell how many shots are in the measurement data.\n" "The circuit has no measurements and the measurement format encodes empty shots into no bytes."); } // Data streaming loop. size_t total_read = 0; while (true) { // Read measurement data and sweep data for a batch of shots. size_t record_count = reader->read_records_into(measurements__minor_shot_index, false); if (sweep_data_reader != nullptr) { size_t sweep_data_count = sweep_data_reader->read_records_into(sweep_bits__minor_shot_index, false); if (sweep_data_count != record_count && !sweep_data_reader->expects_empty_serialized_data_for_each_shot()) { std::stringstream ss; ss << "The sweep data contained a different number of shots than the measurement data.\n"; ss << "There was " << (record_count + total_read) << " shot records total.\n"; if (sweep_data_count < record_count) { ss << "But there was " << (record_count + sweep_data_count) << " sweep records total."; } else { ss << "But there was at least " << (record_count + sweep_data_count) << " sweep records."; } throw std::invalid_argument(ss.str()); } } if (record_count == 0) { break; } total_read += record_count; // Convert measurement data into detection event data. out__minor_shot_index.clear(); measurements_to_detection_events_helper( measurements__minor_shot_index, sweep_bits__minor_shot_index, out__minor_shot_index, noiseless_circuit, circuit_stats, reference_sample, internally_append_observables); out__minor_shot_index.transpose_into(out__major_shot_index); // Write detection event data. for (size_t k = 0; k < record_count; k++) { simd_bits_range_ref record = out__major_shot_index[k]; writer->begin_result_type('D'); writer->write_bits(record.u8, circuit_stats.num_detectors); if (append_observables) { writer->begin_result_type('L'); for (size_t k2 = 0; k2 < circuit_stats.num_observables; k2++) { writer->write_bit(record[circuit_stats.num_detectors + k2]); } } writer->write_end(); if (obs_out != nullptr) { obs_writer->begin_result_type('L'); for (size_t k2 = 0; k2 < circuit_stats.num_observables; k2++) { obs_writer->write_bit(record[circuit_stats.num_detectors + k2]); } obs_writer->write_end(); } } } } } // namespace stim ================================================ FILE: src/stim/simulators/measurements_to_detection_events.pybind.cc ================================================ // 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. #include "stim/simulators/measurements_to_detection_events.pybind.h" #include "stim/circuit/circuit.pybind.h" #include "stim/io/raii_file.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" #include "stim/simulators/measurements_to_detection_events.h" #include "stim/simulators/tableau_simulator.h" using namespace stim; using namespace stim_pybind; CompiledMeasurementsToDetectionEventsConverter::CompiledMeasurementsToDetectionEventsConverter( simd_bits ref_sample, Circuit circuit, bool skip_reference_sample) : skip_reference_sample(skip_reference_sample), ref_sample(ref_sample), circuit_stats(circuit.compute_stats()), circuit(std::move(circuit)) { } std::string CompiledMeasurementsToDetectionEventsConverter::repr() const { std::stringstream result; result << "stim.CompiledMeasurementsToDetectionEventsConverter("; result << circuit_repr(circuit); if (skip_reference_sample) { result << ", skip_reference_sample=True"; } result << ")"; return result.str(); } void CompiledMeasurementsToDetectionEventsConverter::convert_file( std::string_view measurements_filepath, std::string_view measurements_format, const char *sweep_bits_filepath, std::string_view sweep_bits_format, std::string_view detection_events_filepath, std::string_view detection_events_format, bool append_observables, const char *obs_out_filepath, std::string_view obs_out_format) { auto format_in = format_to_enum(measurements_format); auto format_sweep_bits = format_to_enum(sweep_bits_format); auto format_out = format_to_enum(detection_events_format); RaiiFile file_in(measurements_filepath, "rb"); RaiiFile obs_out(obs_out_filepath, "wb"); RaiiFile sweep_bits_in(sweep_bits_filepath, "rb"); RaiiFile detections_out(detection_events_filepath, "wb"); auto parsed_obs_out_format = format_to_enum(obs_out_format); stream_measurements_to_detection_events_helper( file_in.f, format_in, sweep_bits_in.f, format_sweep_bits, detections_out.f, format_out, circuit.aliased_noiseless_circuit(), circuit_stats, append_observables, ref_sample, obs_out.f, parsed_obs_out_format); } pybind11::object CompiledMeasurementsToDetectionEventsConverter::convert( const pybind11::object &measurements, const pybind11::object &sweep_bits, const pybind11::object &separate_observables_obj, const pybind11::object &append_observables_obj, bool bit_pack_result_old_compat, bool bit_pack_result) { bit_pack_result |= bit_pack_result_old_compat; if (separate_observables_obj.is_none() && append_observables_obj.is_none()) { throw std::invalid_argument( "To ignore observable flip data, you must explicitly specify either separate_observables=False or " "append_observables=False."); } bool separate_observables = pybind11::cast(separate_observables_obj); bool append_observables = pybind11::cast(append_observables_obj); size_t num_shots; simd_bit_table measurements_minor_shot_index = numpy_array_to_transposed_simd_table(measurements, circuit_stats.num_measurements, &num_shots); simd_bit_table sweep_bits_minor_shot_index{0, num_shots}; if (!sweep_bits.is_none()) { size_t num_sweep_shots; sweep_bits_minor_shot_index = numpy_array_to_transposed_simd_table(sweep_bits, circuit_stats.num_sweep_bits, &num_sweep_shots); if (num_shots != num_sweep_shots) { throw std::invalid_argument("Need sweep_bits.shape[0] == measurements.shape[0]"); } } size_t num_intermediate_bits = circuit_stats.num_detectors + circuit_stats.num_observables * (append_observables || separate_observables); simd_bit_table out_detection_results_minor_shot_index(num_intermediate_bits, num_shots); stim::measurements_to_detection_events_helper( measurements_minor_shot_index, sweep_bits_minor_shot_index, out_detection_results_minor_shot_index, circuit.aliased_noiseless_circuit(), circuit_stats, ref_sample, append_observables || separate_observables); size_t num_output_bits = circuit_stats.num_detectors + circuit_stats.num_observables * append_observables; pybind11::object obs_data = pybind11::none(); if (separate_observables) { simd_bit_table obs_table(circuit_stats.num_observables, num_shots); for (size_t obs = 0; obs < circuit_stats.num_observables; obs++) { auto obs_slice = out_detection_results_minor_shot_index[circuit_stats.num_detectors + obs]; obs_table[obs] = obs_slice; if (!append_observables) { obs_slice.clear(); } } obs_data = simd_bit_table_to_numpy( obs_table, circuit_stats.num_observables, num_shots, bit_pack_result, true, pybind11::none()); } // Caution: only do this after extracting the observable data, lest it leak into the packed bytes. pybind11::object det_data = simd_bit_table_to_numpy( out_detection_results_minor_shot_index, num_output_bits, num_shots, bit_pack_result, true, pybind11::none()); if (separate_observables) { return pybind11::make_tuple(det_data, obs_data); } return det_data; } pybind11::class_ stim_pybind::pybind_compiled_measurements_to_detection_events_converter(pybind11::module &m) { return pybind11::class_( m, "CompiledMeasurementsToDetectionEventsConverter", "A tool for quickly converting measurements from an analyzed stabilizer circuit into detection events."); } CompiledMeasurementsToDetectionEventsConverter stim_pybind::py_init_compiled_measurements_to_detection_events_converter( const Circuit &circuit, bool skip_reference_sample) { simd_bits ref_sample = skip_reference_sample ? simd_bits(circuit.count_measurements()) : TableauSimulator::reference_sample_circuit(circuit); return CompiledMeasurementsToDetectionEventsConverter(ref_sample, circuit, skip_reference_sample); } void stim_pybind::pybind_compiled_measurements_to_detection_events_converter_methods( pybind11::module &m, pybind11::class_ &c) { using SerializationTuple = std::tuple; c.def( pybind11::init(&py_init_compiled_measurements_to_detection_events_converter), pybind11::arg("circuit"), pybind11::kw_only(), pybind11::arg("skip_reference_sample") = false, clean_doc_string(R"DOC( Creates a measurement-to-detection-events converter for the given circuit. The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is. Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit. Args: circuit: The stim circuit to use for conversions. skip_reference_sample: Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution). Returns: An initialized stim.CompiledMeasurementsToDetectionEventsConverter. Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> converter.convert( ... measurements=np.array([[0], [1]], dtype=np.bool_), ... append_observables=False, ... ) array([[ True], [False]]) )DOC") .data()); c.def( "convert_file", &CompiledMeasurementsToDetectionEventsConverter::convert_file, pybind11::kw_only(), pybind11::arg("measurements_filepath"), pybind11::arg("measurements_format") = "01", pybind11::arg("sweep_bits_filepath") = pybind11::none(), pybind11::arg("sweep_bits_format") = "01", pybind11::arg("detection_events_filepath"), pybind11::arg("detection_events_format") = "01", pybind11::arg("append_observables") = false, pybind11::arg("obs_out_filepath") = nullptr, pybind11::arg("obs_out_format") = "01", clean_doc_string(R"DOC( @signature def convert_file(self, *, measurements_filepath: Union[str, pathlib.Path], measurements_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', sweep_bits_filepath: Optional[Union[str, pathlib.Path]] = None, sweep_bits_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', detection_events_filepath: Union[str, pathlib.Path], detection_events_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01', append_observables: bool = False, obs_out_filepath: Optional[Union[str, pathlib.Path]] = None, obs_out_format: Literal["01", "b8", "r8", "ptb64", "hits", "dets"] = '01') -> None: Reads measurement data from a file and writes detection events to another file. Args: measurements_filepath: A file containing measurement data to be converted. measurements_format: The format the measurement data is stored in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". detection_events_filepath: Where to save detection event data to. detection_events_format: The format to save the detection event data in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". sweep_bits_filepath: Defaults to None. A file containing sweep data, or None. When specified, sweep data (used for `sweep[k]` controls in the circuit, which can vary from shot to shot) will be read from the given file. When not specified, all sweep bits default to False and no sweep-controlled operations occur. sweep_bits_format: The format the sweep data is stored in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". obs_out_filepath: Sample observables as part of each shot, and write them to this file. This keeps the observable data separate from the detector data. obs_out_format: If writing the observables to a file, this is the format to write them in. Valid values are "01", "b8", "r8", "hits", "dets", and "ptb64". Defaults to "01". append_observables: When True, the observables in the circuit are included as part of the detection event data. Specifically, they are treated as if they were additional detectors at the end of the circuit. When False, observable data is not output. Examples: >>> import stim >>> import tempfile >>> converter = stim.Circuit(''' ... X 0 ... M 0 ... DETECTOR rec[-1] ... ''').compile_m2d_converter() >>> with tempfile.TemporaryDirectory() as d: ... with open(f"{d}/measurements.01", "w") as f: ... print("0", file=f) ... print("1", file=f) ... converter.convert_file( ... measurements_filepath=f"{d}/measurements.01", ... detection_events_filepath=f"{d}/detections.01", ... append_observables=False, ... ) ... with open(f"{d}/detections.01") as f: ... print(f.read(), end="") 1 0 )DOC") .data()); c.def( "convert", &CompiledMeasurementsToDetectionEventsConverter::convert, pybind11::kw_only(), pybind11::arg("measurements"), pybind11::arg("sweep_bits") = pybind11::none(), pybind11::arg("separate_observables") = pybind11::none(), pybind11::arg("append_observables") = pybind11::none(), pybind11::arg("bit_packed") = false, pybind11::arg("bit_pack_result") = false, // deprecated variant clean_doc_string(R"DOC( Converts measurement data into detection event data. @overload def convert(self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, append_observables: bool = False, bit_packed: bool = False) -> np.ndarray: @overload def convert(self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, separate_observables: Literal[True], append_observables: bool = False, bit_packed: bool = False) -> Tuple[np.ndarray, np.ndarray]: @signature def convert(self, *, measurements: np.ndarray, sweep_bits: Optional[np.ndarray] = None, separate_observables: bool = False, append_observables: bool = False, bit_packed: bool = False) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: Args: measurements: A numpy array containing measurement data. The dtype of the array is used to determine if it is bit packed or not. dtype=np.bool_ (unpacked data): shape=(num_shots, circuit.num_measurements) dtype=np.uint8 (bit packed data): shape=(num_shots, math.ceil(circuit.num_measurements / 8)) sweep_bits: Optional. A numpy array containing sweep data for the `sweep[k]` controls in the circuit. The dtype of the array is used to determine if it is bit packed or not. dtype=np.bool_ (unpacked data): shape=(num_shots, circuit.num_sweep_bits) dtype=np.uint8 (bit packed data): shape=(num_shots, math.ceil(circuit.num_sweep_bits / 8)) separate_observables: Defaults to False. When set to True, two numpy arrays are returned instead of one, with the second array containing the observable flip data. append_observables: Defaults to False. When set to True, the observables in the circuit are treated as if they were additional detectors. Their results are appended to the end of the detection event data. bit_packed: Defaults to False. When set to True, the returned numpy array contains bit packed data (dtype=np.uint8 with 8 bits per item) instead of unpacked data (dtype=np.bool_). Returns: The detection event data and (optionally) observable data. The result is a single numpy array if separate_observables is false, otherwise it's a tuple of two numpy arrays. When returning two numpy arrays, the first array is the detection event data and the second is the observable flip data. The dtype of the returned arrays is np.bool_ if bit_packed is false, otherwise they're np.uint8 arrays. shape[0] of the array(s) is the number of shots. shape[1] of the array(s) is the number of bits per shot (divided by 8 if bit packed) (e.g. for just detection event data it would be circuit.num_detectors). Examples: >>> import stim >>> import numpy as np >>> converter = stim.Circuit(''' ... X 0 ... M 0 1 ... DETECTOR rec[-1] ... DETECTOR rec[-2] ... OBSERVABLE_INCLUDE(0) rec[-2] ... ''').compile_m2d_converter() >>> dets, obs = converter.convert( ... measurements=np.array([[1, 0], ... [1, 0], ... [1, 0], ... [0, 0], ... [1, 0]], dtype=np.bool_), ... separate_observables=True, ... ) >>> dets array([[False, False], [False, False], [False, False], [False, True], [False, False]]) >>> obs array([[False], [False], [False], [ True], [False]]) )DOC") .data()); c.def( "__repr__", &CompiledMeasurementsToDetectionEventsConverter::repr, "Returns text that is a valid python expression evaluating to an equivalent " "`stim.CompiledMeasurementsToDetectionEventsConverter`."); c.def( pybind11::pickle( // __getstate__ function: returns a tuple to be pickled. [](const CompiledMeasurementsToDetectionEventsConverter &self) -> SerializationTuple { size_t num_ref_bits = self.circuit_stats.num_measurements; pybind11::object ref_sample_numpy = stim_pybind::simd_bits_to_numpy( self.ref_sample, num_ref_bits, /*bit_packed=*/true); return SerializationTuple(self.circuit, self.skip_reference_sample, ref_sample_numpy, num_ref_bits); }, // __setstate__ function: reconstructs the object from the Python tuple. [](SerializationTuple t_py) { const auto &[circuit, skip_ref, ref_bits_npy, num_ref_bits] = t_py; stim::simd_bits reconstructed_ref_sample(num_ref_bits); stim_pybind::memcpy_bits_from_numpy_to_simd(num_ref_bits, ref_bits_npy, reconstructed_ref_sample); return CompiledMeasurementsToDetectionEventsConverter( std::move(reconstructed_ref_sample), circuit, skip_ref); })); } ================================================ FILE: src/stim/simulators/measurements_to_detection_events.pybind.h ================================================ // 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. #ifndef _STIM_PY_SIMULATORS_MEASUREMENTS_TO_DETECTION_EVENTS_PYBIND_H #define _STIM_PY_SIMULATORS_MEASUREMENTS_TO_DETECTION_EVENTS_PYBIND_H #include #include #include #include "stim/circuit/circuit.h" #include "stim/mem/simd_bits.h" namespace stim_pybind { struct CompiledMeasurementsToDetectionEventsConverter { const bool skip_reference_sample; const stim::simd_bits ref_sample; const stim::CircuitStats circuit_stats; const stim::Circuit circuit; CompiledMeasurementsToDetectionEventsConverter() = delete; CompiledMeasurementsToDetectionEventsConverter(const CompiledMeasurementsToDetectionEventsConverter &) = delete; CompiledMeasurementsToDetectionEventsConverter(CompiledMeasurementsToDetectionEventsConverter &&) = default; CompiledMeasurementsToDetectionEventsConverter( stim::simd_bits ref_sample, stim::Circuit circuit, bool skip_reference_sample); pybind11::object convert( const pybind11::object &measurements, const pybind11::object &sweep_bits, const pybind11::object &separate_observables, const pybind11::object &append_observables, bool bit_pack_result_old_compat, bool bit_pack_result); void convert_file( std::string_view measurements_filepath, std::string_view measurements_format, const char *sweep_bits_filepath, std::string_view sweep_bits_format, std::string_view detection_events_filepath, std::string_view detection_events_format, bool append_observables, const char *obs_out_filepath, std::string_view obs_out_format); std::string repr() const; }; pybind11::class_ pybind_compiled_measurements_to_detection_events_converter(pybind11::module &m); void pybind_compiled_measurements_to_detection_events_converter_methods( pybind11::module &m, pybind11::class_ &c); CompiledMeasurementsToDetectionEventsConverter py_init_compiled_measurements_to_detection_events_converter( const stim::Circuit &circuit, bool skip_reference_sample); } // namespace stim_pybind #endif ================================================ FILE: src/stim/simulators/measurements_to_detection_events.test.cc ================================================ // 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. #include "stim/simulators/measurements_to_detection_events.h" #include "gtest/gtest.h" #include "stim/gen/gen_surface_code.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, single_detector_no_sweep_data, { simd_bit_table measurement_data(1, 256); simd_bit_table sweep_data(0, 256); simd_bit_table converted(1, 256); // Matches false expectation. ASSERT_EQ( measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M 0 DETECTOR rec[-1] )CIRCUIT"), false, false), simd_bit_table::from_text("0", 1, 256)); // Violates true expectation. converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M !0 DETECTOR rec[-1] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], true); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( X 0 M 0 DETECTOR rec[-1] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], true); // Violates false expectation. measurement_data[0][0] = true; converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M 0 DETECTOR rec[-1] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], true); // Matches true expectation. converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M !0 DETECTOR rec[-1] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], false); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( X 0 M 0 DETECTOR rec[-1] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], false); // Indexing. converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M 0 1 DETECTOR rec[-1] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], false); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M 0 1 DETECTOR rec[-2] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], true); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M 0 1 DETECTOR rec[-1] rec[-2] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], true); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M 0 1 DETECTOR )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], false); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, sweep_data, { auto expected = simd_bit_table::from_text(R"DETECTOR_DATA( ..1.....1 111....1. .._1..1.. ..1..111. ..1.1..11 ..11..... 11_...... .1_1..... 1.1...11. ..1....11 11_1..1_1 )DETECTOR_DATA"); for (size_t k = 11; k < expected.num_major_bits_padded(); k++) { expected[k][2] = true; } ASSERT_EQ( measurements_to_detection_events( simd_bit_table::from_text(R"MEASUREMENT_DATA( .........1 ........1. .......1.. ......1... .....1.... ....1..... ...1...... ..1....... .1........ 1......... .......... )MEASUREMENT_DATA") .transposed(), simd_bit_table::from_text(R"SWEEP_DATA( .... 1... .1.. ..1. ...1 .... 1... .1.. ..1. ...1 1111 )SWEEP_DATA") .transposed(), Circuit(R"CIRCUIT( CNOT sweep[0] 1 sweep[0] 2 CNOT sweep[1] 3 sweep[1] 4 CNOT sweep[3] 8 sweep[3] 9 CNOT sweep[2] 8 sweep[2] 7 X 3 X_ERROR(1) 4 M 0 1 2 3 4 5 6 7 8 9 DETECTOR rec[-9] DETECTOR rec[-8] DETECTOR rec[-7] DETECTOR rec[-6] DETECTOR rec[-5] DETECTOR rec[-4] DETECTOR rec[-3] DETECTOR rec[-2] DETECTOR rec[-1] )CIRCUIT"), false, false) .transposed(), expected); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, empty_cases, { simd_bit_table measurement_data(256, 256); simd_bit_table converted(256, 256); simd_bit_table sweep_data(0, 256); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( )CIRCUIT"), false, false); ASSERT_EQ(converted.num_major_bits_padded(), 0); ASSERT_EQ(converted.num_minor_bits_padded(), 256); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( X 0 M 0 )CIRCUIT"), false, false); ASSERT_EQ(converted.num_major_bits_padded(), 0); ASSERT_EQ(converted.num_minor_bits_padded(), 256); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, big_shots, { simd_bit_table measurement_data(256, 512); simd_bit_table converted(256, 512); simd_bit_table sweep_data(0, 512); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M 0 !1 REPEAT 50 { DETECTOR rec[-2] DETECTOR rec[-1] } )CIRCUIT"), false, false); ASSERT_EQ(converted[0].popcnt(), 0); ASSERT_EQ(converted[1].popcnt(), 512); ASSERT_EQ(converted[2].popcnt(), 0); ASSERT_EQ(converted[3].popcnt(), 512); ASSERT_EQ(converted[98].popcnt(), 0); ASSERT_EQ(converted[99].popcnt(), 512); ASSERT_EQ(converted[100].popcnt(), 0); ASSERT_EQ(converted[101].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, big_data, { simd_bit_table measurement_data(512, 256); simd_bit_table converted(512, 256); simd_bit_table sweep_data(0, 256); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( M 0 !1 REPEAT 200 { DETECTOR rec[-2] DETECTOR rec[-1] } )CIRCUIT"), false, false); ASSERT_EQ(converted[0].popcnt(), 0); ASSERT_EQ(converted[1].popcnt(), 256); ASSERT_EQ(converted[2].popcnt(), 0); ASSERT_EQ(converted[3].popcnt(), 256); ASSERT_EQ(converted[398].popcnt(), 0); ASSERT_EQ(converted[399].popcnt(), 256); ASSERT_EQ(converted[400].popcnt(), 0); ASSERT_EQ(converted[401].popcnt(), 0); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, append_observables, { simd_bit_table measurement_data(256, 256); simd_bit_table sweep_data(0, 256); simd_bit_table converted(256, 256); size_t min_bits = sizeof(simd_word) * 8; // Appended. converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( X 0 M 0 OBSERVABLE_INCLUDE(9) rec[-1] )CIRCUIT"), true, false); ASSERT_EQ(converted.num_major_bits_padded(), min_bits); ASSERT_EQ(converted.num_minor_bits_padded(), 256); ASSERT_EQ(converted[0][0], 0); ASSERT_EQ(converted[1][0], 0); ASSERT_EQ(converted[9][0], 1); ASSERT_EQ(converted[11][0], 0); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( X 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(9) rec[-1] DETECTOR rec[-1] )CIRCUIT"), true, false); ASSERT_EQ(converted.num_major_bits_padded(), min_bits); ASSERT_EQ(converted.num_minor_bits_padded(), 256); ASSERT_EQ(converted[0][0], 1); ASSERT_EQ(converted[1][0], 1); ASSERT_EQ(converted[9][0], 0); ASSERT_EQ(converted[11][0], 1); // Not appended. converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( X 0 M 0 OBSERVABLE_INCLUDE(9) rec[-1] )CIRCUIT"), false, false); ASSERT_EQ(converted.num_major_bits_padded(), 0); ASSERT_EQ(converted.num_minor_bits_padded(), 256); converted = measurements_to_detection_events( measurement_data, sweep_data, Circuit(R"CIRCUIT( X 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(9) rec[-1] DETECTOR rec[-1] )CIRCUIT"), false, false); ASSERT_EQ(converted[0][0], 1); ASSERT_EQ(converted[1][0], 1); ASSERT_EQ(converted[9][0], 0); ASSERT_EQ(converted[11][0], 0); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, file_01_to_dets_no_obs, { FILE *in = tmpfile(); fprintf(in, "%s", "0\n0\n1\n"); rewind(in); FILE *out = tmpfile(); stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_01, nullptr, (SampleFormat)0, out, SampleFormat::SAMPLE_FORMAT_DETS, Circuit(R"CIRCUIT( X 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(9) rec[-1] DETECTOR DETECTOR rec[-1] )CIRCUIT"), false, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); ASSERT_EQ(rewind_read_close(out), "shot D0 D2\nshot D0 D2\nshot\n"); fclose(in); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, file_01_to_dets_yes_obs, { FILE *in = tmpfile(); fprintf(in, "%s", "0\n0\n1\n"); rewind(in); FILE *out = tmpfile(); stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_01, nullptr, (SampleFormat)0, out, SampleFormat::SAMPLE_FORMAT_DETS, Circuit(R"CIRCUIT( X 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(9) rec[-1] DETECTOR DETECTOR rec[-1] )CIRCUIT"), true, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); fclose(in); ASSERT_EQ(rewind_read_close(out), "shot D0 D2 L9\nshot D0 D2 L9\nshot\n"); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, with_error_propagation, { FILE *in = tmpfile(); fprintf( in, "%s", "00\n" "00\n" "00\n" "00\n" "00\n" "00\n" "01\n" "01\n" "11\n" "11\n"); rewind(in); FILE *in_sweep_bits = tmpfile(); fprintf( in_sweep_bits, "%s", "0000\n" "1000\n" "0100\n" "0010\n" "0001\n" "1111\n" "0000\n" "1111\n" "0000\n" "1111\n"); rewind(in_sweep_bits); FILE *out = tmpfile(); stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_01, in_sweep_bits, SampleFormat::SAMPLE_FORMAT_01, out, SampleFormat::SAMPLE_FORMAT_DETS, Circuit(R"CIRCUIT( CX sweep[0] 0 CX sweep[1] 1 CZ sweep[2] 0 CZ sweep[3] 1 H 0 1 CZ 0 1 M 0 MX 1 DETECTOR rec[-1] rec[-2] )CIRCUIT"), true, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); fclose(in); fclose(in_sweep_bits); ASSERT_EQ( rewind_read_close(out), "shot\n" // No error no flip. "shot\n" // X0 doesn't flip. "shot D0\n" // X1 does flip. "shot\n" // Z0 doesn't flip. "shot\n" // Z1 doesn't flip. "shot D0\n" // All together flips. "shot D0\n" // One excited measurement causes a detection event. "shot\n" // All together restores. "shot\n" // Two excited measurements is not a detection. "shot D0\n" // All together still flips. ); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, many_shots, { FILE *in = tmpfile(); std::string expected; for (size_t k = 0; k < 500; k++) { fprintf(in, "%s", "0\n1\n"); expected += "shot D0 D1\nshot\n"; } rewind(in); FILE *out = tmpfile(); stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_01, nullptr, (SampleFormat)0, out, SampleFormat::SAMPLE_FORMAT_DETS, Circuit(R"CIRCUIT( X 0 M 0 DETECTOR rec[-1] DETECTOR rec[-1] )CIRCUIT"), true, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); fclose(in); ASSERT_EQ(rewind_read_close(out), expected); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, many_measurements_and_detectors, { FILE *in = tmpfile(); std::string expected = "shot"; for (size_t k = 0; k < 500; k++) { fprintf(in, "%s", "01"); expected += " D" + std::to_string(2 * k + 1); } expected += "\n"; fprintf(in, "%s", "\n"); rewind(in); FILE *out = tmpfile(); stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_01, nullptr, (SampleFormat)0, out, SampleFormat::SAMPLE_FORMAT_DETS, Circuit(R"CIRCUIT( REPEAT 500 { M 0 1 DETECTOR rec[-2] DETECTOR rec[-1] } )CIRCUIT"), true, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); fclose(in); ASSERT_EQ(rewind_read_close(out), expected); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, file_01_to_01_yes_obs, { FILE *in = tmpfile(); fprintf(in, "%s", "0\n0\n1\n"); rewind(in); FILE *out = tmpfile(); stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_01, nullptr, (SampleFormat)0, out, SampleFormat::SAMPLE_FORMAT_01, Circuit(R"CIRCUIT( X 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(9) rec[-1] DETECTOR DETECTOR rec[-1] )CIRCUIT"), true, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); fclose(in); ASSERT_EQ(rewind_read_close(out), "1010000000001\n1010000000001\n0000000000000\n"); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, empty_input_01_empty_sweep_b8, { FILE *in = tmpfile(); FILE *sweep = tmpfile(); FILE *out = tmpfile(); stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_01, sweep, SampleFormat::SAMPLE_FORMAT_B8, out, SampleFormat::SAMPLE_FORMAT_01, Circuit(), true, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); fclose(in); fclose(sweep); ASSERT_EQ(rewind_read_close(out), ""); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, some_input_01_empty_sweep_b8, { FILE *in = tmpfile(); fprintf(in, "%s", "\n\n"); rewind(in); FILE *sweep = tmpfile(); FILE *out = tmpfile(); stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_01, sweep, SampleFormat::SAMPLE_FORMAT_B8, out, SampleFormat::SAMPLE_FORMAT_01, Circuit(), true, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); fclose(in); fclose(sweep); ASSERT_EQ(rewind_read_close(out), "\n\n"); }) TEST_EACH_WORD_SIZE_W(measurements_to_detection_events, empty_input_b8_empty_sweep_b8, { FILE *in = tmpfile(); FILE *sweep = tmpfile(); FILE *out = tmpfile(); ASSERT_THROW( { stream_measurements_to_detection_events( in, SampleFormat::SAMPLE_FORMAT_B8, sweep, SampleFormat::SAMPLE_FORMAT_B8, out, SampleFormat::SAMPLE_FORMAT_01, Circuit(), true, false, nullptr, SampleFormat::SAMPLE_FORMAT_01); }, std::invalid_argument); fclose(in); fclose(sweep); ASSERT_EQ(rewind_read_close(out), ""); }) ================================================ FILE: src/stim/simulators/measurements_to_detection_events_test.py ================================================ # 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. import tempfile import numpy as np import pytest import pickle import stim def test_convert_file_without_sweep_bits(): converter = stim.Circuit(''' X_ERROR(0.1) 0 X 0 CNOT sweep[0] 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''').compile_m2d_converter() with tempfile.TemporaryDirectory() as d: with open(f"{d}/measurements.01", "w") as f: print("0", file=f) print("1", file=f) converter.convert_file( measurements_filepath=f"{d}/measurements.01", detection_events_filepath=f"{d}/detections.01", append_observables=False, ) with open(f"{d}/detections.01") as f: assert f.read() == "1\n0\n" with tempfile.TemporaryDirectory() as d: with open(f"{d}/measurements.b8", "wb") as f: f.write((0).to_bytes(1, 'big')) f.write((1).to_bytes(1, 'big')) f.write((0).to_bytes(1, 'big')) f.write((1).to_bytes(1, 'big')) with open(f"{d}/sweep.hits", "w") as f: print("", file=f) print("", file=f) print("0", file=f) print("0", file=f) converter.convert_file( measurements_filepath=f"{d}/measurements.b8", measurements_format="b8", sweep_bits_filepath=f"{d}/sweep.hits", sweep_bits_format="hits", detection_events_filepath=f"{d}/detections.dets", detection_events_format="dets", append_observables=True, ) with open(f"{d}/detections.dets") as f: assert f.read() == "shot D0 L0\nshot\nshot\nshot D0 L0\n" converter.convert_file( measurements_filepath=f"{d}/measurements.b8", measurements_format="b8", sweep_bits_filepath=f"{d}/sweep.hits", sweep_bits_format="hits", detection_events_filepath=f"{d}/detections.dets", detection_events_format="dets", obs_out_filepath=f"{d}/obs.hits", obs_out_format="hits", ) with open(f"{d}/detections.dets") as f: assert f.read() == "shot D0\nshot\nshot\nshot D0\n" with open(f"{d}/obs.hits") as f: assert f.read() == "0\n\n\n0\n" def test_convert(): converter = stim.Circuit(''' X_ERROR(0.1) 0 X 0 CNOT sweep[0] 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''').compile_m2d_converter() result = converter.convert( measurements=np.array([[0], [1]], dtype=np.bool_), append_observables=False, ) assert result.dtype == np.bool_ assert result.shape == (2, 1) np.testing.assert_array_equal(result, [[1], [0]]) result = converter.convert( measurements=np.array([[0], [1]], dtype=np.bool_), append_observables=True, ) assert result.dtype == np.bool_ assert result.shape == (2, 2) np.testing.assert_array_equal(result, [[1, 1], [0, 0]]) result = converter.convert( measurements=np.array([[0], [1], [0], [1]], dtype=np.bool_), sweep_bits=np.array([[0], [0], [1], [1]], dtype=np.bool_), append_observables=True, ) assert result.dtype == np.bool_ assert result.shape == (4, 2) np.testing.assert_array_equal(result, [[1, 1], [0, 0], [0, 0], [1, 1]]) def test_convert_bit_packed(): converter = stim.Circuit(''' REPEAT 100 { X_ERROR(0.1) 0 X 0 MR 0 DETECTOR rec[-1] } ''').compile_m2d_converter() measurements = np.array([[0] * 100, [1] * 100], dtype=np.bool_) expected_detections = np.array([[1] * 100, [0] * 100], dtype=np.bool_) measurements_bit_packed = np.packbits(measurements, axis=1, bitorder='little') expected_detections_packed = np.packbits(expected_detections, axis=1, bitorder='little') for m in measurements, measurements_bit_packed: result = converter.convert( measurements=m, append_observables=False, ) assert result.dtype == np.bool_ assert result.shape == (2, 100) np.testing.assert_array_equal(result, expected_detections) # Check legacy argument name `bit_pack_result`. result = converter.convert( measurements=m, append_observables=False, bit_pack_result=True, ) assert result.dtype == np.uint8 assert result.shape == (2, 13) np.testing.assert_array_equal(result, expected_detections_packed) result = converter.convert( measurements=m, append_observables=False, bit_packed=True, ) assert result.dtype == np.uint8 assert result.shape == (2, 13) np.testing.assert_array_equal(result, expected_detections_packed) def test_convert_bit_packed_swept(): converter = stim.Circuit(''' REPEAT 100 { CNOT sweep[0] 0 X_ERROR(0.1) 0 X 0 MR 0 DETECTOR rec[-1] } ''').compile_m2d_converter() measurements = np.array([[0] * 100, [1] * 100], dtype=np.bool_) sweeps = np.array([[1], [0]], dtype=np.bool_) expected_detections = np.array([[0] * 100, [0] * 100], dtype=np.bool_) measurements_packed = np.packbits(measurements, axis=1, bitorder='little') expected_detections_packed = np.packbits(expected_detections, axis=1, bitorder='little') sweeps_packed = np.packbits(sweeps, axis=1, bitorder='little') for m in measurements, measurements_packed: for s in sweeps, sweeps_packed: result = converter.convert( measurements=m, sweep_bits=s, append_observables=False, ) assert result.dtype == np.bool_ assert result.shape == (2, 100) np.testing.assert_array_equal(result, expected_detections) result = converter.convert( measurements=m, sweep_bits=s, append_observables=False, bit_pack_result=True, ) assert result.dtype == np.uint8 assert result.shape == (2, 13) np.testing.assert_array_equal(result, expected_detections_packed) def test_convert_bit_packed_separate_observables(): converter = stim.Circuit(''' REPEAT 100 { X_ERROR(0.1) 0 X 0 MR 0 DETECTOR rec[-1] } OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(6) rec[-2] OBSERVABLE_INCLUDE(14) rec[-3] ''').compile_m2d_converter() measurements = np.array([[0] * 100, [1] * 100], dtype=np.bool_) expected_dets = np.array([[1] * 100, [0] * 100], dtype=np.bool_) expected_obs = np.array([[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], [0] * 15], dtype=np.bool_) measurements_bit_packed = np.packbits(measurements, axis=1, bitorder='little') expected_dets_packed = np.packbits(expected_dets, axis=1, bitorder='little') expected_obs_packed = np.packbits(expected_obs, axis=1, bitorder='little') for m in measurements, measurements_bit_packed: actual_dets, actual_obs = converter.convert( measurements=m, separate_observables=True, ) assert actual_dets.dtype == actual_obs.dtype == np.bool_ assert actual_dets.shape == (2, 100) assert actual_obs.shape == (2, 15) np.testing.assert_array_equal(actual_dets, expected_dets) np.testing.assert_array_equal(actual_obs, expected_obs) actual_dets, actual_obs = converter.convert( measurements=m, separate_observables=True, bit_pack_result=True, ) assert actual_dets.dtype == actual_obs.dtype == np.uint8 assert actual_dets.shape == (2, 13) assert actual_obs.shape == (2, 2) np.testing.assert_array_equal(actual_dets, expected_dets_packed) np.testing.assert_array_equal(actual_obs, expected_obs_packed) def test_noiseless_conversion(): converter = stim.Circuit(''' MR 0 DETECTOR rec[-1] X 0 MR 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''').compile_m2d_converter() measurements = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=np.bool_) expected_dets = np.array([[0, 1], [0, 0], [1, 1], [1, 0]], dtype=np.bool_) expected_obs = np.array([[1], [0], [1], [0]], dtype=np.bool_) actual_dets, actual_obs = converter.convert( measurements=measurements, separate_observables=True, ) assert actual_dets.dtype == actual_obs.dtype == np.bool_ assert actual_dets.shape == (4, 2) assert actual_obs.shape == (4, 1) np.testing.assert_array_equal(actual_dets, expected_dets) np.testing.assert_array_equal(actual_obs, expected_obs) def test_needs_append_or_separate(): converter = stim.Circuit().compile_m2d_converter() ms = np.zeros(shape=(50, 0), dtype=np.bool_) with pytest.raises(ValueError, match="explicitly specify either separate"): converter.convert(measurements=ms) d1 = converter.convert(measurements=ms, append_observables=True) d2, d3 = converter.convert(measurements=ms, separate_observables=True) d4, d5 = converter.convert(measurements=ms, separate_observables=True, append_observables=True) np.testing.assert_array_equal(d1, d2) np.testing.assert_array_equal(d1, d3) np.testing.assert_array_equal(d1, d4) np.testing.assert_array_equal(d1, d5) def test_anticommuting_pieces_combining_into_deterministic_observable(): c = stim.Circuit(''' MX 0 OBSERVABLE_INCLUDE(0) rec[-1] MX 0 OBSERVABLE_INCLUDE(0) rec[-1] ''').without_noise() m = c.compile_sampler().sample_bit_packed(shots=1000) det, obs = c.compile_m2d_converter().convert(measurements=m, separate_observables=True) np.testing.assert_array_equal(obs, obs * 0) def test_converter_pickle(): converter = stim.Circuit(''' X_ERROR(0.1) 0 X 0 CNOT sweep[0] 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] ''').compile_m2d_converter() roundtripped = pickle.loads(pickle.dumps(converter)) assert str(converter) == str(roundtripped) result = roundtripped.convert( measurements=np.array([[0], [1]], dtype=np.bool_), append_observables=False, ) assert result.dtype == np.bool_ assert result.shape == (2, 1) np.testing.assert_array_equal(result, [[1], [0]]) result = roundtripped.convert( measurements=np.array([[0], [1]], dtype=np.bool_), append_observables=True, ) assert result.dtype == np.bool_ assert result.shape == (2, 2) np.testing.assert_array_equal(result, [[1, 1], [0, 0]]) result = roundtripped.convert( measurements=np.array([[0], [1], [0], [1]], dtype=np.bool_), sweep_bits=np.array([[0], [0], [1], [1]], dtype=np.bool_), append_observables=True, ) assert result.dtype == np.bool_ assert result.shape == (4, 2) np.testing.assert_array_equal(result, [[1, 1], [0, 0], [0, 0], [1, 1]]) ================================================ FILE: src/stim/simulators/sparse_rev_frame_tracker.cc ================================================ // 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. #include "stim/simulators/sparse_rev_frame_tracker.h" #include "stim/circuit/gate_decomposition.h" using namespace stim; void SparseUnsignedRevFrameTracker::undo_gate(const CircuitInstruction &inst) { switch (inst.gate_type) { case GateType::DETECTOR: undo_DETECTOR(inst); break; case GateType::OBSERVABLE_INCLUDE: undo_OBSERVABLE_INCLUDE(inst); break; case GateType::MX: undo_MX(inst); break; case GateType::MY: undo_MY(inst); break; case GateType::M: undo_MZ(inst); break; case GateType::MRX: undo_MRX(inst); break; case GateType::MRY: undo_MRY(inst); break; case GateType::MR: undo_MRZ(inst); break; case GateType::RX: undo_RX(inst); break; case GateType::RY: undo_RY(inst); break; case GateType::R: undo_RZ(inst); break; case GateType::MPP: undo_MPP(inst); break; case GateType::SPP: case GateType::SPP_DAG: undo_SPP(inst); break; case GateType::XCX: undo_XCX(inst); break; case GateType::XCY: undo_XCY(inst); break; case GateType::XCZ: undo_XCZ(inst); break; case GateType::YCX: undo_YCX(inst); break; case GateType::YCY: undo_YCY(inst); break; case GateType::YCZ: undo_YCZ(inst); break; case GateType::CX: undo_ZCX(inst); break; case GateType::CY: undo_ZCY(inst); break; case GateType::CZ: undo_ZCZ(inst); break; case GateType::C_XYZ: case GateType::C_NXYZ: case GateType::C_XNYZ: case GateType::C_XYNZ: undo_C_XYZ(inst); break; case GateType::C_ZYX: case GateType::C_NZYX: case GateType::C_ZNYX: case GateType::C_ZYNX: undo_C_ZYX(inst); break; case GateType::SWAP: undo_SWAP(inst); break; case GateType::CXSWAP: undo_CXSWAP(inst); break; case GateType::CZSWAP: undo_CZSWAP(inst); break; case GateType::SWAPCX: undo_SWAPCX(inst); break; case GateType::MXX: undo_MXX(inst); break; case GateType::MYY: undo_MYY(inst); break; case GateType::MZZ: undo_MZZ(inst); break; case GateType::SQRT_XX: case GateType::SQRT_XX_DAG: undo_SQRT_XX(inst); break; case GateType::SQRT_YY: case GateType::SQRT_YY_DAG: undo_SQRT_YY(inst); break; case GateType::SQRT_ZZ: case GateType::SQRT_ZZ_DAG: undo_SQRT_ZZ(inst); break; case GateType::SQRT_X: case GateType::SQRT_X_DAG: case GateType::H_YZ: case GateType::H_NYZ: undo_H_YZ(inst); break; case GateType::SQRT_Y: case GateType::SQRT_Y_DAG: case GateType::H: case GateType::H_NXZ: undo_H_XZ(inst); break; case GateType::S: case GateType::S_DAG: case GateType::H_XY: case GateType::H_NXY: undo_H_XY(inst); break; case GateType::ISWAP: case GateType::ISWAP_DAG: undo_ISWAP(inst); break; case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::REPEAT: case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::PAULI_CHANNEL_1: case GateType::PAULI_CHANNEL_2: case GateType::E: case GateType::ELSE_CORRELATED_ERROR: case GateType::X: case GateType::Y: case GateType::Z: case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: undo_I(inst); break; case GateType::MPAD: case GateType::HERALDED_ERASE: case GateType::HERALDED_PAULI_CHANNEL_1: undo_MPAD(inst); break; default: throw std::invalid_argument( "Not implemented by SparseUnsignedRevFrameTracker::undo_gate: " + std::string(GATE_DATA[inst.gate_type].name)); } } SparseUnsignedRevFrameTracker::SparseUnsignedRevFrameTracker( uint64_t num_qubits, uint64_t num_measurements_in_past, uint64_t num_detectors_in_past, bool fail_on_anticommute) : xs(num_qubits), zs(num_qubits), rec_bits(), num_measurements_in_past(num_measurements_in_past), num_detectors_in_past(num_detectors_in_past), fail_on_anticommute(fail_on_anticommute), anticommutations() { } void SparseUnsignedRevFrameTracker::fail_due_to_anticommutation(const CircuitInstruction &inst) { std::stringstream ss; ss << "While running backwards through the circuit, during reverse-execution of the instruction\n"; ss << " " << inst << "\n"; ss << "the following detecting region vs dissipation anticommutations occurred\n"; for (auto &[d, g] : anticommutations) { ss << " " << d << " vs " << g << "\n"; } ss << "Therefore invalid detectors/observables are present in the circuit.\n"; throw std::invalid_argument(ss.str()); } void SparseUnsignedRevFrameTracker::handle_xor_gauge( SpanRef sorted1, SpanRef sorted2, const CircuitInstruction &inst, GateTarget location) { if (sorted1 == sorted2) { return; } SparseXorVec dif; dif.xor_sorted_items(sorted1); dif.xor_sorted_items(sorted2); for (const auto &d : dif) { anticommutations.insert({d, location}); } if (fail_on_anticommute) { fail_due_to_anticommutation(inst); } } void SparseUnsignedRevFrameTracker::handle_gauge( SpanRef sorted, const CircuitInstruction &inst, GateTarget location) { if (sorted.empty()) { return; } for (const auto &d : sorted) { anticommutations.insert({d, location}); } if (fail_on_anticommute) { fail_due_to_anticommutation(inst); } } void SparseUnsignedRevFrameTracker::undo_classical_pauli(GateTarget classical_control, GateTarget target) { if (classical_control.is_sweep_bit_target()) { // Sweep bits have no effect on error propagation. return; } assert(classical_control.is_measurement_record_target()); uint64_t measurement_index = num_measurements_in_past + classical_control.value(); SparseXorVec &rec_dst = rec_bits[measurement_index]; auto q = target.data & TARGET_VALUE_MASK; if (target.data & TARGET_PAULI_X_BIT) { rec_dst ^= zs[q]; } if (target.data & TARGET_PAULI_Z_BIT) { rec_dst ^= xs[q]; } if (rec_dst.empty()) { rec_bits.erase(measurement_index); } } void SparseUnsignedRevFrameTracker::undo_ZCX_single(GateTarget c, GateTarget t) { auto cd = c.data; auto td = t.data; cd &= ~TARGET_INVERTED_BIT; td &= ~TARGET_INVERTED_BIT; if (!((cd | td) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { // Pure quantum operation. zs[cd] ^= zs[td]; xs[td] ^= xs[cd]; } else if (!t.is_qubit_target()) { throw std::invalid_argument("CX gate had '" + t.str() + "' as its target, but its target must be a qubit."); } else { undo_classical_pauli(c, GateTarget::x(td)); } } void SparseUnsignedRevFrameTracker::undo_ZCY_single(GateTarget c, GateTarget t) { auto cd = c.data; auto td = t.data; cd &= ~TARGET_INVERTED_BIT; td &= ~TARGET_INVERTED_BIT; if (!((cd | td) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { // Pure quantum operation. zs[cd] ^= zs[td]; zs[cd] ^= xs[td]; xs[td] ^= xs[cd]; zs[td] ^= xs[cd]; } else if (!t.is_qubit_target()) { throw std::invalid_argument("CY gate had '" + t.str() + "' as its target, but its target must be a qubit."); } else { undo_classical_pauli(c, GateTarget::y(td)); } } void SparseUnsignedRevFrameTracker::undo_ZCZ_single(GateTarget c, GateTarget t) { auto cd = c.data; auto td = t.data; cd &= ~TARGET_INVERTED_BIT; td &= ~TARGET_INVERTED_BIT; if (!((cd | td) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { // Pure quantum operation. zs[cd] ^= xs[td]; zs[td] ^= xs[cd]; } else if (!(td & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { undo_classical_pauli(c, GateTarget::z(td)); } else if (!(cd & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { undo_classical_pauli(t, GateTarget::z(cd)); } else { // Both targets are classical. No effect. } } void SparseUnsignedRevFrameTracker::handle_x_gauges(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); handle_gauge(xs[q].range(), dat, GateTarget::x(q)); } } void SparseUnsignedRevFrameTracker::handle_y_gauges(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); handle_xor_gauge(xs[q].range(), zs[q].range(), dat, GateTarget::y(q)); } } void SparseUnsignedRevFrameTracker::handle_z_gauges(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); handle_gauge(zs[q].range(), dat, GateTarget::z(q)); } } void SparseUnsignedRevFrameTracker::undo_MPP(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_mpp_operation( CircuitInstruction{inst.gate_type, inst.args, reversed_targets, inst.tag}, xs.size(), [&](const CircuitInstruction &inst) { if (inst.gate_type == GateType::M) { reversed_measure_targets.clear(); for (size_t k = inst.targets.size(); k--;) { reversed_measure_targets.push_back(inst.targets[k]); } undo_MZ({GateType::M, inst.args, reversed_measure_targets, inst.tag}); } else { undo_gate(inst); } }); } void SparseUnsignedRevFrameTracker::undo_SPP(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_spp_or_spp_dag_operation( CircuitInstruction{inst.gate_type, inst.args, reversed_targets, inst.tag}, xs.size(), false, [&](const CircuitInstruction &inst) { undo_gate(inst); }); } void SparseUnsignedRevFrameTracker::clear_qubits(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); xs[q].clear(); zs[q].clear(); } } void SparseUnsignedRevFrameTracker::undo_RX(const CircuitInstruction &dat) { handle_z_gauges(dat); clear_qubits(dat); } void SparseUnsignedRevFrameTracker::undo_RY(const CircuitInstruction &dat) { handle_y_gauges(dat); clear_qubits(dat); } void SparseUnsignedRevFrameTracker::undo_RZ(const CircuitInstruction &dat) { handle_x_gauges(dat); clear_qubits(dat); } void SparseUnsignedRevFrameTracker::undo_implicit_RZs_at_start_of_circuit() { for (size_t q = 0; q < xs.size(); q++) { for (const auto &d : xs[q]) { anticommutations.insert({d, GateTarget::qubit(q)}); } } if (!anticommutations.empty() && fail_on_anticommute) { std::stringstream ss; ss << "While running backwards through the circuit,\n"; ss << "during reverse-execution of the implicit resets at the beginning of the circuit,\n"; ss << "the following detecting region vs dissipation anticommutations occurred\n"; for (auto &[d, g] : anticommutations) { ss << " " << d << " vs " << g << "\n"; } ss << "Therefore invalid detectors/observables are present in the circuit.\n"; throw std::invalid_argument(ss.str()); } } void SparseUnsignedRevFrameTracker::undo_MPAD(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { num_measurements_in_past--; auto f = rec_bits.find(num_measurements_in_past); if (f != rec_bits.end()) { rec_bits.erase(f); } } } void SparseUnsignedRevFrameTracker::undo_MX(const CircuitInstruction &dat) { handle_z_gauges(dat); for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); num_measurements_in_past--; auto f = rec_bits.find(num_measurements_in_past); if (f != rec_bits.end()) { xs[q].xor_sorted_items(f->second.range()); rec_bits.erase(f); } } } void SparseUnsignedRevFrameTracker::undo_MY(const CircuitInstruction &dat) { handle_y_gauges(dat); for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); num_measurements_in_past--; auto f = rec_bits.find(num_measurements_in_past); if (f != rec_bits.end()) { xs[q].xor_sorted_items(f->second.range()); zs[q].xor_sorted_items(f->second.range()); rec_bits.erase(f); } } } void SparseUnsignedRevFrameTracker::undo_MZ(const CircuitInstruction &dat) { handle_x_gauges(dat); for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); num_measurements_in_past--; auto f = rec_bits.find(num_measurements_in_past); if (f != rec_bits.end()) { zs[q].xor_sorted_items(f->second.range()); rec_bits.erase(f); } } } void SparseUnsignedRevFrameTracker::undo_MXX_disjoint_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. undo_ZCX(CircuitInstruction{GateType::CX, {}, inst.targets, ""}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { undo_MX(CircuitInstruction{GateType::MX, inst.args, SpanRef{&inst.targets[k]}, ""}); } // Untransform from single qubit measurements back to 2 qubit measurements. undo_ZCX(CircuitInstruction{GateType::CX, {}, inst.targets, ""}); } void SparseUnsignedRevFrameTracker::undo_MYY_disjoint_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. undo_ZCY(CircuitInstruction{GateType::CY, {}, inst.targets, ""}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { undo_MY(CircuitInstruction{GateType::MY, inst.args, SpanRef{&inst.targets[k]}, ""}); } // Untransform from single qubit measurements back to 2 qubit measurements. undo_ZCY(CircuitInstruction{GateType::CY, {}, inst.targets, ""}); } void SparseUnsignedRevFrameTracker::undo_MZZ_disjoint_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. undo_XCZ(CircuitInstruction{GateType::XCZ, {}, inst.targets, ""}); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { undo_MZ(CircuitInstruction{GateType::M, inst.args, SpanRef{&inst.targets[k]}, ""}); } // Untransform from single qubit measurements back to 2 qubit measurements. undo_XCZ(CircuitInstruction{GateType::XCZ, {}, inst.targets, ""}); } void SparseUnsignedRevFrameTracker::undo_MXX(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_pair_instruction_into_disjoint_segments( CircuitInstruction{inst.gate_type, inst.args, reversed_targets, ""}, xs.size(), [&](CircuitInstruction segment) { undo_MXX_disjoint_segment(segment); }); } void SparseUnsignedRevFrameTracker::undo_MYY(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_pair_instruction_into_disjoint_segments( CircuitInstruction{inst.gate_type, inst.args, reversed_targets, ""}, xs.size(), [&](CircuitInstruction segment) { undo_MYY_disjoint_segment(segment); }); } void SparseUnsignedRevFrameTracker::undo_MZZ(const CircuitInstruction &inst) { size_t n = inst.targets.size(); std::vector reversed_targets(n); std::vector reversed_measure_targets; for (size_t k = 0; k < n; k++) { reversed_targets[k] = inst.targets[n - k - 1]; } decompose_pair_instruction_into_disjoint_segments( CircuitInstruction{inst.gate_type, inst.args, reversed_targets, ""}, xs.size(), [&](CircuitInstruction segment) { undo_MZZ_disjoint_segment(segment); }); } void SparseUnsignedRevFrameTracker::undo_MRX(const CircuitInstruction &dat) { handle_z_gauges(dat); for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); num_measurements_in_past--; xs[q].clear(); zs[q].clear(); auto f = rec_bits.find(num_measurements_in_past); if (f != rec_bits.end()) { xs[q].xor_sorted_items(f->second.range()); rec_bits.erase(f); } } } void SparseUnsignedRevFrameTracker::undo_MRY(const CircuitInstruction &dat) { handle_y_gauges(dat); for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); num_measurements_in_past--; xs[q].clear(); zs[q].clear(); auto f = rec_bits.find(num_measurements_in_past); if (f != rec_bits.end()) { xs[q].xor_sorted_items(f->second.range()); zs[q].xor_sorted_items(f->second.range()); rec_bits.erase(f); } } } void SparseUnsignedRevFrameTracker::undo_MRZ(const CircuitInstruction &dat) { handle_x_gauges(dat); for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].qubit_value(); num_measurements_in_past--; xs[q].clear(); zs[q].clear(); auto f = rec_bits.find(num_measurements_in_past); if (f != rec_bits.end()) { zs[q].xor_sorted_items(f->second.range()); rec_bits.erase(f); } } } void SparseUnsignedRevFrameTracker::undo_H_XZ(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].data; std::swap(xs[q], zs[q]); } } void SparseUnsignedRevFrameTracker::undo_H_XY(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].data; zs[q] ^= xs[q]; } } void SparseUnsignedRevFrameTracker::undo_H_YZ(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].data; xs[q] ^= zs[q]; } } void SparseUnsignedRevFrameTracker::undo_C_XYZ(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].data; zs[q] ^= xs[q]; xs[q] ^= zs[q]; } } void SparseUnsignedRevFrameTracker::undo_C_ZYX(const CircuitInstruction &dat) { for (size_t k = dat.targets.size(); k-- > 0;) { auto q = dat.targets[k].data; xs[q] ^= zs[q]; zs[q] ^= xs[q]; } } void SparseUnsignedRevFrameTracker::undo_XCX(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto q1 = dat.targets[k].data; auto q2 = dat.targets[k + 1].data; xs[q1] ^= zs[q2]; xs[q2] ^= zs[q1]; } } void SparseUnsignedRevFrameTracker::undo_XCY(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto tx = dat.targets[k].data; auto ty = dat.targets[k + 1].data; xs[tx] ^= xs[ty]; xs[tx] ^= zs[ty]; xs[ty] ^= zs[tx]; zs[ty] ^= zs[tx]; } } void SparseUnsignedRevFrameTracker::undo_YCX(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto tx = dat.targets[k + 1].data; auto ty = dat.targets[k].data; xs[tx] ^= xs[ty]; xs[tx] ^= zs[ty]; xs[ty] ^= zs[tx]; zs[ty] ^= zs[tx]; } } void SparseUnsignedRevFrameTracker::undo_ZCY(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto c = dat.targets[k]; auto t = dat.targets[k + 1]; undo_ZCY_single(c, t); } } void SparseUnsignedRevFrameTracker::undo_YCZ(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto t = dat.targets[k]; auto c = dat.targets[k + 1]; undo_ZCY_single(c, t); } } void SparseUnsignedRevFrameTracker::undo_YCY(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; zs[a] ^= xs[b]; zs[a] ^= zs[b]; xs[a] ^= xs[b]; xs[a] ^= zs[b]; zs[b] ^= xs[a]; zs[b] ^= zs[a]; xs[b] ^= xs[a]; xs[b] ^= zs[a]; } } void SparseUnsignedRevFrameTracker::undo_ZCX(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto c = dat.targets[k]; auto t = dat.targets[k + 1]; undo_ZCX_single(c, t); } } void SparseUnsignedRevFrameTracker::undo_XCZ(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto t = dat.targets[k]; auto c = dat.targets[k + 1]; undo_ZCX_single(c, t); } } void SparseUnsignedRevFrameTracker::undo_ZCZ(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto q1 = dat.targets[k]; auto q2 = dat.targets[k + 1]; undo_ZCZ_single(q1, q2); } } void SparseUnsignedRevFrameTracker::undo_SQRT_XX(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; xs[a] ^= zs[a]; xs[a] ^= zs[b]; xs[b] ^= zs[a]; xs[b] ^= zs[b]; } } void SparseUnsignedRevFrameTracker::undo_SQRT_YY(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; zs[a] ^= xs[a]; zs[b] ^= xs[b]; xs[a] ^= zs[a]; xs[a] ^= zs[b]; xs[b] ^= zs[a]; xs[b] ^= zs[b]; zs[a] ^= xs[a]; zs[b] ^= xs[b]; } } void SparseUnsignedRevFrameTracker::undo_SQRT_ZZ(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; zs[a] ^= xs[a]; zs[a] ^= xs[b]; zs[b] ^= xs[a]; zs[b] ^= xs[b]; } } void SparseUnsignedRevFrameTracker::undo_I(const CircuitInstruction &dat) { } void SparseUnsignedRevFrameTracker::undo_SWAP(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; std::swap(xs[a], xs[b]); std::swap(zs[a], zs[b]); } } void SparseUnsignedRevFrameTracker::undo_CXSWAP(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; zs[a] ^= zs[b]; zs[b] ^= zs[a]; xs[b] ^= xs[a]; xs[a] ^= xs[b]; } } void SparseUnsignedRevFrameTracker::undo_CZSWAP(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; zs[a] ^= xs[b]; zs[b] ^= xs[a]; std::swap(xs[a], xs[b]); std::swap(zs[a], zs[b]); } } void SparseUnsignedRevFrameTracker::undo_SWAPCX(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; zs[b] ^= zs[a]; zs[a] ^= zs[b]; xs[a] ^= xs[b]; xs[b] ^= xs[a]; } } void SparseUnsignedRevFrameTracker::undo_ISWAP(const CircuitInstruction &dat) { for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) { auto a = dat.targets[k].data; auto b = dat.targets[k + 1].data; zs[a] ^= xs[a]; zs[a] ^= xs[b]; zs[b] ^= xs[a]; zs[b] ^= xs[b]; std::swap(xs[a], xs[b]); std::swap(zs[a], zs[b]); } } void SparseUnsignedRevFrameTracker::undo_DETECTOR(const CircuitInstruction &dat) { num_detectors_in_past--; auto det = DemTarget::relative_detector_id(num_detectors_in_past); for (auto t : dat.targets) { int64_t index = t.rec_offset() + (int64_t)num_measurements_in_past; if (index < 0) { throw std::invalid_argument("Referred to a measurement result before the beginning of time."); } rec_bits[(size_t)index].xor_item(det); } } void SparseUnsignedRevFrameTracker::undo_OBSERVABLE_INCLUDE(const CircuitInstruction &dat) { uint64_t obs_id = (uint32_t)dat.args[0]; auto obs = DemTarget::observable_id(obs_id); for (auto t : dat.targets) { if (t.is_measurement_record_target()) { int64_t index = t.rec_offset() + (int64_t)num_measurements_in_past; if (index < 0) { throw std::invalid_argument("Referred to a measurement result before the beginning of time."); } rec_bits[index].xor_item(obs); } else if (t.is_pauli_target()) { if (t.data & TARGET_PAULI_X_BIT) { xs[t.qubit_value()].xor_item(obs); } if (t.data & TARGET_PAULI_Z_BIT) { zs[t.qubit_value()].xor_item(obs); } } else { throw std::invalid_argument("Unexpected target for OBSERVABLE_INCLUDE: " + t.str()); } } } void SparseUnsignedRevFrameTracker::undo_gate(const CircuitInstruction &op, const Circuit &parent) { if (op.gate_type == GateType::REPEAT) { const auto &loop_body = op.repeat_block_body(parent); uint64_t repeats = op.repeat_block_rep_count(); undo_loop(loop_body, repeats); } else { undo_gate(op); } } void SparseUnsignedRevFrameTracker::undo_circuit(const Circuit &circuit) { for (size_t k = circuit.operations.size(); k--;) { undo_gate(circuit.operations[k], circuit); } } void SparseUnsignedRevFrameTracker::undo_loop_by_unrolling(const Circuit &loop, uint64_t iterations) { for (size_t rep = 0; rep < iterations; rep++) { undo_circuit(loop); } } bool _det_vec_is_equal_to_after_shift( SpanRef unshifted, SpanRef expected, int64_t detector_shift) { if (unshifted.size() != expected.size()) { return false; } for (size_t k = 0; k < unshifted.size(); k++) { DemTarget a = unshifted[k]; DemTarget e = expected[k]; a.shift_if_detector_id(detector_shift); if (a != e) { return false; } } return true; } bool _rec_to_det_is_equal_to_after_shift( const std::map> &unshifted, const std::map> &expected, int64_t measure_offset, int64_t detector_offset) { if (unshifted.size() != expected.size()) { return false; } for (const auto &unshifted_entry : unshifted) { const auto &shifted_entry = expected.find(unshifted_entry.first + measure_offset); if (shifted_entry == expected.end()) { return false; } if (!_det_vec_is_equal_to_after_shift( unshifted_entry.second.range(), shifted_entry->second.range(), detector_offset)) { return false; } } return true; } bool _vec_to_det_is_equal_to_after_shift( const std::vector> &unshifted, const std::vector> &expected, int64_t detector_offset) { if (unshifted.size() != expected.size()) { return false; } for (size_t k = 0; k < unshifted.size(); k++) { if (!_det_vec_is_equal_to_after_shift(unshifted[k].range(), expected[k].range(), detector_offset)) { return false; } } return true; } bool SparseUnsignedRevFrameTracker::is_shifted_copy(const SparseUnsignedRevFrameTracker &other) const { int64_t measurement_offset = (int64_t)other.num_measurements_in_past - (int64_t)num_measurements_in_past; int64_t detector_offset = (int64_t)other.num_detectors_in_past - (int64_t)num_detectors_in_past; return _rec_to_det_is_equal_to_after_shift(rec_bits, other.rec_bits, measurement_offset, detector_offset) && _vec_to_det_is_equal_to_after_shift(xs, other.xs, detector_offset) && _vec_to_det_is_equal_to_after_shift(zs, other.zs, detector_offset); } bool SparseUnsignedRevFrameTracker::operator==(const SparseUnsignedRevFrameTracker &other) const { return xs == other.xs && zs == other.zs && rec_bits == other.rec_bits && num_measurements_in_past == other.num_measurements_in_past && num_detectors_in_past == other.num_detectors_in_past; } bool SparseUnsignedRevFrameTracker::operator!=(const SparseUnsignedRevFrameTracker &other) const { return !(*this == other); } void SparseUnsignedRevFrameTracker::shift(int64_t measurement_offset, int64_t detector_offset) { num_measurements_in_past += measurement_offset; num_detectors_in_past += detector_offset; std::vector>> shifted; shifted.reserve(rec_bits.size()); for (const auto &t : rec_bits) { shifted.push_back({t.first + measurement_offset, std::move(t.second)}); for (auto &e : shifted.back().second) { e.shift_if_detector_id(detector_offset); } } rec_bits.clear(); for (const auto &e : shifted) { rec_bits.insert(std::move(e)); } for (auto &x : xs) { for (auto &e : x.sorted_items) { e.shift_if_detector_id(detector_offset); } } for (auto &z : zs) { for (auto &e : z.sorted_items) { e.shift_if_detector_id(detector_offset); } } } void SparseUnsignedRevFrameTracker::undo_loop(const Circuit &loop, uint64_t iterations) { if (iterations < 5) { undo_loop_by_unrolling(loop, iterations); return; } SparseUnsignedRevFrameTracker tortoise(*this); uint64_t hare_steps = 0; uint64_t tortoise_steps = 0; while (true) { undo_circuit(loop); hare_steps++; if (is_shifted_copy(tortoise)) { break; } if (hare_steps > iterations - hare_steps) { undo_loop_by_unrolling(loop, iterations - hare_steps); return; } if ((hare_steps & 1) == 0) { tortoise.undo_circuit(loop); tortoise_steps++; if (is_shifted_copy(tortoise)) { break; } } } uint64_t period = hare_steps - tortoise_steps; assert(period > 0); uint64_t skipped_iterations = (iterations - hare_steps) / period; uint64_t detectors_per_period = tortoise.num_detectors_in_past - num_detectors_in_past; uint64_t measurements_per_period = tortoise.num_measurements_in_past - num_measurements_in_past; shift( -(int64_t)(measurements_per_period * skipped_iterations), -(int64_t)(detectors_per_period * skipped_iterations)); hare_steps += skipped_iterations * period; undo_loop_by_unrolling(loop, iterations - hare_steps); } std::string SparseUnsignedRevFrameTracker::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::ostream &stim::operator<<(std::ostream &out, const SparseUnsignedRevFrameTracker &tracker) { out << "SparseUnsignedRevFrameTracker {\n"; out << " num_measurements_in_past=" << tracker.num_measurements_in_past << "\n"; out << " num_detectors_in_past=" << tracker.num_detectors_in_past << "\n"; for (size_t q = 0; q < tracker.xs.size(); q++) { out << " xs[" << q << "]=" << tracker.xs[q] << "\n"; } for (size_t q = 0; q < tracker.zs.size(); q++) { out << " zs[" << q << "]=" << tracker.zs[q] << "\n"; } for (const auto &t : tracker.rec_bits) { out << " rec_bits[" << t.first << "]=" << t.second << "\n"; } out << "}"; return out; } ================================================ FILE: src/stim/simulators/sparse_rev_frame_tracker.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_SPARSE_REV_FRAME_TRACKER_H #define _STIM_SIMULATORS_SPARSE_REV_FRAME_TRACKER_H #include "stim/circuit/circuit.h" #include "stim/dem/detector_error_model.h" #include "stim/mem/sparse_xor_vec.h" #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/tableau.h" namespace stim { /// Tracks pauli frames through the circuit, assuming few frames depend on any one qubit. struct SparseUnsignedRevFrameTracker { /// Per qubit, what terms have X basis dependence on this qubit. std::vector> xs; /// Per qubit, what terms have Z basis dependence on this qubit. std::vector> zs; /// Per classical bit, what terms have dependence on measurement bits. std::map> rec_bits; /// Number of measurements that have not yet been processed. uint64_t num_measurements_in_past; /// Number of detectors that have not yet been processed. uint64_t num_detectors_in_past; /// If false, anticommuting dets and obs are stored . /// If true, an exception is raised if anticommutation is detected. bool fail_on_anticommute; /// Where anticommuting dets and obs are stored. std::set> anticommutations; SparseUnsignedRevFrameTracker( uint64_t num_qubits, uint64_t num_measurements_in_past, uint64_t num_detectors_in_past, bool fail_on_anticommute = true); template PauliString current_error_sensitivity_for(DemTarget target) const { PauliString result(xs.size()); for (size_t q = 0; q < xs.size(); q++) { result.xs[q] = std::find(xs[q].begin(), xs[q].end(), target) != xs[q].end(); result.zs[q] = std::find(zs[q].begin(), zs[q].end(), target) != zs[q].end(); } return result; } void undo_gate(const CircuitInstruction &inst); void undo_gate(const CircuitInstruction &op, const Circuit &parent); void handle_xor_gauge( SpanRef sorted1, SpanRef sorted2, const CircuitInstruction &inst, GateTarget location); void handle_gauge(SpanRef sorted, const CircuitInstruction &inst, GateTarget location); void fail_due_to_anticommutation(const CircuitInstruction &inst); void undo_classical_pauli(GateTarget classical_control, GateTarget target); void undo_ZCX_single(GateTarget c, GateTarget t); void undo_ZCY_single(GateTarget c, GateTarget t); void undo_ZCZ_single(GateTarget c, GateTarget t); void undo_circuit(const Circuit &circuit); void undo_loop(const Circuit &loop, uint64_t repetitions); void undo_loop_by_unrolling(const Circuit &loop, uint64_t repetitions); void clear_qubits(const CircuitInstruction &inst); void handle_x_gauges(const CircuitInstruction &inst); void handle_y_gauges(const CircuitInstruction &inst); void handle_z_gauges(const CircuitInstruction &inst); void undo_implicit_RZs_at_start_of_circuit(); void undo_DETECTOR(const CircuitInstruction &inst); void undo_OBSERVABLE_INCLUDE(const CircuitInstruction &inst); void undo_RX(const CircuitInstruction &inst); void undo_RY(const CircuitInstruction &inst); void undo_RZ(const CircuitInstruction &inst); void undo_MX(const CircuitInstruction &inst); void undo_MY(const CircuitInstruction &inst); void undo_MZ(const CircuitInstruction &inst); void undo_MPP(const CircuitInstruction &inst); void undo_SPP(const CircuitInstruction &inst); void undo_MXX(const CircuitInstruction &inst); void undo_MYY(const CircuitInstruction &inst); void undo_MZZ(const CircuitInstruction &inst); void undo_MPAD(const CircuitInstruction &inst); void undo_MRX(const CircuitInstruction &inst); void undo_MRY(const CircuitInstruction &inst); void undo_MRZ(const CircuitInstruction &inst); void undo_H_XZ(const CircuitInstruction &inst); void undo_H_XY(const CircuitInstruction &inst); void undo_H_YZ(const CircuitInstruction &inst); void undo_C_XYZ(const CircuitInstruction &inst); void undo_C_ZYX(const CircuitInstruction &inst); void undo_XCX(const CircuitInstruction &inst); void undo_XCY(const CircuitInstruction &inst); void undo_XCZ(const CircuitInstruction &inst); void undo_YCX(const CircuitInstruction &inst); void undo_YCY(const CircuitInstruction &inst); void undo_YCZ(const CircuitInstruction &inst); void undo_ZCX(const CircuitInstruction &inst); void undo_ZCY(const CircuitInstruction &inst); void undo_ZCZ(const CircuitInstruction &inst); void undo_I(const CircuitInstruction &inst); void undo_SQRT_XX(const CircuitInstruction &inst); void undo_SQRT_YY(const CircuitInstruction &inst); void undo_SQRT_ZZ(const CircuitInstruction &inst); void undo_SWAP(const CircuitInstruction &inst); void undo_ISWAP(const CircuitInstruction &inst); void undo_CXSWAP(const CircuitInstruction &inst); void undo_CZSWAP(const CircuitInstruction &inst); void undo_SWAPCX(const CircuitInstruction &inst); template void undo_tableau(const Tableau &tableau, SpanRef targets) { size_t n = tableau.num_qubits; if (n != targets.size()) { throw new std::invalid_argument("tableau.num_qubits != targets.size()"); } std::set target_set; for (size_t k = 0; k < n; k++) { if (!target_set.insert(targets[k]).second) { throw new std::invalid_argument("duplicate target"); } } std::vector> old_xs; std::vector> old_zs; old_xs.reserve(n); old_zs.reserve(n); for (size_t k = 0; k < n; k++) { old_xs.push_back(std::move(xs[targets[k]])); old_zs.push_back(std::move(zs[targets[k]])); xs[targets[k]].clear(); zs[targets[k]].clear(); } for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < n; j++) { uint64_t px = tableau.inverse_x_output_pauli_xyz(j, i); if (px == 1 || px == 2) { xs[targets[i]] ^= old_xs[j]; } if (px == 2 || px == 3) { zs[targets[i]] ^= old_xs[j]; } uint64_t pz = tableau.inverse_z_output_pauli_xyz(j, i); if (pz == 1 || pz == 2) { xs[targets[i]] ^= old_zs[j]; } if (pz == 2 || pz == 3) { zs[targets[i]] ^= old_zs[j]; } } } } bool is_shifted_copy(const SparseUnsignedRevFrameTracker &other) const; void shift(int64_t measurement_offset, int64_t detector_offset); bool operator==(const SparseUnsignedRevFrameTracker &other) const; bool operator!=(const SparseUnsignedRevFrameTracker &other) const; std::string str() const; private: void undo_MXX_disjoint_segment(const CircuitInstruction &inst); void undo_MYY_disjoint_segment(const CircuitInstruction &inst); void undo_MZZ_disjoint_segment(const CircuitInstruction &inst); }; std::ostream &operator<<(std::ostream &out, const SparseUnsignedRevFrameTracker &tracker); } // namespace stim #endif ================================================ FILE: src/stim/simulators/sparse_rev_frame_tracker.test.cc ================================================ // 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. #include "stim/simulators/sparse_rev_frame_tracker.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; template SparseUnsignedRevFrameTracker _tracker_from_pauli_string(const char *text) { auto p = PauliString::from_str(text); SparseUnsignedRevFrameTracker result(p.num_qubits, 0, 0); for (size_t q = 0; q < p.num_qubits; q++) { if (p.xs[q]) { result.xs[q].xor_item(DemTarget::observable_id(0)); } if (p.zs[q]) { result.zs[q].xor_item(DemTarget::observable_id(0)); } } return result; } TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, undo_tableau_h, { SparseUnsignedRevFrameTracker actual(0, 0, 0); std::vector targets{0}; auto tableau = GATE_DATA.at("H").tableau(); actual = _tracker_from_pauli_string("I"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("I")); actual = _tracker_from_pauli_string("X"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("Z")); actual = _tracker_from_pauli_string("Y"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("Y")); actual = _tracker_from_pauli_string("Z"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("X")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, undo_tableau_s, { SparseUnsignedRevFrameTracker actual(0, 0, 0); std::vector targets{0}; auto tableau = GATE_DATA.at("S").tableau(); actual = _tracker_from_pauli_string("I"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("I")); actual = _tracker_from_pauli_string("X"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("Y")); actual = _tracker_from_pauli_string("Y"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("X")); actual = _tracker_from_pauli_string("Z"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("Z")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, undo_tableau_c_xyz, { SparseUnsignedRevFrameTracker actual(0, 0, 0); std::vector targets{0}; auto tableau = GATE_DATA.at("C_XYZ").tableau(); actual = _tracker_from_pauli_string("I"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("I")); actual = _tracker_from_pauli_string("X"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("Z")); actual = _tracker_from_pauli_string("Y"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("X")); actual = _tracker_from_pauli_string("Z"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("Y")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, undo_tableau_cx, { SparseUnsignedRevFrameTracker actual(0, 0, 0); std::vector targets{0, 1}; auto tableau = GATE_DATA.at("CX").tableau(); actual = _tracker_from_pauli_string("II"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("II")); actual = _tracker_from_pauli_string("IZ"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("ZZ")); actual = _tracker_from_pauli_string("ZI"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("ZI")); actual = _tracker_from_pauli_string("XI"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("XX")); actual = _tracker_from_pauli_string("IX"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("IX")); actual = _tracker_from_pauli_string("YY"); actual.undo_tableau(tableau, targets); ASSERT_EQ(actual, _tracker_from_pauli_string("XZ")); }) static std::vector qubit_targets(const std::vector &targets) { std::vector result; for (uint32_t t : targets) { result.push_back(GateTarget::qubit(t)); } return result; } TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, fuzz_all_unitary_gates_vs_tableau, { auto rng = INDEPENDENT_TEST_RNG(); for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { size_t n = (gate.flags & GATE_TARGETS_PAIRS) ? 2 : 1; SparseUnsignedRevFrameTracker tracker_gate(n + 3, 0, 0); for (size_t q = 0; q < n; q++) { uint64_t mask = rng(); for (size_t d = 0; d < 32; d++) { if (mask & 1) { tracker_gate.xs[q].sorted_items.push_back(DemTarget::relative_detector_id(d)); } mask >>= 1; if (mask & 1) { tracker_gate.zs[q].sorted_items.push_back(DemTarget::relative_detector_id(d)); } mask >>= 1; } } SparseUnsignedRevFrameTracker tracker_tableau = tracker_gate; std::vector targets; for (size_t k = 0; k < n; k++) { targets.push_back(n - k + 1); } tracker_gate.undo_gate(CircuitInstruction{gate.id, {}, qubit_targets(targets), ""}); tracker_tableau.undo_tableau(gate.tableau(), targets); EXPECT_EQ(tracker_gate, tracker_tableau) << gate.name; } } }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, RX, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.undo_RX(CircuitInstruction{GateType::RX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("XXX"); actual.undo_RX(CircuitInstruction{GateType::RX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("IXI")); actual = _tracker_from_pauli_string("XIZ"); ASSERT_THROW({ actual.undo_RX({GateType::RX, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("XIZ")); actual = _tracker_from_pauli_string("YIX"); ASSERT_THROW({ actual.undo_RX({GateType::RX, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIX")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, RY, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.undo_RY({GateType::RY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("YYY"); actual.undo_RY({GateType::RY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("IYI")); actual = _tracker_from_pauli_string("YIZ"); ASSERT_THROW({ actual.undo_RY({GateType::RY, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIZ")); actual = _tracker_from_pauli_string("XIY"); ASSERT_THROW({ actual.undo_RY({GateType::RY, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("XIY")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, RZ, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.undo_RZ({GateType::R, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("ZZZ"); actual.undo_RZ({GateType::R, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("IZI")); actual = _tracker_from_pauli_string("YIZ"); ASSERT_THROW({ actual.undo_RZ({GateType::R, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIZ")); actual = _tracker_from_pauli_string("ZIX"); ASSERT_THROW({ actual.undo_RZ({GateType::R, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("ZIX")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, MX, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.undo_MX({GateType::MX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("XXX"); actual.num_measurements_in_past = 2; actual.undo_MX({GateType::MX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("XXX")); actual = _tracker_from_pauli_string("XIZ"); ASSERT_THROW({ actual.undo_MX({GateType::MX, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("XIZ")); actual = _tracker_from_pauli_string("YIX"); ASSERT_THROW({ actual.undo_MX({GateType::MX, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIX")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, MY, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.undo_MY({GateType::MY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("YYY"); actual.num_measurements_in_past = 2; actual.undo_MY({GateType::MY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("YYY")); actual = _tracker_from_pauli_string("YIZ"); ASSERT_THROW({ actual.undo_MY({GateType::MY, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIZ")); actual = _tracker_from_pauli_string("XIY"); ASSERT_THROW({ actual.undo_MY({GateType::MY, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("XIY")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, MZ, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.undo_MZ({GateType::M, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("ZZZ"); actual.num_measurements_in_past = 2; actual.undo_MZ({GateType::M, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("ZZZ")); actual = _tracker_from_pauli_string("YIZ"); ASSERT_THROW({ actual.undo_MZ({GateType::M, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIZ")); actual = _tracker_from_pauli_string("ZIX"); ASSERT_THROW({ actual.undo_MZ({GateType::M, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("ZIX")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, MRX, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.undo_MRX({GateType::MRX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("XXX"); actual.num_measurements_in_past = 2; actual.undo_MRX({GateType::MRX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("IXI")); actual = _tracker_from_pauli_string("XIZ"); ASSERT_THROW({ actual.undo_MRX({GateType::MRX, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("XIZ")); actual = _tracker_from_pauli_string("YIX"); ASSERT_THROW({ actual.undo_MRX({GateType::MRX, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIX")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, MRY, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.undo_MRY({GateType::MRY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("YYY"); actual.num_measurements_in_past = 2; actual.undo_MRY({GateType::MRY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("IYI")); actual = _tracker_from_pauli_string("YIZ"); ASSERT_THROW({ actual.undo_MRY({GateType::MRY, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIZ")); actual = _tracker_from_pauli_string("XIY"); ASSERT_THROW({ actual.undo_MRY({GateType::MRY, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("XIY")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, MRZ, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.undo_MRZ({GateType::MR, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("ZZZ"); actual.num_measurements_in_past = 2; actual.undo_MRZ({GateType::MR, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("IZI")); actual = _tracker_from_pauli_string("YIZ"); ASSERT_THROW({ actual.undo_MRZ({GateType::MR, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("YIZ")); actual = _tracker_from_pauli_string("ZIX"); ASSERT_THROW({ actual.undo_MRZ({GateType::MR, {}, qubit_targets({0, 2}), ""}); }, std::invalid_argument); ASSERT_EQ(actual, _tracker_from_pauli_string("ZIX")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, feedback_from_measurement, { SparseUnsignedRevFrameTracker actual(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MX({GateType::MX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("XII")); actual = _tracker_from_pauli_string("XII"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MX({GateType::MX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MY({GateType::MY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("YII")); actual = _tracker_from_pauli_string("YII"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MY({GateType::MY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MZ({GateType::M, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("ZII")); actual = _tracker_from_pauli_string("ZII"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MZ({GateType::M, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("III")); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MRX({GateType::MRX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("XII")); actual = _tracker_from_pauli_string("XII"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MRX({GateType::MRX, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("XII")); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MRY({GateType::MRY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("YII")); actual = _tracker_from_pauli_string("YII"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MRY({GateType::MRY, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("YII")); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MRZ({GateType::MR, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("ZII")); actual = _tracker_from_pauli_string("ZII"); actual.num_measurements_in_past = 2; actual.rec_bits[0].xor_item(DemTarget::observable_id(0)); actual.undo_MRZ({GateType::MR, {}, qubit_targets({0, 2}), ""}); ASSERT_EQ(actual, _tracker_from_pauli_string("ZII")); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, feedback_into_measurement, { SparseUnsignedRevFrameTracker actual(0, 0, 0); SparseUnsignedRevFrameTracker expected(0, 0, 0); std::vector targets{GateTarget::rec(-5), GateTarget::qubit(0)}; actual = _tracker_from_pauli_string("XII"); actual.num_measurements_in_past = 12; actual.undo_ZCX({GateType::CX, {}, targets, ""}); expected = _tracker_from_pauli_string("XII"); expected.num_measurements_in_past = 12; ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("YII"); actual.num_measurements_in_past = 12; actual.undo_ZCX({GateType::CX, {}, targets, ""}); expected = _tracker_from_pauli_string("YII"); expected.num_measurements_in_past = 12; expected.rec_bits[7].xor_item(DemTarget::observable_id(0)); ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("ZII"); actual.num_measurements_in_past = 12; actual.undo_ZCX({GateType::CX, {}, targets, ""}); expected = _tracker_from_pauli_string("ZII"); expected.num_measurements_in_past = 12; expected.rec_bits[7].xor_item(DemTarget::observable_id(0)); ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("XII"); actual.num_measurements_in_past = 12; actual.undo_ZCY({GateType::CY, {}, targets, ""}); expected = _tracker_from_pauli_string("XII"); expected.num_measurements_in_past = 12; expected.rec_bits[7].xor_item(DemTarget::observable_id(0)); ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("YII"); actual.num_measurements_in_past = 12; actual.undo_ZCY({GateType::CY, {}, targets, ""}); expected = _tracker_from_pauli_string("YII"); expected.num_measurements_in_past = 12; ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("ZII"); actual.num_measurements_in_past = 12; actual.undo_ZCY({GateType::CY, {}, targets, ""}); expected = _tracker_from_pauli_string("ZII"); expected.num_measurements_in_past = 12; expected.rec_bits[7].xor_item(DemTarget::observable_id(0)); ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("XII"); actual.num_measurements_in_past = 12; actual.undo_ZCZ({GateType::CZ, {}, targets, ""}); expected = _tracker_from_pauli_string("XII"); expected.num_measurements_in_past = 12; expected.rec_bits[7].xor_item(DemTarget::observable_id(0)); ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("YII"); actual.num_measurements_in_past = 12; actual.undo_ZCZ({GateType::CZ, {}, targets, ""}); expected = _tracker_from_pauli_string("YII"); expected.num_measurements_in_past = 12; expected.rec_bits[7].xor_item(DemTarget::observable_id(0)); ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("ZII"); actual.num_measurements_in_past = 12; actual.undo_ZCZ({GateType::CZ, {}, targets, ""}); expected = _tracker_from_pauli_string("ZII"); expected.num_measurements_in_past = 12; ASSERT_EQ(actual, expected); }) TEST(SparseUnsignedRevFrameTracker, undo_circuit_feedback) { SparseUnsignedRevFrameTracker actual(20, 100, 10); actual.undo_circuit(Circuit(R"CIRCUIT( MR 0 CX rec[-1] 0 M 0 DETECTOR rec[-1] )CIRCUIT")); SparseUnsignedRevFrameTracker expected(20, 98, 9); expected.zs[0].xor_item(DemTarget::relative_detector_id(9)); ASSERT_EQ(actual, expected); } TEST(SparseUnsignedRevFrameTracker, equal_shifted) { SparseUnsignedRevFrameTracker actual(10, 200, 300); SparseUnsignedRevFrameTracker expected(10, 2000, 3000); ASSERT_TRUE(actual.is_shifted_copy(expected)); actual.rec_bits[200 - 5].xor_item(DemTarget::observable_id(2)); ASSERT_FALSE(actual.is_shifted_copy(expected)); expected.rec_bits[2000 - 5].xor_item(DemTarget::observable_id(2)); ASSERT_TRUE(actual.is_shifted_copy(expected)); actual.rec_bits[200 - 5].xor_item(DemTarget::relative_detector_id(300 - 7)); ASSERT_FALSE(actual.is_shifted_copy(expected)); expected.rec_bits[2000 - 5].xor_item(DemTarget::relative_detector_id(300 - 7)); ASSERT_FALSE(actual.is_shifted_copy(expected)); expected.rec_bits[2000 - 5].xor_item(DemTarget::relative_detector_id(300 - 7)); expected.rec_bits[2000 - 5].xor_item(DemTarget::relative_detector_id(3000 - 7)); ASSERT_TRUE(actual.is_shifted_copy(expected)); actual.xs[5].xor_item(DemTarget::observable_id(3)); ASSERT_FALSE(actual.is_shifted_copy(expected)); expected.xs[5].xor_item(DemTarget::observable_id(3)); ASSERT_TRUE(actual.is_shifted_copy(expected)); actual.zs[6].xor_item(DemTarget::observable_id(3)); ASSERT_FALSE(actual.is_shifted_copy(expected)); expected.zs[6].xor_item(DemTarget::observable_id(3)); ASSERT_TRUE(actual.is_shifted_copy(expected)); actual.xs[5].xor_item(DemTarget::relative_detector_id(300 - 13)); ASSERT_FALSE(actual.is_shifted_copy(expected)); expected.xs[5].xor_item(DemTarget::relative_detector_id(3000 - 13)); ASSERT_TRUE(actual.is_shifted_copy(expected)); actual.zs[6].xor_item(DemTarget::relative_detector_id(300 - 13)); ASSERT_FALSE(actual.is_shifted_copy(expected)); expected.zs[6].xor_item(DemTarget::relative_detector_id(3000 - 13)); ASSERT_TRUE(actual.is_shifted_copy(expected)); actual.shift(2000 - 200, 3000 - 300); ASSERT_EQ(actual, expected); } TEST(SparseUnsignedRevFrameTracker, undo_circuit_loop_big_period) { SparseUnsignedRevFrameTracker actual(20, 5000000000000ULL, 4000000000000ULL); SparseUnsignedRevFrameTracker expected = actual; Circuit body(R"CIRCUIT( CX 0 1 1 2 2 3 3 0 M 0 0 1 DETECTOR rec[-2] rec[-3] OBSERVABLE_INCLUDE(3) rec[-1] )CIRCUIT"); actual.undo_loop(body, 500); expected.undo_loop_by_unrolling(body, 500); ASSERT_EQ(actual, expected); // Make test timeout if folding fails. actual.undo_loop(body, 1000000000001ULL); ASSERT_EQ(actual.zs[0].sorted_items, std::vector{DemTarget::observable_id(3)}); ASSERT_EQ(actual.num_measurements_in_past, 5000000000000ULL - 3000000000003ULL - 1500); ASSERT_EQ(actual.num_detectors_in_past, 4000000000000ULL - 1000000000001ULL - 500); } TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, mpad, { SparseUnsignedRevFrameTracker actual(0, 0, 0); SparseUnsignedRevFrameTracker expected(0, 0, 0); actual = _tracker_from_pauli_string("IIZ"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::relative_detector_id(5)); actual.undo_MPAD({GateType::MRY, {}, qubit_targets({0}), ""}); expected = _tracker_from_pauli_string("IIZ"); expected.num_measurements_in_past = 1; ASSERT_EQ(actual, expected); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, mxx, { SparseUnsignedRevFrameTracker actual(0, 0, 0); SparseUnsignedRevFrameTracker expected(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); actual.undo_circuit(Circuit("MXX 0 1")); expected = _tracker_from_pauli_string("XXI"); expected.num_measurements_in_past = 1; ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("ZZI"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); actual.undo_circuit(Circuit("MXX !0 !1")); expected = _tracker_from_pauli_string("YYI"); expected.num_measurements_in_past = 1; ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("ZXI"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); ASSERT_THROW({ actual.undo_circuit(Circuit("MXX 0 1")); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, myy, { SparseUnsignedRevFrameTracker actual(0, 0, 0); SparseUnsignedRevFrameTracker expected(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); actual.undo_circuit(Circuit("MYY 0 1")); expected = _tracker_from_pauli_string("YYI"); expected.num_measurements_in_past = 1; ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("ZZI"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); actual.undo_circuit(Circuit("MYY !0 !1")); expected = _tracker_from_pauli_string("XXI"); expected.num_measurements_in_past = 1; ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("ZYI"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); ASSERT_THROW({ actual.undo_circuit(Circuit("MYY 0 1")); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(SparseUnsignedRevFrameTracker, mzz, { SparseUnsignedRevFrameTracker actual(0, 0, 0); SparseUnsignedRevFrameTracker expected(0, 0, 0); actual = _tracker_from_pauli_string("III"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); actual.undo_circuit(Circuit("MZZ 0 1")); expected = _tracker_from_pauli_string("ZZI"); expected.num_measurements_in_past = 1; ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("XXI"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); actual.undo_circuit(Circuit("MZZ !0 !1")); expected = _tracker_from_pauli_string("YYI"); expected.num_measurements_in_past = 1; ASSERT_EQ(actual, expected); actual = _tracker_from_pauli_string("ZYI"); actual.num_measurements_in_past = 2; actual.rec_bits[1].xor_item(DemTarget::observable_id(0)); ASSERT_THROW({ actual.undo_circuit(Circuit("MZZ 0 1")); }, std::invalid_argument); }) TEST(SparseUnsignedRevFrameTracker, runs_on_general_circuit) { auto circuit = generate_test_circuit_with_all_operations(); SparseUnsignedRevFrameTracker s(circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors()); s.undo_circuit(circuit); ASSERT_EQ(s.num_measurements_in_past, 0); ASSERT_EQ(s.num_detectors_in_past, 0); } TEST(SparseUnsignedRevFrameTracker, tracks_anticommutation) { Circuit circuit(R"CIRCUIT( R 0 1 2 H 0 CX 0 1 0 2 MX 0 1 2 DETECTOR rec[-1] DETECTOR rec[-1] rec[-2] rec[-3] DETECTOR rec[-2] rec[-3] OBSERVABLE_INCLUDE(2) rec[-3] OBSERVABLE_INCLUDE(1) rec[-1] rec[-2] rec[-3] )CIRCUIT"); SparseUnsignedRevFrameTracker rev( circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors(), false); rev.undo_circuit(circuit); ASSERT_EQ( rev.anticommutations, (std::set>{ {DemTarget::relative_detector_id(0), GateTarget::x(2)}, {DemTarget::relative_detector_id(2), GateTarget::x(2)}, {DemTarget::observable_id(2), GateTarget::x(1)}, {DemTarget::observable_id(2), GateTarget::x(2)}})); SparseUnsignedRevFrameTracker rev2(circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors()); ASSERT_THROW({ rev.undo_circuit(circuit); }, std::invalid_argument); } TEST(SparseUnsignedRevFrameTracker, MZZ) { Circuit circuit(R"CIRCUIT( MZZ 0 1 1 2 M 2 DETECTOR rec[-1] )CIRCUIT"); SparseUnsignedRevFrameTracker rev( circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors(), false); rev.undo_circuit(circuit); ASSERT_TRUE(rev.xs[0].empty()); ASSERT_TRUE(rev.xs[1].empty()); ASSERT_TRUE(rev.xs[2].empty()); ASSERT_TRUE(rev.zs[0].empty()); ASSERT_TRUE(rev.zs[1].empty()); ASSERT_EQ(rev.zs[2].sorted_items, (std::vector{DemTarget::relative_detector_id(0)})); } TEST(SparseUnsignedRevFrameTracker, fail_anticommute) { Circuit circuit(R"CIRCUIT( RX 0 1 2 M 2 DETECTOR rec[-1] )CIRCUIT"); SparseUnsignedRevFrameTracker rev( circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors(), true); ASSERT_THROW({ rev.undo_circuit(circuit); }, std::invalid_argument); } TEST(SparseUnsignedRevFrameTracker, OBS_INCLUDE_PAULIS) { SparseUnsignedRevFrameTracker rev(4, 4, 4); rev.undo_circuit(Circuit("OBSERVABLE_INCLUDE(5) X1 Y2 Z3 rec[-1]")); ASSERT_TRUE(rev.xs[0].empty()); ASSERT_TRUE(rev.zs[0].empty()); ASSERT_EQ(rev.xs[1], (std::vector{DemTarget::observable_id(5)})); ASSERT_TRUE(rev.zs[1].empty()); ASSERT_EQ(rev.xs[2], (std::vector{DemTarget::observable_id(5)})); ASSERT_EQ(rev.zs[2], (std::vector{DemTarget::observable_id(5)})); ASSERT_TRUE(rev.xs[3].empty()); ASSERT_EQ(rev.zs[3], (std::vector{DemTarget::observable_id(5)})); } ================================================ FILE: src/stim/simulators/tableau_simulator.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_TABLEAU_SIMULATOR_H #define _STIM_SIMULATORS_TABLEAU_SIMULATOR_H #include #include #include #include #include #include #include "stim/circuit/circuit.h" #include "stim/io/measure_record.h" #include "stim/stabilizers/tableau.h" #include "stim/stabilizers/tableau_transposed_raii.h" namespace stim { /// A stabilizer circuit simulator that tracks an inverse stabilizer tableau /// and allows interactive usage, where gates and measurements are applied /// on demand. /// /// The template parameter, W, represents the SIMD width template struct TableauSimulator { Tableau inv_state; std::mt19937_64 rng; int8_t sign_bias; MeasureRecord measurement_record; bool last_correlated_error_occurred; /// Args: /// num_qubits: The initial number of qubits in the simulator state. /// rng: The random number generator to use for random operations. /// sign_bias: 0 means collapse randomly, -1 means collapse towards True, +1 means collapse towards False. /// record: Measurement record configuration. explicit TableauSimulator( std::mt19937_64 &&rng, size_t num_qubits = 0, int8_t sign_bias = 0, MeasureRecord record = MeasureRecord()); /// Args: /// other: TableauSimulator to copy state from. /// rng: The random number generator to use for random operations. TableauSimulator(const TableauSimulator &other, std::mt19937_64 &&rng); /// Samples the given circuit in a deterministic fashion. /// /// Discards all noisy operations, and biases all collapse events towards +Z instead of randomly +Z/-Z. static simd_bits reference_sample_circuit(const Circuit &circuit); static simd_bits sample_circuit(const Circuit &circuit, std::mt19937_64 &rng, int8_t sign_bias = 0); static void sample_stream(FILE *in, FILE *out, SampleFormat format, bool interactive, std::mt19937_64 &rng); /// Expands the internal state of the simulator (if needed) to ensure the given qubit exists. /// /// Failing to ensure the state is large enough for a qubit, before that qubit is acted on for the first time, /// results in undefined behavior. void ensure_large_enough_for_qubits(size_t num_qubits); /// Forces the size of the internal state of the simulator. /// /// Shrinking the size will result in qubits beyond the size threshold being collapsed and discarded. void set_num_qubits(size_t new_num_qubits); /// Finds a state vector satisfying the current stabilizer generators, and returns a vector simulator in that state. VectorSimulator to_vector_sim() const; /// Returns a state vector satisfying the current stabilizer generators. std::vector> to_state_vector(bool little_endian) const; /// Collapses then records an observable. /// /// Args: /// pauli_string: The observable to measure. bool measure_pauli_string(const PauliStringRef pauli_string, double flip_probability); /// Determines if a qubit's X observable commutes (vs anti-commutes) with the current stabilizer generators. bool is_deterministic_x(size_t target) const; /// Determines if a qubit's Y observable commutes (vs anti-commutes) with the current stabilizer generators. bool is_deterministic_y(size_t target) const; /// Determines if a qubit's Z observable commutes (vs anti-commutes) with the current stabilizer generators. bool is_deterministic_z(size_t target) const; /// Runs all of the operations in the given circuit. /// /// Automatically expands the tableau simulator's state, if needed. void safe_do_circuit(const Circuit &circuit, uint64_t reps = 1); void do_operation_ensure_size(const CircuitInstruction &operation); void apply_tableau(const Tableau &tableau, const std::vector &targets); std::vector> canonical_stabilizers() const; void do_gate(const CircuitInstruction &inst); /// === SPECIALIZED VECTORIZED OPERATION IMPLEMENTATIONS === void do_I(const CircuitInstruction &inst); void do_H_XZ(const CircuitInstruction &inst); void do_H_YZ(const CircuitInstruction &inst); void do_H_XY(const CircuitInstruction &inst); void do_H_NXY(const CircuitInstruction &inst); void do_H_NXZ(const CircuitInstruction &inst); void do_H_NYZ(const CircuitInstruction &inst); void do_C_XYZ(const CircuitInstruction &inst); void do_C_NXYZ(const CircuitInstruction &inst); void do_C_XNYZ(const CircuitInstruction &inst); void do_C_XYNZ(const CircuitInstruction &inst); void do_C_ZYX(const CircuitInstruction &inst); void do_C_NZYX(const CircuitInstruction &inst); void do_C_ZNYX(const CircuitInstruction &inst); void do_C_ZYNX(const CircuitInstruction &inst); void do_SQRT_X(const CircuitInstruction &inst); void do_SQRT_Y(const CircuitInstruction &inst); void do_SQRT_Z(const CircuitInstruction &inst); void do_SQRT_X_DAG(const CircuitInstruction &inst); void do_SQRT_Y_DAG(const CircuitInstruction &inst); void do_SQRT_Z_DAG(const CircuitInstruction &inst); void do_SQRT_XX(const CircuitInstruction &inst); void do_SQRT_XX_DAG(const CircuitInstruction &inst); void do_SQRT_YY(const CircuitInstruction &inst); void do_SQRT_YY_DAG(const CircuitInstruction &inst); void do_SQRT_ZZ(const CircuitInstruction &inst); void do_SQRT_ZZ_DAG(const CircuitInstruction &inst); void do_ZCX(const CircuitInstruction &inst); void do_ZCY(const CircuitInstruction &inst); void do_ZCZ(const CircuitInstruction &inst); void do_SWAP(const CircuitInstruction &inst); void do_X(const CircuitInstruction &inst); void do_Y(const CircuitInstruction &inst); void do_Z(const CircuitInstruction &inst); void do_ISWAP(const CircuitInstruction &inst); void do_ISWAP_DAG(const CircuitInstruction &inst); void do_CXSWAP(const CircuitInstruction &inst); void do_CZSWAP(const CircuitInstruction &inst); void do_SWAPCX(const CircuitInstruction &inst); void do_XCX(const CircuitInstruction &inst); void do_XCY(const CircuitInstruction &inst); void do_XCZ(const CircuitInstruction &inst); void do_YCX(const CircuitInstruction &inst); void do_YCY(const CircuitInstruction &inst); void do_YCZ(const CircuitInstruction &inst); void do_DEPOLARIZE1(const CircuitInstruction &inst); void do_DEPOLARIZE2(const CircuitInstruction &inst); void do_HERALDED_ERASE(const CircuitInstruction &inst); void do_HERALDED_PAULI_CHANNEL_1(const CircuitInstruction &inst); void do_X_ERROR(const CircuitInstruction &inst); void do_Y_ERROR(const CircuitInstruction &inst); void do_Z_ERROR(const CircuitInstruction &inst); void do_PAULI_CHANNEL_1(const CircuitInstruction &inst); void do_PAULI_CHANNEL_2(const CircuitInstruction &inst); void do_CORRELATED_ERROR(const CircuitInstruction &inst); void do_ELSE_CORRELATED_ERROR(const CircuitInstruction &inst); void do_MPP(const CircuitInstruction &inst); void do_SPP(const CircuitInstruction &inst); void do_SPP_DAG(const CircuitInstruction &inst); void do_MXX(const CircuitInstruction &inst); void do_MYY(const CircuitInstruction &inst); void do_MZZ(const CircuitInstruction &inst); void do_MPAD(const CircuitInstruction &inst); void do_MX(const CircuitInstruction &inst); void do_MY(const CircuitInstruction &inst); void do_MZ(const CircuitInstruction &inst); void do_RX(const CircuitInstruction &inst); void do_RY(const CircuitInstruction &inst); void do_RZ(const CircuitInstruction &inst); void do_MRX(const CircuitInstruction &inst); void do_MRY(const CircuitInstruction &inst); void do_MRZ(const CircuitInstruction &inst); /// Returns the single-qubit stabilizer of a target or, if it is entangled, the identity operation. PauliString peek_bloch(uint32_t target) const; /// Returns the expectation value of measuring the qubit in the X basis. int8_t peek_x(uint32_t target) const; /// Returns the expectation value of measuring the qubit in the Y basis. int8_t peek_y(uint32_t target) const; /// Returns the expectation value of measuring the qubit in the Z basis. int8_t peek_z(uint32_t target) const; /// Projects the system into a desired qubit X observable, or raises an exception if it was impossible. void postselect_x(SpanRef targets, bool desired_result); /// Projects the system into a desired qubit Y observable, or raises an exception if it was impossible. void postselect_y(SpanRef targets, bool desired_result); /// Projects the system into a desired qubit Z observable, or raises an exception if it was impossible. void postselect_z(SpanRef targets, bool desired_result); /// Applies all of the Pauli operations in the given PauliString to the simulator's state. void paulis(const PauliString &paulis); /// Performs a measurement and returns a kickback that flips between the possible post-measurement states. /// /// Deterministic measurements have no kickback. /// This is represented by setting the kickback to the empty Pauli string. std::pair> measure_kickback_z(GateTarget target); std::pair> measure_kickback_y(GateTarget target); std::pair> measure_kickback_x(GateTarget target); bool read_measurement_record(uint32_t encoded_target) const; void single_cx(uint32_t c, uint32_t t); void single_cy(uint32_t c, uint32_t t); /// Forces a qubit to have a collapsed Z observable. /// /// If the qubit already has a collapsed Z observable, this method has no effect. /// Other, the qubit's Z observable anticommutes with the current stabilizers and this method will apply state /// transformations that pick out a single stabilizer generator to destroy and replace with the measurement's /// stabilizer. /// /// Args: /// target: The index of the qubit to collapse. /// transposed_raii: A RAII value whose existence certifies the tableau data is currently transposed /// (to make operations efficient). /// /// Returns: /// SIZE_MAX: Already collapsed. /// Else: The pivot index. The start-of-time qubit whose X flips the measurement. size_t collapse_qubit_z(size_t target, TableauTransposedRaii &transposed_raii); /// Collapses the given qubits into the X basis. /// /// Args: /// targets: The qubits to collapse. /// stride: Defaults to 1. Set to 2 to skip over every other target. void collapse_x(SpanRef targets, size_t stride = 1); /// Collapses the given qubits into the Y basis. /// /// Args: /// targets: The qubits to collapse. /// stride: Defaults to 1. Set to 2 to skip over every other target. void collapse_y(SpanRef targets, size_t stride = 1); /// Collapses the given qubits into the Z basis. /// /// Args: /// targets: The qubits to collapse. /// stride: Defaults to 1. Set to 2 to skip over every other target. void collapse_z(SpanRef targets, size_t stride = 1); /// Completely isolates a qubit from the other qubits tracked by the simulator, so it can be safely discarded. /// /// After this runs, it is guaranteed that the inverse tableau maps the target qubit's X and Z observables to /// themselves (possibly negated) and that it maps all other qubits to Pauli products not involving the target /// qubit. void collapse_isolate_qubit_z(size_t target, TableauTransposedRaii &transposed_raii); /// Determines the expected value of an observable (which will always be -1, 0, or +1). /// /// This is a non-physical operation. /// It reports information about the quantum state without disturbing it. /// /// Args: /// observable: The observable to determine the expected value of. /// /// Returns: /// +1: Observable will be deterministically false when measured. /// -1: Observable will be deterministically true when measured. /// 0: Observable will be random when measured. int8_t peek_observable_expectation(const PauliString &observable) const; /// Forces a desired measurement result, or raises an exception if it was impossible. void postselect_observable(PauliStringRef observable, bool desired_result); private: uint32_t try_isolate_observable_to_qubit_z(PauliStringRef observable, bool undo); void do_MXX_disjoint_controls_segment(const CircuitInstruction &inst); void do_MYY_disjoint_controls_segment(const CircuitInstruction &inst); void do_MZZ_disjoint_controls_segment(const CircuitInstruction &inst); void noisify_new_measurements(SpanRef args, size_t num_targets); void noisify_new_measurements(const CircuitInstruction &target_data); void postselect_helper( SpanRef targets, bool desired_result, GateType basis_change_gate, const char *false_name, const char *true_name); }; template void perform_pauli_errors_via_correlated_errors( const CircuitInstruction &target_data, RESET_FLAG reset_flag, ELSE_CORR else_corr) { double target_p{}; GateTarget target_t[Q]; CircuitInstruction data{GateType::E, {&target_p}, {&target_t[0], &target_t[Q]}, ""}; for (size_t k = 0; k < target_data.targets.size(); k += Q) { reset_flag(); double used_probability = 0; for (size_t pauli = 1; pauli < 1 << (2 * Q); pauli++) { double p = target_data.args[pauli - 1]; if (p == 0) { continue; } double remaining = 1 - used_probability; double conditional_prob = remaining <= 0 ? 0 : remaining <= p ? 1 : p / remaining; used_probability += p; for (size_t q = 0; q < Q; q++) { target_t[q] = target_data.targets[k + q]; bool z = (pauli >> (2 * (Q - q - 1))) & 2; bool y = (pauli >> (2 * (Q - q - 1))) & 1; if (z ^ y) { target_t[q].data |= TARGET_PAULI_X_BIT; } if (z) { target_t[q].data |= TARGET_PAULI_Z_BIT; } } target_p = conditional_prob; else_corr(data); } } } } // namespace stim #include "stim/simulators/tableau_simulator.inl" #endif ================================================ FILE: src/stim/simulators/tableau_simulator.inl ================================================ // 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. #include #include "stim/circuit/gate_decomposition.h" #include "stim/gates/gates.h" #include "stim/simulators/tableau_simulator.h" #include "stim/simulators/vector_simulator.h" #include "stim/util_bot/probability_util.h" namespace stim { template TableauSimulator::TableauSimulator(std::mt19937_64 &&rng, size_t num_qubits, int8_t sign_bias, MeasureRecord record) : inv_state(Tableau::identity(num_qubits)), rng(std::move(rng)), sign_bias(sign_bias), measurement_record(std::move(record)), last_correlated_error_occurred(false) { } template TableauSimulator::TableauSimulator(const TableauSimulator &other, std::mt19937_64 &&rng) : inv_state(other.inv_state), rng(std::move(rng)), sign_bias(other.sign_bias), measurement_record(other.measurement_record), last_correlated_error_occurred(other.last_correlated_error_occurred) { } template bool TableauSimulator::is_deterministic_x(size_t target) const { return !inv_state.xs[target].xs.not_zero(); } template bool TableauSimulator::is_deterministic_y(size_t target) const { return inv_state.xs[target].xs == inv_state.zs[target].xs; } template bool TableauSimulator::is_deterministic_z(size_t target) const { return !inv_state.zs[target].xs.not_zero(); } template void TableauSimulator::do_MPP(const CircuitInstruction &target_data) { decompose_mpp_operation(target_data, inv_state.num_qubits, [&](const CircuitInstruction &inst) { do_gate(inst); }); } template void TableauSimulator::do_SPP(const CircuitInstruction &target_data) { decompose_spp_or_spp_dag_operation(target_data, inv_state.num_qubits, false, [&](const CircuitInstruction &inst) { do_gate(inst); }); } template void TableauSimulator::do_SPP_DAG(const CircuitInstruction &target_data) { decompose_spp_or_spp_dag_operation(target_data, inv_state.num_qubits, false, [&](const CircuitInstruction &inst) { do_gate(inst); }); } template void TableauSimulator::postselect_helper( SpanRef targets, bool desired_result, GateType basis_change_gate, const char *false_name, const char *true_name) { std::set unique_targets; unique_targets.insert(targets.begin(), targets.end()); std::vector unique_targets_vec; unique_targets_vec.insert(unique_targets_vec.end(), unique_targets.begin(), unique_targets.end()); size_t finished = 0; do_gate({basis_change_gate, {}, unique_targets_vec, ""}); { uint8_t old_bias = sign_bias; sign_bias = desired_result ? -1 : +1; TableauTransposedRaii temp_transposed(inv_state); while (finished < targets.size()) { size_t q = (size_t)targets[finished].qubit_value(); collapse_qubit_z(q, temp_transposed); if (inv_state.zs.signs[q] != desired_result) { break; } finished++; } sign_bias = old_bias; } do_gate({basis_change_gate, {}, unique_targets_vec, ""}); if (finished < targets.size()) { std::stringstream msg; msg << "The requested postselection was impossible.\n"; msg << "Desired state: |" << (desired_result ? true_name : false_name) << ">\n"; msg << "Qubit " << targets[finished] << " is in the perpendicular state |" << (desired_result ? false_name : true_name) << ">\n"; if (finished > 0) { msg << finished << " of the requested postselections were finished ("; for (size_t k = 0; k < finished; k++) { msg << "qubit " << targets[k] << ", "; } msg << "[failed here])\n"; } throw std::invalid_argument(msg.str()); } } template uint32_t TableauSimulator::try_isolate_observable_to_qubit_z(PauliStringRef observable, bool undo) { uint32_t pivot = UINT32_MAX; observable.for_each_active_pauli([&](size_t q) { uint8_t p = observable.xs[q] + observable.zs[q] * 2; if (pivot == UINT32_MAX) { pivot = q; if (!undo) { if (p == 1) { inv_state.prepend_H_XZ(pivot); } else if (p == 3) { inv_state.prepend_H_YZ(pivot); } if (observable.sign) { inv_state.prepend_X(pivot); } } } else { if (p == 1) { inv_state.prepend_XCX(pivot, q); } else if (p == 2) { inv_state.prepend_XCZ(pivot, q); } else if (p == 3) { inv_state.prepend_XCY(pivot, q); } } }); if (undo && pivot != UINT32_MAX) { uint8_t p = observable.xs[pivot] + observable.zs[pivot] * 2; if (observable.sign) { inv_state.prepend_X(pivot); } if (p == 1) { inv_state.prepend_H_XZ(pivot); } else if (p == 3) { inv_state.prepend_H_YZ(pivot); } } return pivot; } template void TableauSimulator::postselect_observable(PauliStringRef observable, bool desired_result) { ensure_large_enough_for_qubits(observable.num_qubits); uint32_t pivot = try_isolate_observable_to_qubit_z(observable, false); int8_t expected; if (pivot != UINT32_MAX) { expected = peek_z(pivot); } else { expected = observable.sign ? -1 : +1; } if (desired_result) { expected *= -1; } if (expected != -1 && pivot != UINT32_MAX) { GateTarget t{pivot}; postselect_z(&t, desired_result); } try_isolate_observable_to_qubit_z(observable, true); if (expected == -1) { std::stringstream msg; msg << "It's impossible to postselect into the "; msg << (desired_result ? "-1" : "+1"); msg << " eigenstate of "; msg << observable; msg << " because the system is deterministically in the "; msg << (desired_result ? "+1" : "-1"); msg << " eigenstate."; throw std::invalid_argument(msg.str()); } } template void TableauSimulator::postselect_x(SpanRef targets, bool desired_result) { postselect_helper(targets, desired_result, GateType::H, "+", "-"); } template void TableauSimulator::postselect_y(SpanRef targets, bool desired_result) { postselect_helper(targets, desired_result, GateType::H_YZ, "i", "-i"); } template void TableauSimulator::postselect_z(SpanRef targets, bool desired_result) { postselect_helper(targets, desired_result, GateType::I, "0", "1"); } template void TableauSimulator::do_MX(const CircuitInstruction &target_data) { // Ensure measurement observables are collapsed. collapse_x(target_data.targets); // Record measurement results. for (auto t : target_data.targets) { auto q = t.qubit_value(); bool flipped = t.is_inverted_result_target(); bool b = inv_state.xs.signs[q] ^ flipped; measurement_record.record_result(b); } noisify_new_measurements(target_data); } template void TableauSimulator::do_MXX_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. do_ZCX(CircuitInstruction{GateType::CX, {}, inst.targets, ""}); // Ensure measurement observables are collapsed. collapse_x(inst.targets, 2); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { GateTarget t1 = inst.targets[k]; GateTarget t2 = inst.targets[k + 1]; auto q = t1.qubit_value(); bool flipped = t1.is_inverted_result_target() ^ t2.is_inverted_result_target(); bool b = inv_state.xs.signs[q] ^ flipped; measurement_record.record_result(b); } noisify_new_measurements(inst.args, inst.targets.size() / 2); // Untransform from single qubit measurements back to 2 qubit measurements. do_ZCX(CircuitInstruction{GateType::CX, {}, inst.targets, ""}); } template void TableauSimulator::do_MYY_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. do_ZCY(CircuitInstruction{GateType::CY, {}, inst.targets, ""}); // Ensure measurement observables are collapsed. collapse_y(inst.targets, 2); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { GateTarget t1 = inst.targets[k]; GateTarget t2 = inst.targets[k + 1]; auto q = t1.qubit_value(); bool flipped = t1.is_inverted_result_target() ^ t2.is_inverted_result_target(); bool b = inv_state.eval_y_obs(q).sign ^ flipped; measurement_record.record_result(b); } noisify_new_measurements(inst.args, inst.targets.size() / 2); // Untransform from single qubit measurements back to 2 qubit measurements. do_ZCY(CircuitInstruction{GateType::CY, {}, inst.targets, ""}); } template void TableauSimulator::do_MZZ_disjoint_controls_segment(const CircuitInstruction &inst) { // Transform from 2 qubit measurements to single qubit measurements. do_XCZ(CircuitInstruction{GateType::XCZ, {}, inst.targets, ""}); // Ensure measurement observables are collapsed. collapse_z(inst.targets, 2); // Record measurement results. for (size_t k = 0; k < inst.targets.size(); k += 2) { GateTarget t1 = inst.targets[k]; GateTarget t2 = inst.targets[k + 1]; auto q = t1.qubit_value(); bool flipped = t1.is_inverted_result_target() ^ t2.is_inverted_result_target(); bool b = inv_state.zs.signs[q] ^ flipped; measurement_record.record_result(b); } noisify_new_measurements(inst.args, inst.targets.size() / 2); // Untransform from single qubit measurements back to 2 qubit measurements. do_XCZ(CircuitInstruction{GateType::XCZ, {}, inst.targets, ""}); } template void TableauSimulator::do_MXX(const CircuitInstruction &inst) { decompose_pair_instruction_into_disjoint_segments(inst, inv_state.num_qubits, [&](CircuitInstruction segment) { do_MXX_disjoint_controls_segment(segment); }); } template void TableauSimulator::do_MYY(const CircuitInstruction &inst) { decompose_pair_instruction_into_disjoint_segments(inst, inv_state.num_qubits, [&](CircuitInstruction segment) { do_MYY_disjoint_controls_segment(segment); }); } template void TableauSimulator::do_MZZ(const CircuitInstruction &inst) { decompose_pair_instruction_into_disjoint_segments(inst, inv_state.num_qubits, [&](CircuitInstruction segment) { do_MZZ_disjoint_controls_segment(segment); }); } template void TableauSimulator::do_MPAD(const CircuitInstruction &inst) { for (const auto &t : inst.targets) { measurement_record.record_result(t.qubit_value() != 0); } noisify_new_measurements(inst); } template void TableauSimulator::do_MY(const CircuitInstruction &target_data) { // Ensure measurement observables are collapsed. collapse_y(target_data.targets); // Record measurement results. for (auto t : target_data.targets) { auto q = t.qubit_value(); bool flipped = t.is_inverted_result_target(); bool b = inv_state.eval_y_obs(q).sign ^ flipped; measurement_record.record_result(b); } noisify_new_measurements(target_data); } template void TableauSimulator::do_MZ(const CircuitInstruction &target_data) { // Ensure measurement observables are collapsed. collapse_z(target_data.targets); // Record measurement results. for (auto t : target_data.targets) { auto q = t.qubit_value(); bool flipped = t.is_inverted_result_target(); bool b = inv_state.zs.signs[q] ^ flipped; measurement_record.record_result(b); } noisify_new_measurements(target_data); } template bool TableauSimulator::measure_pauli_string(const PauliStringRef pauli_string, double flip_probability) { if (!(0 <= flip_probability && flip_probability <= 1)) { throw std::invalid_argument("Need 0 <= flip_probability <= 1"); } ensure_large_enough_for_qubits(pauli_string.num_qubits); std::vector targets; targets.reserve(pauli_string.num_qubits * 2); for (size_t k = 0; k < pauli_string.num_qubits; k++) { bool x = pauli_string.xs[k]; bool z = pauli_string.zs[k]; if (x || z) { GateTarget target{(uint32_t)k}; if (x) { target.data |= TARGET_PAULI_X_BIT; } if (z) { target.data |= TARGET_PAULI_Z_BIT; } targets.push_back(target); targets.push_back(GateTarget::combiner()); } } double p = flip_probability; if (pauli_string.sign) { p = 1 - p; } if (targets.empty()) { measurement_record.record_result(std::bernoulli_distribution(p)(rng)); } else { targets.pop_back(); do_MPP(CircuitInstruction{GateType::MPP, &p, targets, ""}); } return measurement_record.lookback(1); } template void TableauSimulator::do_MRX(const CircuitInstruction &target_data) { // Note: Caution when implementing this. Can't group the resets. because the same qubit target may appear twice. // Ensure measurement observables are collapsed. collapse_x(target_data.targets); // Record measurement results while triggering resets. for (auto t : target_data.targets) { auto q = t.qubit_value(); bool flipped = t.is_inverted_result_target(); bool b = inv_state.xs.signs[q] ^ flipped; measurement_record.record_result(b); inv_state.xs.signs[q] = false; inv_state.zs.signs[q] = false; } noisify_new_measurements(target_data); } template void TableauSimulator::do_MRY(const CircuitInstruction &target_data) { // Note: Caution when implementing this. Can't group the resets. because the same qubit target may appear twice. // Ensure measurement observables are collapsed. collapse_y(target_data.targets); // Record measurement results while triggering resets. for (auto t : target_data.targets) { auto q = t.qubit_value(); bool flipped = t.is_inverted_result_target(); bool cur_sign = inv_state.eval_y_obs(q).sign; bool b = cur_sign ^ flipped; measurement_record.record_result(b); inv_state.zs.signs[q] ^= cur_sign; } noisify_new_measurements(target_data); } template void TableauSimulator::do_MRZ(const CircuitInstruction &target_data) { // Note: Caution when implementing this. Can't group the resets. because the same qubit target may appear twice. // Ensure measurement observables are collapsed. collapse_z(target_data.targets); // Record measurement results while triggering resets. for (auto t : target_data.targets) { auto q = t.qubit_value(); bool flipped = t.is_inverted_result_target(); bool b = inv_state.zs.signs[q] ^ flipped; measurement_record.record_result(b); inv_state.xs.signs[q] = false; inv_state.zs.signs[q] = false; } noisify_new_measurements(target_data); } template void TableauSimulator::noisify_new_measurements(SpanRef args, size_t num_targets) { if (args.empty()) { return; } size_t last = measurement_record.storage.size() - 1; RareErrorIterator::for_samples(args[0], num_targets, rng, [&](size_t k) { measurement_record.storage[last - k] = !measurement_record.storage[last - k]; }); } template void TableauSimulator::noisify_new_measurements(const CircuitInstruction &inst) { noisify_new_measurements(inst.args, inst.targets.size()); } template void TableauSimulator::do_RX(const CircuitInstruction &target_data) { // Collapse the qubits to be reset. collapse_x(target_data.targets); // Force the collapsed qubits into the ground state. for (auto q : target_data.targets) { inv_state.xs.signs[q.data] = false; inv_state.zs.signs[q.data] = false; } } template void TableauSimulator::do_RY(const CircuitInstruction &target_data) { // Collapse the qubits to be reset. collapse_y(target_data.targets); // Force the collapsed qubits into the ground state. for (auto q : target_data.targets) { inv_state.xs.signs[q.data] = false; inv_state.zs.signs[q.data] = false; inv_state.zs.signs[q.data] ^= inv_state.eval_y_obs(q.data).sign; } } template void TableauSimulator::do_RZ(const CircuitInstruction &target_data) { // Collapse the qubits to be reset. collapse_z(target_data.targets); // Force the collapsed qubits into the ground state. for (auto q : target_data.targets) { inv_state.xs.signs[q.data] = false; inv_state.zs.signs[q.data] = false; } } template void TableauSimulator::do_I(const CircuitInstruction &target_data) { } template void TableauSimulator::do_H_XZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_H_XZ(q.data); } } template void TableauSimulator::do_H_XY(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_H_XY(q.data); } } template void TableauSimulator::do_H_YZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_H_YZ(q.data); } } template void TableauSimulator::do_H_NXY(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_H_NXY(q.data); } } template void TableauSimulator::do_H_NXZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_H_NXZ(q.data); } } template void TableauSimulator::do_H_NYZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_H_NYZ(q.data); } } template void TableauSimulator::do_C_XYZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_C_ZYX(q.data); } } template void TableauSimulator::do_C_NXYZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_C_ZYNX(q.data); } } template void TableauSimulator::do_C_XNYZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_C_ZNYX(q.data); } } template void TableauSimulator::do_C_XYNZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_C_NZYX(q.data); } } template void TableauSimulator::do_C_ZYX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_C_XYZ(q.data); } } template void TableauSimulator::do_C_NZYX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_C_XYNZ(q.data); } } template void TableauSimulator::do_C_ZNYX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_C_XNYZ(q.data); } } template void TableauSimulator::do_C_ZYNX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_C_NXYZ(q.data); } } template void TableauSimulator::do_SQRT_Z(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_Z_DAG(q.data); } } template void TableauSimulator::do_SQRT_Z_DAG(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_Z(q.data); } } template PauliString TableauSimulator::peek_bloch(uint32_t target) const { PauliStringRef x = inv_state.xs[target]; PauliStringRef z = inv_state.zs[target]; PauliString result(1); if (!x.xs.not_zero()) { result.sign = x.sign; result.xs[0] = true; } else if (!z.xs.not_zero()) { result.sign = z.sign; result.zs[0] = true; } else if (x.xs == z.xs) { PauliString y = inv_state.eval_y_obs(target); result.sign = y.sign; result.xs[0] = true; result.zs[0] = true; } return result; } template int8_t TableauSimulator::peek_x(uint32_t target) const { PauliStringRef x = inv_state.xs[target]; return x.xs.not_zero() ? 0 : x.sign ? -1 : +1; } template int8_t TableauSimulator::peek_y(uint32_t target) const { PauliString y = inv_state.eval_y_obs(target); return y.xs.not_zero() ? 0 : y.sign ? -1 : +1; } template int8_t TableauSimulator::peek_z(uint32_t target) const { PauliStringRef z = inv_state.zs[target]; return z.xs.not_zero() ? 0 : z.sign ? -1 : +1; } template void TableauSimulator::do_SQRT_X(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_X_DAG(q.data); } } template void TableauSimulator::do_SQRT_X_DAG(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_X(q.data); } } template void TableauSimulator::do_SQRT_Y(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_Y_DAG(q.data); } } template void TableauSimulator::do_SQRT_Y_DAG(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_Y(q.data); } } template bool TableauSimulator::read_measurement_record(uint32_t encoded_target) const { if (encoded_target & TARGET_SWEEP_BIT) { // Shot-to-shot variation currently not supported by the tableau simulator. Use frame simulator. return false; } assert(encoded_target & TARGET_RECORD_BIT); return measurement_record.lookback(encoded_target ^ TARGET_RECORD_BIT); } template void TableauSimulator::single_cx(uint32_t c, uint32_t t) { c &= ~TARGET_INVERTED_BIT; t &= ~TARGET_INVERTED_BIT; if (!((c | t) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { inv_state.prepend_ZCX(c, t); } else if (t & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) { throw std::invalid_argument("Measurement record editing is not supported."); } else { if (read_measurement_record(c)) { inv_state.prepend_X(t); } } } template void TableauSimulator::single_cy(uint32_t c, uint32_t t) { c &= ~TARGET_INVERTED_BIT; t &= ~TARGET_INVERTED_BIT; if (!((c | t) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { inv_state.prepend_ZCY(c, t); } else if (t & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) { throw std::invalid_argument("Measurement record editing is not supported."); } else { if (read_measurement_record(c)) { inv_state.prepend_Y(t); } } } template void TableauSimulator::do_ZCX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { single_cx(targets[k].data, targets[k + 1].data); } } template void TableauSimulator::do_ZCY(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { single_cy(targets[k].data, targets[k + 1].data); } } template void TableauSimulator::do_ZCZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; q1 &= ~TARGET_INVERTED_BIT; q2 &= ~TARGET_INVERTED_BIT; if (!((q1 | q2) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { inv_state.prepend_ZCZ(q1, q2); continue; } else if (!(q2 & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { if (read_measurement_record(q1)) { inv_state.prepend_Z(q2); } } else if (!(q1 & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { if (read_measurement_record(q2)) { inv_state.prepend_Z(q1); } } else { // Both targets are bits. No effect. } } } template void TableauSimulator::do_SWAP(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto c = targets[k].data; auto t = targets[k + 1].data; inv_state.prepend_SWAP(c, t); } } template void TableauSimulator::do_CXSWAP(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; inv_state.prepend_ZCX(q2, q1); inv_state.prepend_ZCX(q1, q2); } } template void TableauSimulator::do_CZSWAP(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; inv_state.prepend_ZCZ(q1, q2); inv_state.prepend_SWAP(q2, q1); } } template void TableauSimulator::do_SWAPCX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; inv_state.prepend_ZCX(q1, q2); inv_state.prepend_ZCX(q2, q1); } } template void TableauSimulator::do_ISWAP(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_ISWAP_DAG(q1, q2); } } template void TableauSimulator::do_ISWAP_DAG(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_ISWAP(q1, q2); } } template void TableauSimulator::do_XCX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; inv_state.prepend_XCX(q1, q2); } } template void TableauSimulator::do_SQRT_ZZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_ZZ_DAG(q1, q2); } } template void TableauSimulator::do_SQRT_ZZ_DAG(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_ZZ(q1, q2); } } template void TableauSimulator::do_SQRT_YY(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_YY_DAG(q1, q2); } } template void TableauSimulator::do_SQRT_YY_DAG(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_YY(q1, q2); } } template void TableauSimulator::do_SQRT_XX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_XX_DAG(q1, q2); } } template void TableauSimulator::do_SQRT_XX_DAG(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; // Note: inverted because we're tracking the inverse tableau. inv_state.prepend_SQRT_XX(q1, q2); } } template void TableauSimulator::do_XCY(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; inv_state.prepend_XCY(q1, q2); } } template void TableauSimulator::do_XCZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { single_cx(targets[k + 1].data, targets[k].data); } } template void TableauSimulator::do_YCX(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; inv_state.prepend_YCX(q1, q2); } } template void TableauSimulator::do_YCY(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { auto q1 = targets[k].data; auto q2 = targets[k + 1].data; inv_state.prepend_YCY(q1, q2); } } template void TableauSimulator::do_YCZ(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); for (size_t k = 0; k < targets.size(); k += 2) { single_cy(targets[k + 1].data, targets[k].data); } } template void TableauSimulator::do_DEPOLARIZE1(const CircuitInstruction &target_data) { RareErrorIterator::for_samples(target_data.args[0], target_data.targets, rng, [&](GateTarget q) { auto p = 1 + (rng() % 3); inv_state.xs.signs[q.data] ^= p & 1; inv_state.zs.signs[q.data] ^= p & 2; }); } template void TableauSimulator::do_DEPOLARIZE2(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; assert(!(targets.size() & 1)); auto n = targets.size() >> 1; RareErrorIterator::for_samples(target_data.args[0], n, rng, [&](size_t s) { auto p = 1 + (rng() % 15); auto q1 = targets[s << 1].data; auto q2 = targets[1 | (s << 1)].data; inv_state.xs.signs[q1] ^= p & 1; inv_state.zs.signs[q1] ^= p & 2; inv_state.xs.signs[q2] ^= p & 4; inv_state.zs.signs[q2] ^= p & 8; }); } template void TableauSimulator::do_HERALDED_ERASE(const CircuitInstruction &inst) { auto nt = inst.targets.size(); size_t offset = measurement_record.storage.size(); measurement_record.storage.insert(measurement_record.storage.end(), nt, false); uint64_t rng_buf = 0; size_t buf_size = 0; RareErrorIterator::for_samples(inst.args[0], nt, rng, [&](size_t target) { auto qubit = inst.targets[target].qubit_value(); if (buf_size == 0) { rng_buf = rng(); buf_size = 64; } inv_state.xs.signs[qubit] ^= (bool)(rng_buf & 1); inv_state.zs.signs[qubit] ^= (bool)(rng_buf & 2); measurement_record.storage[offset + target] = true; rng_buf >>= 2; buf_size -= 2; }); } template void TableauSimulator::do_HERALDED_PAULI_CHANNEL_1(const CircuitInstruction &inst) { auto nt = inst.targets.size(); size_t offset = measurement_record.storage.size(); measurement_record.storage.insert(measurement_record.storage.end(), nt, false); double hi = inst.args[0]; double hx = inst.args[1]; double hy = inst.args[2]; double hz = inst.args[3]; double ht = std::min(1.0, hi + hx + hy + hz); std::array conditionals{hx, hy, hz}; if (ht != 0) { conditionals[0] /= ht; conditionals[1] /= ht; conditionals[2] /= ht; } RareErrorIterator::for_samples(ht, nt, rng, [&](size_t target) { measurement_record.storage[offset + target] = true; do_PAULI_CHANNEL_1(CircuitInstruction{GateType::PAULI_CHANNEL_1, conditionals, &inst.targets[target], ""}); }); } template void TableauSimulator::do_X_ERROR(const CircuitInstruction &target_data) { RareErrorIterator::for_samples(target_data.args[0], target_data.targets, rng, [&](GateTarget q) { inv_state.zs.signs[q.data] ^= true; }); } template void TableauSimulator::do_Y_ERROR(const CircuitInstruction &target_data) { RareErrorIterator::for_samples(target_data.args[0], target_data.targets, rng, [&](GateTarget q) { inv_state.xs.signs[q.data] ^= true; inv_state.zs.signs[q.data] ^= true; }); } template void TableauSimulator::do_Z_ERROR(const CircuitInstruction &target_data) { RareErrorIterator::for_samples(target_data.args[0], target_data.targets, rng, [&](GateTarget q) { inv_state.xs.signs[q.data] ^= true; }); } template void TableauSimulator::do_PAULI_CHANNEL_1(const CircuitInstruction &target_data) { bool tmp = last_correlated_error_occurred; perform_pauli_errors_via_correlated_errors<1>( target_data, [&]() { last_correlated_error_occurred = false; }, [&](const CircuitInstruction &d) { do_ELSE_CORRELATED_ERROR(d); }); last_correlated_error_occurred = tmp; } template void TableauSimulator::do_PAULI_CHANNEL_2(const CircuitInstruction &target_data) { bool tmp = last_correlated_error_occurred; perform_pauli_errors_via_correlated_errors<2>( target_data, [&]() { last_correlated_error_occurred = false; }, [&](const CircuitInstruction &d) { do_ELSE_CORRELATED_ERROR(d); }); last_correlated_error_occurred = tmp; } template void TableauSimulator::do_CORRELATED_ERROR(const CircuitInstruction &target_data) { last_correlated_error_occurred = false; do_ELSE_CORRELATED_ERROR(target_data); } template void TableauSimulator::do_ELSE_CORRELATED_ERROR(const CircuitInstruction &target_data) { if (last_correlated_error_occurred) { return; } last_correlated_error_occurred = std::bernoulli_distribution(target_data.args[0])(rng); if (!last_correlated_error_occurred) { return; } for (auto qxz : target_data.targets) { auto q = qxz.qubit_value(); if (qxz.data & TARGET_PAULI_X_BIT) { inv_state.prepend_X(q); } if (qxz.data & TARGET_PAULI_Z_BIT) { inv_state.prepend_Z(q); } } } template void TableauSimulator::do_X(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_X(q.data); } } template void TableauSimulator::do_Y(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_Y(q.data); } } template void TableauSimulator::do_Z(const CircuitInstruction &target_data) { const auto &targets = target_data.targets; for (auto q : targets) { inv_state.prepend_Z(q.data); } } template simd_bits TableauSimulator::sample_circuit(const Circuit &circuit, std::mt19937_64 &rng, int8_t sign_bias) { TableauSimulator sim(std::move(rng), circuit.count_qubits(), sign_bias); sim.safe_do_circuit(circuit); const std::vector &v = sim.measurement_record.storage; simd_bits result(v.size()); for (size_t k = 0; k < v.size(); k++) { result[k] ^= v[k]; } rng = std::move(sim.rng); return result; } template void TableauSimulator::ensure_large_enough_for_qubits(size_t num_qubits) { if (num_qubits <= inv_state.num_qubits) { return; } inv_state.expand(num_qubits, 1.1); } template void TableauSimulator::sample_stream( FILE *in, FILE *out, SampleFormat format, bool interactive, std::mt19937_64 &rng) { TableauSimulator sim(std::move(rng), 1); auto writer = MeasureRecordWriter::make(out, format); Circuit unprocessed; while (true) { unprocessed.clear(); if (interactive) { try { unprocessed.append_from_file(in, true); } catch (const std::exception &ex) { std::cerr << "\033[31m" << ex.what() << "\033[0m\n"; continue; } } else { unprocessed.append_from_file(in, true); } if (unprocessed.operations.empty()) { break; } sim.ensure_large_enough_for_qubits(unprocessed.count_qubits()); unprocessed.for_each_operation([&](const CircuitInstruction &op) { sim.do_gate(op); sim.measurement_record.write_unwritten_results_to(*writer); if (interactive && op.count_measurement_results()) { putc('\n', out); fflush(out); } }); } rng = std::move(sim.rng); writer->write_end(); } template VectorSimulator TableauSimulator::to_vector_sim() const { auto inv = inv_state.inverse(); std::vector> stabilizers; for (size_t k = 0; k < inv.num_qubits; k++) { stabilizers.push_back(inv.zs[k]); } return VectorSimulator::from_stabilizers(stabilizers); } template void TableauSimulator::apply_tableau(const Tableau &tableau, const std::vector &targets) { inv_state.inplace_scatter_prepend(tableau.inverse(), targets); } template std::vector> TableauSimulator::to_state_vector(bool little_endian) const { auto sim = to_vector_sim(); if (!little_endian && inv_state.num_qubits > 0) { for (size_t q = 0; q < inv_state.num_qubits - q - 1; q++) { sim.apply(GateType::SWAP, q, inv_state.num_qubits - q - 1); } } return sim.state; } template void TableauSimulator::collapse_x(SpanRef targets, size_t stride) { // Find targets that need to be collapsed. std::set unique_collapse_targets; for (size_t k = 0; k < targets.size(); k += stride) { GateTarget t = targets[k]; t.data &= TARGET_VALUE_MASK; if (!is_deterministic_x(t.data)) { unique_collapse_targets.insert(t); } } // Only pay the cost of transposing if collapsing is needed. if (!unique_collapse_targets.empty()) { std::vector collapse_targets(unique_collapse_targets.begin(), unique_collapse_targets.end()); do_H_XZ({GateType::H, {}, collapse_targets, ""}); { TableauTransposedRaii temp_transposed(inv_state); for (auto q : collapse_targets) { collapse_qubit_z(q.data, temp_transposed); } } do_H_XZ({GateType::H, {}, collapse_targets, ""}); } } template void TableauSimulator::collapse_y(SpanRef targets, size_t stride) { // Find targets that need to be collapsed. std::set unique_collapse_targets; for (size_t k = 0; k < targets.size(); k += stride) { GateTarget t = targets[k]; t.data &= TARGET_VALUE_MASK; if (!is_deterministic_y(t.data)) { unique_collapse_targets.insert(t); } } // Only pay the cost of transposing if collapsing is needed. if (!unique_collapse_targets.empty()) { std::vector collapse_targets(unique_collapse_targets.begin(), unique_collapse_targets.end()); do_H_YZ({GateType::H_YZ, {}, collapse_targets, ""}); { TableauTransposedRaii temp_transposed(inv_state); for (auto q : collapse_targets) { collapse_qubit_z(q.data, temp_transposed); } } do_H_YZ({GateType::H_YZ, {}, collapse_targets, ""}); } } template void TableauSimulator::collapse_z(SpanRef targets, size_t stride) { // Find targets that need to be collapsed. std::vector collapse_targets; collapse_targets.reserve(targets.size()); for (size_t k = 0; k < targets.size(); k += stride) { GateTarget t = targets[k]; t.data &= TARGET_VALUE_MASK; if (!is_deterministic_z(t.data)) { collapse_targets.push_back(t); } } // Only pay the cost of transposing if collapsing is needed. if (!collapse_targets.empty()) { TableauTransposedRaii temp_transposed(inv_state); for (auto target : collapse_targets) { collapse_qubit_z(target.data, temp_transposed); } } } template size_t TableauSimulator::collapse_qubit_z(size_t target, TableauTransposedRaii &transposed_raii) { auto n = inv_state.num_qubits; // Search for any stabilizer generator that anti-commutes with the measurement observable. size_t pivot = 0; while (pivot < n && !transposed_raii.tableau.zs.xt[pivot][target]) { pivot++; } if (pivot == n) { // No anti-commuting stabilizer generator. Measurement is deterministic. return SIZE_MAX; } // Perform partial Gaussian elimination over the stabilizer generators that anti-commute with the measurement. // Do this by introducing no-effect-because-control-is-zero CNOTs at the beginning of time. for (size_t k = pivot + 1; k < n; k++) { if (transposed_raii.tableau.zs.xt[k][target]) { transposed_raii.append_ZCX(pivot, k); } } // Swap the now-isolated anti-commuting stabilizer generator for one that commutes with the measurement. if (transposed_raii.tableau.zs.zt[pivot][target]) { transposed_raii.append_H_YZ(pivot); } else { transposed_raii.append_H_XZ(pivot); } // Assign a measurement result. bool result_if_measured = sign_bias == 0 ? (rng() & 1) : sign_bias < 0; if (inv_state.zs.signs[target] != result_if_measured) { transposed_raii.append_X(pivot); } return pivot; } template void TableauSimulator::collapse_isolate_qubit_z(size_t target, TableauTransposedRaii &transposed_raii) { // Force T(Z_target) to be a product of Z operations. collapse_qubit_z(target, transposed_raii); // Ensure T(Z_target) is a product of Z operations containing Z_target. auto n = inv_state.num_qubits; for (size_t q = 0; true; q++) { assert(q < n); if (transposed_raii.tableau.zs.zt[q][target]) { if (q != target) { transposed_raii.append_SWAP(q, target); } break; } } // Ensure T(Z_target) = +-Z_target. for (size_t q = 0; q < n; q++) { if (q != target && transposed_raii.tableau.zs.zt[q][target]) { // Cancel Z term on non-target q. transposed_raii.append_ZCX(q, target); } } // Note T(X_target) now contains X_target or Y_target because it has to anti-commute with T(Z_target) = Z_target. // Ensure T(X_target) contains X_target instead of Y_target. if (transposed_raii.tableau.xs.zt[target][target]) { transposed_raii.append_S(target); } // Ensure T(X_target) = +-X_target. for (size_t q = 0; q < n; q++) { if (q != target) { int p = transposed_raii.tableau.xs.xt[q][target] + 2 * transposed_raii.tableau.xs.zt[q][target]; if (p == 1) { transposed_raii.append_ZCX(target, q); } else if (p == 2) { transposed_raii.append_ZCZ(target, q); } else if (p == 3) { transposed_raii.append_ZCY(target, q); } } } } template void TableauSimulator::safe_do_circuit(const Circuit &circuit, uint64_t reps) { ensure_large_enough_for_qubits(circuit.count_qubits()); for (uint64_t k = 0; k < reps; k++) { circuit.for_each_operation([&](const CircuitInstruction &op) { do_gate(op); }); } } template simd_bits TableauSimulator::reference_sample_circuit(const Circuit &circuit) { std::mt19937_64 irrelevant_rng(0); return TableauSimulator::sample_circuit(circuit.aliased_noiseless_circuit(), irrelevant_rng, +1); } template void TableauSimulator::paulis(const PauliString &paulis) { auto nw = paulis.xs.num_simd_words; inv_state.zs.signs.word_range_ref(0, nw) ^= paulis.xs; inv_state.xs.signs.word_range_ref(0, nw) ^= paulis.zs; } template void TableauSimulator::do_operation_ensure_size(const CircuitInstruction &operation) { uint64_t n = 0; for (const auto &t : operation.targets) { if (t.has_qubit_value()) { n = std::max(n, (uint64_t)t.qubit_value() + 1); } } ensure_large_enough_for_qubits(n); do_gate(operation); } template void TableauSimulator::set_num_qubits(size_t new_num_qubits) { if (new_num_qubits >= inv_state.num_qubits) { ensure_large_enough_for_qubits(new_num_qubits); return; } // Collapse qubits past the new size and ensure the internal state totally decouples them. { TableauTransposedRaii temp_transposed(inv_state); for (size_t q = new_num_qubits; q < inv_state.num_qubits; q++) { collapse_isolate_qubit_z(q, temp_transposed); } } Tableau old_state = std::move(inv_state); inv_state = Tableau(new_num_qubits); inv_state.xs.signs.truncated_overwrite_from(old_state.xs.signs, new_num_qubits); inv_state.zs.signs.truncated_overwrite_from(old_state.zs.signs, new_num_qubits); for (size_t q = 0; q < new_num_qubits; q++) { inv_state.xs[q].xs.truncated_overwrite_from(old_state.xs[q].xs, new_num_qubits); inv_state.xs[q].zs.truncated_overwrite_from(old_state.xs[q].zs, new_num_qubits); inv_state.zs[q].xs.truncated_overwrite_from(old_state.zs[q].xs, new_num_qubits); inv_state.zs[q].zs.truncated_overwrite_from(old_state.zs[q].zs, new_num_qubits); } } template std::pair> TableauSimulator::measure_kickback_z(GateTarget target) { bool flipped = target.is_inverted_result_target(); uint32_t q = target.qubit_value(); PauliString kickback(0); bool has_kickback = !is_deterministic_z(q); // Note: do this before transposing the state! { TableauTransposedRaii temp_transposed(inv_state); if (has_kickback) { size_t pivot = collapse_qubit_z(q, temp_transposed); kickback = temp_transposed.unsigned_x_input(pivot); } bool result = inv_state.zs.signs[q] ^ flipped; measurement_record.storage.push_back(result); // Prevent later measure_kickback calls from unnecessarily targeting this qubit with a Z gate. collapse_isolate_qubit_z(q, temp_transposed); return {result, kickback}; } } template std::pair> TableauSimulator::measure_kickback_y(GateTarget target) { do_H_YZ({GateType::H, {}, &target, ""}); auto result = measure_kickback_z(target); do_H_YZ({GateType::H, {}, &target, ""}); if (result.second.num_qubits) { // Also conjugate the kickback by H_YZ. result.second.xs[target.qubit_value()] ^= result.second.zs[target.qubit_value()]; } return result; } template std::pair> TableauSimulator::measure_kickback_x(GateTarget target) { do_H_XZ({GateType::H, {}, &target, ""}); auto result = measure_kickback_z(target); do_H_XZ({GateType::H, {}, &target, ""}); if (result.second.num_qubits) { // Also conjugate the kickback by H_XZ. result.second.xs[target.qubit_value()].swap_with(result.second.zs[target.qubit_value()]); } return result; } template std::vector> TableauSimulator::canonical_stabilizers() const { return inv_state.inverse().stabilizers(true); } template int8_t TableauSimulator::peek_observable_expectation(const PauliString &observable) const { TableauSimulator state = *this; // Kick the observable onto an ancilla qubit's Z observable. auto n = (uint32_t)std::max(state.inv_state.num_qubits, observable.num_qubits); state.ensure_large_enough_for_qubits(n + 1); GateTarget anc{n}; if (observable.sign) { state.do_X({GateType::X, {}, &anc, ""}); } observable.ref().for_each_active_pauli([&](size_t q) { int p = observable.xs[q] + (observable.zs[q] << 1); std::array targets{GateTarget{(uint32_t)q}, anc}; GateType c2_type = GateType::CX; if (p == 1) { c2_type = GateType::XCX; } else if (p == 3) { c2_type = GateType::YCX; } state.do_gate({c2_type, {}, targets, ""}); }); // Use simulator features to determines if the measurement is deterministic. if (!state.is_deterministic_z(anc.data)) { return 0; } state.do_MZ({GateType::M, {}, &anc, ""}); return state.measurement_record.storage.back() ? -1 : +1; } template void TableauSimulator::do_gate(const CircuitInstruction &inst) { switch (inst.gate_type) { case GateType::DETECTOR: do_I(inst); break; case GateType::OBSERVABLE_INCLUDE: do_I(inst); break; case GateType::TICK: do_I(inst); break; case GateType::QUBIT_COORDS: do_I(inst); break; case GateType::SHIFT_COORDS: do_I(inst); break; case GateType::REPEAT: do_I(inst); break; case GateType::MX: do_MX(inst); break; case GateType::MY: do_MY(inst); break; case GateType::M: do_MZ(inst); break; case GateType::MRX: do_MRX(inst); break; case GateType::MRY: do_MRY(inst); break; case GateType::MR: do_MRZ(inst); break; case GateType::RX: do_RX(inst); break; case GateType::RY: do_RY(inst); break; case GateType::R: do_RZ(inst); break; case GateType::MPP: do_MPP(inst); break; case GateType::SPP: do_SPP(inst); break; case GateType::SPP_DAG: do_SPP_DAG(inst); break; case GateType::MXX: do_MXX(inst); break; case GateType::MYY: do_MYY(inst); break; case GateType::MZZ: do_MZZ(inst); break; case GateType::MPAD: do_MPAD(inst); break; case GateType::XCX: do_XCX(inst); break; case GateType::XCY: do_XCY(inst); break; case GateType::XCZ: do_XCZ(inst); break; case GateType::YCX: do_YCX(inst); break; case GateType::YCY: do_YCY(inst); break; case GateType::YCZ: do_YCZ(inst); break; case GateType::CX: do_ZCX(inst); break; case GateType::CY: do_ZCY(inst); break; case GateType::CZ: do_ZCZ(inst); break; case GateType::H: do_H_XZ(inst); break; case GateType::H_XY: do_H_XY(inst); break; case GateType::H_YZ: do_H_YZ(inst); break; case GateType::H_NXY: do_H_NXY(inst); break; case GateType::H_NXZ: do_H_NXZ(inst); break; case GateType::H_NYZ: do_H_NYZ(inst); break; case GateType::DEPOLARIZE1: do_DEPOLARIZE1(inst); break; case GateType::DEPOLARIZE2: do_DEPOLARIZE2(inst); break; case GateType::X_ERROR: do_X_ERROR(inst); break; case GateType::Y_ERROR: do_Y_ERROR(inst); break; case GateType::Z_ERROR: do_Z_ERROR(inst); break; case GateType::PAULI_CHANNEL_1: do_PAULI_CHANNEL_1(inst); break; case GateType::PAULI_CHANNEL_2: do_PAULI_CHANNEL_2(inst); break; case GateType::E: do_CORRELATED_ERROR(inst); break; case GateType::ELSE_CORRELATED_ERROR: do_ELSE_CORRELATED_ERROR(inst); break; case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: do_I(inst); break; case GateType::X: do_X(inst); break; case GateType::Y: do_Y(inst); break; case GateType::Z: do_Z(inst); break; case GateType::C_XYZ: do_C_XYZ(inst); break; case GateType::C_NXYZ: do_C_NXYZ(inst); break; case GateType::C_XNYZ: do_C_XNYZ(inst); break; case GateType::C_XYNZ: do_C_XYNZ(inst); break; case GateType::C_ZYX: do_C_ZYX(inst); break; case GateType::C_NZYX: do_C_NZYX(inst); break; case GateType::C_ZNYX: do_C_ZNYX(inst); break; case GateType::C_ZYNX: do_C_ZYNX(inst); break; case GateType::SQRT_X: do_SQRT_X(inst); break; case GateType::SQRT_X_DAG: do_SQRT_X_DAG(inst); break; case GateType::SQRT_Y: do_SQRT_Y(inst); break; case GateType::SQRT_Y_DAG: do_SQRT_Y_DAG(inst); break; case GateType::S: do_SQRT_Z(inst); break; case GateType::S_DAG: do_SQRT_Z_DAG(inst); break; case GateType::SQRT_XX: do_SQRT_XX(inst); break; case GateType::SQRT_XX_DAG: do_SQRT_XX_DAG(inst); break; case GateType::SQRT_YY: do_SQRT_YY(inst); break; case GateType::SQRT_YY_DAG: do_SQRT_YY_DAG(inst); break; case GateType::SQRT_ZZ: do_SQRT_ZZ(inst); break; case GateType::SQRT_ZZ_DAG: do_SQRT_ZZ_DAG(inst); break; case GateType::SWAP: do_SWAP(inst); break; case GateType::ISWAP: do_ISWAP(inst); break; case GateType::ISWAP_DAG: do_ISWAP_DAG(inst); break; case GateType::CXSWAP: do_CXSWAP(inst); break; case GateType::CZSWAP: do_CZSWAP(inst); break; case GateType::SWAPCX: do_SWAPCX(inst); break; case GateType::HERALDED_ERASE: do_HERALDED_ERASE(inst); break; case GateType::HERALDED_PAULI_CHANNEL_1: do_HERALDED_PAULI_CHANNEL_1(inst); break; default: throw std::invalid_argument( "Not implemented by TableauSimulator::do_gate: " + std::string(GATE_DATA[inst.gate_type].name)); } } } // namespace stim ================================================ FILE: src/stim/simulators/tableau_simulator.perf.cc ================================================ // 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. #include "stim/simulators/tableau_simulator.h" #include "stim/gen/circuit_gen_params.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(TableauSimulator_CX_10Kqubits) { size_t num_qubits = 10 * 1000; TableauSimulator sim(std::mt19937_64(0), num_qubits); std::vector targets; for (uint32_t k = 0; k < (uint32_t)num_qubits; k++) { targets.push_back(GateTarget{k}); } CircuitInstruction op_data{GateType::CX, {}, targets, ""}; benchmark_go([&]() { sim.do_ZCX(op_data); }) .goal_millis(5) .show_rate("OpQubits", targets.size()); } ================================================ FILE: src/stim/simulators/tableau_simulator.pybind.cc ================================================ #include "stim/simulators/tableau_simulator.pybind.h" #include "stim/circuit/circuit_instruction.pybind.h" #include "stim/circuit/circuit_repeat_block.pybind.h" #include "stim/py/base.pybind.h" #include "stim/simulators/tableau_simulator.h" #include "stim/stabilizers/pauli_string.pybind.h" #include "stim/stabilizers/tableau.h" #include "stim/util_bot/str_util.h" #include "stim/util_top/circuit_vs_amplitudes.h" #include "stim/util_top/stabilizers_to_tableau.h" using namespace stim; using namespace stim_pybind; template void do_obj(TableauSimulator &self, const pybind11::object &obj) { if (pybind11::isinstance(obj)) { self.safe_do_circuit(pybind11::cast(obj)); } else if (pybind11::isinstance(obj)) { const CircuitRepeatBlock &block = pybind11::cast(obj); self.safe_do_circuit(block.body, block.repeat_count); } else if (pybind11::isinstance(obj)) { const FlexPauliString &pauli_string = pybind11::cast(obj); self.ensure_large_enough_for_qubits(pauli_string.value.num_qubits); self.paulis(pauli_string.value); } else if (pybind11::isinstance(obj)) { const PyCircuitInstruction &circuit_instruction = pybind11::cast(obj); self.do_operation_ensure_size(circuit_instruction); } else { std::stringstream ss; ss << "Don't know how to handle "; ss << pybind11::repr(obj); throw std::invalid_argument(ss.str()); } } template TableauSimulator create_tableau_simulator(const pybind11::object &seed) { return TableauSimulator(make_py_seeded_rng(seed)); } template std::vector arg_to_qubit_or_qubits(TableauSimulator &self, const pybind11::object &obj) { std::vector arguments; uint32_t max_q = 0; try { try { uint32_t q = pybind11::cast(obj); arguments.push_back(GateTarget::qubit(q)); max_q = q; } catch (const pybind11::cast_error &) { for (const auto &e : obj) { uint32_t q = e.cast(); max_q = std::max(max_q, q); arguments.push_back(GateTarget::qubit(q)); } } } catch (const pybind11::cast_error &) { throw std::out_of_range("'targets' must be a non-negative integer or iterable of non-negative integers."); } // Note: quadratic behavior. self.ensure_large_enough_for_qubits(max_q + 1); return arguments; } template PyCircuitInstruction build_single_qubit_gate_instruction_ensure_size( TableauSimulator &self, GateType gate_type, const pybind11::args &args, SpanRef gate_args = {}) { std::vector targets; uint32_t max_q = 0; try { for (const auto &e : args) { if (pybind11::isinstance(e)) { targets.push_back(pybind11::cast(e)); } else { uint32_t q = e.cast(); max_q = std::max(max_q, q & TARGET_VALUE_MASK); targets.push_back(GateTarget{q}); } } } catch (const pybind11::cast_error &) { throw std::out_of_range("Target qubits must be non-negative integers."); } std::vector gate_args_vec; for (const auto &e : gate_args) { gate_args_vec.push_back(e); } // Note: quadratic behavior. self.ensure_large_enough_for_qubits(max_q + 1); return PyCircuitInstruction(gate_type, targets, gate_args_vec, ""); } template PyCircuitInstruction build_two_qubit_gate_instruction_ensure_size( TableauSimulator &self, GateType gate_type, const pybind11::args &args, SpanRef gate_args = {}) { if (pybind11::len(args) & 1) { throw std::invalid_argument("Two qubit operation requires an even number of targets."); } auto result = build_single_qubit_gate_instruction_ensure_size(self, gate_type, args, gate_args); for (size_t k = 0; k < result.targets.size(); k += 2) { if (result.targets[k] == result.targets[k + 1]) { throw std::invalid_argument("Two qubit operation can't target the same qubit twice."); } } return result; } pybind11::class_> stim_pybind::pybind_tableau_simulator(pybind11::module &m) { return pybind11::class_>( m, "TableauSimulator", clean_doc_string(R"DOC( A stabilizer circuit simulator that tracks an inverse stabilizer tableau. Supports interactive usage, where gates and measurements are applied on demand. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> if s.measure(0): ... s.h(1) ... s.cnot(1, 2) >>> s.measure(1) == s.measure(2) True >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+X_"), stim.PauliString("+XZ"), ], ) )DOC") .data()); } void stim_pybind::pybind_tableau_simulator_methods( pybind11::module &m, pybind11::class_> &c) { c.def( pybind11::init(&create_tableau_simulator), pybind11::kw_only(), pybind11::arg("seed") = pybind11::none(), clean_doc_string(R"DOC( @signature def __init__(self, *, seed: Optional[int] = None) -> None: Initializes a stim.TableauSimulator. Args: seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng is seeded from system entropy. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Returns: An initialized stim.TableauSimulator. Examples: >>> import stim >>> s = stim.TableauSimulator(seed=0) >>> s2 = stim.TableauSimulator(seed=0) >>> s.h(0) >>> s2.h(0) >>> s.measure(0) == s2.measure(0) True )DOC") .data()); c.def( "current_inverse_tableau", [](TableauSimulator &self) { return self.inv_state; }, clean_doc_string(R"DOC( Returns a copy of the internal state of the simulator as a stim.Tableau. Returns: A stim.Tableau copy of the simulator's state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> s.cnot(0, 1) >>> s.current_inverse_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+ZX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+X_"), stim.PauliString("+XZ"), ], ) )DOC") .data()); c.def( "state_vector", [](const TableauSimulator &self, std::string_view endian) { bool little_endian; if (endian == "little") { little_endian = true; } else if (endian == "big") { little_endian = false; } else { throw std::invalid_argument("endian not in ['little', 'big']"); } auto complex_vec = self.to_state_vector(little_endian); std::complex *buffer = new std::complex[complex_vec.size()]; for (size_t k = 0; k < complex_vec.size(); k++) { buffer[k] = complex_vec[k]; } pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast *>(f); }); return pybind11::array_t>( {(pybind11::ssize_t)complex_vec.size()}, {(pybind11::ssize_t)sizeof(std::complex)}, buffer, free_when_done); }, pybind11::kw_only(), pybind11::arg("endian") = "little", clean_doc_string(R"DOC( @signature def state_vector(self, *, endian: Literal["little", "big"] = 'little') -> np.ndarray[np.complex64]: Returns a wavefunction for the simulator's current state. This function takes O(n * 2**n) time and O(2**n) space, where n is the number of qubits. The computation is done by initialization a random state vector and iteratively projecting it into the +1 eigenspace of each stabilizer of the state. The state is then canonicalized so that zero values are actually exactly 0, and so that the first non-zero entry is positive. Args: endian: "little" (default): state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A `numpy.ndarray[numpy.complex64]` of computational basis amplitudes. If the result is in little endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_0, the qubit with index 1 is storing the bit b_1, etc. If the result is in big endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_{n-1}, the qubit with index 1 is storing the bit b_{n-2}, etc. Examples: >>> import stim >>> import numpy as np >>> s = stim.TableauSimulator() >>> s.x(2) >>> s.state_vector(endian='little') array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> s.state_vector(endian='big') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> s.sqrt_x(1, 2) >>> s.state_vector() array([0.5+0.j , 0. +0.j , 0. -0.5j, 0. +0.j , 0. +0.5j, 0. +0.j , 0.5+0.j , 0. +0.j ], dtype=complex64) )DOC") .data()); c.def( "canonical_stabilizers", [](const TableauSimulator &self) { auto stabilizers = self.canonical_stabilizers(); std::vector result; result.reserve(stabilizers.size()); for (auto &s : stabilizers) { result.emplace_back(std::move(s), false); } return result; }, clean_doc_string(R"DOC( Returns a standardized list of the simulator's current stabilizer generators. Two simulators have the same canonical stabilizers if and only if their current quantum state is equal (and tracking the same number of qubits). The canonical form is computed as follows: 1. Get a list of stabilizers using the `z_output`s of `simulator.current_inverse_tableau()**-1`. 2. Perform Gaussian elimination on each generator g. 2a) The generators are considered in order X0, Z0, X1, Z1, X2, Z2, etc. 2b) Pick any stabilizer that uses the generator g. If there are none, go to the next g. 2c) Multiply that stabilizer into all other stabilizers that use the generator g. 2d) Swap that stabilizer with the stabilizer at position `next_output` then increment `next_output`. Returns: A List[stim.PauliString] of the simulator's state's stabilizers. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.x(2) >>> for e in s.canonical_stabilizers(): ... print(repr(e)) stim.PauliString("+XX_") stim.PauliString("+ZZ_") stim.PauliString("-__Z") >>> # Scramble the stabilizers then check the canonical form is unchanged. >>> s.set_inverse_tableau(s.current_inverse_tableau()**-1) >>> s.cnot(0, 1) >>> s.cz(0, 2) >>> s.s(0, 2) >>> s.cy(2, 1) >>> s.set_inverse_tableau(s.current_inverse_tableau()**-1) >>> for e in s.canonical_stabilizers(): ... print(repr(e)) stim.PauliString("+XX_") stim.PauliString("+ZZ_") stim.PauliString("-__Z") )DOC") .data()); c.def( "current_measurement_record", [](const TableauSimulator &self) { return self.measurement_record.storage; }, clean_doc_string(R"DOC( Returns a copy of the record of all measurements performed by the simulator. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.current_measurement_record() [] >>> s.measure(0) False >>> s.x(0) >>> s.measure(0) True >>> s.current_measurement_record() [False, True] >>> s.do(stim.Circuit("M 0")) >>> s.current_measurement_record() [False, True, True] Returns: A list of booleans containing the result of every measurement performed by the simulator so far. )DOC") .data()); c.def( "do", &do_obj, pybind11::arg("circuit_or_pauli_string"), clean_doc_string(R"DOC( Applies a circuit or pauli string to the simulator's state. @signature def do(self, circuit_or_pauli_string: Union[stim.Circuit, stim.PauliString, stim.CircuitInstruction, stim.CircuitRepeatBlock]) -> None: Args: circuit_or_pauli_string: A stim.Circuit, stim.PauliString, stim.CircuitInstruction, or stim.CircuitRepeatBlock with operations to apply to the simulator's state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do(stim.Circuit(''' ... X 0 ... M 0 ... ''')) >>> s.current_measurement_record() [True] >>> s = stim.TableauSimulator() >>> s.do(stim.PauliString("IXYZ")) >>> s.measure_many(0, 1, 2, 3) [False, True, True, False] )DOC") .data()); c.def( "do_pauli_string", [](TableauSimulator &self, FlexPauliString &pauli_string) { self.ensure_large_enough_for_qubits(pauli_string.value.num_qubits); self.paulis(pauli_string.value); }, pybind11::arg("pauli_string"), clean_doc_string(R"DOC( Applies the paulis from a pauli string to the simulator's state. Args: pauli_string: A stim.PauliString containing Paulis to apply. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do_pauli_string(stim.PauliString("IXYZ")) >>> s.measure_many(0, 1, 2, 3) [False, True, True, False] )DOC") .data()); c.def( "do_circuit", [](TableauSimulator &self, const Circuit &circuit) { self.safe_do_circuit(circuit); }, pybind11::arg("circuit"), clean_doc_string(R"DOC( Applies a circuit to the simulator's state. Args: circuit: A stim.Circuit containing operations to apply. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.do_circuit(stim.Circuit(''' ... X 0 ... M 0 ... ''')) >>> s.current_measurement_record() [True] )DOC") .data()); c.def( "do_tableau", [](TableauSimulator &self, const Tableau &tableau, const std::vector &targets) { if (targets.size() != tableau.num_qubits) { throw std::invalid_argument("len(tableau) != len(targets)"); } size_t max_target = 0; for (size_t i = 0; i < targets.size(); i++) { max_target = std::max(max_target, targets[i]); for (size_t j = i + 1; j < targets.size(); j++) { if (targets[i] == targets[j]) { std::stringstream ss; ss << "targets contains duplicates: "; ss << comma_sep(targets); throw std::invalid_argument(ss.str()); } } } self.ensure_large_enough_for_qubits(max_target + 1); self.apply_tableau(tableau, targets); }, pybind11::arg("tableau"), pybind11::arg("targets"), clean_doc_string(R"DOC( Applies a custom tableau operation to qubits in the simulator. Note that this method has to compute the inverse of the tableau, because the simulator's internal state is an inverse tableau. Args: tableau: A stim.Tableau representing the Clifford operation to apply. targets: The indices of the qubits to operate on. Examples: >>> import stim >>> sim = stim.TableauSimulator() >>> sim.h(1) >>> sim.h_yz(2) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+X', '+Y', '+Z'] >>> rot3 = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("_X_"), ... stim.PauliString("__X"), ... stim.PauliString("X__"), ... ], ... zs=[ ... stim.PauliString("_Z_"), ... stim.PauliString("__Z"), ... stim.PauliString("Z__"), ... ], ... ) >>> sim.do_tableau(rot3, [1, 2, 3]) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+Z', '+X', '+Y'] >>> sim.do_tableau(rot3, [1, 2, 3]) >>> [str(sim.peek_bloch(k)) for k in range(4)] ['+Z', '+Y', '+Z', '+X'] )DOC") .data()); c.def( "h", [](TableauSimulator &self, const pybind11::args &args) { self.do_H_XZ(build_single_qubit_gate_instruction_ensure_size(self, GateType::H, args)); }, clean_doc_string(R"DOC( Applies a Hadamard gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z -Y +X )DOC") .data()); c.def( "depolarize1", [](TableauSimulator &self, const pybind11::args &args, const pybind11::kwargs &kwargs) { double p = pybind11::cast(kwargs["p"]); if (kwargs.size() != 1) { throw std::invalid_argument("Unexpected argument. Expected position-only targets and p=probability."); } self.do_DEPOLARIZE1( build_single_qubit_gate_instruction_ensure_size( self, GateType::DEPOLARIZE1, args, &p)); }, clean_doc_string(R"DOC( @signature def depolarize1(self, *targets: int, p: float): Probabilistically applies single-qubit depolarization to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.depolarize1(0, 1, 2, p=0.01) )DOC") .data()); c.def( "depolarize2", [](TableauSimulator &self, const pybind11::args &args, const pybind11::kwargs &kwargs) { double p = pybind11::cast(kwargs["p"]); if (kwargs.size() != 1) { throw std::invalid_argument("Unexpected argument. Expected position-only targets and p=probability."); } self.do_DEPOLARIZE2( build_two_qubit_gate_instruction_ensure_size(self, GateType::DEPOLARIZE2, args, &p)); }, clean_doc_string(R"DOC( @signature def depolarize2(self, *targets: int, p: float): Probabilistically applies two-qubit depolarization to targets. Args: *targets: The indices of the qubits to target with the noise. The pairs of qubits are formed by zip(targets[::1], targets[1::2]). p: The chance of the error being applied, independently, to each qubit pair. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.depolarize1(0, 1, 4, 5, p=0.01) )DOC") .data()); c.def( "x_error", [](TableauSimulator &self, const pybind11::args &args, const pybind11::kwargs &kwargs) { double p = pybind11::cast(kwargs["p"]); if (kwargs.size() != 1) { throw std::invalid_argument("Unexpected argument. Expected position-only targets and p=probability."); } self.do_X_ERROR( build_single_qubit_gate_instruction_ensure_size( self, GateType::X_ERROR, args, {&p})); }, clean_doc_string(R"DOC( @signature def x_error(self, *targets: int, p: float): Probabilistically applies X errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the X error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x_error(0, 1, 2, p=0.01) )DOC") .data()); c.def( "y_error", [](TableauSimulator &self, const pybind11::args &args, const pybind11::kwargs &kwargs) { double p = pybind11::cast(kwargs["p"]); if (kwargs.size() != 1) { throw std::invalid_argument("Unexpected argument. Expected position-only targets and p=probability."); } self.do_Y_ERROR( build_single_qubit_gate_instruction_ensure_size(self, GateType::Y_ERROR, args, &p)); }, clean_doc_string(R"DOC( @signature def y_error(self, *targets: int, p: float): Probabilistically applies Y errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the Y error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.y_error(0, 1, 2, p=0.01) )DOC") .data()); c.def( "z_error", [](TableauSimulator &self, const pybind11::args &args, const pybind11::kwargs &kwargs) { double p = pybind11::cast(kwargs["p"]); if (kwargs.size() != 1) { throw std::invalid_argument("Unexpected argument. Expected position-only targets and p=probability."); } self.do_Z_ERROR( build_single_qubit_gate_instruction_ensure_size(self, GateType::Z_ERROR, args, &p)); }, clean_doc_string(R"DOC( @signature def z_error(self, *targets: int, p: float): Probabilistically applies Z errors to targets. Args: *targets: The indices of the qubits to target with the noise. p: The chance of the Z error being applied, independently, to each qubit. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.z_error(0, 1, 2, p=0.01) )DOC") .data()); c.def( "h_xz", [](TableauSimulator &self, const pybind11::args &args) { self.do_H_XZ(build_single_qubit_gate_instruction_ensure_size(self, GateType::H, args)); }, clean_doc_string(R"DOC( Applies a Hadamard gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_xz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z -Y +X )DOC") .data()); c.def( "c_xyz", [](TableauSimulator &self, const pybind11::args &args) { self.do_C_XYZ( build_single_qubit_gate_instruction_ensure_size(self, GateType::C_XYZ, args)); }, clean_doc_string(R"DOC( Applies a C_XYZ gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.c_xyz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y +Z +X )DOC") .data()); c.def( "c_zyx", [](TableauSimulator &self, const pybind11::args &args) { self.do_C_ZYX( build_single_qubit_gate_instruction_ensure_size(self, GateType::C_ZYX, args)); }, clean_doc_string(R"DOC( Applies a C_ZYX gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.c_zyx(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z +X +Y )DOC") .data()); c.def( "h_xy", [](TableauSimulator &self, const pybind11::args &args) { self.do_H_XY( build_single_qubit_gate_instruction_ensure_size(self, GateType::H_XY, args)); }, clean_doc_string(R"DOC( Applies an operation that swaps the X and Y axes to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_xy(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y +X -Z )DOC") .data()); c.def( "h_yz", [](TableauSimulator &self, const pybind11::args &args) { self.do_H_YZ( build_single_qubit_gate_instruction_ensure_size(self, GateType::H_YZ, args)); }, clean_doc_string(R"DOC( Applies an operation that swaps the Y and Z axes to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.h_yz(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X +Z +Y )DOC") .data()); c.def( "x", [](TableauSimulator &self, const pybind11::args &args) { self.do_X(build_single_qubit_gate_instruction_ensure_size(self, GateType::X, args)); }, clean_doc_string(R"DOC( Applies a Pauli X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.x(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X -Y -Z )DOC") .data()); c.def( "y", [](TableauSimulator &self, const pybind11::args &args) { self.do_Y(build_single_qubit_gate_instruction_ensure_size(self, GateType::Y, args)); }, clean_doc_string(R"DOC( Applies a Pauli Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.y(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X +Y -Z )DOC") .data()); c.def( "z", [](TableauSimulator &self, pybind11::args targets) { self.do_Z(build_single_qubit_gate_instruction_ensure_size(self, GateType::Z, targets)); }, clean_doc_string(R"DOC( Applies a Pauli Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.z(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -X -Y +Z )DOC") .data()); c.def( "s", [](TableauSimulator &self, const pybind11::args &args) { self.do_SQRT_Z(build_single_qubit_gate_instruction_ensure_size(self, GateType::S, args)); }, clean_doc_string(R"DOC( Applies a SQRT_Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.s(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Y -X +Z )DOC") .data()); c.def( "s_dag", [](TableauSimulator &self, const pybind11::args &args) { self.do_SQRT_Z_DAG( build_single_qubit_gate_instruction_ensure_size(self, GateType::S_DAG, args)); }, clean_doc_string(R"DOC( Applies a SQRT_Z_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.s_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -Y +X +Z )DOC") .data()); c.def( "sqrt_x", [](TableauSimulator &self, const pybind11::args &args) { self.do_SQRT_X( build_single_qubit_gate_instruction_ensure_size(self, GateType::SQRT_X, args)); }, clean_doc_string(R"DOC( Applies a SQRT_X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_x(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Z -Y )DOC") .data()); c.def( "sqrt_x_dag", [](TableauSimulator &self, const pybind11::args &args) { self.do_SQRT_X_DAG( build_single_qubit_gate_instruction_ensure_size(self, GateType::SQRT_X_DAG, args)); }, clean_doc_string(R"DOC( Applies a SQRT_X_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_x_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X -Z +Y )DOC") .data()); c.def( "sqrt_y", [](TableauSimulator &self, const pybind11::args &args) { self.do_SQRT_Y( build_single_qubit_gate_instruction_ensure_size(self, GateType::SQRT_Y, args)); }, clean_doc_string(R"DOC( Applies a SQRT_Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_y(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) -Z +Y +X )DOC") .data()); c.def( "sqrt_y_dag", [](TableauSimulator &self, const pybind11::args &args) { self.do_SQRT_Y_DAG( build_single_qubit_gate_instruction_ensure_size(self, GateType::SQRT_Y_DAG, args)); }, clean_doc_string(R"DOC( Applies a SQRT_Y_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +X +Y +Z >>> s.sqrt_y_dag(0, 1, 2) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(3))) +Z +Y -X )DOC") .data()); c.def( "swap", [](TableauSimulator &self, const pybind11::args &args) { self.do_SWAP(build_two_qubit_gate_instruction_ensure_size(self, GateType::SWAP, args)); }, clean_doc_string(R"DOC( Applies a swap gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.swap(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +Y +X +X +Z )DOC") .data()); c.def( "iswap", [](TableauSimulator &self, const pybind11::args &args) { self.do_ISWAP(build_two_qubit_gate_instruction_ensure_size(self, GateType::ISWAP, args)); }, clean_doc_string(R"DOC( Applies an ISWAP gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.iswap(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Y +Z )DOC") .data()); c.def( "iswap_dag", [](TableauSimulator &self, const pybind11::args &args) { self.do_ISWAP_DAG( build_two_qubit_gate_instruction_ensure_size(self, GateType::ISWAP_DAG, args)); }, clean_doc_string(R"DOC( Applies an ISWAP_DAG gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.iswap_dag(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ -Y +Z )DOC") .data()); c.def( "cnot", [](TableauSimulator &self, const pybind11::args &args) { self.do_ZCX(build_two_qubit_gate_instruction_ensure_size(self, GateType::CX, args)); }, clean_doc_string(R"DOC( Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cnot(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X )DOC") .data()); c.def( "zcx", [](TableauSimulator &self, const pybind11::args &args) { self.do_ZCX(build_two_qubit_gate_instruction_ensure_size(self, GateType::CX, args)); }, clean_doc_string(R"DOC( Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X )DOC") .data()); c.def( "cx", [](TableauSimulator &self, const pybind11::args &args) { self.do_ZCX(build_two_qubit_gate_instruction_ensure_size(self, GateType::CX, args)); }, clean_doc_string(R"DOC( Applies a controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X )DOC") .data()); c.def( "cz", [](TableauSimulator &self, const pybind11::args &args) { self.do_ZCZ(build_two_qubit_gate_instruction_ensure_size(self, GateType::CZ, args)); }, clean_doc_string(R"DOC( Applies a controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X )DOC") .data()); c.def( "zcz", [](TableauSimulator &self, const pybind11::args &args) { self.do_ZCZ(build_two_qubit_gate_instruction_ensure_size(self, GateType::CZ, args)); }, clean_doc_string(R"DOC( Applies a controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X )DOC") .data()); c.def( "cy", [](TableauSimulator &self, const pybind11::args &args) { self.do_ZCY(build_two_qubit_gate_instruction_ensure_size(self, GateType::CY, args)); }, clean_doc_string(R"DOC( Applies a controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.cy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X )DOC") .data()); c.def( "zcy", [](TableauSimulator &self, const pybind11::args &args) { self.do_ZCY(build_two_qubit_gate_instruction_ensure_size(self, GateType::CY, args)); }, clean_doc_string(R"DOC( Applies a controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.zcy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X )DOC") .data()); c.def( "xcx", [](TableauSimulator &self, const pybind11::args &args) { self.do_XCX(build_two_qubit_gate_instruction_ensure_size(self, GateType::XCX, args)); }, clean_doc_string(R"DOC( Applies an X-controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X )DOC") .data()); c.def( "xcy", [](TableauSimulator &self, const pybind11::args &args) { self.do_XCY(build_two_qubit_gate_instruction_ensure_size(self, GateType::XCY, args)); }, clean_doc_string(R"DOC( Applies an X-controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ )DOC") .data()); c.def( "xcz", [](TableauSimulator &self, const pybind11::args &args) { self.do_XCZ(build_two_qubit_gate_instruction_ensure_size(self, GateType::XCZ, args)); }, clean_doc_string(R"DOC( Applies an X-controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.xcz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ )DOC") .data()); c.def( "ycx", [](TableauSimulator &self, const pybind11::args &args) { self.do_YCX(build_two_qubit_gate_instruction_ensure_size(self, GateType::YCX, args)); }, clean_doc_string(R"DOC( Applies a Y-controlled X gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycx(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +Z +X )DOC") .data()); c.def( "ycy", [](TableauSimulator &self, const pybind11::args &args) { self.do_YCY(build_two_qubit_gate_instruction_ensure_size(self, GateType::YCY, args)); }, clean_doc_string(R"DOC( Applies a Y-controlled Y gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycy(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +_ +_ )DOC") .data()); c.def( "ycz", [](TableauSimulator &self, const pybind11::args &args) { self.do_YCZ(build_two_qubit_gate_instruction_ensure_size(self, GateType::YCZ, args)); }, clean_doc_string(R"DOC( Applies a Y-controlled Z gate to the simulator's state. Args: *targets: The indices of the qubits to target with the gate. Applies the gate to the first two targets, then the next two targets, and so forth. There must be an even number of targets. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0, 3) >>> s.reset_y(1) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +X +Y +Z +X >>> s.ycz(0, 1, 2, 3) >>> print(" ".join(str(s.peek_bloch(k)) for k in range(4))) +_ +_ +_ +_ )DOC") .data()); c.def( "reset", [](TableauSimulator &self, const pybind11::args &args) { self.do_RZ(build_single_qubit_gate_instruction_ensure_size(self, GateType::R, args)); }, clean_doc_string(R"DOC( Resets qubits to the |0> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(0) >>> s.reset(0) >>> s.peek_bloch(0) stim.PauliString("+Z") )DOC") .data()); c.def( "reset_x", [](TableauSimulator &self, const pybind11::args &args) { self.do_RX(build_single_qubit_gate_instruction_ensure_size(self, GateType::RX, args)); }, clean_doc_string(R"DOC( Resets qubits to the |+> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.peek_bloch(0) stim.PauliString("+X") )DOC") .data()); c.def( "reset_y", [](TableauSimulator &self, const pybind11::args &args) { self.do_RY(build_single_qubit_gate_instruction_ensure_size(self, GateType::RY, args)); }, clean_doc_string(R"DOC( Resets qubits to the |i> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_y(0) >>> s.peek_bloch(0) stim.PauliString("+Y") )DOC") .data()); c.def( "reset_z", [](TableauSimulator &self, const pybind11::args &args) { self.do_RZ(build_single_qubit_gate_instruction_ensure_size(self, GateType::R, args)); }, clean_doc_string(R"DOC( Resets qubits to the |0> state. Args: *targets: The indices of the qubits to reset. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.reset_z(0) >>> s.peek_bloch(0) stim.PauliString("+Z") )DOC") .data()); c.def( "peek_x", [](TableauSimulator &self, uint32_t target) -> int8_t { self.ensure_large_enough_for_qubits(target + 1); return self.peek_x(target); }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns the expected value of a qubit's X observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |+> state. -1: Qubit is in the |-> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_z(0) >>> s.peek_x(0) 0 >>> s.reset_x(0) >>> s.peek_x(0) 1 >>> s.z(0) >>> s.peek_x(0) -1 )DOC") .data()); c.def( "peek_y", [](TableauSimulator &self, uint32_t target) -> int8_t { self.ensure_large_enough_for_qubits(target + 1); return self.peek_y(target); }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns the expected value of a qubit's Y observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |i> state. -1: Qubit is in the |-i> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_z(0) >>> s.peek_y(0) 0 >>> s.reset_y(0) >>> s.peek_y(0) 1 >>> s.z(0) >>> s.peek_y(0) -1 )DOC") .data()); c.def( "peek_z", [](TableauSimulator &self, uint32_t target) -> int8_t { self.ensure_large_enough_for_qubits(target + 1); return self.peek_z(target); }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns the expected value of a qubit's Z observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: target: The qubit to analyze. Returns: +1: Qubit is in the |0> state. -1: Qubit is in the |1> state. 0: Qubit is in some other state. Example: >>> import stim >>> s = stim.TableauSimulator() >>> s.reset_x(0) >>> s.peek_z(0) 0 >>> s.reset_z(0) >>> s.peek_z(0) 1 >>> s.x(0) >>> s.peek_z(0) -1 )DOC") .data()); c.def( "peek_bloch", [](TableauSimulator &self, size_t target) { self.ensure_large_enough_for_qubits(target + 1); return FlexPauliString(self.peek_bloch(target)); }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns the state of the qubit as a single-qubit stim.PauliString stabilizer. This is a non-physical operation. It reports information about the qubit without disturbing it. Args: target: The qubit to peek at. Returns: stim.PauliString("I"): The qubit is entangled. Its bloch vector is x=y=z=0. stim.PauliString("+Z"): The qubit is in the |0> state. Its bloch vector is z=+1, x=y=0. stim.PauliString("-Z"): The qubit is in the |1> state. Its bloch vector is z=-1, x=y=0. stim.PauliString("+Y"): The qubit is in the |i> state. Its bloch vector is y=+1, x=z=0. stim.PauliString("-Y"): The qubit is in the |-i> state. Its bloch vector is y=-1, x=z=0. stim.PauliString("+X"): The qubit is in the |+> state. Its bloch vector is x=+1, y=z=0. stim.PauliString("-X"): The qubit is in the |-> state. Its bloch vector is x=-1, y=z=0. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_bloch(0) stim.PauliString("+Z") >>> s.x(0) >>> s.peek_bloch(0) stim.PauliString("-Z") >>> s.h(0) >>> s.peek_bloch(0) stim.PauliString("-X") >>> s.sqrt_x(1) >>> s.peek_bloch(1) stim.PauliString("-Y") >>> s.cz(0, 1) >>> s.peek_bloch(0) stim.PauliString("+_") )DOC") .data()); c.def( "peek_observable_expectation", [](const TableauSimulator &self, const FlexPauliString &observable) -> int8_t { if (observable.imag) { throw std::invalid_argument( "Observable isn't Hermitian; it has imaginary sign. Need observable.sign in [1, -1]."); } return self.peek_observable_expectation(observable.value); }, pybind11::arg("observable"), clean_doc_string(R"DOC( Determines the expected value of an observable. Because the simulator's state is always a stabilizer state, the expectation will always be exactly -1, 0, or +1. This is a non-physical operation. It reports information about the quantum state without disturbing it. Args: observable: The observable to determine the expected value of. This observable must have a real sign, not an imaginary sign. Returns: +1: Observable will be deterministically false when measured. -1: Observable will be deterministically true when measured. 0: Observable will be random when measured. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_observable_expectation(stim.PauliString("+Z")) 1 >>> s.peek_observable_expectation(stim.PauliString("+X")) 0 >>> s.peek_observable_expectation(stim.PauliString("-Z")) -1 >>> s.do(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''')) >>> queries = ['XX', 'YY', 'ZZ', '-ZZ', 'ZI', 'II', 'IIZ'] >>> for q in queries: ... print(q, s.peek_observable_expectation(stim.PauliString(q))) XX 1 YY -1 ZZ 1 -ZZ -1 ZI 0 II 1 IIZ 1 )DOC") .data()); c.def( "measure_observable", [](TableauSimulator &self, const FlexPauliString &observable, double flip_probability) -> bool { if (observable.imag) { throw std::invalid_argument( "Observable isn't Hermitian; it has imaginary sign. Need observable.sign in [1, -1]."); } return self.measure_pauli_string(observable.value, flip_probability); }, pybind11::arg("observable"), pybind11::kw_only(), pybind11::arg("flip_probability") = 0.0, clean_doc_string(R"DOC( Measures an pauli string observable, as if by an MPP instruction. Args: observable: The observable to measure, specified as a stim.PauliString. flip_probability: Probability of the recorded measurement result being flipped. Returns: The result of the measurement. The result is also recorded into the measurement record. Raises: ValueError: The given pauli string isn't Hermitian, or the given probability isn't a valid probability. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.measure_observable(stim.PauliString("XX")) False >>> s.measure_observable(stim.PauliString("YY")) True >>> s.measure_observable(stim.PauliString("-ZZ")) True )DOC") .data()); c.def( "postselect_observable", [](TableauSimulator &self, const FlexPauliString &observable, bool desired_value) { if (observable.imag) { throw std::invalid_argument( "Observable isn't Hermitian; it has imaginary sign. Need observable.sign in [1, -1]."); } self.postselect_observable(observable.value, desired_value); }, pybind11::arg("observable"), pybind11::kw_only(), pybind11::arg("desired_value") = false, clean_doc_string(R"DOC( Projects into a desired observable, or raises an exception if it was impossible. Postselecting an observable forces it to collapse to a specific eigenstate, as if it was measured and that state was the result of the measurement. Args: observable: The observable to postselect, specified as a pauli string. The pauli string's sign must be -1 or +1 (not -i or +i). desired_value: False (default): Postselect into the +1 eigenstate of the observable. True: Postselect into the -1 eigenstate of the observable. Raises: ValueError: The given observable had an imaginary sign. OR The postselection was impossible. The observable was in the opposite eigenstate, so measuring it would never ever return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.postselect_observable(stim.PauliString("+XX")) >>> s.postselect_observable(stim.PauliString("+ZZ")) >>> s.peek_observable_expectation(stim.PauliString("+YY")) -1 )DOC") .data()); c.def( "measure", [](TableauSimulator &self, uint32_t target) { self.ensure_large_enough_for_qubits(target + 1); GateTarget g{target}; self.do_MZ(CircuitInstruction{GateType::M, {}, &g, ""}); return (bool)self.measurement_record.storage.back(); }, pybind11::arg("target"), clean_doc_string(R"DOC( Measures a single qubit. Unlike the other methods on TableauSimulator, this one does not broadcast over multiple targets. This is to avoid returning a list, which would create a pitfall where typing `if sim.measure(qubit)` would be a bug. To measure multiple qubits, use `TableauSimulator.measure_many`. Args: target: The index of the qubit to measure. Returns: The measurement result as a bool. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(1) >>> s.measure(0) False >>> s.measure(1) True )DOC") .data()); c.def( "measure_many", [](TableauSimulator &self, const pybind11::args &args) { auto converted_args = build_single_qubit_gate_instruction_ensure_size(self, GateType::M, args); self.do_MZ(converted_args); auto e = self.measurement_record.storage.end(); return std::vector(e - converted_args.targets.size(), e); }, clean_doc_string(R"DOC( Measures multiple qubits. Args: *targets: The indices of the qubits to measure. Returns: The measurement results as a list of bools. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.x(1) >>> s.measure_many(0, 1) [False, True] )DOC") .data()); c.def( "postselect_x", [](TableauSimulator &self, const pybind11::object &targets, bool desired_value) { auto gate_targets = arg_to_qubit_or_qubits(self, targets); self.postselect_x(gate_targets, desired_value); }, pybind11::arg("targets"), pybind11::kw_only(), pybind11::arg("desired_value"), clean_doc_string(R"DOC( @signature def postselect_x(self, targets: Union[int, Iterable[int]], *, desired_value: bool) -> None: Postselects qubits in the X basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |+> state. True: postselect targets into the |-> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_x(0) 0 >>> s.postselect_x(0, desired_value=False) >>> s.peek_x(0) 1 >>> s.h(0) >>> s.peek_x(0) 0 >>> s.postselect_x(0, desired_value=True) >>> s.peek_x(0) -1 )DOC") .data()); c.def( "postselect_y", [](TableauSimulator &self, const pybind11::object &targets, bool desired_value) { auto gate_targets = arg_to_qubit_or_qubits(self, targets); self.postselect_y(gate_targets, desired_value); }, pybind11::arg("targets"), pybind11::kw_only(), pybind11::arg("desired_value"), clean_doc_string(R"DOC( @signature def postselect_y(self, targets: Union[int, Iterable[int]], *, desired_value: bool) -> None: Postselects qubits in the Y basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |i> state. True: postselect targets into the |-i> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.peek_y(0) 0 >>> s.postselect_y(0, desired_value=False) >>> s.peek_y(0) 1 >>> s.reset_x(0) >>> s.peek_y(0) 0 >>> s.postselect_y(0, desired_value=True) >>> s.peek_y(0) -1 )DOC") .data()); c.def( "postselect_z", [](TableauSimulator &self, const pybind11::object &targets, bool desired_value) { auto gate_targets = arg_to_qubit_or_qubits(self, targets); self.postselect_z(gate_targets, desired_value); }, pybind11::arg("targets"), pybind11::kw_only(), pybind11::arg("desired_value"), clean_doc_string(R"DOC( @signature def postselect_z(self, targets: Union[int, Iterable[int]], *, desired_value: bool) -> None: Postselects qubits in the Z basis, or raises an exception. Postselecting a qubit forces it to collapse to a specific state, as if it was measured and that state was the result of the measurement. Args: targets: The qubit index or indices to postselect. desired_value: False: postselect targets into the |0> state. True: postselect targets into the |1> state. Raises: ValueError: The postselection failed. One of the qubits was in a state orthogonal to the desired state, so it was literally impossible for a measurement of the qubit to return the desired result. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.peek_z(0) 0 >>> s.postselect_z(0, desired_value=False) >>> s.peek_z(0) 1 >>> s.h(0) >>> s.peek_z(0) 0 >>> s.postselect_z(0, desired_value=True) >>> s.peek_z(0) -1 )DOC") .data()); c.def_property_readonly( "num_qubits", [](const TableauSimulator &self) -> size_t { return self.inv_state.num_qubits; }, clean_doc_string(R"DOC( Returns the number of qubits currently being tracked by the simulator. Note that the number of qubits being tracked will implicitly increase if qubits beyond the current limit are touched. Untracked qubits are always assumed to be in the |0> state. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.num_qubits 0 >>> s.h(2) >>> s.num_qubits 3 )DOC") .data()); c.def( "set_num_qubits", [](TableauSimulator &self, uint32_t new_num_qubits) { self.set_num_qubits(new_num_qubits); }, pybind11::arg("new_num_qubits"), clean_doc_string(R"DOC( Resizes the simulator's internal state. This forces the simulator's internal state to track exactly the qubits whose indices are in `range(new_num_qubits)`. Note that untracked qubits are always assumed to be in the |0> state. Therefore, calling this method will effectively force any qubit whose index is outside `range(new_num_qubits)` to be reset to |0>. Note that this method does not prevent future operations from implicitly expanding the size of the tracked state (e.g. setting the number of qubits to 5 will not prevent a Hadamard from then being applied to qubit 100, increasing the number of qubits back to 101). Args: new_num_qubits: The length of the range of qubits the internal simulator should be tracking. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> len(s.current_inverse_tableau()) 0 >>> s.set_num_qubits(5) >>> len(s.current_inverse_tableau()) 5 >>> s.x(0, 1, 2, 3) >>> s.set_num_qubits(2) >>> s.measure_many(0, 1, 2, 3) [True, True, False, False] )DOC") .data()); c.def( "set_inverse_tableau", [](TableauSimulator &self, const Tableau &new_inverse_tableau) { self.inv_state = new_inverse_tableau; }, pybind11::arg("new_inverse_tableau"), clean_doc_string(R"DOC( Overwrites the simulator's internal state with the given inverse tableau. The inverse tableau specifies how Pauli product observables of qubits at the current time transform into equivalent Pauli product observables at the beginning of time, when all qubits were in the |0> state. For example, if the Z observable on qubit 5 maps to a product of Z observables at the start of time then a Z basis measurement on qubit 5 will be deterministic and equal to the sign of the product. Whereas if it mapped to a product of observables including an X or a Y then the Z basis measurement would be random. Any qubits not within the length of the tableau are implicitly in the |0> state. Args: new_inverse_tableau: The tableau to overwrite the internal state with. Examples: >>> import stim >>> s = stim.TableauSimulator() >>> t = stim.Tableau.random(4) >>> s.set_inverse_tableau(t) >>> s.current_inverse_tableau() == t True )DOC") .data()); c.def( "copy", [](const TableauSimulator &self, bool copy_rng, pybind11::object &seed) { if (copy_rng && !seed.is_none()) { throw std::invalid_argument("seed and copy_rng are incompatible"); } if (!copy_rng || !seed.is_none()) { TableauSimulator copy_with_new_rng(self, make_py_seeded_rng(seed)); return copy_with_new_rng; } TableauSimulator copy = self; return copy; }, pybind11::kw_only(), pybind11::arg("copy_rng") = false, pybind11::arg("seed") = pybind11::none(), clean_doc_string(R"DOC( @signature def copy(self, *, copy_rng: bool = False, seed: Optional[int] = None) -> stim.TableauSimulator: Returns a simulator with the same internal state, except perhaps its prng. Args: copy_rng: By default, new simulator's prng is reinitialized with a random seed. However, one can set this argument to True in order to have the prng state copied together with the rest of the original simulator's state. Consequently, in this case the two simulators will produce the same measurement outcomes for the same quantum circuits. If both seed and copy_rng are set, an exception is raised. Defaults to False. seed: PARTIALLY determines simulation results by deterministically seeding the random number generator. Must be None or an integer in range(2**64). Defaults to None. When None, the prng state is either copied from the original simulator or reseeded from system entropy, depending on the copy_rng argument. When set to an integer, making the exact same series calls on the exact same machine with the exact same version of Stim will produce the exact same simulation results. CAUTION: simulation results *WILL NOT* be consistent between versions of Stim. This restriction is present to make it possible to have future optimizations to the random sampling, and is enforced by introducing intentional differences in the seeding strategy from version to version. CAUTION: simulation results *MAY NOT* be consistent across machines that differ in the width of supported SIMD instructions. For example, using the same seed on a machine that supports AVX instructions and one that only supports SSE instructions may produce different simulation results. CAUTION: simulation results *MAY NOT* be consistent if you vary how the circuit is executed. For example, reordering whether a reset on one qubit happens before or after a reset on another qubit can result in different measurement results being observed starting from the same seed. Examples: >>> import stim >>> s1 = stim.TableauSimulator() >>> s1.set_inverse_tableau(stim.Tableau.random(1)) >>> s2 = s1.copy() >>> s2 is s1 False >>> s2.current_inverse_tableau() == s1.current_inverse_tableau() True >>> s1 = stim.TableauSimulator() >>> s2 = s1.copy(copy_rng=True) >>> s1.h(0) >>> s2.h(0) >>> assert s1.measure(0) == s2.measure(0) >>> s = stim.TableauSimulator() >>> def brute_force_post_select(qubit, desired_result): ... global s ... while True: ... s2 = s.copy() ... if s2.measure(qubit) == desired_result: ... s = s2 ... break >>> s.h(0) >>> brute_force_post_select(qubit=0, desired_result=True) >>> s.measure(0) True )DOC") .data()); c.def( "measure_kickback", [](TableauSimulator &self, uint32_t target) { self.ensure_large_enough_for_qubits(target + 1); auto result = self.measure_kickback_z({target}); if (result.second.num_qubits == 0) { return pybind11::make_tuple(result.first, pybind11::none()); } return pybind11::make_tuple(result.first, FlexPauliString(result.second)); }, pybind11::arg("target"), clean_doc_string(R"DOC( Measures a qubit and returns the result as well as its Pauli kickback (if any). The "Pauli kickback" of a stabilizer circuit measurement is a set of Pauli operations that flip the post-measurement system state between the two possible post-measurement states. For example, consider measuring one of the qubits in the state |00>+|11> in the Z basis. If the measurement result is False, then the system projects into the state |00>. If the measurement result is True, then the system projects into the state |11>. Applying a Pauli X operation to both qubits flips between |00> and |11>. Therefore the Pauli kickback of the measurement is `stim.PauliString("XX")`. Note that there are often many possible equivalent Pauli kickbacks. For example, if in the previous example there was a third qubit in the |0> state, then both `stim.PauliString("XX_")` and `stim.PauliString("XXZ")` are valid kickbacks. Measurements with deterministic results don't have a Pauli kickback. Args: target: The index of the qubit to measure. Returns: A (result, kickback) tuple. The result is a bool containing the measurement's output. The kickback is either None (meaning the measurement was deterministic) or a stim.PauliString (meaning the measurement was random, and the operations in the Pauli string flip between the two possible post-measurement states). Examples: >>> import stim >>> s = stim.TableauSimulator() >>> s.measure_kickback(0) (False, None) >>> s.h(0) >>> s.measure_kickback(0)[1] stim.PauliString("+X") >>> def pseudo_post_select(qubit, desired_result): ... m, kick = s.measure_kickback(qubit) ... if m != desired_result: ... if kick is None: ... raise ValueError("Post-selected the impossible!") ... s.do(kick) >>> s = stim.TableauSimulator() >>> s.h(0) >>> s.cnot(0, 1) >>> s.cnot(0, 2) >>> pseudo_post_select(qubit=2, desired_result=True) >>> s.measure_many(0, 1, 2) [True, True, True] )DOC") .data()); c.def( "set_state_from_stabilizers", [](TableauSimulator &self, pybind11::object &stabilizers, bool allow_redundant, bool allow_underconstrained) { std::vector> converted_stabilizers; for (const auto &stabilizer : stabilizers) { const FlexPauliString &p = pybind11::cast(stabilizer); if (p.imag) { throw std::invalid_argument("Stabilizers can't have imaginary sign."); } converted_stabilizers.push_back(p.value); } self.inv_state = stabilizers_to_tableau( converted_stabilizers, allow_redundant, allow_underconstrained, true); }, pybind11::arg("stabilizers"), pybind11::kw_only(), pybind11::arg("allow_redundant") = false, pybind11::arg("allow_underconstrained") = false, clean_doc_string(R"DOC( @signature def set_state_from_stabilizers(self, stabilizers: Iterable[stim.PauliString], *, allow_redundant: bool = False, allow_underconstrained: bool = False) -> None: Sets the tableau simulator's state to a state satisfying the given stabilizers. The old quantum state is completely overwritten, even if the new state is underconstrained by the given stabilizers. The number of qubits is changed to exactly match the number of qubits in the longest given stabilizer. Args: stabilizers: A list of `stim.PauliString`s specifying the stabilizers that the new state must have. It is permitted for stabilizers to have different lengths. All stabilizers are padded up to the length of the longest stabilizer by appending identity terms. allow_redundant: Defaults to False. If set to False, then the given stabilizers must all be independent. If any one of them is a product of the others (including the empty product), an exception will be raised. If set to True, then redundant stabilizers are simply ignored. allow_underconstrained: Defaults to False. If set to False, then the given stabilizers must form a complete set of generators. They must exactly specify the desired stabilizer state, with no degrees of freedom left over. For an n-qubit state there must be n independent stabilizers. If set to True, then there can be leftover degrees of freedom which can be set arbitrarily. Returns: Nothing. Mutates the states of the simulator to match the desired stabilizers. Guarantees that self.current_inverse_tableau().inverse_z_output(k) will be equal to the k'th independent stabilizer from the `stabilizers` argument. Raises: ValueError: A stabilizer is redundant but allow_redundant=True wasn't set. OR The given stabilizers are contradictory (e.g. "+Z" and "-Z" both specified). OR The given stabilizers anticommute (e.g. "+Z" and "+X" both specified). OR The stabilizers left behind a degree of freedom but allow_underconstrained=True wasn't set. OR A stabilizer has an imaginary sign (i or -i). Examples: >>> import stim >>> tab_sim = stim.TableauSimulator() >>> tab_sim.set_state_from_stabilizers([ ... stim.PauliString("XX"), ... stim.PauliString("ZZ"), ... ]) >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) >>> tab_sim.set_state_from_stabilizers([ ... stim.PauliString("XX_"), ... stim.PauliString("ZZ_"), ... stim.PauliString("-YY_"), ... stim.PauliString(""), ... ], allow_underconstrained=True, allow_redundant=True) >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z__"), stim.PauliString("+_X_"), stim.PauliString("+__X"), ], zs=[ stim.PauliString("+XX_"), stim.PauliString("+ZZ_"), stim.PauliString("+__Z"), ], ) )DOC") .data()); c.def( "set_state_from_state_vector", [](TableauSimulator &self, pybind11::object &state_vector, std::string_view endian) { bool little_endian; if (endian == "little") { little_endian = true; } else if (endian == "big") { little_endian = false; } else { throw std::invalid_argument("endian not in ['little', 'big']"); } std::vector> v; for (const auto &obj : state_vector) { v.push_back(pybind11::cast>(obj)); } self.inv_state = circuit_to_tableau( stabilizer_state_vector_to_circuit(v, little_endian), false, false, false) .inverse(); }, pybind11::arg("state_vector"), pybind11::kw_only(), pybind11::arg("endian"), clean_doc_string(R"DOC( @signature def set_state_from_state_vector(self, state_vector: Iterable[float], *, endian: Literal["little", "big"]) -> None: Sets the simulator's state to a superposition specified by an amplitude vector. Args: state_vector: A list of complex amplitudes specifying a superposition. The vector must correspond to a state that is reachable using Clifford operations, and must be normalized (i.e. it must be a unit vector). endian: "little": state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: Nothing. Mutates the states of the simulator to match the desired state. Raises: ValueError: The given state vector isn't a list of complex values specifying a stabilizer state. OR The given endian value isn't 'little' or 'big'. Examples: >>> import stim >>> tab_sim = stim.TableauSimulator() >>> tab_sim.set_state_from_state_vector([ ... 0.5**0.5, ... 0.5**0.5 * 1j, ... ], endian='little') >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+Y"), ], ) >>> tab_sim.set_state_from_state_vector([ ... 0.5**0.5, ... 0, ... 0, ... 0.5**0.5, ... ], endian='little') >>> tab_sim.current_inverse_tableau().inverse() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) )DOC") .data()); } ================================================ FILE: src/stim/simulators/tableau_simulator.pybind.h ================================================ // 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. #ifndef _STIM_SIMULATORS_TABLEAU_SIMULATOR_PYBIND_H #define _STIM_SIMULATORS_TABLEAU_SIMULATOR_PYBIND_H #include #include "stim/simulators/tableau_simulator.h" namespace stim_pybind { pybind11::class_> pybind_tableau_simulator(pybind11::module &m); void pybind_tableau_simulator_methods( pybind11::module &m, pybind11::class_> &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/simulators/tableau_simulator.test.cc ================================================ // 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. #include "stim/simulators/tableau_simulator.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/vector_simulator.h" #include "stim/util_bot/test_util.test.h" using namespace stim; static std::vector qubit_targets(const std::vector &targets) { std::vector result; for (uint32_t t : targets) { result.push_back(GateTarget::qubit(t & ~TARGET_INVERTED_BIT, t & TARGET_INVERTED_BIT)); } return result; } struct OpDat { std::vector targets; OpDat(uint32_t u) : targets(qubit_targets({u})) { } OpDat(std::vector u) : targets(qubit_targets(u)) { } operator CircuitInstruction() const { return {(GateType)0, {}, targets, ""}; } }; TEST_EACH_WORD_SIZE_W(TableauSimulator, identity, { auto s = TableauSimulator(INDEPENDENT_TEST_RNG(), 1); ASSERT_EQ(s.measurement_record.storage, (std::vector{})); s.do_MZ({GateType::Z, {}, qubit_targets({0}), ""}); ASSERT_EQ(s.measurement_record.storage, (std::vector{false})); s.do_MZ({GateType::Z, {}, qubit_targets({0 | TARGET_INVERTED_BIT}), ""}); ASSERT_EQ(s.measurement_record.storage, (std::vector{false, true})); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, bit_flip, { auto s = TableauSimulator(INDEPENDENT_TEST_RNG(), 1); s.do_H_XZ(OpDat(0)); s.do_SQRT_Z(OpDat(0)); s.do_SQRT_Z(OpDat(0)); s.do_H_XZ(OpDat(0)); s.do_MZ(OpDat(0)); s.do_X(OpDat(0)); s.do_MZ(OpDat(0)); ASSERT_EQ(s.measurement_record.storage, (std::vector{true, false})); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, identity2, { auto s = TableauSimulator(INDEPENDENT_TEST_RNG(), 2); s.do_MZ(OpDat(0)); ASSERT_EQ(s.measurement_record.storage, (std::vector{false})); s.do_MZ(OpDat(1)); ASSERT_EQ(s.measurement_record.storage, (std::vector{false, false})); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, bit_flip_2, { auto s = TableauSimulator(INDEPENDENT_TEST_RNG(), 2); s.do_H_XZ(OpDat(0)); s.do_SQRT_Z(OpDat(0)); s.do_SQRT_Z(OpDat(0)); s.do_H_XZ(OpDat(0)); s.do_MZ(OpDat(0)); ASSERT_EQ(s.measurement_record.storage, (std::vector{true})); s.do_MZ(OpDat(1)); ASSERT_EQ(s.measurement_record.storage, (std::vector{true, false})); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, epr, { auto s = TableauSimulator(INDEPENDENT_TEST_RNG(), 2); s.do_H_XZ(OpDat(0)); s.do_ZCX(OpDat({0, 1})); ASSERT_EQ(s.is_deterministic_z(0), false); ASSERT_EQ(s.is_deterministic_z(1), false); s.do_MZ(OpDat(0)); ASSERT_EQ(s.is_deterministic_z(0), true); ASSERT_EQ(s.is_deterministic_z(1), true); s.do_MZ(OpDat(1)); ASSERT_EQ(s.measurement_record.storage[0], s.measurement_record.storage[1]); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, big_determinism, { auto s = TableauSimulator(INDEPENDENT_TEST_RNG(), 1000); s.do_H_XZ(OpDat(0)); s.do_H_YZ(OpDat(1)); ASSERT_FALSE(s.is_deterministic_z(0)); ASSERT_FALSE(s.is_deterministic_z(1)); ASSERT_TRUE(s.is_deterministic_x(0)); ASSERT_FALSE(s.is_deterministic_x(1)); ASSERT_FALSE(s.is_deterministic_y(0)); ASSERT_TRUE(s.is_deterministic_y(1)); for (size_t k = 2; k < 1000; k++) { ASSERT_TRUE(s.is_deterministic_z(k)); ASSERT_FALSE(s.is_deterministic_x(k)); ASSERT_FALSE(s.is_deterministic_y(k)); } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, phase_kickback_consume_s_state, { for (size_t k = 0; k < 8; k++) { auto s = TableauSimulator(INDEPENDENT_TEST_RNG(), 2); s.do_H_XZ(OpDat(1)); s.do_SQRT_Z(OpDat(1)); s.do_H_XZ(OpDat(0)); s.do_ZCX(OpDat({0, 1})); ASSERT_EQ(s.is_deterministic_z(1), false); s.do_MZ(OpDat(1)); auto v1 = s.measurement_record.storage.back(); if (v1) { s.do_SQRT_Z(OpDat(0)); s.do_SQRT_Z(OpDat(0)); } s.do_SQRT_Z(OpDat(0)); s.do_H_XZ(OpDat(0)); ASSERT_EQ(s.is_deterministic_z(0), true); s.do_MZ(OpDat(0)); ASSERT_EQ(s.measurement_record.storage.back(), true); } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, phase_kickback_preserve_s_state, { auto s = TableauSimulator(INDEPENDENT_TEST_RNG(), 2); // Prepare S state. s.do_H_XZ(OpDat(1)); s.do_SQRT_Z(OpDat(1)); // Prepare test input. s.do_H_XZ(OpDat(0)); // Kickback. s.do_ZCX(OpDat({0, 1})); s.do_H_XZ(OpDat(1)); s.do_ZCX(OpDat({0, 1})); s.do_H_XZ(OpDat(1)); // Check. s.do_SQRT_Z(OpDat(0)); s.do_H_XZ(OpDat(0)); ASSERT_EQ(s.is_deterministic_z(0), true); s.do_MZ(OpDat(0)); ASSERT_EQ(s.measurement_record.storage.back(), true); s.do_SQRT_Z(OpDat(1)); s.do_H_XZ(OpDat(1)); ASSERT_EQ(s.is_deterministic_z(1), true); s.do_MZ(OpDat(1)); ASSERT_EQ(s.measurement_record.storage.back(), true); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, kickback_vs_stabilizer, { auto sim = TableauSimulator(INDEPENDENT_TEST_RNG(), 3); sim.do_H_XZ(OpDat(2)); sim.do_ZCX(OpDat({2, 0})); sim.do_ZCX(OpDat({2, 1})); sim.do_SQRT_Z(OpDat(0)); sim.do_SQRT_Z(OpDat(1)); sim.do_H_XZ(OpDat(0)); sim.do_H_XZ(OpDat(1)); sim.do_H_XZ(OpDat(2)); ASSERT_EQ( sim.inv_state.str(), "+-xz-xz-xz-\n" "| +- +- ++\n" "| ZY __ _X\n" "| __ ZY _X\n" "| XX XX XZ"); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, s_state_distillation_low_depth, { for (size_t reps = 0; reps < 10; reps++) { auto sim = TableauSimulator(INDEPENDENT_TEST_RNG(), 9); std::vector> stabilizers = { {0, 1, 2, 3}, {0, 1, 4, 5}, {0, 2, 4, 6}, {1, 2, 4, 7}, }; std::vector>> checks{ {{"s", {0}}, {"q", stabilizers[0]}}, {{"s", {1}}, {"q", stabilizers[1]}}, {{"s", {2}}, {"q", stabilizers[2]}}, }; std::vector stabilizer_measurements; uint32_t anc = 8; for (const auto &stabilizer : stabilizers) { sim.do_H_XZ(OpDat(anc)); for (const auto &k : stabilizer) { sim.do_ZCX(OpDat({anc, k})); } sim.do_H_XZ(OpDat(anc)); ASSERT_EQ(sim.is_deterministic_z(anc), false); sim.do_MZ(OpDat(anc)); bool v = sim.measurement_record.storage.back(); if (v) { sim.do_X(OpDat(anc)); } stabilizer_measurements.push_back(v); } std::vector qubit_measurements; for (size_t k = 0; k < 7; k++) { sim.do_SQRT_Z(OpDat(k)); sim.do_H_XZ(OpDat(k)); sim.do_MZ(OpDat(k)); qubit_measurements.push_back(sim.measurement_record.storage.back()); } bool sum = false; for (auto e : stabilizer_measurements) { sum ^= e; } for (auto e : qubit_measurements) { sum ^= e; } if (sum) { sim.do_Z(OpDat(7)); } sim.do_SQRT_Z(OpDat(7)); sim.do_H_XZ(OpDat(7)); ASSERT_EQ(sim.is_deterministic_z(7), true); sim.do_MZ(OpDat(7)); ASSERT_EQ(sim.measurement_record.storage.back(), false); for (const auto &c : checks) { bool r = false; for (auto k : c.at("s")) { r ^= stabilizer_measurements[k]; } for (auto k : c.at("q")) { r ^= qubit_measurements[k]; } ASSERT_EQ(r, false); } } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, s_state_distillation_low_space, { for (size_t rep = 0; rep < 10; rep++) { auto sim = TableauSimulator(INDEPENDENT_TEST_RNG(), 5); std::vector> phasors = { { 0, }, { 1, }, { 2, }, {0, 1, 2}, {0, 1, 3}, {0, 2, 3}, {1, 2, 3}, }; uint32_t anc = 4; for (const auto &phasor : phasors) { sim.do_H_XZ(OpDat(anc)); for (const auto &k : phasor) { sim.do_ZCX(OpDat({anc, k})); } sim.do_H_XZ(OpDat(anc)); sim.do_SQRT_Z(OpDat(anc)); sim.do_H_XZ(OpDat(anc)); ASSERT_EQ(sim.is_deterministic_z(anc), false); sim.do_MZ(OpDat(anc)); bool v = sim.measurement_record.storage.back(); if (v) { for (const auto &k : phasor) { sim.do_X(OpDat(k)); } sim.do_X(OpDat(anc)); } } for (size_t k = 0; k < 3; k++) { ASSERT_EQ(sim.is_deterministic_z(k), true); sim.do_MZ(OpDat(k)); ASSERT_EQ(sim.measurement_record.storage.back(), false); } sim.do_SQRT_Z(OpDat(3)); sim.do_H_XZ(OpDat(3)); ASSERT_EQ(sim.is_deterministic_z(3), true); sim.do_MZ(OpDat(3)); ASSERT_EQ(sim.measurement_record.storage.back(), true); } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, unitary_gates_consistent_with_tableau_data, { auto rng = INDEPENDENT_TEST_RNG(); auto t = Tableau::random(10, rng); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 10); for (const auto &gate : GATE_DATA.items) { if (!gate.has_known_unitary_matrix()) { continue; } sim.inv_state = t; const auto &inverse_op_tableau = gate.inverse().tableau(); if (inverse_op_tableau.num_qubits == 2) { sim.do_gate({gate.id, {}, qubit_targets({7, 4}), ""}); t.inplace_scatter_prepend(inverse_op_tableau, {7, 4}); } else { sim.do_gate({gate.id, {}, qubit_targets({5}), ""}); t.inplace_scatter_prepend(inverse_op_tableau, {5}); } EXPECT_EQ(sim.inv_state, t) << gate.name; } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, certain_errors_consistent_with_gates, { TableauSimulator sim1(INDEPENDENT_TEST_RNG(), 2); TableauSimulator sim2(INDEPENDENT_TEST_RNG(), 2); GateTarget targets[]{GateTarget{0}}; double p0 = 0.0; double p1 = 1.0; CircuitInstruction d0{(GateType)0, {&p0}, {targets}, ""}; CircuitInstruction d1{(GateType)0, {&p1}, {targets}, ""}; sim1.do_X_ERROR(d1); sim2.do_X(d0); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.do_X_ERROR(d0); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.do_Y_ERROR(d1); sim2.do_Y(d0); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.do_Y_ERROR(d0); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.do_Z_ERROR(d1); sim2.do_Z(d0); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.do_Z_ERROR(d0); ASSERT_EQ(sim1.inv_state, sim2.inv_state); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, simulate, { auto rng = INDEPENDENT_TEST_RNG(); auto results = TableauSimulator::sample_circuit( Circuit( "H 0\n" "CNOT 0 1\n" "M 0\n" "M 1\n" "M 2\n"), rng); ASSERT_EQ(results[0], results[1]); ASSERT_EQ(results[2], false); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, simulate_reset, { auto rng = INDEPENDENT_TEST_RNG(); auto results = TableauSimulator::sample_circuit( Circuit( "X 0\n" "M 0\n" "R 0\n" "M 0\n" "R 0\n" "M 0\n"), rng); ASSERT_EQ(results[0], true); ASSERT_EQ(results[1], false); ASSERT_EQ(results[2], false); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, to_vector_sim, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim_tab(INDEPENDENT_TEST_RNG(), 2); VectorSimulator sim_vec(2); ASSERT_TRUE(sim_tab.to_vector_sim().approximate_equals(sim_vec, true)); sim_tab.do_X(OpDat(0)); sim_vec.apply(GateType::X, 0); ASSERT_TRUE(sim_tab.to_vector_sim().approximate_equals(sim_vec, true)); sim_tab.do_H_XZ(OpDat(0)); sim_vec.apply(GateType::H, 0); ASSERT_TRUE(sim_tab.to_vector_sim().approximate_equals(sim_vec, true)); sim_tab.do_SQRT_Z(OpDat(0)); sim_vec.apply(GateType::S, 0); ASSERT_TRUE(sim_tab.to_vector_sim().approximate_equals(sim_vec, true)); sim_tab.do_ZCX(OpDat({0, 1})); sim_vec.apply(GateType::CX, 0, 1); ASSERT_TRUE(sim_tab.to_vector_sim().approximate_equals(sim_vec, true)); sim_tab.inv_state = Tableau::random(10, rng); sim_vec = sim_tab.to_vector_sim(); ASSERT_TRUE(sim_tab.to_vector_sim().approximate_equals(sim_vec, true)); sim_tab.do_gate({GateType::XCX, {}, qubit_targets({4, 7}), ""}); sim_vec.apply(GateType::XCX, 4, 7); ASSERT_TRUE(sim_tab.to_vector_sim().approximate_equals(sim_vec, true)); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, to_state_vector, { auto v = TableauSimulator(INDEPENDENT_TEST_RNG(), 0).to_state_vector(true); ASSERT_EQ(v.size(), 1); auto r = v[0].real(); auto i = v[0].imag(); ASSERT_LT(r * r + i * i - 1, 1e-4); TableauSimulator sim_tab(INDEPENDENT_TEST_RNG(), 3); auto sim_vec = sim_tab.to_vector_sim(); VectorSimulator sim_vec2(3); sim_vec2.state = sim_tab.to_state_vector(true); ASSERT_TRUE(sim_vec.approximate_equals(sim_vec2, true)); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, to_state_vector_endian, { VectorSimulator sim_vec0(3); VectorSimulator sim_vec2(3); sim_vec0.apply(GateType::H, 0); sim_vec2.apply(GateType::H, 2); TableauSimulator sim_tab(INDEPENDENT_TEST_RNG(), 3); sim_tab.do_H_XZ(OpDat(2)); VectorSimulator cmp(3); cmp.state = sim_tab.to_state_vector(true); ASSERT_TRUE(cmp.approximate_equals(sim_vec2, true)); cmp.state = sim_tab.to_state_vector(false); ASSERT_TRUE(cmp.approximate_equals(sim_vec0, true)); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, to_state_vector_canonical, { TableauSimulator sim_tab(INDEPENDENT_TEST_RNG(), 3); sim_tab.do_H_XZ(OpDat(2)); std::vector expected; auto actual = sim_tab.to_state_vector(true); expected = {sqrtf(0.5), 0, 0, 0, sqrtf(0.5), 0, 0, 0}; ASSERT_EQ(actual.size(), expected.size()); for (size_t k = 0; k < 8; k++) { ASSERT_LT(abs(actual[k] - expected[k]), 1e-4) << k; } actual = sim_tab.to_state_vector(false); expected = {sqrtf(0.5), sqrtf(0.5), 0, 0, 0, 0, 0, 0}; ASSERT_EQ(actual.size(), expected.size()); for (size_t k = 0; k < 8; k++) { ASSERT_LT(abs(actual[k] - expected[k]), 1e-4) << k; } }) template bool vec_sim_corroborates_measurement_process( const Tableau &state, const std::vector &measurement_targets) { TableauSimulator sim_tab(INDEPENDENT_TEST_RNG(), 2); sim_tab.inv_state = state; auto vec_sim = sim_tab.to_vector_sim(); sim_tab.do_MZ(OpDat(measurement_targets)); PauliString buf(sim_tab.inv_state.num_qubits); size_t k = 0; for (auto t : measurement_targets) { buf.zs[t] = true; buf.sign = sim_tab.measurement_record.storage[k++]; float f = vec_sim.template project(buf); if (fabs(f - 0.5) > 1e-4 && fabsf(f - 1) > 1e-4) { return false; } buf.zs[t] = false; } return true; } TEST_EACH_WORD_SIZE_W(TableauSimulator, measurement_vs_vector_sim, { auto rng = INDEPENDENT_TEST_RNG(); for (size_t k = 0; k < 5; k++) { auto state = Tableau::random(2, rng); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {0})); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {1})); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {0, 1})); } for (size_t k = 0; k < 5; k++) { auto state = Tableau::random(4, rng); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {0, 1})); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {2, 1})); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {0, 1, 2, 3})); } { auto state = Tableau::random(8, rng); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {0, 1, 2, 3})); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {0, 6, 7})); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {7, 3, 4})); ASSERT_TRUE(vec_sim_corroborates_measurement_process(state, {0, 1, 2, 3, 4, 5, 6, 7})); } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, correlated_error, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits expected(5); expected.clear(); ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( CORRELATED_ERROR(0) X0 X1 ELSE_CORRELATED_ERROR(0) X1 X2 ELSE_CORRELATED_ERROR(0) X2 X3 M 0 1 2 3 )circuit"), rng), expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( CORRELATED_ERROR(1) X0 X1 ELSE_CORRELATED_ERROR(0) X1 X2 ELSE_CORRELATED_ERROR(0) X2 X3 M 0 1 2 3 )circuit"), rng), expected); expected.clear(); expected[1] = true; expected[2] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( CORRELATED_ERROR(0) X0 X1 ELSE_CORRELATED_ERROR(1) X1 X2 ELSE_CORRELATED_ERROR(0) X2 X3 M 0 1 2 3 )circuit"), rng), expected); expected.clear(); expected[2] = true; expected[3] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( CORRELATED_ERROR(0) X0 X1 ELSE_CORRELATED_ERROR(0) X1 X2 ELSE_CORRELATED_ERROR(1) X2 X3 M 0 1 2 3 )circuit"), rng), expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( CORRELATED_ERROR(1) X0 X1 ELSE_CORRELATED_ERROR(1) X1 X2 ELSE_CORRELATED_ERROR(0) X2 X3 M 0 1 2 3 )circuit"), rng), expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( CORRELATED_ERROR(1) X0 X1 ELSE_CORRELATED_ERROR(1) X1 X2 ELSE_CORRELATED_ERROR(1) X2 X3 M 0 1 2 3 )circuit"), rng), expected); expected.clear(); expected[0] = true; expected[1] = true; expected[3] = true; expected[4] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( CORRELATED_ERROR(1) X0 X1 ELSE_CORRELATED_ERROR(1) X1 X2 ELSE_CORRELATED_ERROR(1) X2 X3 CORRELATED_ERROR(1) X3 X4 M 0 1 2 3 4 )circuit"), rng), expected); int hits[3]{}; size_t n = 5000; for (size_t k = 0; k < n; k++) { auto sample = TableauSimulator::sample_circuit( Circuit(R"circuit( CORRELATED_ERROR(0.5) X0 ELSE_CORRELATED_ERROR(0.25) X1 ELSE_CORRELATED_ERROR(0.75) X2 M 0 1 2 )circuit"), rng); hits[0] += sample[0]; hits[1] += sample[1]; hits[2] += sample[2]; } ASSERT_TRUE(0.45 * n < hits[0] && hits[0] < 0.55 * n); ASSERT_TRUE((0.125 - 0.08) * n < hits[1] && hits[1] < (0.125 + 0.08) * n); ASSERT_TRUE((0.28125 - 0.08) * n < hits[2] && hits[2] < (0.28125 + 0.08) * n); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, quantum_cannot_control_classical, { auto rng = INDEPENDENT_TEST_RNG(); // Quantum controlling classical operation is not allowed. ASSERT_THROW( { TableauSimulator::sample_circuit( Circuit(R"circuit( M 0 CNOT 1 rec[-1] )circuit"), rng); }, std::invalid_argument); ASSERT_THROW( { TableauSimulator::sample_circuit( Circuit(R"circuit( M 0 CY 1 rec[-1] )circuit"), rng); }, std::invalid_argument); ASSERT_THROW( { TableauSimulator::sample_circuit( Circuit(R"circuit( M 0 YCZ rec[-1] 1 )circuit"), rng); }, std::invalid_argument); ASSERT_THROW( { TableauSimulator::sample_circuit( Circuit(R"circuit( M 0 XCZ rec[-1] 1 )circuit"), rng); }, std::invalid_argument); ASSERT_THROW( { TableauSimulator::sample_circuit( Circuit(R"circuit( M 0 SWAP 1 rec[-1] )circuit"), rng); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, classical_can_control_quantum, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits expected(5); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( M !0 CX rec[-1] 1 M 1 )circuit"), rng), expected); ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( M !0 CY rec[-1] 1 M 1 )circuit"), rng), expected); ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( M !0 XCZ 1 rec[-1] M 1 )circuit"), rng), expected); ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( M !0 YCZ 1 rec[-1] M 1 )circuit"), rng), expected); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, classical_control_cases, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits expected(5); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( M !0 H 1 CZ rec[-1] 1 H 1 M 1 )circuit"), rng), expected); expected.clear(); expected[0] = true; expected[1] = true; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( M !0 CY rec[-1] 1 M 1 )circuit"), rng), expected); expected.clear(); expected[0] = false; expected[1] = false; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( M 0 CX rec[-1] 1 M 1 )circuit"), rng), expected); expected.clear(); expected[0] = true; expected[1] = false; expected[2] = true; expected[3] = false; expected[4] = false; ASSERT_EQ( TableauSimulator::sample_circuit( Circuit(R"circuit( X 0 M 0 R 0 M 0 CX rec[-2] 1 CX rec[-1] 2 M 1 2 3 )circuit"), rng), expected); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, mr_repeated_target, { auto rng = INDEPENDENT_TEST_RNG(); simd_bits expected(2); expected[0] = true; auto r = TableauSimulator::sample_circuit(Circuit("X 0\nMR 0 0"), rng); ASSERT_EQ(r, expected); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, peek_bloch, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 3); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Z")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("+Z")); sim.do_H_XZ(OpDat(0)); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("+Z")); sim.do_X(OpDat(1)); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("+Z")); sim.do_H_YZ(OpDat(2)); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("+Y")); sim.do_X(OpDat(0)); sim.do_X(OpDat(1)); sim.do_X(OpDat(2)); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("-Y")); sim.do_Y(OpDat(0)); sim.do_Y(OpDat(1)); sim.do_Y(OpDat(2)); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("-Y")); sim.do_ZCZ(OpDat({0, 1})); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("-Y")); sim.do_ZCZ(OpDat({1, 2})); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("+Y")); sim.do_ZCZ(OpDat({0, 2})); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+I")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("+I")); sim.do_X(OpDat(0)); sim.do_X(OpDat(1)); sim.do_X(OpDat(2)); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+I")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Z")); ASSERT_EQ(sim.peek_bloch(2), PauliString::from_str("+I")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, paulis, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim1(INDEPENDENT_TEST_RNG(), 300); TableauSimulator sim2(INDEPENDENT_TEST_RNG(), 300); sim1.inv_state = Tableau::random(300, rng); sim2.inv_state = sim1.inv_state; sim1.paulis(PauliString(300)); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.paulis(PauliString(5)); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.paulis(PauliString(0)); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.paulis(PauliString::from_str("IXYZ")); sim2.do_X(OpDat(1)); sim2.do_Y(OpDat(2)); sim2.do_Z(OpDat(3)); ASSERT_EQ(sim1.inv_state, sim2.inv_state); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, set_num_qubits, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim1(INDEPENDENT_TEST_RNG(), 10); TableauSimulator sim2(INDEPENDENT_TEST_RNG(), 10); sim1.inv_state = Tableau::random(10, rng); sim2.inv_state = sim1.inv_state; sim1.set_num_qubits(20); sim1.set_num_qubits(10); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.set_num_qubits(20); sim1.do_X(OpDat(10)); sim1.do_Z(OpDat(11)); sim1.do_H_XZ(OpDat(12)); sim1.do_ZCX(OpDat({12, 13})); sim1.set_num_qubits(10); ASSERT_EQ(sim1.inv_state, sim2.inv_state); sim1.set_num_qubits(20); sim2.ensure_large_enough_for_qubits(20); ASSERT_EQ(sim1.inv_state, sim2.inv_state); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, set_num_qubits_reduce_random, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 10); sim.inv_state = Tableau::random(10, rng); sim.set_num_qubits(5); ASSERT_EQ(sim.inv_state.num_qubits, 5); ASSERT_TRUE(sim.inv_state.satisfies_invariants()); }) template void scramble_stabilizers(TableauSimulator &s) { auto rng = INDEPENDENT_TEST_RNG(); TableauTransposedRaii tmp(s.inv_state); for (size_t i = 0; i < s.inv_state.num_qubits; i++) { for (size_t j = i + 1; j < s.inv_state.num_qubits; j++) { if (rng() & 1) { tmp.append_ZCX(i, j); } if (rng() & 1) { tmp.append_ZCX(j, i); } if (rng() & 1) { tmp.append_ZCZ(i, j); } } if (rng() & 1) { tmp.append_S(i); } } } TEST_EACH_WORD_SIZE_W(TableauSimulator, canonical_stabilizers, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 2); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); ASSERT_EQ( sim.canonical_stabilizers(), (std::vector>{ PauliString::from_str("XX"), PauliString::from_str("ZZ"), })); sim.do_SQRT_Y(OpDat({0, 1})); ASSERT_EQ( sim.canonical_stabilizers(), (std::vector>{ PauliString::from_str("XX"), PauliString::from_str("ZZ"), })); sim.do_SQRT_X(OpDat({0, 1})); ASSERT_EQ( sim.canonical_stabilizers(), (std::vector>{ PauliString::from_str("XX"), PauliString::from_str("-ZZ"), })); sim.set_num_qubits(3); ASSERT_EQ( sim.canonical_stabilizers(), (std::vector>{ PauliString::from_str("+XX_"), PauliString::from_str("-ZZ_"), PauliString::from_str("+__Z"), })); sim.do_ZCX(OpDat({2, 0})); ASSERT_EQ( sim.canonical_stabilizers(), (std::vector>{ PauliString::from_str("+XX_"), PauliString::from_str("-ZZ_"), PauliString::from_str("+__Z"), })); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, canonical_stabilizers_random, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 4); sim.inv_state = Tableau::random(4, rng); auto s1 = sim.canonical_stabilizers(); scramble_stabilizers(sim); auto s2 = sim.canonical_stabilizers(); ASSERT_EQ(s1, s2); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, set_num_qubits_reduce_preserves_scrambled_stabilizers, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 4); sim.inv_state = Tableau::random(4, rng); auto s1 = sim.canonical_stabilizers(); sim.inv_state.expand(8, 1.0); scramble_stabilizers(sim); sim.set_num_qubits(4); auto s2 = sim.canonical_stabilizers(); ASSERT_EQ(s1, s2); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_kickback_z, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 4); sim.do_H_XZ(OpDat({0, 2})); sim.do_ZCX(OpDat({0, 1, 2, 3})); auto k1 = sim.measure_kickback_z(GateTarget::qubit(1)); auto k2 = sim.measure_kickback_z(GateTarget::qubit(2)); auto k3 = sim.measure_kickback_z(GateTarget::qubit(3)); ASSERT_EQ(k1.second, PauliString::from_str("XX__")); ASSERT_EQ(k2.second, PauliString::from_str("__XX")); ASSERT_EQ(k3.second, PauliString(0)); ASSERT_EQ(k2.first, k3.first); auto p = PauliString::from_str("+Z"); auto pn = PauliString::from_str("-Z"); ASSERT_EQ(sim.peek_bloch(0), k1.first ? pn : p); ASSERT_EQ(sim.peek_bloch(1), k1.first ? pn : p); ASSERT_EQ(sim.peek_bloch(2), k2.first ? pn : p); ASSERT_EQ(sim.peek_bloch(3), k2.first ? pn : p); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_kickback_x, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 4); sim.do_H_XZ(OpDat({0, 2})); sim.do_ZCX(OpDat({0, 1, 2, 3})); auto k1 = sim.measure_kickback_x(GateTarget::qubit(1)); auto k2 = sim.measure_kickback_x(GateTarget::qubit(2)); auto k3 = sim.measure_kickback_x(GateTarget::qubit(3)); ASSERT_EQ(k1.second, PauliString::from_str("ZZ__")); ASSERT_EQ(k2.second, PauliString::from_str("__ZZ")); ASSERT_EQ(k3.second, PauliString(0)); ASSERT_EQ(k2.first, k3.first); auto p = PauliString::from_str("+X"); auto pn = PauliString::from_str("-X"); ASSERT_EQ(sim.peek_bloch(0), k1.first ? pn : p); ASSERT_EQ(sim.peek_bloch(1), k1.first ? pn : p); ASSERT_EQ(sim.peek_bloch(2), k2.first ? pn : p); ASSERT_EQ(sim.peek_bloch(3), k2.first ? pn : p); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_kickback_y, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 4); sim.do_H_XZ(OpDat({0, 2})); sim.do_ZCX(OpDat({0, 1, 2, 3})); auto k1 = sim.measure_kickback_y(GateTarget::qubit(1)); auto k2 = sim.measure_kickback_y(GateTarget::qubit(2)); auto k3 = sim.measure_kickback_y(GateTarget::qubit(3)); ASSERT_EQ(k1.second, PauliString::from_str("ZX__")); ASSERT_EQ(k2.second, PauliString::from_str("__ZX")); ASSERT_EQ(k3.second, PauliString(0)); ASSERT_NE(k2.first, k3.first); auto p = PauliString::from_str("+Y"); auto pn = PauliString::from_str("-Y"); ASSERT_EQ(sim.peek_bloch(0), k1.first ? p : pn); ASSERT_EQ(sim.peek_bloch(1), k1.first ? pn : p); ASSERT_EQ(sim.peek_bloch(2), k2.first ? pn : p); ASSERT_EQ(sim.peek_bloch(3), k2.first ? p : pn); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_kickback_isolates, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 4); sim.inv_state = Tableau::random(4, rng); for (size_t k = 0; k < 4; k++) { auto result = sim.measure_kickback_z(GateTarget::qubit(k)); for (size_t j = 0; j < result.second.num_qubits && j < k; j++) { ASSERT_FALSE(result.second.xs[j]); ASSERT_FALSE(result.second.zs[j]); } } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, collapse_isolate_completely, { auto rng = INDEPENDENT_TEST_RNG(); for (size_t k = 0; k < 10; k++) { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 6); sim.inv_state = Tableau::random(6, rng); { TableauTransposedRaii tmp(sim.inv_state); sim.collapse_isolate_qubit_z(2, tmp); } PauliString x2 = sim.inv_state.xs[2]; PauliString z2 = sim.inv_state.zs[2]; x2.sign = false; z2.sign = false; ASSERT_EQ(x2, PauliString::from_str("__X___")); ASSERT_EQ(z2, PauliString::from_str("__Z___")); } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, reset_pure, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 1); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+Z")); t.do_RY(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+Y")); t.do_RX(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+X")); t.do_RY(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+Y")); t.do_RZ(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+Z")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, reset_random, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator t(INDEPENDENT_TEST_RNG(), 5); t.inv_state = Tableau::random(5, rng); t.do_RX(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+X")); t.inv_state = Tableau::random(5, rng); t.do_RY(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+Y")); t.inv_state = Tableau::random(5, rng); t.do_RZ(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+Z")); t.inv_state = Tableau::random(5, rng); t.do_MRX(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+X")); t.inv_state = Tableau::random(5, rng); t.do_MRY(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+Y")); t.inv_state = Tableau::random(5, rng); t.do_MRZ(OpDat(0)); ASSERT_EQ(t.peek_bloch(0), PauliString::from_str("+Z")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, reset_x_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_RX(OpDat(0)); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p2.sign = false; ASSERT_EQ(p1, PauliString::from_str("+X")); ASSERT_EQ(p2, PauliString::from_str("+X")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, reset_y_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_RY(OpDat(0)); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p2.sign = false; ASSERT_EQ(p1, PauliString::from_str("+Y")); ASSERT_EQ(p2, PauliString::from_str("+Y")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, reset_z_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_RZ(OpDat(0)); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p2.sign = false; ASSERT_EQ(p1, PauliString::from_str("+Z")); ASSERT_EQ(p2, PauliString::from_str("+Z")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_x_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_MX(OpDat(0)); auto b = t.measurement_record.storage.back(); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p1.sign ^= b; p2.sign ^= b; ASSERT_EQ(p1, PauliString::from_str("+X")); ASSERT_EQ(p2, PauliString::from_str("+X")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_y_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_MY(OpDat(0)); auto b = t.measurement_record.storage.back(); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p1.sign ^= b; p2.sign ^= !b; ASSERT_EQ(p1, PauliString::from_str("+Y")); ASSERT_EQ(p2, PauliString::from_str("+Y")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_z_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_MZ(OpDat(0)); auto b = t.measurement_record.storage.back(); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p1.sign ^= b; p2.sign ^= b; ASSERT_EQ(p1, PauliString::from_str("+Z")); ASSERT_EQ(p2, PauliString::from_str("+Z")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_reset_x_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_MRX(OpDat(0)); auto b = t.measurement_record.storage.back(); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p2.sign ^= b; ASSERT_EQ(p1, PauliString::from_str("+X")); ASSERT_EQ(p2, PauliString::from_str("+X")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_reset_y_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_MRY(OpDat(0)); auto b = t.measurement_record.storage.back(); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p2.sign ^= !b; ASSERT_EQ(p1, PauliString::from_str("+Y")); ASSERT_EQ(p2, PauliString::from_str("+Y")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_reset_z_entangled, { TableauSimulator t(INDEPENDENT_TEST_RNG(), 2); t.do_H_XZ(OpDat(0)); t.do_ZCX(OpDat({0, 1})); t.do_MRZ(OpDat(0)); auto b = t.measurement_record.storage.back(); auto p1 = t.peek_bloch(0); auto p2 = t.peek_bloch(1); p2.sign ^= b; ASSERT_EQ(p1, PauliString::from_str("+Z")); ASSERT_EQ(p2, PauliString::from_str("+Z")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, reset_vs_measurements, { auto rng = INDEPENDENT_TEST_RNG(); auto check = [&](const char *circuit, std::vector results) { simd_bits ref(results.size()); for (size_t k = 0; k < results.size(); k++) { ref[k] = results[k]; } for (size_t reps = 0; reps < 5; reps++) { simd_bits t = TableauSimulator::sample_circuit(Circuit(circuit), rng); if (t != ref) { return false; } } return true; }; ASSERT_TRUE(check( R"circuit( RX 0 RY 1 RZ 2 H_XZ 0 H_YZ 1 M 0 1 2 )circuit", { false, false, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 1 2 H_YZ 3 4 5 X_ERROR(1) 0 3 6 Y_ERROR(1) 1 4 7 Z_ERROR(1) 2 5 8 MX 0 1 2 MY 3 4 5 MZ 6 7 8 )circuit", { false, true, true, true, false, true, true, true, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 1 2 H_YZ 3 4 5 X_ERROR(1) 0 3 6 Y_ERROR(1) 1 4 7 Z_ERROR(1) 2 5 8 MX !0 !1 !2 MY !3 !4 !5 MZ !6 !7 !8 )circuit", { true, false, false, false, true, false, false, false, true, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 1 2 H_YZ 3 4 5 X_ERROR(1) 0 3 6 Y_ERROR(1) 1 4 7 Z_ERROR(1) 2 5 8 MRX 0 1 2 MRY 3 4 5 MRZ 6 7 8 H_XZ 0 H_YZ 3 M 0 3 6 )circuit", { false, true, true, true, false, true, true, true, false, false, false, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 1 2 H_YZ 3 4 5 X_ERROR(1) 0 3 6 Y_ERROR(1) 1 4 7 Z_ERROR(1) 2 5 8 MRX !0 !1 !2 MRY !3 !4 !5 MRZ !6 !7 !8 H_XZ 0 H_YZ 3 M 0 3 6 )circuit", { true, false, false, false, true, false, false, false, true, false, false, false, })); ASSERT_TRUE(check( R"circuit( H_XZ 0 H_YZ 1 Z_ERROR(1) 0 1 X_ERROR(1) 2 MRX 0 0 MRY 1 1 MRZ 2 2 )circuit", { true, false, true, false, true, false, })); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, sample_circuit_mutates_rng_state, { std::mt19937_64 rng(1234); TableauSimulator::sample_circuit(Circuit("H 0\nM 0"), rng); ASSERT_NE(rng, std::mt19937_64(1234)); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, sample_stream_mutates_rng_state, { FILE *in = tmpfile(); FILE *out = tmpfile(); fprintf(in, "H 0\nM 0\n"); rewind(in); std::mt19937_64 rng(2345); TableauSimulator::sample_stream(in, out, SampleFormat::SAMPLE_FORMAT_B8, false, rng); ASSERT_NE(rng, std::mt19937_64(2345)); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, noisy_measurement_x, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( RX 0 REPEAT 10000 { MX(0.05) 0 } )CIRCUIT")); auto m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); t.safe_do_circuit(Circuit("MX 0")); ASSERT_FALSE(t.measurement_record.storage.back()); t.measurement_record.storage.clear(); t.safe_do_circuit(Circuit(R"CIRCUIT( RX 0 1 Y 0 1 REPEAT 5000 { MX(0.05) 0 1 } )CIRCUIT")); m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 10000 - 700); ASSERT_LT(m1, 10000 - 300); t.safe_do_circuit(Circuit("MX 0")); ASSERT_TRUE(t.measurement_record.storage.back()); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, noisy_measurement_y, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( RY 0 REPEAT 10000 { MY(0.05) 0 } )CIRCUIT")); auto m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); t.safe_do_circuit(Circuit("MY 0")); ASSERT_FALSE(t.measurement_record.storage.back()); t.measurement_record.storage.clear(); t.safe_do_circuit(Circuit(R"CIRCUIT( RY 0 1 X 0 1 REPEAT 5000 { MY(0.05) 0 1 } )CIRCUIT")); m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 10000 - 700); ASSERT_LT(m1, 10000 - 300); t.safe_do_circuit(Circuit("MY 0")); ASSERT_TRUE(t.measurement_record.storage.back()); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, noisy_measurement_z, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( RZ 0 REPEAT 10000 { MZ(0.05) 0 } )CIRCUIT")); auto m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); t.safe_do_circuit(Circuit("MZ 0")); ASSERT_FALSE(t.measurement_record.storage.back()); t.measurement_record.storage.clear(); t.safe_do_circuit(Circuit(R"CIRCUIT( RZ 0 1 X 0 1 REPEAT 5000 { MZ(0.05) 0 1 } )CIRCUIT")); m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 10000 - 700); ASSERT_LT(m1, 10000 - 300); t.safe_do_circuit(Circuit("MZ 0")); ASSERT_TRUE(t.measurement_record.storage.back()); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, noisy_measure_reset_x, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( RX 0 REPEAT 10000 { MRX(0.05) 0 } )CIRCUIT")); auto m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); t.safe_do_circuit(Circuit("MX 0")); ASSERT_FALSE(t.measurement_record.storage.back()); t.measurement_record.storage.clear(); t.safe_do_circuit(Circuit(R"CIRCUIT( RX 0 1 REPEAT 5000 { Z 0 1 MRX(0.05) 0 1 } )CIRCUIT")); m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 10000 - 700); ASSERT_LT(m1, 10000 - 300); t.safe_do_circuit(Circuit("MX 0")); ASSERT_FALSE(t.measurement_record.storage.back()); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, noisy_measure_reset_y, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( RY 0 1 REPEAT 5000 { MRY(0.05) 0 1 } )CIRCUIT")); auto m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); t.safe_do_circuit(Circuit("MY 0")); ASSERT_FALSE(t.measurement_record.storage.back()); t.measurement_record.storage.clear(); t.safe_do_circuit(Circuit(R"CIRCUIT( RY 0 1 REPEAT 5000 { X 0 1 MRY(0.05) 0 1 } )CIRCUIT")); m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 10000 - 700); ASSERT_LT(m1, 10000 - 300); t.safe_do_circuit(Circuit("MY 0")); ASSERT_FALSE(t.measurement_record.storage.back()); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, noisy_measure_reset_z, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( RZ 0 1 REPEAT 5000 { MRZ(0.05) 0 1 } )CIRCUIT")); auto m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); t.safe_do_circuit(Circuit("MZ 0")); ASSERT_FALSE(t.measurement_record.storage.back()); t.measurement_record.storage.clear(); t.safe_do_circuit(Circuit(R"CIRCUIT( RZ 0 1 REPEAT 5000 { X 0 1 MRZ(0.05) 0 1 } )CIRCUIT")); m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 10000 - 700); ASSERT_LT(m1, 10000 - 300); t.safe_do_circuit(Circuit("MZ 0")); ASSERT_FALSE(t.measurement_record.storage.back()); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_pauli_product_bad, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit("MPP X0*X0 !X0*X0")); ASSERT_EQ(t.measurement_record.storage, (std::vector{false, true})); ASSERT_THROW({ t.safe_do_circuit(Circuit("MPP X0*Z0")); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_pauli_product_1, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( REPEAT 100 { RX 0 RY 1 RZ 2 MPP X0 Y1 Z2 X0*Y1*Z2 } )CIRCUIT")); ASSERT_EQ(t.measurement_record.storage.size(), 400); ASSERT_EQ(std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0), 0); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_pauli_product_4body, { for (size_t k = 0; k < 10; k++) { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( MPP X0*X1*X2*X3 MX 0 1 2 3 4 5 MPP X2*X3*X4*X5 MPP Z0*Z1*Z4*Z5 !Y0*Y1*Y4*Y5 )CIRCUIT")); auto x0123 = t.measurement_record.storage[0]; auto x0 = t.measurement_record.storage[1]; auto x1 = t.measurement_record.storage[2]; auto x2 = t.measurement_record.storage[3]; auto x3 = t.measurement_record.storage[4]; auto x4 = t.measurement_record.storage[5]; auto x5 = t.measurement_record.storage[6]; auto x2345 = t.measurement_record.storage[7]; auto mz0145 = t.measurement_record.storage[8]; auto y0145 = t.measurement_record.storage[9]; ASSERT_EQ(x0123, x0 ^ x1 ^ x2 ^ x3); ASSERT_EQ(x2345, x2 ^ x3 ^ x4 ^ x5); ASSERT_EQ(y0145 ^ mz0145 ^ 1, x0123 ^ x2345); } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_pauli_product_epr, { for (size_t k = 0; k < 10; k++) { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( MPP X0*X1 Z0*Z1 Y0*Y1 CNOT 0 1 H 0 M 0 1 )CIRCUIT")); auto x01 = t.measurement_record.storage[0]; auto z01 = t.measurement_record.storage[1]; auto y01 = t.measurement_record.storage[2]; auto m0 = t.measurement_record.storage[3]; auto m1 = t.measurement_record.storage[4]; ASSERT_EQ(m0, x01); ASSERT_EQ(m1, z01); ASSERT_EQ(x01 ^ z01, y01 ^ 1); } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_pauli_product_inversions, { for (size_t k = 0; k < 10; k++) { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( MPP !X0*!X1 !X0*X1 X0*!X1 X0*X1 X0 X1 !X0 !X1 )CIRCUIT")); auto a = t.measurement_record.storage[0]; auto b = t.measurement_record.storage[1]; auto c = t.measurement_record.storage[2]; auto d = t.measurement_record.storage[3]; auto e = t.measurement_record.storage[4]; auto f = t.measurement_record.storage[5]; auto g = t.measurement_record.storage[6]; auto h = t.measurement_record.storage[7]; ASSERT_EQ(a, d); ASSERT_EQ(b, c); ASSERT_NE(a, b); ASSERT_EQ(a, e ^ f); ASSERT_NE(e, g); ASSERT_NE(f, h); } }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_pauli_product_noisy, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( H 0 CNOT 0 1 REPEAT 5000 { MPP(0.05) X0*X1 Z0*Z1 } )CIRCUIT")); auto m1 = std::accumulate(t.measurement_record.storage.begin(), t.measurement_record.storage.end(), 0); ASSERT_GT(m1, 300); ASSERT_LT(m1, 700); t.safe_do_circuit(Circuit("MPP Y0*Y1")); ASSERT_EQ(t.measurement_record.storage.back(), true); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, ignores_sweep_controls, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( X 0 CNOT sweep[0] 0 M 0 )CIRCUIT")); ASSERT_EQ(t.measurement_record.lookback(1), true); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, peek_observable_expectation, { TableauSimulator t(INDEPENDENT_TEST_RNG()); t.safe_do_circuit(Circuit(R"CIRCUIT( H 0 CNOT 0 1 X 0 )CIRCUIT")); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("XX")), 1); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("YY")), 1); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("ZZ")), -1); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("-XX")), -1); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("-ZZ")), 1); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("")), 1); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("-I")), -1); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("X")), 0); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("Z")), 0); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("Z_")), 0); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("ZZZ")), -1); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("XXX")), 0); ASSERT_EQ(t.peek_observable_expectation(PauliString::from_str("ZZZZZZZZ")), -1); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, postselect_x, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 2); // Postselect from +X. sim.do_RX(OpDat(0)); sim.postselect_x(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); // Postselect from -X. sim.do_RX(OpDat(0)); sim.do_Z(OpDat(0)); ASSERT_THROW({ sim.postselect_x(std::vector{GateTarget::qubit(0)}, false); }, std::invalid_argument); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-X")); // Postselect from +Y. sim.do_RY(OpDat(0)); sim.postselect_x(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); // Postselect from -Y. sim.do_RY(OpDat(0)); sim.do_X(OpDat(0)); sim.postselect_x(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); // Postselect from +Z. sim.do_RZ(OpDat(0)); sim.postselect_x(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); // Postselect from -Z. sim.do_RZ(OpDat(0)); sim.do_X(OpDat(0)); sim.postselect_x(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); // Postselect entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.postselect_x(std::vector{GateTarget::qubit(1)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+X")); // Postselect opposite state entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.postselect_x(std::vector{GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-X")); // Postselect both independent. sim.do_RZ(OpDat({0, 1})); sim.postselect_x(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-X")); // Postselect both entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.postselect_x(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-X")); // Contradiction reached during second postselection. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.do_Z(OpDat(0)); ASSERT_THROW( { sim.postselect_x(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); }, std::invalid_argument); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-X")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+X")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, postselect_y, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 2); // Postselect from +X. sim.do_RX(OpDat(0)); sim.postselect_y(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Y")); // Postselect from -X. sim.do_RX(OpDat(0)); sim.do_Z(OpDat(0)); sim.postselect_y(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Y")); // Postselect from +Y. sim.do_RY(OpDat(0)); sim.postselect_y(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Y")); // Postselect from -Y. sim.do_RY(OpDat(0)); sim.do_X(OpDat(0)); ASSERT_THROW({ sim.postselect_y(std::vector{GateTarget::qubit(0)}, false); }, std::invalid_argument); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Y")); // Postselect from +Z. sim.do_RZ(OpDat(0)); sim.postselect_y(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Y")); // Postselect from -Z. sim.do_RZ(OpDat(0)); sim.do_X(OpDat(0)); sim.postselect_y(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Y")); // Postselect entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.postselect_y(std::vector{GateTarget::qubit(1)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Y")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Y")); // Postselect opposite state entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.postselect_y(std::vector{GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Y")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Y")); // Postselect both independent. sim.do_RZ(OpDat({0, 1})); sim.postselect_y(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Y")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Y")); // Postselect both entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.do_Z(OpDat(0)); sim.postselect_y(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Y")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Y")); // Contradiction reached during second postselection. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); ASSERT_THROW( { sim.postselect_y(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); }, std::invalid_argument); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Y")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Y")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, postselect_z, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 2); // Postselect from +X. sim.do_RX(OpDat(0)); sim.postselect_z(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Z")); // Postselect from -X. sim.do_RX(OpDat(0)); sim.do_Z(OpDat(0)); sim.postselect_z(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Z")); // Postselect from +Y. sim.do_RY(OpDat(0)); sim.postselect_z(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Z")); // Postselect from -Y. sim.do_RY(OpDat(0)); sim.do_X(OpDat(0)); sim.postselect_z(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Z")); // Postselect from +Z. sim.do_RZ(OpDat(0)); sim.postselect_z(std::vector{GateTarget::qubit(0)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Z")); // Postselect from -Z. sim.do_RZ(OpDat(0)); sim.do_X(OpDat(0)); ASSERT_THROW({ sim.postselect_z(std::vector{GateTarget::qubit(0)}, false); }, std::invalid_argument); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Z")); // Postselect entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.postselect_z(std::vector{GateTarget::qubit(1)}, false); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("+Z")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Z")); // Postselect opposite state entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.postselect_z(std::vector{GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); // Postselect both independent. sim.do_RX(OpDat({0, 1})); sim.postselect_z(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); // Postselect both entangled. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.postselect_z(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-Z")); // Contradiction reached during second postselection. sim.do_RZ(OpDat({0, 1})); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.do_X(OpDat(0)); ASSERT_THROW( { sim.postselect_z(std::vector{GateTarget::qubit(0), GateTarget::qubit(1)}, true); }, std::invalid_argument); ASSERT_EQ(sim.peek_bloch(0), PauliString::from_str("-Z")); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Z")); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, peek_x, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 3); ASSERT_EQ(sim.peek_x(0), 0); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), +1); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), +1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), 0); ASSERT_EQ(sim.peek_z(2), +1); sim.do_H_XZ(OpDat(0)); ASSERT_EQ(sim.peek_x(0), +1); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), +1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), 0); ASSERT_EQ(sim.peek_z(2), +1); sim.do_X(OpDat(1)); ASSERT_EQ(sim.peek_x(0), +1); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), -1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), 0); ASSERT_EQ(sim.peek_z(2), +1); sim.do_H_YZ(OpDat(2)); ASSERT_EQ(sim.peek_x(0), +1); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), -1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), +1); ASSERT_EQ(sim.peek_z(2), 0); sim.do_X(OpDat(0)); sim.do_X(OpDat(1)); sim.do_X(OpDat(2)); ASSERT_EQ(sim.peek_x(0), +1); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), +1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), -1); ASSERT_EQ(sim.peek_z(2), 0); sim.do_Y(OpDat(0)); sim.do_Y(OpDat(1)); sim.do_Y(OpDat(2)); ASSERT_EQ(sim.peek_x(0), -1); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), -1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), -1); ASSERT_EQ(sim.peek_z(2), 0); sim.do_ZCZ(OpDat({0, 1})); ASSERT_EQ(sim.peek_x(0), +1); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), -1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), -1); ASSERT_EQ(sim.peek_z(2), 0); sim.do_ZCZ(OpDat({1, 2})); ASSERT_EQ(sim.peek_x(0), +1); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), -1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), +1); ASSERT_EQ(sim.peek_z(2), 0); sim.do_ZCZ(OpDat({0, 2})); ASSERT_EQ(sim.peek_x(0), 0); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), -1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), 0); ASSERT_EQ(sim.peek_z(2), 0); sim.do_X(OpDat(0)); sim.do_X(OpDat(1)); sim.do_X(OpDat(2)); ASSERT_EQ(sim.peek_x(0), 0); ASSERT_EQ(sim.peek_y(0), 0); ASSERT_EQ(sim.peek_z(0), 0); ASSERT_EQ(sim.peek_x(1), 0); ASSERT_EQ(sim.peek_y(1), 0); ASSERT_EQ(sim.peek_z(1), +1); ASSERT_EQ(sim.peek_x(2), 0); ASSERT_EQ(sim.peek_y(2), 0); ASSERT_EQ(sim.peek_z(2), 0); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, apply_tableau, { auto cnot = GATE_DATA.at("CNOT").tableau(); auto s = GATE_DATA.at("S").tableau(); auto h = GATE_DATA.at("H").tableau(); auto cxyz = GATE_DATA.at("C_XYZ").tableau(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 4); sim.apply_tableau(h, {1}); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+X")); sim.apply_tableau(s, {1}); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("+Y")); sim.apply_tableau(s, {1}); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-X")); sim.apply_tableau(cnot, {2, 1}); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("-X")); sim.apply_tableau(cnot, {1, 2}); ASSERT_EQ(sim.peek_bloch(1), PauliString::from_str("I")); ASSERT_EQ(sim.peek_observable_expectation(PauliString::from_str("IXXI")), -1); ASSERT_EQ(sim.peek_observable_expectation(PauliString::from_str("IZZI")), +1); sim.apply_tableau(cxyz, {2}); sim.apply_tableau(cxyz.inverse(), {1}); ASSERT_EQ(sim.peek_observable_expectation(PauliString::from_str("IZYI")), -1); ASSERT_EQ(sim.peek_observable_expectation(PauliString::from_str("IYXI")), +1); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, measure_pauli_string, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 4); sim.do_H_XZ(OpDat(0)); sim.do_ZCX(OpDat({0, 1})); sim.do_X(OpDat(0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("XX"), 0.0)); ASSERT_TRUE(sim.measure_pauli_string(PauliString::from_str("XX"), 1.0)); ASSERT_TRUE(sim.measure_pauli_string(PauliString::from_str("-XX"), 0.0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("-XX"), 1.0)); ASSERT_TRUE(sim.measure_pauli_string(PauliString::from_str("ZZ"), 0.0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("ZZ"), 1.0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("-ZZ"), 0.0)); ASSERT_TRUE(sim.measure_pauli_string(PauliString::from_str("-ZZ"), 1.0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("YY"), 0.0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("XXZ"), 0.0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("__Z"), 0.0)); auto b = sim.measure_pauli_string(PauliString::from_str("XXX"), 0.0); ASSERT_EQ(sim.measure_pauli_string(PauliString::from_str("XXX"), 0.0), b); ASSERT_EQ(sim.measure_pauli_string(PauliString::from_str("-XXX"), 0.0), !b); ASSERT_EQ(sim.measure_pauli_string(PauliString::from_str("XXX"), 1.0), !b); ASSERT_TRUE(sim.measure_pauli_string(PauliString::from_str("XX"), 1.0)); ASSERT_THROW({ sim.measure_pauli_string(PauliString::from_str(""), -0.5); }, std::invalid_argument); ASSERT_THROW({ sim.measure_pauli_string(PauliString::from_str(""), 2.5); }, std::invalid_argument); ASSERT_THROW({ sim.measure_pauli_string(PauliString::from_str(""), NAN); }, std::invalid_argument); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("+"), 0.0)); ASSERT_TRUE(sim.measure_pauli_string(PauliString::from_str("+"), 1.0)); ASSERT_TRUE(sim.measure_pauli_string(PauliString::from_str("-"), 0.0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("-"), 1.0)); ASSERT_FALSE(sim.measure_pauli_string(PauliString::from_str("____________Z"), 0.0)); ASSERT_EQ(sim.inv_state.num_qubits, 13); ASSERT_EQ(sim.measurement_record.storage, (std::vector{0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, b, b, !b, !b, 1, 0, 1, 1, 0, 0})); size_t t = 0; for (size_t k = 0; k < 10000; k++) { t += sim.measure_pauli_string(PauliString::from_str("-ZZ"), 0.2); } ASSERT_GT(t / 10000.0, 0.05); ASSERT_LT(t / 10000.0, 0.35); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, amortized_resizing, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 5120); sim.ensure_large_enough_for_qubits(5121); ASSERT_GT(sim.inv_state.xs.xt.num_minor_bits_padded(), 5600); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, mpad, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 5); ASSERT_EQ(sim.inv_state, Tableau(5)); ASSERT_EQ(sim.measurement_record.storage, (std::vector{})); sim.safe_do_circuit(Circuit("MPAD 0")); ASSERT_EQ(sim.inv_state, Tableau(5)); ASSERT_EQ(sim.measurement_record.storage, (std::vector{0})); sim.safe_do_circuit(Circuit("MPAD 1")); ASSERT_EQ(sim.inv_state, Tableau(5)); ASSERT_EQ(sim.measurement_record.storage, (std::vector{0, 1})); sim.safe_do_circuit(Circuit("MPAD 0 0 1 1 0")); ASSERT_EQ(sim.inv_state, Tableau(5)); ASSERT_EQ(sim.measurement_record.storage, (std::vector{0, 1, 0, 0, 1, 1, 0})); }) template void expect_same_final_state(const Tableau &start, const Circuit &c1, const Circuit &c2, bool unsigned_stabilizers) { size_t n = start.num_qubits; TableauSimulator sim1(INDEPENDENT_TEST_RNG(), n); TableauSimulator sim2(INDEPENDENT_TEST_RNG(), n); sim1.inv_state = start; sim2.inv_state = start; sim1.safe_do_circuit(c1); sim2.safe_do_circuit(c2); auto t1 = sim1.canonical_stabilizers(); auto t2 = sim2.canonical_stabilizers(); if (unsigned_stabilizers) { for (auto &e1 : t1) { e1.sign = false; } for (auto &e2 : t2) { e2.sign = false; } } EXPECT_EQ(t1, t2); } TEST_EACH_WORD_SIZE_W(TableauSimulator, mxx_myy_mzz_vs_mpp_unsigned, { auto rng = INDEPENDENT_TEST_RNG(); expect_same_final_state( Tableau::random(5, rng), Circuit("MXX 1 3 1 2 3 4"), Circuit("MPP X1*X3 X1*X2 X3*X4"), true); expect_same_final_state( Tableau::random(5, rng), Circuit("MYY 1 3 1 2 3 4"), Circuit("MPP Y1*Y3 Y1*Y2 Y3*Y4"), true); expect_same_final_state( Tableau::random(5, rng), Circuit("MZZ 1 3 1 2 3 4"), Circuit("MPP Z1*Z3 Z1*Z2 Z3*Z4"), true); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, mxx, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 5); sim.safe_do_circuit(Circuit("RX 0 1")); sim.safe_do_circuit(Circuit("MXX 0 1")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{false})); sim.measurement_record.storage.clear(); sim.inv_state = Tableau::random(5, rng); sim.safe_do_circuit(Circuit("MXX 1 3")); bool x13 = sim.measurement_record.storage.back(); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MXX 1 3")); sim.safe_do_circuit(Circuit("MXX 1 !3")); sim.safe_do_circuit(Circuit("MXX !1 3")); sim.safe_do_circuit(Circuit("MXX !1 !3")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x13, !x13, !x13, x13})); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MXX 2 3")); bool x23 = sim.measurement_record.storage.back(); bool x12 = x13 ^ x23; sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MXX 1 2")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x12})); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MXX 3 4")); bool x34 = sim.measurement_record.storage.back(); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MXX 1 2 3 4 2 3 1 3")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x12, x34, x23, x13})); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, myy, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 5); sim.safe_do_circuit(Circuit("RY 0 1")); sim.safe_do_circuit(Circuit("MYY 0 1")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{false})); sim.measurement_record.storage.clear(); sim.inv_state = Tableau::random(5, rng); sim.safe_do_circuit(Circuit("MYY 1 3")); bool x13 = sim.measurement_record.storage.back(); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MYY 1 3")); sim.safe_do_circuit(Circuit("MYY 1 !3")); sim.safe_do_circuit(Circuit("MYY !1 3")); sim.safe_do_circuit(Circuit("MYY !1 !3")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x13, !x13, !x13, x13})); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MYY 2 3")); bool x23 = sim.measurement_record.storage.back(); bool x12 = x13 ^ x23; sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MYY 1 2")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x12})); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MYY 3 4")); bool x34 = sim.measurement_record.storage.back(); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MYY 1 2 3 4 2 3 1 3")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x12, x34, x23, x13})); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, mzz, { auto rng = INDEPENDENT_TEST_RNG(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 5); sim.safe_do_circuit(Circuit("RZ 0 1")); sim.safe_do_circuit(Circuit("MZZ 0 1")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{false})); sim.measurement_record.storage.clear(); sim.inv_state = Tableau::random(5, rng); sim.safe_do_circuit(Circuit("MZZ 1 3")); bool x13 = sim.measurement_record.storage.back(); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MZZ 1 3")); sim.safe_do_circuit(Circuit("MZZ 1 !3")); sim.safe_do_circuit(Circuit("MZZ !1 3")); sim.safe_do_circuit(Circuit("MZZ !1 !3")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x13, !x13, !x13, x13})); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MZZ 2 3")); bool x23 = sim.measurement_record.storage.back(); bool x12 = x13 ^ x23; sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MZZ 1 2")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x12})); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MZZ 3 4")); bool x34 = sim.measurement_record.storage.back(); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit("MZZ 1 2 3 4 2 3 1 3")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{x12, x34, x23, x13})); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, runs_on_general_circuit, { auto circuit = generate_test_circuit_with_all_operations(); TableauSimulator sim(INDEPENDENT_TEST_RNG(), 1); sim.safe_do_circuit(circuit); ASSERT_GT(sim.inv_state.xs.num_qubits, 1); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, heralded_erase, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 1); sim.safe_do_circuit(Circuit(R"CIRCUIT( HERALDED_ERASE(0) 0 1 2 3 10 11 12 13 )CIRCUIT")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{0, 0, 0, 0, 0, 0, 0, 0})); sim.measurement_record.storage.clear(); ASSERT_EQ(sim.inv_state, Tableau(14)); sim.safe_do_circuit(Circuit(R"CIRCUIT( HERALDED_ERASE(1) 0 1 2 3 4 5 6 10 11 12 13 )CIRCUIT")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})); ASSERT_NE(sim.inv_state, Tableau(14)); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, heralded_pauli_channel_1, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 1); Tableau expected(14); sim.safe_do_circuit(Circuit(R"CIRCUIT( HERALDED_PAULI_CHANNEL_1(0, 0, 0, 0) 0 1 2 3 10 11 12 13 )CIRCUIT")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{0, 0, 0, 0, 0, 0, 0, 0})); ASSERT_EQ(sim.inv_state, Tableau(14)); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit(R"CIRCUIT( HERALDED_PAULI_CHANNEL_1(1, 0, 0, 0) 0 1 2 3 4 5 6 10 11 12 13 )CIRCUIT")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})); ASSERT_EQ(sim.inv_state, Tableau(14)); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit(R"CIRCUIT( HERALDED_PAULI_CHANNEL_1(0, 1, 0, 0) 13 )CIRCUIT")); ASSERT_EQ(sim.measurement_record.storage, (std::vector{true})); expected.prepend_X(13); ASSERT_EQ(sim.inv_state, expected); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit(R"CIRCUIT( HERALDED_PAULI_CHANNEL_1(0, 0, 1, 0) 5 10 )CIRCUIT")); expected.prepend_Y(5); expected.prepend_Y(10); ASSERT_EQ(sim.measurement_record.storage, (std::vector{1, 1})); ASSERT_EQ(sim.inv_state, expected); sim.measurement_record.storage.clear(); sim.safe_do_circuit(Circuit(R"CIRCUIT( HERALDED_PAULI_CHANNEL_1(0, 0, 0, 1) 1 10 11 )CIRCUIT")); expected.prepend_Z(1); expected.prepend_Z(10); expected.prepend_Z(11); ASSERT_EQ(sim.measurement_record.storage, (std::vector{1, 1, 1})); ASSERT_EQ(sim.inv_state, expected); sim.measurement_record.storage.clear(); }) TEST_EACH_WORD_SIZE_W(TableauSimulator, postselect_observable, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 0); sim.postselect_observable(PauliString("ZZ"), false); sim.postselect_observable(PauliString("XX"), false); auto initial_state = sim.inv_state; ASSERT_THROW({ sim.postselect_observable(PauliString("YY"), false); }, std::invalid_argument); sim.postselect_observable(PauliString("ZZ"), false); ASSERT_EQ(sim.inv_state, initial_state); sim.postselect_observable(PauliString("ZZ"), false); ASSERT_EQ(sim.inv_state, initial_state); ASSERT_THROW({ sim.postselect_observable(PauliString("ZZ"), true); }, std::invalid_argument); ASSERT_EQ(sim.inv_state, initial_state); ASSERT_THROW({ sim.postselect_observable(PauliString("ZZ"), true); }, std::invalid_argument); ASSERT_EQ(sim.inv_state, initial_state); sim.postselect_observable(PauliString("ZZ"), false); ASSERT_EQ(sim.inv_state, initial_state); sim.postselect_observable(PauliString("XX"), false); ASSERT_EQ(sim.inv_state, initial_state); sim.postselect_observable(PauliString("-YY"), false); ASSERT_EQ(sim.inv_state, initial_state); sim.postselect_observable(PauliString("YY"), true); ASSERT_EQ(sim.inv_state, initial_state); sim.postselect_observable(PauliString("XZ"), true); ASSERT_NE(sim.inv_state, initial_state); }) ================================================ FILE: src/stim/simulators/tableau_simulator_pybind_test.py ================================================ # 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. import pytest import stim import numpy as np def test_basic(): s = stim.TableauSimulator() assert s.measure(0) is False assert s.measure(0) is False s.x(0) assert s.measure(0) is True assert s.measure(0) is True s.reset(0) assert s.measure(0) is False s.h(0) s.h(0) s.sqrt_x(1) s.sqrt_x(1) assert s.measure_many(0, 1) == [False, True] def test_access_tableau(): s = stim.TableauSimulator() assert s.current_inverse_tableau() == stim.Tableau(0) s.h(0) assert s.current_inverse_tableau() == stim.Tableau.from_named_gate("H") s.h(0) assert s.current_inverse_tableau() == stim.Tableau(1) s.h(1) s.h(1) assert s.current_inverse_tableau() == stim.Tableau(2) s.h(2) assert s.current_inverse_tableau() == stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X__"), stim.PauliString("_X_"), stim.PauliString("__Z"), ], zs=[ stim.PauliString("Z__"), stim.PauliString("_Z_"), stim.PauliString("__X"), ], ) @pytest.mark.parametrize("name", [ "x", "y", "z", "h", "h_xy", "h_yz", "sqrt_x", "sqrt_x_dag", "sqrt_y", "sqrt_y_dag", "s", "s_dag", "swap", "iswap", "iswap_dag", "xcx", "xcy", "xcz", "ycx", "ycy", "ycz", "cnot", "cy", "cz", ]) def test_gates_present(name: str): t = stim.Tableau.from_named_gate(name) n = len(t) s1 = stim.TableauSimulator() s2 = stim.TableauSimulator() for k in range(n): s1.h(k) s2.h(k) s1.cnot(k, k + n) s2.cnot(k, k + n) getattr(s1, name)(*range(n)) s2.do(stim.Circuit(f"{name} " + " ".join(str(e) for e in range(n)))) assert s1.current_inverse_tableau() == s2.current_inverse_tableau() def test_do(): s = stim.TableauSimulator() s.do(stim.Circuit(""" S 0 """)) assert s.current_inverse_tableau() == stim.Tableau.from_named_gate("S_DAG") def test_peek_bloch(): s = stim.TableauSimulator() assert s.peek_bloch(0) == stim.PauliString("+Z") s.x(0) assert s.peek_bloch(0) == stim.PauliString("-Z") s.h(0) assert s.peek_bloch(0) == stim.PauliString("-X") s.sqrt_x(1) assert s.peek_bloch(1) == stim.PauliString("-Y") s.cz(0, 1) assert s.peek_bloch(0) == stim.PauliString("+I") assert s.peek_bloch(1) == stim.PauliString("+I") def test_copy(): s = stim.TableauSimulator() s.h(0) s2 = s.copy() assert s.current_inverse_tableau() == s2.current_inverse_tableau() assert s is not s2 def test_paulis(): s = stim.TableauSimulator() s.h(*range(0, 22, 2)) s.cnot(*range(22)) s.do(stim.PauliString("ZZZ_YYY_XXX")) s.z(0, 1, 2) s.y(4, 5, 6) s.x(8, 9, 10) s.cnot(*range(22)) s.h(*range(0, 22, 2)) assert s.measure_many(*range(22)) == [False] * 22 s = stim.TableauSimulator() s.do(stim.PauliString("Z" * 500)) assert s.measure_many(*range(500)) == [False] * 500 s.do(stim.PauliString("X" * 500)) assert s.measure_many(*range(500)) == [True] * 500 def test_measure_kickback(): s = stim.TableauSimulator() assert s.measure_kickback(0) == (False, None) assert s.measure_kickback(0) == (False, None) assert s.current_measurement_record() == [False, False] s.h(0) v = s.measure_kickback(0) assert isinstance(v[0], bool) assert v[1] == stim.PauliString("X") assert s.measure_kickback(0) == (v[0], None) assert s.current_measurement_record() == [False, False, v[0], v[0]] s = stim.TableauSimulator() s.h(0) s.cnot(0, 1) v = s.measure_kickback(0) assert isinstance(v[0], bool) assert v[1] == stim.PauliString("XX") assert s.measure_kickback(0) == (v[0], None) s = stim.TableauSimulator() s.h(0) s.cnot(0, 1) v = s.measure_kickback(1) assert isinstance(v[0], bool) assert v[1] == stim.PauliString("XX") assert s.measure_kickback(0) == (v[0], None) def test_post_select_using_measure_kickback(): s = stim.TableauSimulator() def pseudo_post_select(qubit, desired_result): m, kick = s.measure_kickback(qubit) if m != desired_result: if kick is None: raise ValueError("Deterministic measurement differed from desired result.") s.do(kick) s.h(0) s.cnot(0, 1) s.cnot(0, 2) pseudo_post_select(qubit=2, desired_result=True) assert s.measure_many(0, 1, 2) == [True, True, True] def test_measure_kickback_random_branches(): s = stim.TableauSimulator() s.set_inverse_tableau(stim.Tableau.random(8)) r = s.peek_bloch(4) if r[0] == 3: # +-Z? assert s.measure_kickback(4) == (r.sign == -1, None) return post_false = None post_true = None for _ in range(100): if post_false is not None and post_true is not None: break s2 = s.copy() if s2.measure(4): post_true = s2 else: post_false = s2 assert post_false is not None and post_true is not None result, kick = s.measure_kickback(4) assert isinstance(kick, stim.PauliString) and len(kick) == 8 if result: s.do(kick) assert s.canonical_stabilizers() == post_false.canonical_stabilizers() s.do(kick) assert s.canonical_stabilizers() == post_true.canonical_stabilizers() def test_set_num_qubits(): s = stim.TableauSimulator() s.h(0) s.cnot(0, 1) s.cnot(0, 2) s.cnot(0, 3) t = s.current_inverse_tableau() s.set_num_qubits(8) s.set_num_qubits(4) assert s.current_inverse_tableau() == t assert s.peek_bloch(0) == stim.PauliString("_") s.set_num_qubits(8) s.set_num_qubits(4) s.cnot(0, 4) s.set_num_qubits(4) assert s.peek_bloch(0) in [stim.PauliString("+Z"), stim.PauliString("-Z")] def test_canonical_stabilizers(): s = stim.TableauSimulator() s.h(0) s.h(1) s.h(2) s.cz(0, 1) s.cz(1, 2) assert s.canonical_stabilizers() == [ stim.PauliString("+X_X"), stim.PauliString("+ZXZ"), stim.PauliString("+_ZX"), ] s.s(1) assert s.canonical_stabilizers() == [ stim.PauliString("+X_X"), stim.PauliString("-ZXY"), stim.PauliString("+_ZX"), ] def test_classical_control_cnot(): s = stim.TableauSimulator() with pytest.raises(IndexError, match="beginning of time"): s.cnot(stim.target_rec(-1), 0) assert not s.measure(1) s.cnot(stim.target_rec(-1), 0) assert not s.measure(0) s.x(1) assert s.measure(1) s.cnot(stim.target_rec(-1), 0) assert s.measure(0) def test_collision(): s = stim.TableauSimulator() with pytest.raises(ValueError, match="same target"): s.cnot(0, 0) with pytest.raises(ValueError, match="same target"): s.swap(0, 1, 2, 2) s.swap(0, 2, 2, 1) def is_parallel_state_vector(actual, expected) -> bool: actual = np.array(actual, dtype=np.complex64) expected = np.array(expected, dtype=np.complex64) assert len(expected.shape) == 1 if actual.shape != expected.shape: return False assert abs(np.linalg.norm(actual) - 1) < 1e-4 assert abs(np.linalg.norm(expected) - 1) < 1e-4 return abs(abs(np.dot(actual, np.conj(expected))) - 1) < 1e-4 def test_is_parallel_state_vector(): assert is_parallel_state_vector([1], [1]) assert is_parallel_state_vector([1], [1j]) assert is_parallel_state_vector([1j], [1]) assert not is_parallel_state_vector([1], [1, 2]) assert is_parallel_state_vector([0.5, 0.5, 0.5, 0.5], [0.5, 0.5, 0.5, 0.5]) assert is_parallel_state_vector([0.5, 0.5, 0.5, 0.5], [0.5j, 0.5j, 0.5j, 0.5j]) assert is_parallel_state_vector([0.5, 0.5, 0.5, 0.5], [-0.5j, -0.5j, -0.5j, -0.5j]) assert not is_parallel_state_vector([0.5, 0.5, 0.5, 0.5], [-0.5j, -0.5j, -0.5j, 0.5j]) assert not is_parallel_state_vector([0.5, 0.5, 0.5, 0.5], [1, 0, 0, 0]) def test_to_state_vector(): s = stim.TableauSimulator() assert is_parallel_state_vector(s.state_vector(), [1]) s.set_num_qubits(1) assert is_parallel_state_vector(s.state_vector(), [1, 0]) s.set_num_qubits(2) s.x(0) assert is_parallel_state_vector(s.state_vector(), [0, 1, 0, 0]) s.h(1) assert is_parallel_state_vector(s.state_vector(), [0, 0.5**0.5, 0, 0.5**0.5]) s.h(0) assert is_parallel_state_vector(s.state_vector(), [0.5, -0.5, 0.5, -0.5]) s.cnot(1, 0) assert is_parallel_state_vector(s.state_vector(), [0.5, -0.5, -0.5, 0.5]) s.x(2) assert is_parallel_state_vector(s.state_vector(), [0, 0, 0, 0, 0.5, -0.5, -0.5, 0.5]) v = s.state_vector().reshape((2,) * 3) assert v[0, 0, 0] == 0 assert v[1, 0, 0] != 0 assert v[0, 1, 0] == 0 assert v[0, 0, 1] == 0 s = stim.TableauSimulator() s.set_num_qubits(3) s.sqrt_x(2) np.testing.assert_allclose( s.state_vector(endian='little'), [np.sqrt(0.5), 0, 0, 0, -1j*np.sqrt(0.5), 0, 0, 0], atol=1e-4, ) np.testing.assert_allclose( s.state_vector(endian='big'), [np.sqrt(0.5), -1j*np.sqrt(0.5), 0, 0, 0, 0, 0, 0], atol=1e-4, ) with pytest.raises(ValueError, match="endian"): s.state_vector(endian='unknown') # Exact precision. s.h(1) np.testing.assert_array_equal( s.state_vector(), [0.5, 0, 0.5, 0, -0.5j, 0, -0.5j, 0], ) def test_peek_observable_expectation(): s = stim.TableauSimulator() s.do(stim.Circuit(''' H 0 CNOT 0 1 0 2 X 0 ''')) assert s.peek_observable_expectation(stim.PauliString("ZZ_")) == -1 assert s.peek_observable_expectation(stim.PauliString("_ZZ")) == 1 assert s.peek_observable_expectation(stim.PauliString("Z_Z")) == -1 assert s.peek_observable_expectation(stim.PauliString("XXX")) == 1 assert s.peek_observable_expectation(stim.PauliString("-XXX")) == -1 assert s.peek_observable_expectation(stim.PauliString("YYX")) == +1 assert s.peek_observable_expectation(stim.PauliString("XYY")) == -1 assert s.peek_observable_expectation(stim.PauliString("")) == 1 assert s.peek_observable_expectation(stim.PauliString("-I")) == -1 assert s.peek_observable_expectation(stim.PauliString("_____")) == 1 assert s.peek_observable_expectation(stim.PauliString("XXXZZZZZ")) == 1 assert s.peek_observable_expectation(stim.PauliString("XXXZZZZX")) == 0 with pytest.raises(ValueError, match="imaginary sign"): s.peek_observable_expectation(stim.PauliString("iZZ")) with pytest.raises(ValueError, match="imaginary sign"): s.peek_observable_expectation(stim.PauliString("-iZZ")) def test_postselect(): s = stim.TableauSimulator() s.h(0) s.cnot(0, 1) s.postselect_x(0, desired_value=False) assert s.peek_bloch(0) == stim.PauliString("+X") assert s.peek_bloch(1) == stim.PauliString("+X") s.postselect_y([2, 3, 4], desired_value=False) assert s.peek_bloch(4) == stim.PauliString("+Y") s.postselect_x(8, desired_value=True) assert s.peek_bloch(8) == stim.PauliString("-X") s.postselect_z(9, desired_value=False) assert s.peek_bloch(9) == stim.PauliString("+Z") with pytest.raises(ValueError, match="impossible"): s.postselect_z(10, desired_value=True) s.postselect_y(1000, desired_value=True) assert s.peek_bloch(1000) == stim.PauliString("-Y") def test_peek_pauli(): s = stim.TableauSimulator() assert s.peek_x(0) == 0 assert s.peek_y(0) == 0 assert s.peek_z(0) == +1 assert s.peek_x(1000) == 0 assert s.peek_y(1000) == 0 assert s.peek_z(1000) == +1 s.h(100) s.z(100) assert s.peek_x(100) == -1 assert s.peek_y(100) == 0 assert s.peek_z(100) == 0 s.h_xy(100) assert s.peek_x(100) == 0 assert s.peek_y(100) == -1 assert s.peek_z(100) == 0 def test_do_circuit(): s = stim.TableauSimulator() s.do_circuit(stim.Circuit(""" H 0 """)) assert s.peek_bloch(0) == stim.PauliString('+X') def test_do_pauli_string(): s = stim.TableauSimulator() s.do_pauli_string(stim.PauliString("IXYZ")) assert s.peek_bloch(0) == stim.PauliString('+Z') assert s.peek_bloch(1) == stim.PauliString('-Z') assert s.peek_bloch(2) == stim.PauliString('-Z') assert s.peek_bloch(3) == stim.PauliString('+Z') def test_do_tableau(): s = stim.TableauSimulator() s.do_tableau(stim.Tableau.from_named_gate("H"), [0]) assert s.peek_bloch(0) == stim.PauliString('+X') s.do_tableau(stim.Tableau.from_named_gate("CNOT"), [0, 1]) assert s.peek_bloch(0) == stim.PauliString('+I') assert s.peek_observable_expectation(stim.PauliString('XX')) == +1 assert s.peek_observable_expectation(stim.PauliString('ZZ')) == +1 with pytest.raises(ValueError, match='len'): s.do_tableau(stim.Tableau(1), [1, 2]) with pytest.raises(ValueError, match='duplicates'): s.do_tableau(stim.Tableau(3), [2, 3, 2]) s.do_tableau(stim.Tableau(0), []) def test_c_xyz_zyx(): s = stim.TableauSimulator() s.c_xyz(0, 2) s.c_zyx(1, 2) assert s.peek_bloch(0) == stim.PauliString("X") assert s.peek_bloch(1) == stim.PauliString("Y") assert s.peek_bloch(2) == stim.PauliString("Z") def test_gate_aliases(): s = stim.TableauSimulator() s.h_xz(0) assert s.peek_bloch(0) == stim.PauliString("X") s.zcx(0, 1) assert s.canonical_stabilizers() == [ stim.PauliString("+XX"), stim.PauliString("+ZZ") ] s.cx(0, 1) assert s.canonical_stabilizers() == [ stim.PauliString("+X_"), stim.PauliString("+_Z") ] s.zcy(0, 1) assert s.canonical_stabilizers() == [ stim.PauliString("+XY"), stim.PauliString("+ZZ") ] s.zcz(0, 1) assert s.canonical_stabilizers() == [ stim.PauliString("-XY"), stim.PauliString("+ZZ") ] def test_num_qubits(): s = stim.TableauSimulator() assert s.num_qubits == 0 s.cx(3, 1) assert s.num_qubits == 4 def test_set_state_from_state_vector(): s = stim.TableauSimulator() expected = [0.5, 0.5, 0, 0, -0.5, 0.5, 0, 0] s.set_state_from_state_vector(expected, endian='little') np.testing.assert_allclose(s.state_vector(), expected, atol=1e-6) def test_set_state_from_stabilizers(): s = stim.TableauSimulator() s.set_state_from_stabilizers([]) assert s.current_inverse_tableau() == stim.Tableau(0) s.set_state_from_stabilizers([stim.PauliString("XXX"), stim.PauliString("_ZZ"), stim.PauliString("ZZ_")]) np.testing.assert_allclose(s.state_vector(), [0.5**0.5, 0, 0, 0, 0, 0, 0, 0.5**0.5], atol=1e-6) def test_seed(): ss1 = [stim.TableauSimulator(seed=0) for _ in range(5)] ss2 = [stim.TableauSimulator(seed=1) for _ in range(5)] def hadamard_and_measure(sim, reps=5): """Repeats Hadamard+measurement reps times and returns result as reps-bit integer.""" r, v = 0, 1 for _ in range(reps): sim.h(0) r += v * sim.measure(0) v *= 2 return r ms1 = {hadamard_and_measure(sim) for sim in ss1} ms2 = {hadamard_and_measure(sim) for sim in ss2} assert len(ms1) == 1 assert len(ms2) == 1 assert ms1 != ms2 def test_copy_without_fresh_entropy(): s1 = stim.TableauSimulator(seed=0) s2 = s1.copy(copy_rng=True) for _ in range(100): s1.h(0) s2.h(0) assert s1.measure(0) == s2.measure(0) def test_copy_with_fresh_entropy(): s1 = stim.TableauSimulator(seed=0) s2 = s1.copy() eq = set() for _ in range(100): s1.h(0) s2.h(0) eq.add(s1.measure(0) == s2.measure(0)) assert eq == {False, True} def test_copy_with_explicit_seed(): s1 = stim.TableauSimulator(seed=0) s2 = stim.TableauSimulator(seed=1) s3 = s1.copy(seed=1) eq = set() for _ in range(100): s1.h(0) s2.h(0) s3.h(0) m1 = s1.measure(0) m2 = s2.measure(0) m3 = s3.measure(0) assert m2 == m3 eq.add(m1 == m3) assert eq == {False, True} def test_copy_with_explicit_copy_rng_and_seed(): s = stim.TableauSimulator() with pytest.raises(ValueError, match='seed and copy_rng are incompatible'): _ = s.copy(copy_rng=True, seed=0) def test_do_circuit_instruction(): s = stim.TableauSimulator() assert s.peek_z(0) == +1 s.do(stim.Circuit("X 0")[0]) assert s.peek_z(0) == -1 s.do(stim.Circuit(""" REPEAT 100 { CNOT 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 0 } """)[0]) assert s.peek_z(0) == +1 assert s.peek_z(1) == +1 assert s.peek_z(2) == +1 assert s.peek_z(3) == -1 assert s.peek_z(4) == +1 assert s.peek_z(5) == -1 assert s.peek_z(6) == +1 assert s.peek_z(7) == +1 s.do(stim.Circuit("X 500")[0]) assert s.peek_z(499) == +1 assert s.peek_z(500) == -1 assert s.peek_z(501) == +1 def test_measure_observable(): s = stim.TableauSimulator() with pytest.raises(ValueError, match="0 <= flip"): s.measure_observable(stim.PauliString("XX"), flip_probability=-0.1) with pytest.raises(ValueError, match="Hermitian"): s.measure_observable(stim.PauliString("iXX")) s.h(0) s.cnot(0, 1) assert not s.measure_observable(stim.PauliString("ZZ")) assert s.measure_observable(stim.PauliString("YY")) assert s.measure_observable(stim.PauliString("-")) assert not s.measure_observable(stim.PauliString(0)) assert not s.measure_observable(stim.PauliString(2)) assert not s.measure_observable(stim.PauliString(5)) n = sum(s.measure_observable(stim.PauliString(0), flip_probability=0.1) for _ in range(1000)) assert 25 <= n <= 300 def test_x_error(): s = stim.TableauSimulator() assert s.peek_bloch(0) == stim.PauliString("+Z") s.x_error(0, 1, 2, p=0) assert s.peek_bloch(0) == stim.PauliString("+Z") s.x_error(0, p=1) assert s.peek_bloch(0) == stim.PauliString("-Z") def test_z_error(): s = stim.TableauSimulator() s.reset_x(0) assert s.peek_bloch(0) == stim.PauliString("+X") s.z_error(0, p=0) assert s.peek_bloch(0) == stim.PauliString("+X") s.z_error(0, p=1) assert s.peek_bloch(0) == stim.PauliString("-X") def test_y_error(): s = stim.TableauSimulator() s.reset_y(0) assert s.peek_bloch(0) == stim.PauliString("+Y") s.x_error(0, p=0) assert s.peek_bloch(0) == stim.PauliString("+Y") s.x_error(0, p=1) assert s.peek_bloch(0) == stim.PauliString("-Y") def test_depolarize1_error(): s = stim.TableauSimulator() s.h(0) s.cnot(0, 1) t = s.current_inverse_tableau() s.depolarize1(0, p=0) assert s.current_inverse_tableau() == t s.depolarize1(0, p=1) assert s.current_inverse_tableau() != t def test_depolarize2_error(): s = stim.TableauSimulator() s.h(0, 1) s.cnot(0, 2, 1, 3) t = s.current_inverse_tableau() s.depolarize2(0, 1, p=0) assert s.current_inverse_tableau() == t with pytest.raises(ValueError, match='Two qubit'): s.depolarize2(1, p=1) assert s.current_inverse_tableau() == t s.depolarize2(0, 1, p=1) assert s.current_inverse_tableau() != t with pytest.raises(ValueError, match='Unexpected argument'): s.depolarize2(1, p=1, q=2) def test_bad_inverse_padding_issue_is_fixed(): circuit = stim.Circuit() circuit.append("H", range(467)) sim = stim.TableauSimulator() sim.do(circuit) stabs = sim.canonical_stabilizers() assert stabs[-1] == stim.PauliString(466 * '_' + 'X') def test_postselect_observable(): sim = stim.TableauSimulator() assert sim.peek_bloch(0) == stim.PauliString("+Z") sim.postselect_observable(stim.PauliString("+X")) assert sim.peek_bloch(0) == stim.PauliString("+X") sim.postselect_observable(stim.PauliString("+Z")) assert sim.peek_bloch(0) == stim.PauliString("+Z") sim.postselect_observable(stim.PauliString("-X")) assert sim.peek_bloch(0) == stim.PauliString("-X") sim.postselect_observable(stim.PauliString("+Z")) assert sim.peek_bloch(0) == stim.PauliString("+Z") sim.postselect_observable(stim.PauliString("-X"), desired_value=True) assert sim.peek_bloch(0) == stim.PauliString("+X") with pytest.raises(ValueError, match="impossible"): sim.postselect_observable(stim.PauliString("-X")) assert sim.peek_bloch(0) == stim.PauliString("+X") with pytest.raises(ValueError, match="imaginary sign"): sim.postselect_observable(stim.PauliString("iZ")) assert sim.peek_bloch(0) == stim.PauliString("+X") sim.postselect_observable(stim.PauliString("+XX")) sim.postselect_observable(stim.PauliString("+ZZ")) assert sim.peek_observable_expectation(stim.PauliString("+YY")) == -1 ================================================ FILE: src/stim/simulators/vector_simulator.cc ================================================ // 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. #include "stim/simulators/vector_simulator.h" #include #include "stim/gates/gates.h" #include "stim/mem/simd_util.h" #include "stim/stabilizers/pauli_string.h" using namespace stim; VectorSimulator::VectorSimulator(size_t num_qubits) { state.resize(size_t{1} << num_qubits, 0.0f); state[0] = 1; } inline std::vector> mat_vec_mul( const std::vector>> &matrix, const std::vector> &vec) { std::vector> result; for (size_t row = 0; row < vec.size(); row++) { std::complex v = 0; for (size_t col = 0; col < vec.size(); col++) { v += matrix[row][col] * vec[col]; } result.push_back(v); } return result; } void VectorSimulator::apply( const std::vector>> &matrix, const std::vector &qubits) { size_t n = size_t{1} << qubits.size(); assert(matrix.size() == n); std::vector masks; for (size_t k = 0; k < n; k++) { size_t m = 0; for (size_t q = 0; q < qubits.size(); q++) { if ((k >> q) & 1) { m |= size_t{1} << qubits[q]; } } masks.push_back(m); } assert(masks.back() < state.size()); for (size_t base = 0; base < state.size(); base++) { if (base & masks.back()) { continue; } std::vector> in; in.reserve(masks.size()); for (auto m : masks) { in.push_back(state[base | m]); } auto out = mat_vec_mul(matrix, in); for (size_t k = 0; k < masks.size(); k++) { state[base | masks[k]] = out[k]; } } } void VectorSimulator::apply(GateType gate, size_t qubit) { try { apply(GATE_DATA[gate].unitary(), {qubit}); } catch (const std::out_of_range &) { throw std::out_of_range( "Single qubit gate isn't supported by VectorSimulator: " + std::string(GATE_DATA[gate].name)); } } void VectorSimulator::apply(GateType gate, size_t qubit1, size_t qubit2) { try { apply(GATE_DATA[gate].unitary(), {qubit1, qubit2}); } catch (const std::out_of_range &) { throw std::out_of_range( "Two qubit gate isn't supported by VectorSimulator: " + std::string(GATE_DATA[gate].name)); } } void VectorSimulator::smooth_stabilizer_state(std::complex base_value) { std::vector> ratio_values{ {0, 0}, {1, 0}, {-1, 0}, {0, 1}, {0, -1}, }; for (size_t k = 0; k < state.size(); k++) { auto ratio = state[k] / base_value; bool solved = false; for (const auto &r : ratio_values) { if (std::norm(ratio - r) < 0.125) { state[k] = r; solved = true; } } if (!solved) { throw std::invalid_argument("The state vector wasn't a stabilizer state."); } } } bool VectorSimulator::approximate_equals(const VectorSimulator &other, bool up_to_global_phase) const { if (state.size() != other.state.size()) { return false; } std::complex dot = 0; float mag1 = 0; float mag2 = 0; for (size_t k = 0; k < state.size(); k++) { auto c = state[k]; auto c2 = other.state[k]; dot += c * std::conj(c2); mag1 += c.real() * c.real() + c.imag() * c.imag(); mag2 += c2.real() * c2.real() + c2.imag() * c2.imag(); } assert(1 - 1e-4 <= mag1 && mag1 <= 1 + 1e-4); assert(1 - 1e-4 <= mag2 && mag2 <= 1 + 1e-4); float f; if (up_to_global_phase) { f = dot.real() * dot.real() + dot.imag() * dot.imag(); } else { f = dot.real(); } return 1 - 1e-4 <= f && f <= 1 + 1e-4; } std::string VectorSimulator::str() const { std::stringstream ss; ss << *this; return ss.str(); } std::ostream &stim::operator<<(std::ostream &out, const VectorSimulator &sim) { out << "VectorSimulator {\n"; for (size_t k = 0; k < sim.state.size(); k++) { out << " " << k << ": " << sim.state[k] << "\n"; } out << "}"; return out; } void VectorSimulator::canonicalize_assuming_stabilizer_state(double norm2) { // Find a solid non-zero entry. size_t nz = 0; for (size_t k = 1; k < state.size(); k++) { if (abs(state[k]) > abs(state[nz]) * 2) { nz = k; } } // Rescale so that the non-zero entries are 1, -1, 1j, or -1j. size_t num_non_zero = 0; std::complex big_v = state[nz]; for (auto &v : state) { v /= big_v; if (abs(v) < 0.1) { v = 0; continue; } num_non_zero++; if (abs(v - std::complex{1, 0}) < 0.1) { v = std::complex{1, 0}; } else if (abs(v - std::complex{0, 1}) < 0.1) { v = std::complex{0, 1}; } else if (abs(v - std::complex{-1, 0}) < 0.1) { v = std::complex{-1, 0}; } else if (abs(v - std::complex{0, -1}) < 0.1) { v = std::complex{0, -1}; } else { throw std::invalid_argument("State vector extraction failed. This shouldn't occur."); } } // Normalize the entries so the result is a unit vector. std::complex scale = (float)(sqrt(norm2 / (double)num_non_zero)); for (auto &v : state) { v *= scale; } } void VectorSimulator::do_unitary_circuit(const Circuit &circuit) { std::vector targets1{1}; std::vector targets2{1, 2}; circuit.for_each_operation([&](const CircuitInstruction &op) { const auto &gate_data = GATE_DATA[op.gate_type]; if (!(gate_data.flags & GATE_IS_UNITARY)) { std::stringstream ss; ss << "Not a unitary gate: " << gate_data.name; throw std::invalid_argument(ss.str()); } auto unitary = gate_data.unitary(); for (auto t : op.targets) { if (!t.is_qubit_target() || (size_t{1} << t.data) >= state.size()) { std::stringstream ss; ss << "Targets out of range: " << op; throw std::invalid_argument(ss.str()); } } if (gate_data.flags & stim::GATE_TARGETS_PAIRS) { for (size_t k = 0; k < op.targets.size(); k += 2) { targets2[0] = (size_t)op.targets[k].data; targets2[1] = (size_t)op.targets[k + 1].data; apply(unitary, targets2); } } else { for (auto t : op.targets) { targets1[0] = (size_t)t.data; apply(unitary, targets1); } } }); } ================================================ FILE: src/stim/simulators/vector_simulator.h ================================================ /* * 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. */ #ifndef _STIM_SIMULATORS_VECTOR_SIMULATOR_H #define _STIM_SIMULATORS_VECTOR_SIMULATOR_H #include #include #include #include #include "stim/circuit/circuit.h" #include "stim/mem/simd_word.h" #include "stim/util_bot/probability_util.h" namespace stim { template struct PauliStringRef; template struct PauliString; /// A state vector quantum circuit simulator. /// /// Not intended to be particularly performant. Mostly used as a reference when testing. struct VectorSimulator { std::vector> state; /// Creates a state vector for the given number of qubits, initialized to the zero state. explicit VectorSimulator(size_t num_qubits); /// Returns a VectorSimulator with a state vector satisfying all the given stabilizers. /// /// Assumes the stabilizers commute. Works by generating a random state vector and projecting onto /// each of the given stabilizers. Global phase will vary. template static VectorSimulator from_stabilizers(const std::vector> &stabilizers) { VectorSimulator result(0); result.state = state_vector_from_stabilizers(stabilizers, 1); return result; } template static std::vector> state_vector_from_stabilizers( const std::vector> &stabilizers, float norm2 = 1) { size_t num_qubits = stabilizers.empty() ? 0 : stabilizers[0].num_qubits; VectorSimulator sim(num_qubits); // Create an initial state $|A\rangle^{\otimes n}$ which overlaps with all possible stabilizers. std::uniform_real_distribution dist(-1.0, +1.0); auto rng = externally_seeded_rng(); for (auto &s : sim.state) { s = {dist(rng), dist(rng)}; } // Project out the non-overlapping parts. for (const auto &p : stabilizers) { sim.project(p); } if (stabilizers.empty()) { sim.project(PauliString(0)); } sim.canonicalize_assuming_stabilizer_state(norm2); return sim.state; } /// Applies a unitary operation to the given qubits, updating the state vector. void apply(const std::vector>> &matrix, const std::vector &qubits); /// Helper method for applying named single qubit gates. void apply(GateType gate, size_t qubit); /// Helper method for applying named two qubit gates. void apply(GateType gate, size_t qubit1, size_t qubit2); /// Helper method for applying the gates in a Pauli string. template void apply(const PauliStringRef &gate, size_t qubit_offset) { if (gate.sign) { for (auto &e : state) { e *= -1; } } for (size_t k = 0; k < gate.num_qubits; k++) { bool x = gate.xs[k]; bool z = gate.zs[k]; size_t q = qubit_offset + k; if (x && z) { apply(GateType::Y, q); } else if (x) { apply(GateType::X, q); } else if (z) { apply(GateType::Z, q); } } } /// Applies the unitary operations within a circuit to the simulator's state. void do_unitary_circuit(const Circuit &circuit); /// Modifies the state vector to be EXACTLY entries of 0, 1, -1, i, or -i. /// /// Each entry is a ratio relative to the given base value. /// If any entry has a ratio not near the desired set, an exception is raised. void smooth_stabilizer_state(std::complex base_value); /// Projects the state vector into the +1 eigenstate of the given observable, and renormalizes. /// /// Returns: /// The 2-norm of the component of the state vector that was already in the +1 eigenstate. /// In other words, the probability that measuring the observable would have returned +1 instead of -1. template float project(const PauliStringRef &observable) { assert(1ULL << observable.num_qubits == state.size()); auto basis_change = [&]() { for (size_t k = 0; k < observable.num_qubits; k++) { if (observable.xs[k]) { if (observable.zs[k]) { apply(GateType::H_YZ, k); } else { apply(GateType::H, k); } } } }; uint64_t mask = 0; for (size_t k = 0; k < observable.num_qubits; k++) { if (observable.xs[k] || observable.zs[k]) { mask |= 1ULL << k; } } basis_change(); float mag2 = 0; for (size_t i = 0; i < state.size(); i++) { bool reject = observable.sign; reject ^= (std::popcount(i & mask) & 1) != 0; if (reject) { state[i] = 0; } else { mag2 += state[i].real() * state[i].real() + state[i].imag() * state[i].imag(); } } assert(mag2 > 1e-8); auto w = sqrtf(mag2); for (size_t i = 0; i < state.size(); i++) { state[i] /= w; } basis_change(); return mag2; } /// Determines if two vector simulators have similar state vectors. bool approximate_equals(const VectorSimulator &other, bool up_to_global_phase = false) const; /// A description of the state vector's state. std::string str() const; void canonicalize_assuming_stabilizer_state(double norm2); }; /// Writes a description of the state vector's state to an output stream. std::ostream &operator<<(std::ostream &out, const VectorSimulator &sim); } // namespace stim #endif ================================================ FILE: src/stim/simulators/vector_simulator.test.cc ================================================ // 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. #include "stim/simulators/vector_simulator.h" #include "gtest/gtest.h" #include "stim/gates/gates.h" #include "stim/mem/simd_word.test.h" #include "stim/stabilizers/pauli_string.h" using namespace stim; static float complex_distance(std::complex a, std::complex b) { auto d = a - b; return sqrtf(d.real() * d.real() + d.imag() * d.imag()); } #define ASSERT_NEAR_C(a, b) ASSERT_LE(complex_distance(a, b), 1e-4) TEST(vector_sim, qubit_order) { VectorSimulator sim(2); sim.apply(GateType::H, 0); sim.apply(GateType::CX, 0, 1); ASSERT_NEAR_C(sim.state[0], sqrtf(0.5)); ASSERT_NEAR_C(sim.state[1], 0); ASSERT_NEAR_C(sim.state[2], 0); ASSERT_NEAR_C(sim.state[3], sqrtf(0.5)); } TEST(vector_sim, h_squared) { VectorSimulator sim(1); sim.apply(GateType::H, 0); sim.apply(GateType::H, 0); ASSERT_NEAR_C(sim.state[0], 1); ASSERT_NEAR_C(sim.state[1], 0); } TEST(vector_sim, sqrt_x_squared) { VectorSimulator sim(1); sim.apply(GateType::SQRT_X_DAG, 0); sim.apply(GateType::SQRT_X_DAG, 0); ASSERT_NEAR_C(sim.state[0], 0); ASSERT_NEAR_C(sim.state[1], 1); } TEST(vector_sim, state_channel_duality_cnot) { VectorSimulator sim(4); sim.apply(GateType::H, 0); sim.apply(GateType::H, 1); sim.apply(GateType::CX, 0, 2); sim.apply(GateType::CX, 1, 3); sim.apply(GateType::CX, 2, 3); auto u = GATE_DATA.at("ZCX").unitary(); for (size_t row = 0; row < 4; row++) { for (size_t col = 0; col < 4; col++) { ASSERT_NEAR_C(sim.state[row * 4 + col], u[row][col] * 0.5f); } } } TEST(vector_sim, state_channel_duality_y) { VectorSimulator sim(2); sim.apply(GateType::H, 0); sim.apply(GateType::CX, 0, 1); sim.apply(GateType::Y, 1); auto u = GATE_DATA.at("Y").unitary(); for (size_t row = 0; row < 2; row++) { for (size_t col = 0; col < 2; col++) { ASSERT_NEAR_C(sim.state[row * 2 + col], u[row][col] * sqrtf(0.5f)); } } } TEST_EACH_WORD_SIZE_W(vector_sim, apply_pauli, { VectorSimulator sim(2); sim.apply(PauliString::from_str("+II").ref(), 0); ASSERT_NEAR_C(sim.state[0], 1); ASSERT_NEAR_C(sim.state[1], 0); ASSERT_NEAR_C(sim.state[2], 0); ASSERT_NEAR_C(sim.state[3], 0); sim.apply(PauliString::from_str("-II").ref(), 0); ASSERT_NEAR_C(sim.state[0], -1); ASSERT_NEAR_C(sim.state[1], 0); ASSERT_NEAR_C(sim.state[2], 0); ASSERT_NEAR_C(sim.state[3], 0); sim.apply(PauliString::from_str("+XI").ref(), 0); ASSERT_NEAR_C(sim.state[0], 0); ASSERT_NEAR_C(sim.state[1], -1); ASSERT_NEAR_C(sim.state[2], 0); ASSERT_NEAR_C(sim.state[3], 0); sim.apply(PauliString::from_str("+IZ").ref(), 0); ASSERT_NEAR_C(sim.state[0], 0); ASSERT_NEAR_C(sim.state[1], -1); ASSERT_NEAR_C(sim.state[2], 0); ASSERT_NEAR_C(sim.state[3], 0); sim.apply(PauliString::from_str("+ZI").ref(), 0); ASSERT_NEAR_C(sim.state[0], 0); ASSERT_NEAR_C(sim.state[1], 1); ASSERT_NEAR_C(sim.state[2], 0); ASSERT_NEAR_C(sim.state[3], 0); sim.apply(PauliString::from_str("+IY").ref(), 0); ASSERT_NEAR_C(sim.state[0], 0); ASSERT_NEAR_C(sim.state[1], 0); ASSERT_NEAR_C(sim.state[2], 0); ASSERT_NEAR_C(sim.state[3], std::complex(0, 1)); sim.apply(PauliString::from_str("+XX").ref(), 0); ASSERT_NEAR_C(sim.state[0], std::complex(0, 1)); ASSERT_NEAR_C(sim.state[1], 0); ASSERT_NEAR_C(sim.state[2], 0); ASSERT_NEAR_C(sim.state[3], 0); sim.apply(PauliString::from_str("+X").ref(), 1); ASSERT_NEAR_C(sim.state[0], 0); ASSERT_NEAR_C(sim.state[1], 0); ASSERT_NEAR_C(sim.state[2], std::complex(0, 1)); ASSERT_NEAR_C(sim.state[3], 0); }) TEST(vector_sim, approximate_equals) { VectorSimulator s1(2); VectorSimulator s2(2); ASSERT_TRUE(s1.approximate_equals(s2)); ASSERT_TRUE(s1.approximate_equals(s2, false)); ASSERT_TRUE(s1.approximate_equals(s2, true)); s1.state[0] *= -1; ASSERT_FALSE(s1.approximate_equals(s2)); ASSERT_FALSE(s1.approximate_equals(s2, false)); ASSERT_TRUE(s1.approximate_equals(s2, true)); s1.state[0] *= std::complex(0, 1); ASSERT_FALSE(s1.approximate_equals(s2)); ASSERT_FALSE(s2.approximate_equals(s1)); ASSERT_FALSE(s1.approximate_equals(s2, false)); ASSERT_TRUE(s1.approximate_equals(s2, true)); ASSERT_FALSE(s2.approximate_equals(s1, false)); ASSERT_TRUE(s2.approximate_equals(s1, true)); s1.state[0] = 0; s1.state[1] = 1; ASSERT_FALSE(s1.approximate_equals(s2)); ASSERT_FALSE(s1.approximate_equals(s2, false)); ASSERT_FALSE(s1.approximate_equals(s2, true)); s2.state[0] = 0; s2.state[1] = 1; ASSERT_TRUE(s1.approximate_equals(s2)); s1.state[0] = sqrtf(0.5); s1.state[1] = sqrtf(0.5); s2.state[0] = sqrtf(0.5); s2.state[1] = sqrtf(0.5); ASSERT_TRUE(s1.approximate_equals(s2)); s1.state[0] *= -1; ASSERT_FALSE(s1.approximate_equals(s2)); } TEST_EACH_WORD_SIZE_W(vector_sim, project_empty, { VectorSimulator sim(0); sim.project(PauliString(0)); VectorSimulator ref(0); ref.state = {1}; ASSERT_TRUE(sim.approximate_equals(ref, true)); }) TEST_EACH_WORD_SIZE_W(vector_sim, project, { VectorSimulator sim(2); VectorSimulator ref(2); sim.state = {0.5, 0.5, 0.5, 0.5}; ASSERT_NEAR_C(sim.project(PauliString::from_str("ZI")), 0.5); ref.state = {sqrtf(0.5), 0, sqrtf(0.5), 0}; ASSERT_TRUE(sim.approximate_equals(ref)); ASSERT_NEAR_C(sim.project(PauliString::from_str("ZI")), 1); ASSERT_TRUE(sim.approximate_equals(ref)); sim.state = {0.5, 0.5, 0.5, 0.5}; sim.project(PauliString::from_str("-ZI")); ref.state = {0, sqrtf(0.5), 0, sqrtf(0.5)}; ASSERT_TRUE(sim.approximate_equals(ref)); sim.state = {0.5, 0.5, 0.5, 0.5}; sim.project(PauliString::from_str("IZ")); ref.state = {sqrtf(0.5), sqrtf(0.5), 0, 0}; ASSERT_TRUE(sim.approximate_equals(ref)); sim.state = {0.5, 0.5, 0.5, 0.5}; sim.project(PauliString::from_str("-IZ")); ref.state = {0, 0, sqrtf(0.5), sqrtf(0.5)}; ASSERT_TRUE(sim.approximate_equals(ref)); sim.state = {0.5, 0.5, 0.5, 0.5}; sim.project(PauliString::from_str("ZZ")); ref.state = {sqrtf(0.5), 0, 0, sqrtf(0.5)}; ASSERT_TRUE(sim.approximate_equals(ref)); sim.state = {0.5, 0.5, 0.5, 0.5}; sim.project(PauliString::from_str("-ZZ")); ref.state = {0, sqrtf(0.5), sqrtf(0.5), 0}; ASSERT_TRUE(sim.approximate_equals(ref)); sim.project(PauliString::from_str("ZI")); sim.state = {1, 0, 0, 0}; sim.project(PauliString::from_str("ZZ")); ref.state = {1, 0, 0, 0}; ASSERT_TRUE(sim.approximate_equals(ref)); sim.project(PauliString::from_str("XX")); ref.state = {sqrtf(0.5f), 0, 0, sqrtf(0.5f)}; ASSERT_TRUE(sim.approximate_equals(ref)); sim.project(PauliString::from_str("-YZ")); ref.state = {0.5, {0, -0.5}, {0, -0.5}, 0.5}; ASSERT_TRUE(sim.approximate_equals(ref)); sim.project(PauliString::from_str("-ZI")); ref.state = {0, {0, -sqrtf(0.5)}, 0, sqrtf(0.5)}; ASSERT_TRUE(sim.approximate_equals(ref)); }) TEST_EACH_WORD_SIZE_W(vector_sim, from_stabilizers, { VectorSimulator ref(2); auto sim = VectorSimulator::from_stabilizers({PauliString::from_str("ZI"), PauliString::from_str("IZ")}); ref.state = {1, 0, 0, 0}; ASSERT_TRUE(sim.approximate_equals(ref, true)); sim = VectorSimulator::from_stabilizers({PauliString::from_str("-YX"), PauliString::from_str("ZZ")}); ref.state = {sqrtf(0.5), 0, 0, {0, -sqrtf(0.5)}}; ASSERT_TRUE(sim.approximate_equals(ref, true)); sim = VectorSimulator::from_stabilizers({PauliString::from_str("ZI"), PauliString::from_str("ZZ")}); ref.state = {1, 0, 0, 0}; ASSERT_TRUE(sim.approximate_equals(ref, true)); sim = VectorSimulator::from_stabilizers({PauliString::from_str("ZI"), PauliString::from_str("-ZZ")}); ref.state = {0, 0, 1, 0}; ASSERT_TRUE(sim.approximate_equals(ref, true)); sim = VectorSimulator::from_stabilizers({PauliString::from_str("ZI"), PauliString::from_str("IX")}); ref.state = {sqrtf(0.5), 0, sqrtf(0.5), 0}; ASSERT_TRUE(sim.approximate_equals(ref, true)); sim = VectorSimulator::from_stabilizers({PauliString::from_str("ZZ"), PauliString::from_str("XX")}); ref.state = {sqrtf(0.5), 0, 0, sqrtf(0.5)}; ASSERT_TRUE(sim.approximate_equals(ref, true)); sim = VectorSimulator::from_stabilizers( {PauliString::from_str("XXX"), PauliString::from_str("ZZI"), PauliString::from_str("IZZ")}); ref.state = {sqrtf(0.5), 0, 0, 0, 0, 0, 0, sqrtf(0.5)}; ASSERT_TRUE(sim.approximate_equals(ref, true)); sim = VectorSimulator::from_stabilizers( {PauliString::from_str("YYY"), PauliString::from_str("ZZI"), PauliString::from_str("IZZ")}); ref.state = {sqrtf(0.5), 0, 0, 0, 0, 0, 0, {0, -sqrtf(0.5)}}; ASSERT_TRUE(sim.approximate_equals(ref, true)); sim = VectorSimulator::from_stabilizers( {PauliString::from_str("-YYY"), PauliString::from_str("-ZZI"), PauliString::from_str("IZZ")}); ref.state = {0, sqrtf(0.5), 0, 0, 0, 0, {0, -sqrtf(0.5)}, 0}; ASSERT_TRUE(sim.approximate_equals(ref, true)); }) TEST(vector_sim, smooth_stabilizer_state) { VectorSimulator sim(0); sim.state = {{1, 0}, {1, 1}}; ASSERT_THROW({ sim.smooth_stabilizer_state(1); }, std::invalid_argument); sim.state = {{0.25, 0.25}, {-0.25, 0.25}}; ASSERT_THROW({ sim.smooth_stabilizer_state(1); }, std::invalid_argument); sim.state = {{0.25, 0.25}, {-0.25, 0.25}}; sim.smooth_stabilizer_state({-0.25, -0.25}); ASSERT_EQ(sim.state, (std::vector>{{-1, 0}, {0, -1}})); } TEST(vector_sim, do_unitary_circuit) { VectorSimulator sim(3); sim.do_unitary_circuit(Circuit(R"CIRCUIT( H 0 1 S 0 )CIRCUIT")); sim.smooth_stabilizer_state(0.5); ASSERT_EQ(sim.state, (std::vector>({1, {0, 1}, 1, {0, 1}, 0, 0, 0, 0}))); sim.do_unitary_circuit(Circuit(R"CIRCUIT( CNOT 0 2 2 0 0 2 )CIRCUIT")); ASSERT_EQ(sim.state, (std::vector>({1, 0, 1, 0, {0, 1}, 0, {0, 1}, 0}))); ASSERT_THROW({ sim.do_unitary_circuit(Circuit("H 3")); }, std::invalid_argument); ASSERT_THROW({ sim.do_unitary_circuit(Circuit("CX rec[-1] 0")); }, std::invalid_argument); ASSERT_THROW({ sim.do_unitary_circuit(Circuit("X_ERROR(0.1) 0")); }, std::invalid_argument); } ================================================ FILE: src/stim/stabilizers/README.md ================================================ # `stabilizers` directory This directory contains data structures and code related to the stabilizer formalism, such as code for conjugating Pauli products with Clifford operations. ================================================ FILE: src/stim/stabilizers/clifford_string.h ================================================ /* * 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. */ #ifndef _STIM_STABILIZERS_CLIFFORD_STRING_H #define _STIM_STABILIZERS_CLIFFORD_STRING_H #include "stim/circuit/circuit.h" #include "stim/gates/gates.h" #include "stim/mem/simd_bits.h" namespace stim { /// A fixed-size list of W single-qubit Clifford rotations. template struct CliffordWord { Word x_signs; Word z_signs; Word inv_x2x; // Inverted so that zero-initializing gives the identity gate. Word x2z; Word z2x; Word inv_z2z; // Inverted so that zero-initializing gives the identity gate. }; constexpr std::array INT_TO_SINGLE_QUBIT_CLIFFORD_TABLE{ GateType::I, GateType::X, GateType::Z, GateType::Y, GateType::NOT_A_GATE, // These should be impossible if the class is in a good state. GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::S, GateType::H_XY, GateType::S_DAG, GateType::H_NXY, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::SQRT_X_DAG, GateType::SQRT_X, GateType::H_YZ, GateType::H_NYZ, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::C_ZYX, GateType::C_ZNYX, GateType::C_ZYNX, GateType::C_NZYX, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::NOT_A_GATE, GateType::C_XYZ, GateType::C_XYNZ, GateType::C_XNYZ, GateType::C_NXYZ, GateType::H, GateType::SQRT_Y_DAG, GateType::SQRT_Y, GateType::H_NXZ, }; inline GateType bits2gate(std::array bits) { int k = (bits[0] << 0) | (bits[1] << 1) | (bits[2] << 2) | (bits[3] << 3) | (bits[4] << 4) | (bits[5] << 5); return INT_TO_SINGLE_QUBIT_CLIFFORD_TABLE[k]; } inline std::array gate_to_bits(GateType gate_type) { Gate g = GATE_DATA[gate_type]; if (!(g.flags & GATE_IS_SINGLE_QUBIT_GATE) || !(g.flags & GATE_IS_UNITARY)) { throw std::invalid_argument("Not a single qubit Clifford gate: " + std::string(g.name)); } const auto &flows = g.flow_data; std::string_view tx = flows[0]; std::string_view tz = flows[1]; bool z_sign = tz[0] == '-'; bool inv_x2x = !(tx[1] == 'X' || tx[1] == 'Y'); bool x2z = tx[1] == 'Z' || tx[1] == 'Y'; bool x_sign = tx[0] == '-'; bool z2x = tz[1] == 'X' || tz[1] == 'Y'; bool inv_z2z = !(tz[1] == 'Z' || tz[1] == 'Y'); return {z_sign, x_sign, inv_x2x, x2z, z2x, inv_z2z}; } /// Returns the result of multiplying W rotations pair-wise. template inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord &rhs) { CliffordWord result; // I don't have a simple explanation of why this is correct. It was produced by starting from something that was // obviously correct, having tests to check all 24*24 cases, then iteratively applying simple rewrites to reduce // the number of operations. So the result is correct, but somewhat incomprehensible. result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ (lhs.z2x & rhs.x2z); result.x2z = andnot(rhs.inv_x2x, lhs.x2z) ^ andnot(lhs.inv_z2z, rhs.x2z); result.z2x = andnot(lhs.inv_x2x, rhs.z2x) ^ andnot(rhs.inv_z2z, lhs.z2x); result.inv_z2z = (lhs.x2z & rhs.z2x) ^ (lhs.inv_z2z | rhs.inv_z2z); // I *especially* don't have an explanation of why this part is correct. But every case is tested and verified. Word rhs_x2y = andnot(rhs.inv_x2x, rhs.x2z); Word rhs_z2y = andnot(rhs.inv_z2z, rhs.z2x); Word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; result.x_signs = rhs.x_signs ^ andnot(rhs.inv_x2x, lhs.x_signs) ^ (rhs_x2y & dy) ^ (rhs.x2z & lhs.z_signs); result.z_signs = rhs.z_signs ^ (rhs.z2x & lhs.x_signs) ^ (rhs_z2y & dy) ^ andnot(rhs.inv_z2z, lhs.z_signs); return result; } template struct CliffordString; template std::ostream &operator<<(std::ostream &out, const CliffordString &v); template GateType single_qubit_tableau_to_gate_type(const stim::Tableau &tableau) { return bits2gate(std::array{ tableau.zs.signs[0], tableau.xs.signs[0], !tableau.xs.xt[0][0], tableau.xs.zt[0][0], tableau.zs.xt[0][0], !tableau.zs.zt[0][0], }); } /// A string of single-qubit Clifford rotations. template struct CliffordString { size_t num_qubits; // The 2 sign bits of a single qubit Clifford, packed into arrays for easy processing. simd_bits x_signs; simd_bits z_signs; // The 4 tableau bits of a single qubit Clifford, packed into arrays for easy processing. // The x2x and z2z terms are inverted so that zero-initializing produces the identity gate. simd_bits inv_x2x; simd_bits x2z; simd_bits z2x; simd_bits inv_z2z; /// Constructs an identity CliffordString for the given number of qubits. explicit CliffordString(size_t num_qubits) : num_qubits(num_qubits), x_signs(num_qubits), z_signs(num_qubits), inv_x2x(num_qubits), x2z(num_qubits), z2x(num_qubits), inv_z2z(num_qubits) { } void randomize(std::mt19937_64 &rng) { CliffordString result(num_qubits); x_signs.randomize(num_qubits, rng); z_signs.randomize(num_qubits, rng); for (size_t k = 0; k < num_qubits; k++) { uint64_t v = rng() % 6; uint8_t p1 = v % 3 + 1; uint8_t p2 = v / 3 + 1; p2 += p2 >= p1; inv_x2x[k] = !(p1 & 1); x2z[k] = p1 & 2; z2x[k] = p2 & 1; inv_z2z[k] = !(p2 & 2); } } static CliffordString random(size_t num_qubits, std::mt19937_64 &rng) { CliffordString result(num_qubits); result.randomize(rng); return result; } CliffordString operator+(const CliffordString &other) const { if (num_qubits + other.num_qubits < num_qubits) { throw std::invalid_argument("Couldn't concatenate Clifford strings due to size overflowing."); } CliffordString result(num_qubits + other.num_qubits); for (size_t k = 0; k < num_qubits; k++) { result.set_gate_at(k, gate_at(k)); } for (size_t k = 0; k < other.num_qubits; k++) { result.set_gate_at(num_qubits + k, other.gate_at(k)); } return result; } CliffordString &operator+=(const CliffordString &other) { CliffordString tmp = *this + other; *this = std::move(tmp); return *this; } CliffordString operator*(size_t repetitions) const { if (repetitions == 0) { return CliffordString(0); } size_t new_num_qubits = num_qubits * repetitions; if (new_num_qubits / repetitions != num_qubits) { throw std::invalid_argument("Couldn't repeat CliffordString due to size overflowing."); } CliffordString result(new_num_qubits); for (size_t k = 0; k < num_qubits; k++) { GateType g = gate_at(k); for (size_t k2 = k; k2 < new_num_qubits; k2 += num_qubits) { result.set_gate_at(k2, g); } } return result; } CliffordString &operator*=(size_t repetitions) { CliffordString tmp = *this * repetitions; *this = std::move(tmp); return *this; } /// Extracts rotations k*W through (k+1)*W into a CliffordWord. inline CliffordWord> word_at(size_t k) const { return CliffordWord>{ x_signs.ptr_simd[k], z_signs.ptr_simd[k], inv_x2x.ptr_simd[k], x2z.ptr_simd[k], z2x.ptr_simd[k], inv_z2z.ptr_simd[k], }; } /// Writes rotations k*W through (k+1)*W from a CliffordWord. inline void set_word_at(size_t k, CliffordWord> new_value) const { x_signs.ptr_simd[k] = new_value.x_signs; z_signs.ptr_simd[k] = new_value.z_signs; inv_x2x.ptr_simd[k] = new_value.inv_x2x; x2z.ptr_simd[k] = new_value.x2z; z2x.ptr_simd[k] = new_value.z2x; inv_z2z.ptr_simd[k] = new_value.inv_z2z; } /// Converts the internal rotation representation into a GateType. GateType gate_at(size_t q) const { return bits2gate(std::array{z_signs[q], x_signs[q], inv_x2x[q], x2z[q], z2x[q], inv_z2z[q]}); } /// Sets an internal rotation from a GateType. void set_gate_at(size_t q, GateType gate_type) { std::array bits = gate_to_bits(gate_type); z_signs[q] = bits[0]; x_signs[q] = bits[1]; inv_x2x[q] = bits[2]; x2z[q] = bits[3]; z2x[q] = bits[4]; inv_z2z[q] = bits[5]; } /// Inplace right-multiplication of rotations. CliffordString &operator*=(const CliffordString &rhs) { ensure_num_qubits(rhs.num_qubits); for (size_t k = 0; k < rhs.x_signs.num_simd_words; k++) { auto lhs_w = word_at(k); auto rhs_w = rhs.word_at(k); set_word_at(k, lhs_w * rhs_w); } return *this; } void inplace_then(CircuitInstruction inst) { // Ignore annotations. switch (inst.gate_type) { case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: return; default: break; } std::array v = gate_to_bits(inst.gate_type); for (const auto &t : inst.targets) { if (!t.is_qubit_target()) { continue; } uint32_t q = t.qubit_value(); if (q >= num_qubits) { throw std::invalid_argument("Circuit acted on qubit past end of string."); } size_t w = q / W; size_t k = q % W; CliffordWord> tmp{}; bit_ref(&tmp.z_signs, k) ^= v[0]; bit_ref(&tmp.x_signs, k) ^= v[1]; bit_ref(&tmp.inv_x2x, k) ^= v[2]; bit_ref(&tmp.x2z, k) ^= v[3]; bit_ref(&tmp.z2x, k) ^= v[4]; bit_ref(&tmp.inv_z2z, k) ^= v[5]; set_word_at(w, tmp * word_at(w)); } } static CliffordString from_circuit(const Circuit &circuit) { CliffordString result(circuit.count_qubits()); circuit.for_each_operation([&](CircuitInstruction inst) { result.inplace_then(inst); }); return result; } Circuit to_circuit() const { Circuit result; for (size_t q = 0; q < num_qubits; q++) { GateType g = gate_at(q); if (g != GateType::I || q + 1 == num_qubits) { GateTarget t = GateTarget::qubit(q); result.safe_append(CircuitInstruction{g, {}, &t, {}}); } } return result; } void ensure_num_qubits(size_t min_num_qubits) { if (num_qubits < min_num_qubits) { num_qubits = min_num_qubits; x_signs.preserving_resize(num_qubits); z_signs.preserving_resize(num_qubits); inv_x2x.preserving_resize(num_qubits); x2z.preserving_resize(num_qubits); z2x.preserving_resize(num_qubits); inv_z2z.preserving_resize(num_qubits); } } /// Inplace left-multiplication of rotations. CliffordString &inplace_left_mul_by(const CliffordString &lhs) { ensure_num_qubits(lhs.num_qubits); for (size_t k = 0; k < x_signs.num_simd_words; k++) { auto lhs_w = lhs.word_at(k); auto rhs_w = word_at(k); set_word_at(k, lhs_w * rhs_w); } return *this; } /// Inplace raises to a power. void ipow(int64_t power) const { power %= 12; if (power < 0) { power += 12; } for (size_t k = 0; k < x_signs.num_simd_words; k++) { auto delta = word_at(k); CliffordWord> total{}; for (size_t step = 0; step < power; step++) { total = total * delta; } set_word_at(k, total); } } PauliString x_outputs() const { PauliString result(num_qubits); result.xs = inv_x2x; result.zs = x2z; result.xs.invert_bits(); result.xs.clear_bits_past(num_qubits); return result; } PauliString y_outputs_and_signs(simd_bits_range_ref y_signs_out) const { PauliString result(num_qubits); result.xs = inv_x2x; result.zs = x2z; y_signs_out = x_signs; y_signs_out ^= z_signs; simd_bits_range_ref(result.xs).for_each_word( result.zs, z2x, inv_z2z, y_signs_out, [](simd_word &x_out, simd_word &z_out, const simd_word &x2, const simd_word &inv_z2, simd_word &y_sign) { y_sign ^= ~x_out & ~inv_z2 & (z_out ^ x2); y_sign ^= x_out & inv_z2 & z_out & x2; x_out ^= ~x2; z_out ^= ~inv_z2; }); result.xs.clear_bits_past(num_qubits); result.zs.clear_bits_past(num_qubits); return result; } PauliString z_outputs() const { PauliString result(num_qubits); result.xs = z2x; result.zs = inv_z2z; result.zs.invert_bits(); result.zs.clear_bits_past(num_qubits); return result; } /// Out-of-place multiplication of rotations. CliffordString operator*(const CliffordString &rhs) const { CliffordString result = CliffordString(std::max(num_qubits, rhs.num_qubits)); size_t min_words = std::min(x_signs.num_simd_words, rhs.x_signs.num_simd_words); for (size_t k = 0; k < min_words; k++) { auto lhs_w = word_at(k); auto rhs_w = rhs.word_at(k); result.set_word_at(k, lhs_w * rhs_w); } // The longer string copies its tail into the result. size_t min_qubits = std::min(num_qubits, rhs.num_qubits); for (size_t q = min_qubits; q < num_qubits; q++) { result.x_signs[q] = x_signs[q]; result.z_signs[q] = z_signs[q]; result.inv_x2x[q] = inv_x2x[q]; result.x2z[q] = x2z[q]; result.z2x[q] = z2x[q]; result.inv_z2z[q] = inv_z2z[q]; } for (size_t q = min_qubits; q < rhs.num_qubits; q++) { result.x_signs[q] = rhs.x_signs[q]; result.z_signs[q] = rhs.z_signs[q]; result.inv_x2x[q] = rhs.inv_x2x[q]; result.x2z[q] = rhs.x2z[q]; result.z2x[q] = rhs.z2x[q]; result.inv_z2z[q] = rhs.inv_z2z[q]; } return result; } /// Determines if two Clifford strings have the same length and contents. bool operator==(const CliffordString &other) const { return x_signs == other.x_signs && z_signs == other.z_signs && inv_x2x == other.inv_x2x && x2z == other.x2z && z2x == other.z2x && inv_z2z == other.inv_z2z; } /// Determines if two Clifford strings have different lengths or contents. bool operator!=(const CliffordString &other) const { return !(*this == other); } /// Returns a description of the Clifford string. std::string str() const { std::stringstream ss; ss << *this; return ss.str(); } std::string py_str() const { std::stringstream ss; for (size_t q = 0; q < num_qubits; q++) { if (q) { ss << ','; } ss << GATE_DATA[gate_at(q)].name; } return ss.str(); } CliffordString py_get_slice(int64_t start, int64_t step, int64_t slice_length) const { assert(slice_length >= 0); assert(slice_length == 0 || start >= 0); CliffordString result(slice_length); for (size_t k = 0; k < (size_t)slice_length; k++) { size_t old_k = start + step * k; result.x_signs[k] = x_signs[old_k]; result.z_signs[k] = z_signs[old_k]; result.inv_x2x[k] = inv_x2x[old_k]; result.x2z[k] = x2z[old_k]; result.z2x[k] = z2x[old_k]; result.inv_z2z[k] = inv_z2z[old_k]; } return result; } std::string py_repr() const { std::stringstream ss; ss << "stim.CliffordString(\""; for (size_t q = 0; q < num_qubits; q++) { if (q) { ss << ','; } ss << GATE_DATA[gate_at(q)].name; } ss << "\")"; return ss.str(); } }; template std::ostream &operator<<(std::ostream &out, const CliffordString &v) { for (size_t q = 0; q < v.num_qubits; q++) { if (q > 0) { out << " "; } int c = v.inv_x2x[q] + v.x2z[q] * 2 + v.z2x[q] * 4 + v.inv_z2z[q] * 8; int p = v.z_signs[q] + v.x_signs[q] * 2; out << "_?S?V??d??????uH"[c]; out << "IXZY"[p]; } return out; } } // namespace stim #endif ================================================ FILE: src/stim/stabilizers/clifford_string.perf.cc ================================================ #include "stim/stabilizers/clifford_string.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(CliffordString_multiplication_10K) { size_t n = 10 * 1000; CliffordString p1(n); CliffordString p2(n); benchmark_go([&]() { p1 *= p2; }) .goal_nanos(430) .show_rate("Rots", n); } ================================================ FILE: src/stim/stabilizers/clifford_string.pybind.cc ================================================ // 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. #include "stim/stabilizers/clifford_string.pybind.h" #include "stim/gates/gates.pybind.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" #include "stim/stabilizers/flex_pauli_string.h" #include "stim/stabilizers/pauli_string.h" using namespace stim; using namespace stim_pybind; pybind11::class_> stim_pybind::pybind_clifford_string(pybind11::module &m) { return pybind11::class_>( m, "CliffordString", clean_doc_string(R"DOC( A tensor product of single qubit Clifford gates (e.g. "H \u2297 X \u2297 S"). Represents a collection of Clifford operations applied pairwise to a collection of qubits. Ignores global phase. Examples: >>> import stim >>> stim.CliffordString("H,S,C_XYZ") * stim.CliffordString("H,H,H") stim.CliffordString("I,C_ZYX,SQRT_X_DAG") )DOC") .data()); } static std::string_view trim(std::string_view text) { size_t s = 0; size_t e = text.size(); while (s < e && std::isspace(text[s])) { s++; } while (s < e && std::isspace(text[e - 1])) { e--; } return text.substr(s, e - s); } void stim_pybind::pybind_clifford_string_methods( pybind11::module &m, pybind11::class_> &c) { c.def( pybind11::init([](const pybind11::object &arg) -> CliffordString { if (pybind11::isinstance(arg)) { return CliffordString(pybind11::cast(arg)); } if (pybind11::isinstance(arg)) { std::string_view text = pybind11::cast(arg); text = trim(text); if (text.empty()) { return CliffordString(0); } if (text.ends_with(',')) { text = text.substr(0, text.size() - 1); } size_t n = 1; for (char e : text) { n += e == ','; } CliffordString result(n); size_t start = 0; size_t out_index = 0; for (size_t end = 0; end <= text.size(); end++) { if (end == text.size() || text[end] == ',') { std::string_view segment = text.substr(start, end - start); segment = trim(segment); auto [z_sign, x_sign, inv_x2x, x2z, z2x, inv_z2z] = gate_to_bits(GATE_DATA.at(segment).id); result.z_signs[out_index] = z_sign; result.x_signs[out_index] = x_sign; result.inv_x2x[out_index] = inv_x2x; result.x2z[out_index] = x2z; result.z2x[out_index] = z2x; result.inv_z2z[out_index] = inv_z2z; start = end + 1; out_index += 1; } } return result; } if (pybind11::isinstance>(arg)) { auto copy = pybind11::cast &>(arg); return copy; } if (pybind11::isinstance(arg)) { const FlexPauliString &other = pybind11::cast(arg); CliffordString result(other.value.num_qubits); result.z_signs = other.value.xs; result.x_signs = other.value.zs; return result; } if (pybind11::isinstance(arg)) { const Circuit &circuit = pybind11::cast(arg); return CliffordString::from_circuit(circuit); } pybind11::module collections = pybind11::module::import("collections.abc"); pybind11::object iterable_type = collections.attr("Iterable"); if (pybind11::isinstance(arg, iterable_type)) { std::vector gates; for (const auto &t : arg) { if (pybind11::isinstance(t)) { gates.push_back(pybind11::cast(t).type); continue; } else if (pybind11::isinstance(t)) { gates.push_back(GATE_DATA.at(pybind11::cast(t)).id); continue; } throw std::invalid_argument( "Don't know how to convert the following item into a Clifford: " + pybind11::cast(pybind11::repr(t))); } CliffordString result(gates.size()); for (size_t k = 0; k < gates.size(); k++) { result.set_gate_at(k, gates[k]); } return result; } throw std::invalid_argument( "Don't know how to initialize a stim.CliffordString from " + pybind11::cast(pybind11::repr(arg))); }), pybind11::arg("arg"), pybind11::pos_only(), clean_doc_string(R"DOC( @signature def __init__(self, arg: Union[int, str, stim.CliffordString, stim.PauliString, stim.Circuit], /) -> None: Initializes a stim.CliffordString from the given argument. Args: arg [position-only]: This can be a variety of types, including: int: initializes an identity Clifford string of the given length. str: initializes by parsing a comma-separated list of gate names. stim.CliffordString: initializes by copying the given Clifford string. stim.PauliString: initializes by copying from the given Pauli string (ignores the sign of the Pauli string). stim.Circuit: initializes a CliffordString equivalent to the action of the circuit (as long as the circuit only contains single qubit unitary operations and annotations). Iterable: initializes by interpreting each item as a Clifford. Each item can be a single-qubit Clifford gate name (like "SQRT_X") or stim.GateData instance. Examples: >>> import stim >>> stim.CliffordString(5) stim.CliffordString("I,I,I,I,I") >>> stim.CliffordString("X,Y,Z,SQRT_X") stim.CliffordString("X,Y,Z,SQRT_X") >>> stim.CliffordString(["H", stim.gate_data("S")]) stim.CliffordString("H,S") >>> stim.CliffordString(stim.PauliString("XYZ")) stim.CliffordString("X,Y,Z") >>> stim.CliffordString(stim.CliffordString("X,Y,Z")) stim.CliffordString("X,Y,Z") >>> stim.CliffordString(stim.Circuit(''' ... H 0 1 2 ... S 2 3 ... TICK ... S 3 ... I 6 ... ''')) stim.CliffordString("H,H,C_ZYX,Z,I,I,I") )DOC") .data()); c.def( "__str__", &CliffordString::py_str, "Returns a string representation of the CliffordString's operations."); c.def( "__repr__", &CliffordString::py_repr, "Returns text that is a valid python expression evaluating to an equivalent `stim.CliffordString`."); c.def( "__len__", [](const CliffordString &self) { return self.num_qubits; }, clean_doc_string(R"DOC( Returns the number of Clifford operations in the string. Examples: >>> import stim >>> len(stim.CliffordString("I,X,Y,Z,H")) 5 )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two Clifford strings have identical contents."); c.def(pybind11::self != pybind11::self, "Determines if two Clifford strings have non-identical contents."); c.def( "__getitem__", [](const CliffordString &self, const pybind11::object &index_or_slice) { pybind11::ssize_t index, step, slice_length; if (normalize_index_or_slice(index_or_slice, self.num_qubits, &index, &step, &slice_length)) { return pybind11::cast(self.py_get_slice(index, step, slice_length)); } return pybind11::cast(GateTypeWrapper{self.gate_at(index)}); }, pybind11::arg("index_or_slice"), clean_doc_string(R"DOC( @overload def __getitem__(self, index_or_slice: int) -> stim.GateData: @overload def __getitem__(self, index_or_slice: slice) -> stim.CliffordString: @signature def __getitem__(self, index_or_slice: Union[int, slice]) -> Union[stim.GateData, stim.CliffordString]: Returns a Clifford or substring from the CliffordString. Args: index_or_slice: The index of the Clifford to return, or the slice corresponding to the sub CliffordString to return. Returns: The indexed Clifford (as a stim.GateData instance) or the sliced CliffordString. Examples: >>> import stim >>> s = stim.CliffordString("I,X,Y,Z,H") >>> s[2] stim.gate_data('Y') >>> s[-1] stim.gate_data('H') >>> s[:-1] stim.CliffordString("I,X,Y,Z") >>> s[::2] stim.CliffordString("I,Y,H") )DOC") .data()); c.def( "__setitem__", [](CliffordString &self, const pybind11::object &index_or_slice, const pybind11::object &new_value) { pybind11::ssize_t index, step, slice_length; bool is_slice = normalize_index_or_slice(index_or_slice, self.num_qubits, &index, &step, &slice_length); if (pybind11::isinstance(new_value)) { GateType g = pybind11::cast(new_value).type; for (size_t k = 0; k < (size_t)slice_length; k++) { size_t target_k = index + step * k; self.set_gate_at(target_k, g); } return; } else if (pybind11::isinstance(new_value)) { GateType g = GATE_DATA.at(pybind11::cast(new_value)).id; for (size_t k = 0; k < (size_t)slice_length; k++) { size_t target_k = index + step * k; self.set_gate_at(target_k, g); } return; } else if (pybind11::isinstance>(new_value)) { const Tableau &t = pybind11::cast &>(new_value); if (t.num_qubits == 1) { GateType g = single_qubit_tableau_to_gate_type(t); for (size_t k = 0; k < (size_t)slice_length; k++) { size_t target_k = index + step * k; self.set_gate_at(target_k, g); } return; } } else if (is_slice && pybind11::isinstance>(new_value)) { const CliffordString &v = pybind11::cast &>(new_value); if (v.num_qubits != (size_t)slice_length) { std::stringstream ss; ss << "Length mismatch. The targeted slice covers " << slice_length; ss << " values but the given CliffordString has " << v.num_qubits << " values."; throw std::invalid_argument(ss.str()); } for (size_t k = 0; k < (size_t)slice_length; k++) { size_t target_k = index + step * k; self.set_gate_at(target_k, v.gate_at(k)); } return; } else if (is_slice && pybind11::isinstance(new_value)) { const FlexPauliString &v = pybind11::cast(new_value); if (v.value.num_qubits != (size_t)slice_length) { std::stringstream ss; ss << "Length mismatch. The targeted slice covers " << slice_length; ss << " values but the given PauliString has " << v.value.num_qubits << " values."; throw std::invalid_argument(ss.str()); } for (size_t k = 0; k < (size_t)slice_length; k++) { size_t target_k = index + step * k; self.inv_x2x[target_k] = 0; self.x2z[target_k] = 0; self.z2x[target_k] = 0; self.inv_z2z[target_k] = 0; self.x_signs[target_k] = v.value.zs[k]; self.z_signs[target_k] = v.value.xs[k]; } return; } std::stringstream ss; ss << "Don't know how to write an object of type "; ss << pybind11::repr(pybind11::type::of(new_value)); ss << " to index "; ss << pybind11::repr(index_or_slice); throw std::invalid_argument(ss.str()); }, pybind11::arg("index_or_slice"), pybind11::arg("new_value"), clean_doc_string(R"DOC( @signature def __setitem__(self, index_or_slice: Union[int, slice], new_value: Union[str, stim.GateData, stim.CliffordString, stim.PauliString, stim.Tableau]) -> None: Overwrites an indexed Clifford, or slice of Cliffords, with the given value. Args: index_or_slice: The index of the Clifford to overwrite, or the slice of Cliffords to overwrite. new_value: Specifies the value to write into the Clifford string. This can be set to a few different types of values: - str: Name of the single qubit Clifford gate to write to the index or broadcast over the slice. - stim.GateData: The single qubit Clifford gate to write to the index or broadcast over the slice. - stim.Tableau: Must be a single qubit tableau. Specifies the single qubit Clifford gate to write to the index or broadcast over the slice. - stim.CliffordString: String of Cliffords to write into the slice. Examples: >>> import stim >>> s = stim.CliffordString("I,I,I,I,I") >>> s[1] = 'H' >>> s stim.CliffordString("I,H,I,I,I") >>> s[2:] = 'SQRT_X' >>> s stim.CliffordString("I,H,SQRT_X,SQRT_X,SQRT_X") >>> s[0] = stim.gate_data('S_DAG').inverse >>> s stim.CliffordString("S,H,SQRT_X,SQRT_X,SQRT_X") >>> s[:] = 'I' >>> s stim.CliffordString("I,I,I,I,I") >>> s[::2] = stim.CliffordString("X,Y,Z") >>> s stim.CliffordString("X,I,Y,I,Z") >>> s[0] = stim.Tableau.from_named_gate("H") >>> s stim.CliffordString("H,I,Y,I,Z") >>> s[:] = stim.Tableau.from_named_gate("S") >>> s stim.CliffordString("S,S,S,S,S") >>> s[:4] = stim.PauliString("IXYZ") >>> s stim.CliffordString("I,X,Y,Z,S") )DOC") .data()); c.def_static( "random", [](size_t num_qubits) { auto rng = make_py_seeded_rng(pybind11::none()); return CliffordString::random(num_qubits, rng); }, pybind11::arg("num_qubits"), clean_doc_string(R"DOC( Samples a uniformly random CliffordString. Args: num_qubits: The number of qubits the CliffordString should act upon. Examples: >>> import stim >>> p = stim.CliffordString.random(5) >>> len(p) 5 Returns: The sampled Clifford string. )DOC") .data()); c.def_static( "all_cliffords_string", []() -> CliffordString { CliffordString result(24); result.set_gate_at(0, GateType::I); result.set_gate_at(4, GateType::H_XY); result.set_gate_at(8, GateType::H); result.set_gate_at(12, GateType::H_YZ); result.set_gate_at(16, GateType::C_XYZ); result.set_gate_at(20, GateType::C_ZYX); for (size_t q = 0; q < 24; q++) { if (q % 4) { result.set_gate_at(q, result.gate_at(q - 1)); } } CliffordString ixyz(24); for (size_t q = 0; q < 24; q += 4) { ixyz.set_gate_at(q + 0, GateType::I); ixyz.set_gate_at(q + 1, GateType::X); ixyz.set_gate_at(q + 2, GateType::Y); ixyz.set_gate_at(q + 3, GateType::Z); } result *= ixyz; return result; }, clean_doc_string(R"DOC( Returns a stim.CliffordString containing each single qubit Clifford once. Useful for things like testing that a method works on every single Clifford. Examples: >>> import stim >>> cliffords = stim.CliffordString.all_cliffords_string() >>> len(cliffords) 24 >>> print(cliffords[:8]) I,X,Y,Z,H_XY,S,S_DAG,H_NXY >>> print(cliffords[8:16]) H,SQRT_Y_DAG,H_NXZ,SQRT_Y,H_YZ,H_NYZ,SQRT_X,SQRT_X_DAG >>> print(cliffords[16:]) C_XYZ,C_XYNZ,C_NXYZ,C_XNYZ,C_ZYX,C_ZNYX,C_NZYX,C_ZYNX )DOC") .data()); c.def( "__ipow__", [](pybind11::object self, int64_t power) { pybind11::cast &>(self).ipow(power); return self; }, pybind11::arg("num_qubits"), clean_doc_string(R"DOC( Mutates the CliffordString into itself raised to a power. Args: power: The power to raise the CliffordString's Cliffords to. This value can be negative (e.g. -1 inverts the string). Returns: The mutated Clifford string. Examples: >>> import stim >>> p = stim.CliffordString("I,X,H,S,C_XYZ") >>> p **= 3 >>> p stim.CliffordString("I,X,H,S_DAG,I") >>> p **= 2 >>> p stim.CliffordString("I,I,I,Z,I") >>> alias = p >>> alias **= 2 >>> p stim.CliffordString("I,I,I,I,I") )DOC") .data()); c.def( "__pow__", [](const CliffordString &self, int64_t power) -> CliffordString { auto copy = self; copy.ipow(power); return copy; }, pybind11::arg("power"), clean_doc_string(R"DOC( Returns the CliffordString raised to a power. Args: power: The power to raise the CliffordString's Cliffords to. This value can be negative (e.g. -1 returns the inverse string). Returns: The Clifford string raised to the power. Examples: >>> import stim >>> p = stim.CliffordString("I,X,H,S,C_XYZ") >>> p**0 stim.CliffordString("I,I,I,I,I") >>> p**1 stim.CliffordString("I,X,H,S,C_XYZ") >>> p**12000001 stim.CliffordString("I,X,H,S,C_XYZ") >>> p**2 stim.CliffordString("I,I,I,Z,C_ZYX") >>> p**3 stim.CliffordString("I,X,H,S_DAG,I") >>> p**-1 stim.CliffordString("I,X,H,S_DAG,C_ZYX") )DOC") .data()); c.def( "__add__", &CliffordString::operator+, pybind11::arg("rhs"), clean_doc_string(R"DOC( Concatenates two CliffordStrings. Args: rhs: The suffix of the concatenation. Returns: The concatenated Clifford string. Examples: >>> import stim >>> stim.CliffordString("I,X,H") + stim.CliffordString("Y,S") stim.CliffordString("I,X,H,Y,S") )DOC") .data()); c.def( "copy", [](const CliffordString &self) -> CliffordString { return self; }, clean_doc_string(R"DOC( Returns a copy of the CliffordString. Returns: The copy. Examples: >>> import stim >>> c = stim.CliffordString("H,X") >>> alias = c >>> copy = c.copy() >>> c *= 5 >>> alias stim.CliffordString("H,X,H,X,H,X,H,X,H,X") >>> copy stim.CliffordString("H,X") )DOC") .data()); c.def( "__iadd__", &CliffordString::operator+=, pybind11::arg("rhs"), clean_doc_string(R"DOC( Mutates the CliffordString by concatenating onto it. Args: rhs: The suffix to concatenate onto the target CliffordString. Returns: The mutated Clifford string. Examples: >>> import stim >>> c = stim.CliffordString("I,X,H") >>> alias = c >>> alias += stim.CliffordString("Y,S") >>> c stim.CliffordString("I,X,H,Y,S") )DOC") .data()); c.def( "__rmul__", [](const CliffordString &self, size_t rhs) -> CliffordString { return self * rhs; }, pybind11::arg("lhs"), clean_doc_string(R"DOC( CliffordString left-multiplication. Args: lhs: The number of times to repeat the Clifford string's contents. Returns: The repeated Clifford string. Examples: >>> import stim >>> 2 * stim.CliffordString("I,X,H") stim.CliffordString("I,X,H,I,X,H") >>> 0 * stim.CliffordString("I,X,H") stim.CliffordString("") >>> 5 * stim.CliffordString("I") stim.CliffordString("I,I,I,I,I") )DOC") .data()); c.def( "__mul__", [](const CliffordString &self, pybind11::object rhs) -> CliffordString { if (pybind11::isinstance(rhs)) { return self * pybind11::cast(rhs); } else if (pybind11::isinstance>(rhs)) { return self * pybind11::cast &>(rhs); } else { std::stringstream ss; ss << "Don't know how to multiply by "; ss << pybind11::repr(rhs); throw std::invalid_argument(ss.str()); } }, clean_doc_string(R"DOC( @signature def __mul__(self, rhs: Union[stim.CliffordString, int]) -> stim.CliffordString: CliffordString multiplication. Args: rhs: Either a stim.CliffordString or an int. If rhs is a stim.CliffordString, then the Cliffords from each string are multiplied pairwise. If rhs is an int, it is the number of times to repeat the Clifford string's contents. Examples: >>> import stim >>> stim.CliffordString("S,X,X") * stim.CliffordString("S,Z,H,Z") stim.CliffordString("Z,Y,SQRT_Y,Z") >>> stim.CliffordString("I,X,H") * 3 stim.CliffordString("I,X,H,I,X,H,I,X,H") )DOC") .data()); c.def( "__imul__", [](pybind11::object self_obj, pybind11::object rhs) -> pybind11::object { CliffordString &self = pybind11::cast &>(self_obj); if (pybind11::isinstance(rhs)) { self *= pybind11::cast(rhs); return self_obj; } else if (pybind11::isinstance>(rhs)) { self *= pybind11::cast &>(rhs); return self_obj; } else { std::stringstream ss; ss << "Don't know how to multiply by "; ss << pybind11::repr(rhs); throw std::invalid_argument(ss.str()); } }, pybind11::arg("rhs"), clean_doc_string(R"DOC( @signature def __imul__(self, rhs: Union[stim.CliffordString, int]) -> stim.CliffordString: Inplace CliffordString multiplication. Mutates the CliffordString into itself multiplied by another CliffordString (via pairwise Clifford multipliation) or by an integer (via repeating the contents). Args: rhs: Either a stim.CliffordString or an int. If rhs is a stim.CliffordString, then the Cliffords from each string are multiplied pairwise. If rhs is an int, it is the number of times to repeat the Clifford string's contents. Returns: The mutated Clifford string. Examples: >>> import stim >>> c = stim.CliffordString("S,X,X") >>> alias = c >>> alias *= stim.CliffordString("S,Z,H,Z") >>> c stim.CliffordString("Z,Y,SQRT_Y,Z") >>> c = stim.CliffordString("I,X,H") >>> alias = c >>> alias *= 2 >>> c stim.CliffordString("I,X,H,I,X,H") )DOC") .data()); c.def( "x_outputs", [](const CliffordString &self, bool bit_packed_signs) -> pybind11::object { auto ps = self.x_outputs(); FlexPauliString result(std::move(ps)); return pybind11::make_tuple( pybind11::cast(result), simd_bits_to_numpy(self.x_signs, self.num_qubits, bit_packed_signs)); }, pybind11::kw_only(), pybind11::arg("bit_packed_signs") = false, clean_doc_string(R"DOC( @signature def x_outputs(self, *, bit_packed_signs: bool = False) -> tuple[stim.PauliString, np.ndarray]: Returns what each Clifford in the CliffordString conjugates an X input into. For example, H conjugates X into +Z and S_DAG conjugates X into -Y. Combined with `z_outputs`, the results of this method completely specify the single qubit Clifford applied to each qubit. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> x_paulis, x_signs = stim.CliffordString("I,Y,H,S").x_outputs() >>> x_paulis stim.PauliString("+XXZY") >>> x_signs array([False, True, False, False]) >>> stim.CliffordString("I,Y,H,S").x_outputs(bit_packed_signs=True)[1] array([2], dtype=uint8) )DOC") .data()); c.def( "y_outputs", [](CliffordString &self, bool bit_packed_signs) -> pybind11::object { simd_bits signs(self.num_qubits); auto ys = self.y_outputs_and_signs(signs); FlexPauliString result(std::move(ys)); return pybind11::make_tuple( pybind11::cast(result), simd_bits_to_numpy(signs, self.num_qubits, bit_packed_signs)); }, pybind11::kw_only(), pybind11::arg("bit_packed_signs") = false, clean_doc_string(R"DOC( @signature def y_outputs(self, *, bit_packed_signs: bool = False) -> tuple[stim.PauliString, np.ndarray]: Returns what each Clifford in the CliffordString conjugates a Y input into. For example, H conjugates Y into -Y and S_DAG conjugates Y into +X. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> y_paulis, y_signs = stim.CliffordString("I,X,H,S").y_outputs() >>> y_paulis stim.PauliString("+YYYX") >>> y_signs array([False, True, True, True]) >>> stim.CliffordString("I,X,H,S").y_outputs(bit_packed_signs=True)[1] array([14], dtype=uint8) )DOC") .data()); c.def( "z_outputs", [](const CliffordString &self, bool bit_packed_signs) -> pybind11::object { auto ps = self.z_outputs(); FlexPauliString result(std::move(ps)); return pybind11::make_tuple( pybind11::cast(result), simd_bits_to_numpy(self.z_signs, self.num_qubits, bit_packed_signs)); }, pybind11::kw_only(), pybind11::arg("bit_packed_signs") = false, clean_doc_string(R"DOC( @signature def z_outputs(self, *, bit_packed_signs: bool = False) -> tuple[stim.PauliString, np.ndarray]: Returns what each Clifford in the CliffordString conjugates a Z input into. For example, H conjugates Z into +X and SQRT_X conjugates Z into -Y. Combined with `x_outputs`, the results of this method completely specify the single qubit Clifford applied to each qubit. Args: bit_packed_signs: Defaults to False. When False, the sign data is returned in a numpy array with dtype `np.bool_`. When True, the dtype is instead `np.uint8` and 8 bits are packed into each byte (in little endian order). Returns: A (paulis, signs) tuple. `paulis` has type stim.PauliString. Its sign is always positive. `signs` has type np.ndarray and an argument-dependent shape: bit_packed_signs=False: dtype=np.bool_ shape=(num_qubits,) bit_packed_signs=True: dtype=np.uint8 shape=(math.ceil(num_qubits / 8),) Examples: >>> import stim >>> z_paulis, z_signs = stim.CliffordString("I,Y,H,S").z_outputs() >>> z_paulis stim.PauliString("+ZZXZ") >>> z_signs array([False, True, False, False]) >>> stim.CliffordString("I,Y,H,S").z_outputs(bit_packed_signs=True)[1] array([2], dtype=uint8) )DOC") .data()); } ================================================ FILE: src/stim/stabilizers/clifford_string.pybind.h ================================================ // 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. #ifndef _STIM_STABILIZERS_CLIFFORD_STRING_PYBIND_H #define _STIM_STABILIZERS_CLIFFORD_STRING_PYBIND_H #include #include "stim/stabilizers/clifford_string.h" namespace stim_pybind { pybind11::class_> pybind_clifford_string(pybind11::module &m); void pybind_clifford_string_methods( pybind11::module &m, pybind11::class_> &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/stabilizers/clifford_string.test.cc ================================================ #include "stim/stabilizers/clifford_string.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/stabilizers/tableau.h" #include "stim/util_bot/test_util.test.h" using namespace stim; std::vector single_qubit_clifford_rotations() { std::vector result; for (size_t g = 0; g < NUM_DEFINED_GATES; g++) { const Gate &gate = GATE_DATA[(GateType)g]; if ((gate.flags & GateFlags::GATE_IS_SINGLE_QUBIT_GATE) && (gate.flags & GateFlags::GATE_IS_UNITARY)) { result.push_back(gate); } } assert(result.size() == 24); return result; } TEST_EACH_WORD_SIZE_W(clifford_string, set_gate_at_vs_str_vs_gate_at, { CliffordString p = CliffordString(24); int x = 0; p.set_gate_at(x++, GateType::I); p.set_gate_at(x++, GateType::X); p.set_gate_at(x++, GateType::Y); p.set_gate_at(x++, GateType::Z); p.set_gate_at(x++, GateType::H); p.set_gate_at(x++, GateType::SQRT_Y_DAG); p.set_gate_at(x++, GateType::H_NXZ); p.set_gate_at(x++, GateType::SQRT_Y); p.set_gate_at(x++, GateType::S); p.set_gate_at(x++, GateType::H_XY); p.set_gate_at(x++, GateType::H_NXY); p.set_gate_at(x++, GateType::S_DAG); p.set_gate_at(x++, GateType::SQRT_X_DAG); p.set_gate_at(x++, GateType::SQRT_X); p.set_gate_at(x++, GateType::H_NYZ); p.set_gate_at(x++, GateType::H_YZ); p.set_gate_at(x++, GateType::C_XYZ); p.set_gate_at(x++, GateType::C_XYNZ); p.set_gate_at(x++, GateType::C_NXYZ); p.set_gate_at(x++, GateType::C_XNYZ); p.set_gate_at(x++, GateType::C_ZYX); p.set_gate_at(x++, GateType::C_ZNYX); p.set_gate_at(x++, GateType::C_NZYX); p.set_gate_at(x++, GateType::C_ZYNX); ASSERT_EQ(p.str(), "_I _X _Y _Z HI HX HY HZ SI SX SY SZ VI VX VY VZ uI uX uY uZ dI dX dY dZ"); x = 0; EXPECT_EQ(p.gate_at(x++), GateType::I); EXPECT_EQ(p.gate_at(x++), GateType::X); EXPECT_EQ(p.gate_at(x++), GateType::Y); EXPECT_EQ(p.gate_at(x++), GateType::Z); EXPECT_EQ(p.gate_at(x++), GateType::H); EXPECT_EQ(p.gate_at(x++), GateType::SQRT_Y_DAG); EXPECT_EQ(p.gate_at(x++), GateType::H_NXZ); EXPECT_EQ(p.gate_at(x++), GateType::SQRT_Y); EXPECT_EQ(p.gate_at(x++), GateType::S); EXPECT_EQ(p.gate_at(x++), GateType::H_XY); EXPECT_EQ(p.gate_at(x++), GateType::H_NXY); EXPECT_EQ(p.gate_at(x++), GateType::S_DAG); EXPECT_EQ(p.gate_at(x++), GateType::SQRT_X_DAG); EXPECT_EQ(p.gate_at(x++), GateType::SQRT_X); EXPECT_EQ(p.gate_at(x++), GateType::H_NYZ); EXPECT_EQ(p.gate_at(x++), GateType::H_YZ); EXPECT_EQ(p.gate_at(x++), GateType::C_XYZ); EXPECT_EQ(p.gate_at(x++), GateType::C_XYNZ); EXPECT_EQ(p.gate_at(x++), GateType::C_NXYZ); EXPECT_EQ(p.gate_at(x++), GateType::C_XNYZ); EXPECT_EQ(p.gate_at(x++), GateType::C_ZYX); EXPECT_EQ(p.gate_at(x++), GateType::C_ZNYX); EXPECT_EQ(p.gate_at(x++), GateType::C_NZYX); EXPECT_EQ(p.gate_at(x++), GateType::C_ZYNX); }); TEST_EACH_WORD_SIZE_W(clifford_string, multiplication_table_vs_tableau_multiplication, { std::vector single_qubit_gates = single_qubit_clifford_rotations(); std::map t2g; for (const auto &g : single_qubit_gates) { t2g[g.tableau().str()] = g.id; } CliffordString p1 = CliffordString(24 * 24); CliffordString p2 = CliffordString(24 * 24); CliffordString p12 = CliffordString(24 * 24); for (size_t k1 = 0; k1 < 24; k1++) { for (size_t k2 = 0; k2 < 24; k2++) { size_t k = k1 * 24 + k2; Gate g1 = single_qubit_gates[k1]; Gate g2 = single_qubit_gates[k2]; p1.set_gate_at(k, g1.id); p2.set_gate_at(k, g2.id); auto t1 = g1.tableau(); auto t2 = g2.tableau(); auto t3 = t2.then(t1); auto g3 = t2g[t3.str()]; p12.set_gate_at(k, g3); } } ASSERT_EQ(p1 * p2, p12); }) TEST_EACH_WORD_SIZE_W(clifford_string, to_from_circuit, { Circuit circuit(R"CIRCUIT( H 0 H_XY 1 H_YZ 2 H_NXY 3 H_NXZ 4 H_NYZ 5 S_DAG 6 X 7 Y 8 Z 9 C_XYZ 10 C_ZYX 11 C_NXYZ 12 C_XNYZ 13 C_XYNZ 14 C_NZYX 15 C_ZNYX 16 C_ZYNX 17 SQRT_X 18 SQRT_X_DAG 19 SQRT_Y 20 SQRT_Y_DAG 21 S 22 I 23 )CIRCUIT"); CliffordString s = CliffordString::from_circuit(circuit); ASSERT_EQ(s.to_circuit(), circuit); }) TEST_EACH_WORD_SIZE_W(clifford_string, known_identities, { auto s1 = CliffordString::from_circuit(Circuit(R"CIRCUIT( H 0 )CIRCUIT")); auto s2 = CliffordString::from_circuit(Circuit(R"CIRCUIT( H 0 )CIRCUIT")); auto s3 = CliffordString::from_circuit(Circuit(R"CIRCUIT( I 0 )CIRCUIT")); ASSERT_EQ(s2 * s1, s3); s1 = CliffordString::from_circuit(Circuit(R"CIRCUIT( S 0 )CIRCUIT")); s2 = CliffordString::from_circuit(Circuit(R"CIRCUIT( S 0 )CIRCUIT")); s3 = CliffordString::from_circuit(Circuit(R"CIRCUIT( Z 0 )CIRCUIT")); ASSERT_EQ(s2 * s1, s3); s1 = CliffordString::from_circuit(Circuit(R"CIRCUIT( S_DAG 0 )CIRCUIT")); s2 = CliffordString::from_circuit(Circuit(R"CIRCUIT( H 0 )CIRCUIT")); s3 = CliffordString::from_circuit(Circuit(R"CIRCUIT( C_XYZ 0 )CIRCUIT")); ASSERT_EQ(s2 * s1, s3); }) TEST_EACH_WORD_SIZE_W(clifford_string, random, { auto rng = INDEPENDENT_TEST_RNG(); CliffordString c = CliffordString::random(256, rng); std::array counts{}; for (size_t k = 0; k < 256; k++) { for (size_t q = 0; q < 256; q++) { GateType t = c.gate_at(q); counts[(uint8_t)t] += 1; } c.randomize(rng); } ASSERT_EQ(counts[(uint8_t)GateType::NOT_A_GATE], 0); size_t seen_gates = 0; for (const auto &g : GATE_DATA.items) { if ((g.flags & GATE_IS_UNITARY) && (g.flags & GATE_IS_SINGLE_QUBIT_GATE)) { ASSERT_LT(counts[(uint8_t)g.id], 256.0 * 256.0 / 24.0 * (1.0 + 0.5)); ASSERT_GT(counts[(uint8_t)g.id], 256.0 * 256.0 / 24.0 * (1.0 - 0.5)); seen_gates++; } } ASSERT_EQ(seen_gates, 24); }) ================================================ FILE: src/stim/stabilizers/clifford_string_pybind_test.py ================================================ # 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. import numpy as np import pytest import stim def test_trivial(): p = stim.CliffordString(3) assert repr(p) == 'stim.CliffordString("I,I,I")' assert len(p) == 3 assert p[1:] == stim.CliffordString(2) assert p[0] == stim.gate_data('I') def test_simple(): assert stim.CliffordString("X,Y,Z,H,SQRT_X,C_XYZ,H_NXZ") == stim.CliffordString(" X , Y , Z , H_XZ , SQRT_X,C_XYZ,H_NXZ, ") p = stim.CliffordString("X,Y,Z,H,SQRT_X,C_XYZ,H_NXZ") assert repr(p) == 'stim.CliffordString("X,Y,Z,H,SQRT_X,C_XYZ,H_NXZ")' assert str(p) == 'X,Y,Z,H,SQRT_X,C_XYZ,H_NXZ' assert len(p) == 7 assert p != stim.CliffordString("Y,Y,Z,H,SQRT_X,C_XYZ,H_NXZ") assert not (p != stim.CliffordString("X,Y,Z,H,SQRT_X,C_XYZ,H_NXZ")) assert not (p == stim.CliffordString("Y,Y,Z,H,SQRT_X,C_XYZ,H_NXZ")) assert p[1::2] == stim.CliffordString("Y,H,C_XYZ") assert stim.CliffordString(6) == stim.CliffordString("I,I,I,I,I,I") assert stim.CliffordString(stim.PauliString("XYZ_XYZ")) == stim.CliffordString("X,Y,Z,I,X,Y,Z") v = stim.CliffordString("X,Y,H") v2 = stim.CliffordString(v) assert v == v2 assert v is not v2 assert stim.CliffordString(['X', 'Y', 'Z', stim.gate_data('H'), 'S']) == stim.CliffordString('X,Y,Z,H,S') def test_multiplication(): a = stim.CliffordString("Z,H,S,C_XYZ") b = stim.CliffordString("S,Z,S,C_XYZ,I") assert a * b == stim.CliffordString("S_DAG,SQRT_Y,Z,C_ZYX,I") a *= b assert a == stim.CliffordString("S_DAG,SQRT_Y,Z,C_ZYX,I") assert stim.CliffordString("X") * stim.CliffordString("H") == stim.CliffordString("H") * stim.CliffordString("Z") assert stim.CliffordString("X") * stim.CliffordString("H") != stim.CliffordString("Z") * stim.CliffordString("H") assert stim.CliffordString("X") * stim.CliffordString("H") == stim.CliffordString("SQRT_Y") def test_random(): c1 = stim.CliffordString.random(128) c2 = stim.CliffordString.random(128) assert len(c1) == len(c2) == 128 assert c1 != c2 def test_set_item(): c = stim.CliffordString(5) c[1] = "H" assert c == stim.CliffordString("I,H,I,I,I") with pytest.raises(ValueError, match="index"): c[2:3] = None with pytest.raises(ValueError, match="index"): c[2] = None c[2:4] = stim.CliffordString("X,Y") assert c == stim.CliffordString("I,H,X,Y,I") c[::2] = stim.CliffordString("S,Z,S_DAG") assert c == stim.CliffordString("S,H,Z,Y,S_DAG") c[:] = 'H' assert c == stim.CliffordString("H,H,H,H,H") c[:-2] = stim.gate_data('S') assert c == stim.CliffordString("S,S,S,H,H") c[0] = stim.gate_data('X') assert c == stim.CliffordString("X,S,S,H,H") with pytest.raises(ValueError, match="object of type"): c[0] = stim.CliffordString("Y") with pytest.raises(ValueError, match="Length mismatch"): c[:2] = stim.CliffordString("Y") assert c == stim.CliffordString("X,S,S,H,H") c[:2] = stim.CliffordString("Y,Y") assert c == stim.CliffordString("Y,Y,S,H,H") def all_cliffords_string_from_gate_data(): c = stim.CliffordString(24) r = 0 for g in stim.gate_data().values(): if g.is_unitary and g.is_single_qubit_gate: c[r] = g r += 1 return c def test_x_outputs(): paulis, signs = stim.CliffordString("I,X,Y,Z,H,S,S_DAG,C_XYZ,C_ZYX,SQRT_X,SQRT_X_DAG").x_outputs() assert paulis == stim.PauliString("XXXXZYYYZXX") np.testing.assert_array_equal(signs, [0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0]) c = all_cliffords_string_from_gate_data() paulis, signs = c.x_outputs() for k in range(len(c)): expected = c[k].tableau.x_output(0) assert (-1 if signs[k] else 1) == expected.sign assert paulis[k] == expected[0] def test_y_outputs(): paulis, signs = stim.CliffordString("I,X,Y,Z,H,S,S_DAG,C_XYZ,C_ZYX,SQRT_X,SQRT_X_DAG").y_outputs() assert paulis == stim.PauliString("YYYYYXXZXZZ") np.testing.assert_array_equal(signs, [0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1]) c = all_cliffords_string_from_gate_data() paulis, signs = c.y_outputs() for k in range(len(c)): expected = c[k].tableau.y_output(0) assert (-1 if signs[k] else 1) == expected.sign assert paulis[k] == expected[0] def test_z_outputs(): paulis, signs = stim.CliffordString("I,X,Y,Z,H,S,S_DAG,C_XYZ,C_ZYX,SQRT_X,SQRT_X_DAG").z_outputs() assert paulis == stim.PauliString("ZZZZXZZXYYY") np.testing.assert_array_equal(signs, [0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0]) c = all_cliffords_string_from_gate_data() paulis, signs = c.z_outputs() for k in range(len(c)): expected = c[k].tableau.z_output(0) assert (-1 if signs[k] else 1) == expected.sign assert paulis[k] == expected[0] def test_all_cliffords_string(): c = stim.CliffordString.all_cliffords_string() assert len(c) == 24 assert set(e.name for e in all_cliffords_string_from_gate_data()) == set(e.name for e in c) ================================================ FILE: src/stim/stabilizers/flex_pauli_string.cc ================================================ // 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. #include "stim/stabilizers/flex_pauli_string.h" using namespace stim; FlexPauliString::FlexPauliString(size_t num_qubits) : value(num_qubits), imag(false) { } FlexPauliString::FlexPauliString(const PauliStringRef val, bool imag) : value(val), imag(imag) { } FlexPauliString::FlexPauliString(PauliString &&val, bool imag) : value(std::move(val)), imag(imag) { } FlexPauliString FlexPauliString::operator+(const FlexPauliString &rhs) const { FlexPauliString copy = *this; copy += rhs; return copy; } FlexPauliString &FlexPauliString::operator+=(const FlexPauliString &rhs) { if (&rhs == this) { *this *= 2; return *this; } size_t n = value.num_qubits; value.ensure_num_qubits(value.num_qubits + rhs.value.num_qubits, 1.1); for (size_t k = 0; k < rhs.value.num_qubits; k++) { value.xs[k + n] = rhs.value.xs[k]; value.zs[k + n] = rhs.value.zs[k]; } *this *= rhs.get_phase(); return *this; } FlexPauliString FlexPauliString::operator*(std::complex scale) const { FlexPauliString copy = *this; copy *= scale; return copy; } FlexPauliString FlexPauliString::operator/(const std::complex &scale) const { FlexPauliString copy = *this; copy /= scale; return copy; } FlexPauliString FlexPauliString::operator*(size_t power) const { FlexPauliString copy = *this; copy *= power; return copy; } FlexPauliString &FlexPauliString::operator*=(size_t power) { switch (power & 3) { case 0: imag = false; value.sign = false; break; case 1: break; case 2: value.sign = imag; imag = false; break; case 3: value.sign ^= imag; break; } value = PauliString::from_func(value.sign, value.num_qubits * power, [&](size_t k) { return "_XZY"[value.xs[k % value.num_qubits] + 2 * value.zs[k % value.num_qubits]]; }); return *this; } FlexPauliString &FlexPauliString::operator/=(const std::complex &rhs) { if (rhs == std::complex{+1, 0}) { return *this; } else if (rhs == std::complex{-1, 0}) { return *this *= std::complex{-1, 0}; } else if (rhs == std::complex{0, 1}) { return *this *= std::complex{0, -1}; } else if (rhs == std::complex{0, -1}) { return *this *= std::complex{0, +1}; } throw std::invalid_argument("divisor not in (1, -1, 1j, -1j)"); } FlexPauliString &FlexPauliString::operator*=(std::complex scale) { if (scale == std::complex(-1)) { value.sign ^= true; } else if (scale == std::complex(0, 1)) { value.sign ^= imag; imag ^= true; } else if (scale == std::complex(0, -1)) { imag ^= true; value.sign ^= imag; } else if (scale != std::complex(1)) { throw std::invalid_argument("phase factor not in [1, -1, 1, 1j]"); } return *this; } bool FlexPauliString::operator==(const FlexPauliString &other) const { return value == other.value && imag == other.imag; } bool FlexPauliString::operator!=(const FlexPauliString &other) const { return !(*this == other); } std::complex FlexPauliString::get_phase() const { std::complex result{value.sign ? -1.0f : +1.0f}; if (imag) { result *= std::complex{0, 1}; } return result; } FlexPauliString FlexPauliString::operator*(const FlexPauliString &rhs) const { FlexPauliString copy = *this; copy *= rhs; return copy; } FlexPauliString &FlexPauliString::operator*=(const FlexPauliString &rhs) { value.ensure_num_qubits(rhs.value.num_qubits, 1.1); if (rhs.value.num_qubits < value.num_qubits) { FlexPauliString copy = rhs; copy.value.ensure_num_qubits(value.num_qubits, 1.0); *this *= copy; return *this; } uint8_t log_i = value.ref().inplace_right_mul_returning_log_i_scalar(rhs.value.ref()); if (log_i & 2) { value.sign ^= true; } if (log_i & 1) { *this *= std::complex{0, 1}; } if (rhs.imag) { *this *= std::complex{0, 1}; } return *this; } std::ostream &stim::operator<<(std::ostream &out, const FlexPauliString &v) { out << "+-"[v.value.sign]; if (v.imag) { out << 'i'; } for (size_t k = 0; k < v.value.num_qubits; k++) { out << "_XZY"[v.value.xs[k] + 2 * v.value.zs[k]]; } return out; } std::string FlexPauliString::str() const { std::stringstream ss; ss << *this; return ss.str(); } static size_t parse_size_of_pauli_string_shorthand_if_sparse(std::string_view text) { uint64_t cur_index = 0; bool has_cur_index = false; size_t num_qubits = 0; auto flush = [&]() { if (has_cur_index) { num_qubits = std::max(num_qubits, (size_t)cur_index + 1); if (cur_index == UINT64_MAX || num_qubits <= cur_index) { throw std::invalid_argument(""); } cur_index = 0; has_cur_index = false; } }; for (char c : text) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': has_cur_index = true; cur_index = mul_saturate(cur_index, 10); cur_index = add_saturate(cur_index, c - '0'); break; default: flush(); break; } } flush(); return num_qubits; } static void parse_sparse_pauli_string(std::string_view text, FlexPauliString *out) { uint64_t cur_index = 0; bool has_cur_index = false; char cur_pauli = '\0'; auto flush = [&]() { if (cur_pauli == '\0' || !has_cur_index || cur_index > out->value.num_qubits) { throw std::invalid_argument(""); } if (cur_pauli != 'I') { out->value.right_mul_pauli( GateTarget::pauli_xz( cur_index, cur_pauli == 'X' || cur_pauli == 'Y', cur_pauli == 'Z' || cur_pauli == 'Y'), &out->imag); } has_cur_index = false; cur_pauli = '\0'; cur_index = 0; }; for (char c : text) { switch (c) { case '*': flush(); break; case 'I': case 'x': case 'X': case 'y': case 'Y': case 'z': case 'Z': if (cur_pauli != '\0') { throw std::invalid_argument(""); } cur_pauli = toupper(c); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if (cur_pauli == '\0') { throw std::invalid_argument(""); } has_cur_index = true; cur_index = mul_saturate(cur_index, 10); cur_index = add_saturate(cur_index, c - '0'); break; default: throw std::invalid_argument(""); } } flush(); } FlexPauliString FlexPauliString::from_text(std::string_view text) { bool negated = false; bool imaginary = false; if (text.starts_with("-")) { negated = true; text = text.substr(1); } else if (text.starts_with("+")) { text = text.substr(1); } if (text.starts_with("i")) { imaginary = true; text = text.substr(1); } size_t sparse_size = parse_size_of_pauli_string_shorthand_if_sparse(text); size_t num_qubits = sparse_size > 0 ? sparse_size : text.size(); FlexPauliString result(num_qubits); result.imag = imaginary; result.value.sign = negated; if (sparse_size > 0) { try { parse_sparse_pauli_string(text, &result); } catch (const std::invalid_argument &) { throw std::invalid_argument("Not a valid Pauli string shorthand: '" + std::string(text) + "'"); } } else { for (size_t k = 0; k < text.size(); k++) { switch (text[k]) { case 'I': case '_': break; case 'x': case 'X': result.value.xs[k] = true; break; case 'y': case 'Y': result.value.xs[k] = true; result.value.zs[k] = true; break; case 'z': case 'Z': result.value.zs[k] = true; break; default: throw std::invalid_argument("Not a valid Pauli string shorthand: '" + std::string(text) + "'"); } } } return result; } ================================================ FILE: src/stim/stabilizers/flex_pauli_string.h ================================================ // 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. #ifndef _STIM_STABILIZERS_FLEX_PAULI_STRING_H #define _STIM_STABILIZERS_FLEX_PAULI_STRING_H #include #include "stim/mem/sparse_xor_vec.h" #include "stim/stabilizers/pauli_string.h" namespace stim { /// This is the backing class for the python stim.PauliString. /// It's more flexible than the C++ stim::PauliString. /// For example, it allows imaginary signs. struct FlexPauliString { stim::PauliString value; bool imag; FlexPauliString(size_t num_qubits); FlexPauliString(const stim::PauliStringRef val, bool imag = false); FlexPauliString(stim::PauliString &&val, bool imag = false); static FlexPauliString from_text(std::string_view text); std::complex get_phase() const; FlexPauliString operator+(const FlexPauliString &rhs) const; FlexPauliString &operator+=(const FlexPauliString &rhs); FlexPauliString operator*(size_t power) const; FlexPauliString operator*(std::complex scale) const; FlexPauliString operator*(const FlexPauliString &rhs) const; FlexPauliString operator/(const std::complex &divisor) const; FlexPauliString &operator*=(size_t power); FlexPauliString &operator*=(std::complex scale); FlexPauliString &operator*=(const FlexPauliString &rhs); FlexPauliString &operator/=(const std::complex &divisor); bool operator==(const FlexPauliString &other) const; bool operator!=(const FlexPauliString &other) const; std::string str() const; }; std::ostream &operator<<(std::ostream &out, const FlexPauliString &v); } // namespace stim #endif ================================================ FILE: src/stim/stabilizers/flex_pauli_string.test.cc ================================================ #include "stim/stabilizers/flex_pauli_string.h" #include "gtest/gtest.h" using namespace stim; TEST(flex_pauli_string, mul) { FlexPauliString p1 = FlexPauliString::from_text("iXYZ"); FlexPauliString p2 = FlexPauliString::from_text("i__Z"); ASSERT_EQ(p1 * p2, FlexPauliString::from_text("-XY_")); } TEST(flex_pauli_string, from_text) { auto f = FlexPauliString::from_text("-iIXYZ_xyz"); ASSERT_EQ(f.value.num_qubits, 8); ASSERT_EQ(f.imag, true); ASSERT_EQ(f.value.sign, true); ASSERT_EQ(f.value.xs.as_u64(), 0b01100110); ASSERT_EQ(f.value.zs.as_u64(), 0b11001100); f = FlexPauliString::from_text("iX"); ASSERT_EQ(f.value.num_qubits, 1); ASSERT_EQ(f.imag, true); ASSERT_EQ(f.value.sign, false); ASSERT_EQ(f.value.xs.as_u64(), 0b1); ASSERT_EQ(f.value.zs.as_u64(), 0b0); f = FlexPauliString::from_text("Y"); ASSERT_EQ(f.value.num_qubits, 1); ASSERT_EQ(f.imag, false); ASSERT_EQ(f.value.sign, false); ASSERT_EQ(f.value.xs.as_u64(), 0b1); ASSERT_EQ(f.value.zs.as_u64(), 0b1); f = FlexPauliString::from_text("+Z"); ASSERT_EQ(f.value.num_qubits, 1); ASSERT_EQ(f.imag, false); ASSERT_EQ(f.value.sign, false); ASSERT_EQ(f.value.xs.as_u64(), 0b0); ASSERT_EQ(f.value.zs.as_u64(), 0b1); f = FlexPauliString::from_text("X8"); ASSERT_EQ(f.value.num_qubits, 9); ASSERT_EQ(f.imag, false); ASSERT_EQ(f.value.sign, false); ASSERT_EQ(f.value.xs.as_u64(), 0b100000000); ASSERT_EQ(f.value.zs.as_u64(), 0b000000000); f = FlexPauliString::from_text("X8*Y2"); ASSERT_EQ(f.value.num_qubits, 9); ASSERT_EQ(f.imag, false); ASSERT_EQ(f.value.sign, false); ASSERT_EQ(f.value.xs.as_u64(), 0b100000100); ASSERT_EQ(f.value.zs.as_u64(), 0b000000100); f = FlexPauliString::from_text("X8*Y2*X8"); ASSERT_EQ(f.value.num_qubits, 9); ASSERT_EQ(f.imag, false); ASSERT_EQ(f.value.sign, false); ASSERT_EQ(f.value.xs.as_u64(), 0b000000100); ASSERT_EQ(f.value.zs.as_u64(), 0b000000100); f = FlexPauliString::from_text("X8*Y2*Y8"); ASSERT_EQ(f.value.num_qubits, 9); ASSERT_EQ(f.imag, true); ASSERT_EQ(f.value.sign, false); ASSERT_EQ(f.value.xs.as_u64(), 0b000000100); ASSERT_EQ(f.value.zs.as_u64(), 0b100000100); f = FlexPauliString::from_text("Y8*Y2*X8"); ASSERT_EQ(f.value.num_qubits, 9); ASSERT_EQ(f.imag, true); ASSERT_EQ(f.value.sign, true); ASSERT_EQ(f.value.xs.as_u64(), 0b000000100); ASSERT_EQ(f.value.zs.as_u64(), 0b100000100); ASSERT_EQ(FlexPauliString::from_text("X1"), FlexPauliString::from_text("_X")); ASSERT_EQ(FlexPauliString::from_text("X20*I21"), FlexPauliString::from_text("____________________X_")); } ================================================ FILE: src/stim/stabilizers/flow.h ================================================ /* * 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. */ #ifndef _STIM_STABILIZERS_FLOW_H #define _STIM_STABILIZERS_FLOW_H #include "stim/stabilizers/pauli_string.h" namespace stim { template struct Flow { /// The stabilizer at the beginning of the circuit. stim::PauliString input; /// The equivalent stabilizer at the end the circuit. stim::PauliString output; /// The measurements mediating the equivalence. /// Indexing follows python convention: -1 is the last element, 0 is the first element. /// ENTRIES MUST BE SORTED AND UNIQUE. std::vector measurements; /// Indices of observables included in the flow. /// Determines which OBSERVABLE_INCLUDE instructions are included. /// ENTRIES MUST BE SORTED AND UNIQUE. std::vector observables; /// Fixes non-unique non-sorted measurements and observables. void canonicalize(); static Flow from_str(std::string_view text); Flow operator*(const Flow &rhs) const; bool operator<(const Flow &other) const; bool operator==(const Flow &other) const; bool operator!=(const Flow &other) const; std::string str() const; }; template std::ostream &operator<<(std::ostream &out, const Flow &flow); } // namespace stim #include "stim/stabilizers/flow.inl" #endif ================================================ FILE: src/stim/stabilizers/flow.inl ================================================ #include "stim/circuit/circuit.h" #include "stim/stabilizers/flex_pauli_string.h" #include "stim/stabilizers/flow.h" #include "stim/util_bot/arg_parse.h" #include "stim/mem/span_ref.h" namespace stim { inline bool parse_obs_index(std::string_view obs, uint32_t *out) { if (obs.size() < 6 || obs[0] != 'o' || obs[1] != 'b' || obs[2] != 's' || obs[3] != '[' || obs.back() != ']') { throw std::invalid_argument(""); // Caught and given a message by caller. } int64_t i = 0; if (!parse_int64(obs.substr(4, obs.size() - 5), &i)) { return false; } if (i >= 0 && i <= UINT32_MAX) { *out = (uint32_t)i; return true; } return false; } inline bool parse_rec_allowing_non_negative(std::string_view rec, int32_t *out) { if (rec.size() < 6 || rec[0] != 'r' || rec[1] != 'e' || rec[2] != 'c' || rec[3] != '[' || rec.back() != ']') { throw std::invalid_argument(""); // Caught and given a message by caller. } int64_t i = 0; if (!parse_int64(rec.substr(4, rec.size() - 5), &i)) { return false; } if (i >= INT32_MIN && i <= INT32_MAX) { *out = (int32_t)i; return true; } return false; } template PauliString parse_non_empty_pauli_string_allowing_i(std::string_view text, bool *imag_out) { *imag_out = false; if (text == "+1" || text == "1") { return PauliString(0); } if (text == "-1") { PauliString r(0); r.sign = true; return r; } if (text.empty()) { throw std::invalid_argument("Got an ambiguously blank pauli string. Use '1' for the empty Pauli string."); } auto flex = FlexPauliString::from_text(text); *imag_out = flex.imag; PauliString result(flex.value.num_qubits); size_t nb = std::min(flex.value.xs.num_u8_padded(), result.xs.num_u8_padded()); memcpy(result.xs.u8, flex.value.xs.u8, nb); memcpy(result.zs.u8, flex.value.zs.u8, nb); result.sign = flex.value.sign; return result; } template Flow Flow::from_str(std::string_view text) { try { auto parts = split_view('>', text); if (parts.size() != 2 || parts[0].empty() || parts[0].back() != '-') { throw std::invalid_argument(""); // Caught and given a message below. } parts[0] = parts[0].substr(0, parts[0].size() - 1); while (!parts[0].empty() && parts[0].back() == ' ') { parts[0] = parts[0].substr(0, parts[0].size() - 1); } bool imag_inp = false; bool imag_out = false; PauliString inp = parse_non_empty_pauli_string_allowing_i(parts[0], &imag_inp); parts = split_view(' ', parts[1]); size_t k = 0; while (k < parts.size() && parts[k].empty()) { k += 1; } if (k >= parts.size()) { throw std::invalid_argument(""); // Caught and given a message below. } PauliString out(0); std::vector measurements; std::vector observables; bool flip_out = false; if (parts[k].starts_with('-')) { flip_out = true; parts[k] = parts[k].substr(1); } if (!parts[k].empty() && parts[k][0] != 'r' && parts[k][0] != 'o') { out = parse_non_empty_pauli_string_allowing_i(parts[k], &imag_out); } else if (parts[k][0] == 'r') { int32_t rec; if (!parse_rec_allowing_non_negative(parts[k], &rec)) { throw std::invalid_argument(""); // Caught and given a message below. } measurements.push_back(rec); } else if (parts[k][0] == 'o') { uint32_t rec; if (!parse_obs_index(parts[k], &rec)) { throw std::invalid_argument(""); // Caught and given a message below. } observables.push_back(rec); } else { throw std::invalid_argument("Parsing reached an expected-to-be-impossible state. " "Failed to understand a Pauli term."); } out.sign ^= flip_out; k++; while (k < parts.size()) { if (parts[k] != "xor" || k + 1 == parts.size()) { throw std::invalid_argument(""); // Caught and given a message below. } if (parts[k + 1].starts_with("r")) { int32_t rec; if (!parse_rec_allowing_non_negative(parts[k + 1], &rec)) { throw std::invalid_argument(""); // Caught and given a message below. } measurements.push_back(rec); } else if (parts[k + 1].starts_with("o")) { uint32_t obs; if (!parse_obs_index(parts[k + 1], &obs)) { throw std::invalid_argument(""); // Caught and given a message below. } observables.push_back(obs); } else { throw std::invalid_argument(""); // Caught and given a message below. } k += 2; } if (imag_inp != imag_out) { throw std::invalid_argument("Anti-Hermitian flows aren't allowed."); } size_t measurements_kept = inplace_xor_sort(SpanRef(measurements)).size(); size_t obs_kept = inplace_xor_sort(SpanRef(observables)).size(); measurements.resize(measurements_kept); observables.resize(obs_kept); return Flow{inp, out, measurements, observables}; } catch (const std::invalid_argument &ex) { if (*ex.what() != '\0') { throw; } throw std::invalid_argument("Invalid stabilizer flow text: '" + std::string(text) + "'."); } } template bool Flow::operator==(const Flow &other) const { return input == other.input && output == other.output && measurements == other.measurements && observables == other.observables; } template bool Flow::operator<(const Flow &other) const { if (input != other.input) { return input < other.input; } if (output != other.output) { return output < other.output; } if (measurements != other.measurements) { return SpanRef(measurements) < SpanRef(other.measurements); } if (observables != other.observables) { return SpanRef(observables) < SpanRef(other.observables); } return false; } template bool Flow::operator!=(const Flow &other) const { return !(*this == other); } template std::string Flow::str() const { std::stringstream result; result << *this; return result.str(); } template std::ostream &operator<<(std::ostream &out, const Flow &flow) { bool use_sparse = false; // Sparse is only useful if most terms are identity. if (flow.input.num_qubits > 8 && flow.input.ref().weight() * 8 <= flow.input.num_qubits) { use_sparse = true; } if (flow.output.num_qubits > 8 && flow.output.ref().weight() * 8 <= flow.output.num_qubits) { use_sparse = true; } // Sparse would lose length data if the last pauli is an identity. if (flow.input.num_qubits > 0 && !flow.input.xs[flow.input.num_qubits - 1] && !flow.input.zs[flow.input.num_qubits - 1]) { use_sparse = false; } if (flow.output.num_qubits > 0 && !flow.output.xs[flow.output.num_qubits - 1] && !flow.output.zs[flow.output.num_qubits - 1]) { use_sparse = false; } auto write_sparse = [&](const PauliString &ps) -> bool { if (ps.sign) { out << "-"; } bool has_any = false; if (use_sparse) { ps.ref().for_each_active_pauli([&](size_t q) { uint8_t p = ps.xs[q] + 2 * ps.zs[q]; if (has_any) { out << "*"; } out << "_XZY"[p]; out << q; has_any = true; }); } else { for (size_t q = 0; q < ps.num_qubits; q++) { uint8_t p = ps.xs[q] + 2 * ps.zs[q]; out << "_XZY"[p]; has_any = true; } } return has_any; }; if (!write_sparse(flow.input)) { out << "1"; } out << " -> "; bool has_out = write_sparse(flow.output); for (const auto &t : flow.measurements) { if (has_out) { out << " xor "; } has_out = true; out << "rec[" << t << "]"; } for (const auto &t : flow.observables) { if (has_out) { out << " xor "; } has_out = true; out << "obs[" << t << "]"; } if (!has_out) { out << "1"; } return out; } template void Flow::canonicalize() { size_t measurements_kept = inplace_xor_sort(SpanRef(measurements)).size(); size_t observables_kept = inplace_xor_sort(SpanRef(observables)).size(); measurements.resize(measurements_kept); observables.resize(observables_kept); } template Flow Flow::operator*(const Flow &rhs) const { Flow result = *this; result.input.ensure_num_qubits(rhs.input.num_qubits, 1.1); result.output.ensure_num_qubits(rhs.output.num_qubits, 1.1); uint8_t log_i = 0; log_i -= result.input.ref().inplace_right_mul_returning_log_i_scalar(rhs.input.ref()); log_i += result.output.ref().inplace_right_mul_returning_log_i_scalar(rhs.output.ref()); if (log_i & 1) { throw std::invalid_argument(str() + " anticommutes with " + rhs.str()); } if (log_i & 2) { result.output.sign ^= true; } result.measurements.insert(result.measurements.end(), rhs.measurements.begin(), rhs.measurements.end()); result.observables.insert(result.observables.end(), rhs.observables.begin(), rhs.observables.end()); result.canonicalize(); return result; } } // namespace stim ================================================ FILE: src/stim/stabilizers/flow.pybind.cc ================================================ // 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. #include "stim/stabilizers/flow.h" #include "stim/py/base.pybind.h" #include "stim/stabilizers/flow.pybind.h" using namespace stim; using namespace stim_pybind; pybind11::class_> stim_pybind::pybind_flow(pybind11::module &m) { return pybind11::class_>( m, "Flow", clean_doc_string(R"DOC( A stabilizer flow (e.g. "XI -> XX xor rec[-1]"). Stabilizer circuits implement, and can be defined by, how they turn input stabilizers into output stabilizers mediated by measurements. These relationships are called stabilizer flows, and `stim.Flow` is a representation of such a flow. For example, a `stim.Flow` can be given to `stim.Circuit.has_flow` to verify that a circuit implements the flow. A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer P at the start of the circuit to the instantaneous stabilizer Q at the end of the circuit. The flow may be mediated by certain measurements. For example, a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and the CNOT flows implemented by the circuit involve these measurements. A flow like P -> Q means the circuit transforms P into Q. A flow like 1 -> P means the circuit prepares P. A flow like P -> 1 means the circuit measures P. A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). References: Stim's gate documentation includes the stabilizer flows of each gate. Appendix A of https://arxiv.org/abs/2302.02192 describes how flows are defined and provides a circuit construction for experimentally verifying their presence. Examples: >>> import stim >>> c = stim.Circuit("CNOT 2 4") >>> c.has_flow(stim.Flow("__X__ -> __X_X")) True >>> c.has_flow(stim.Flow("X2*X4 -> X2")) True >>> c.has_flow(stim.Flow("Z4 -> Z4")) False )DOC") .data()); } static Flow py_init_flow( const pybind11::object &arg, const pybind11::object &input, const pybind11::object &output, const pybind11::object &measurements, const pybind11::object &included_observables) { if (arg.is_none()) { Flow result{PauliString(0), PauliString(0)}; bool imag = false; if (!input.is_none()) { auto f = pybind11::cast(input); imag ^= f.imag; result.input = std::move(f.value); } if (!output.is_none()) { auto f = pybind11::cast(output); imag ^= f.imag; result.output = std::move(f.value); } if (imag) { throw std::invalid_argument("Anti-Hermitian flows aren't allowed."); } if (!measurements.is_none()) { for (const auto &h : measurements) { if (pybind11::isinstance(h)) { GateTarget g = pybind11::cast(h); if (!g.is_measurement_record_target()) { throw std::invalid_argument("Not a measurement offset: " + g.str()); } result.measurements.push_back(g.rec_offset()); } else { result.measurements.push_back(pybind11::cast(h)); } } } if (!included_observables.is_none()) { for (const auto &h : included_observables) { result.observables.push_back(pybind11::cast(h)); } } result.canonicalize(); return result; } if (!input.is_none()) { throw std::invalid_argument("Can't specify both a positional argument and `input=`."); } if (!output.is_none()) { throw std::invalid_argument("Can't specify both a positional argument and `output=`."); } if (!measurements.is_none()) { throw std::invalid_argument("Can't specify both a positional argument and `measurements=`."); } if (!included_observables.is_none()) { throw std::invalid_argument("Can't specify both a positional argument and `included_observables=`."); } if (pybind11::isinstance>(arg)) { return pybind11::cast>(arg); } else if (pybind11::isinstance(arg)) { return Flow::from_str(pybind11::cast(arg)); } std::stringstream ss; ss << "Don't know how to turn '" << arg << " into a flow."; throw std::invalid_argument(ss.str()); } void stim_pybind::pybind_flow_methods(pybind11::module &m, pybind11::class_> &c) { c.def( pybind11::init(&py_init_flow), pybind11::arg("arg") = pybind11::none(), pybind11::pos_only(), pybind11::kw_only(), pybind11::arg("input") = pybind11::none(), pybind11::arg("output") = pybind11::none(), pybind11::arg("measurements") = pybind11::none(), pybind11::arg("included_observables") = pybind11::none(), clean_doc_string(R"DOC( @signature def __init__(self, arg: Union[None, str, stim.Flow] = None, /, *, input: Optional[stim.PauliString] = None, output: Optional[stim.PauliString] = None, measurements: Optional[Iterable[Union[int, GateTarget]]] = None, included_observables: Optional[Iterable[int]] = None) -> None: Initializes a stim.Flow. When given a string, the string is parsed as flow shorthand. For example, the string "X_ -> ZZ xor rec[-1]" will result in a flow with input pauli string "X_", output pauli string "ZZ", and measurement indices [-1]. Args: arg [position-only]: Defaults to None. Must be specified by itself if used. str: Initializes a flow by parsing the given shorthand text. stim.Flow: Initializes a copy of the given flow. None (default): Initializes an empty flow. input: Defaults to None. Can be set to a stim.PauliString to directly specify the flow's input stabilizer. output: Defaults to None. Can be set to a stim.PauliString to directly specify the flow's output stabilizer. measurements: Defaults to None. Can be set to a list of integers or gate targets like `stim.target_rec(-1)`, to specify the measurements that mediate the flow. Negative and positive measurement indices are allowed. Indexes follow the python convention where -1 is the last measurement in a circuit and 0 is the first measurement in a circuit. included_observables: Defaults to None. `OBSERVABLE_INCLUDE` instructions that target an observable index from this list will be implicitly included in the flow. This allows flows to refer to observables. For example, the flow "X5 -> obs[3]" says "At the start of the circuit, observable 3 should be an X term on qubit 5. By the end of the circuit it will be measured. The `OBSERVABLE_INCLUDE(3)` instructions in the circuit should explain how this happened.". Examples: >>> import stim >>> stim.Flow("X2 -> -Y2*Z4 xor rec[-1]") stim.Flow("__X -> -__Y_Z xor rec[-1]") >>> stim.Flow("Z -> 1 xor rec[-1]") stim.Flow("Z -> rec[-1]") >>> stim.Flow( ... input=stim.PauliString("XX"), ... output=stim.PauliString("_X"), ... measurements=[], ... ) stim.Flow("XX -> _X") >>> # Identical terms cancel. >>> stim.Flow("X2 -> Y2*Y2 xor rec[-2] xor rec[-2]") stim.Flow("__X -> ___") >>> stim.Flow("X -> Y xor obs[3] xor obs[3] xor obs[3]") stim.Flow("X -> Y xor obs[3]") )DOC") .data()); c.def( "input_copy", [](const Flow &self) -> FlexPauliString { return FlexPauliString{self.input, false}; }, clean_doc_string(R"DOC( Returns a copy of the flow's input stabilizer. Examples: >>> import stim >>> f = stim.Flow(input=stim.PauliString('XX')) >>> f.input_copy() stim.PauliString("+XX") >>> f.input_copy() is f.input_copy() False )DOC") .data()); c.def( "output_copy", [](const Flow &self) -> FlexPauliString { return FlexPauliString{self.output, false}; }, clean_doc_string(R"DOC( Returns a copy of the flow's output stabilizer. Examples: >>> import stim >>> f = stim.Flow(output=stim.PauliString('XX')) >>> f.output_copy() stim.PauliString("+XX") >>> f.output_copy() is f.output_copy() False )DOC") .data()); c.def( "measurements_copy", [](const Flow &self) -> std::vector { return self.measurements; }, clean_doc_string(R"DOC( Returns a copy of the flow's measurement indices. Examples: >>> import stim >>> f = stim.Flow(measurements=[-1, 2]) >>> f.measurements_copy() [-1, 2] >>> f.measurements_copy() is f.measurements_copy() False )DOC") .data()); c.def( "included_observables_copy", [](const Flow &self) -> std::vector { return self.observables; }, clean_doc_string(R"DOC( Returns a copy of the flow's included observable indices. When an observable is included in a flow, the flow implicitly includes all measurements and pauli terms from `OBSERVABLE_INCLUDE` instructions targeting that observable index. Examples: >>> import stim >>> f = stim.Flow(included_observables=[3, 2]) >>> f.included_observables_copy() [2, 3] >>> f.included_observables_copy() is f.included_observables_copy() False >>> f = stim.Flow("X2 -> obs[3]") >>> f.included_observables_copy() [3] >>> stim.Circuit("OBSERVABLE_INCLUDE(3) X2").has_flow(f) True )DOC") .data()); c.def( "__mul__", &Flow::operator*, pybind11::arg("rhs"), clean_doc_string(R"DOC( Computes the product of two flows. Args: rhs: The right hand side of the multiplication. Returns: The product of the two flows. Raises: ValueError: The inputs anti-commute (their product would be anti-Hermitian). For example, 1 -> X times 1 -> Y fails because it would give 1 -> iZ. Examples: >>> import stim >>> stim.Flow("X -> X") * stim.Flow("Z -> Z") stim.Flow("Y -> Y") >>> stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ") stim.Flow("1 -> -YY") >>> stim.Flow("X -> rec[-1]") * stim.Flow("X -> rec[-2]") stim.Flow("_ -> rec[-2] xor rec[-1]") )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two flows have identical contents."); c.def(pybind11::self != pybind11::self, "Determines if two flows have non-identical contents."); c.def("__str__", &Flow::str, "Returns a shorthand description of the flow."); c.def( "__repr__", [](const Flow &self) { return "stim.Flow(\"" + self.str() + "\")"; }, "Returns valid python code evaluating to an equivalent `stim.Flow`."); } ================================================ FILE: src/stim/stabilizers/flow.pybind.h ================================================ // 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. #ifndef _STIM_STABILIZERS_FLOW_PYBIND_H #define _STIM_STABILIZERS_FLOW_PYBIND_H #include #include "stim/stabilizers/flow.h" namespace stim_pybind { pybind11::class_> pybind_flow(pybind11::module &m); void pybind_flow_methods(pybind11::module &m, pybind11::class_> &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/stabilizers/flow.test.cc ================================================ #include "stim/stabilizers/flow.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.h" #include "stim/mem/simd_word.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(stabilizer_flow, from_str_dedupe, { EXPECT_EQ( Flow::from_str( "X -> Y xor rec[-1] xor rec[-1] xor rec[-1] xor rec[-2] xor rec[-2] xor rec[-3] xor obs[1] xor obs[1] xor " "obs[3] xor obs[3] xor obs[3]"), (Flow{ .input = PauliString::from_str("X"), .output = PauliString::from_str("Y"), .measurements = {-3, -1}, .observables = {3}, })); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, from_str, { ASSERT_THROW({ Flow::from_str(""); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X>X"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X-X"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X > X"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X - X"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("->X"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X->"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("rec[0] -> X"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> rec[ -1]"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> X rec[-1]"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> X xor"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> rec[-1] xor X"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> obs[-1]"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> obs[A]"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> obs[]"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> obs[ 5]"); }, std::invalid_argument); ASSERT_THROW({ Flow::from_str("X -> rec[]"); }, std::invalid_argument); ASSERT_EQ( Flow::from_str("1 -> 1"), (Flow{ .input = PauliString::from_str(""), .output = PauliString::from_str(""), .measurements = {}, .observables = {}, })); ASSERT_EQ( Flow::from_str("1 -> -rec[0]"), (Flow{ .input = PauliString::from_str(""), .output = PauliString::from_str("-"), .measurements = {0}, .observables = {}, })); ASSERT_EQ( Flow::from_str("i -> -i"), (Flow{ .input = PauliString::from_str(""), .output = PauliString::from_str("-"), .measurements = {}, .observables = {}, })); ASSERT_EQ( Flow::from_str("iX -> -iY"), (Flow{ .input = PauliString::from_str("X"), .output = PauliString::from_str("-Y"), .measurements = {}, .observables = {}, })); ASSERT_EQ( Flow::from_str("X->-Y"), (Flow{ .input = PauliString::from_str("X"), .output = PauliString::from_str("-Y"), .measurements = {}, .observables = {}, })); ASSERT_EQ( Flow::from_str("X -> -Y"), (Flow{ .input = PauliString::from_str("X"), .output = PauliString::from_str("-Y"), .measurements = {}, .observables = {}, })); ASSERT_EQ( Flow::from_str("-X -> Y"), (Flow{ .input = PauliString::from_str("-X"), .output = PauliString::from_str("Y"), .measurements = {}, .observables = {}, })); ASSERT_EQ( Flow::from_str("XYZ -> -Z_Z"), (Flow{ .input = PauliString::from_str("XYZ"), .output = PauliString::from_str("-Z_Z"), .measurements = {}, .observables = {}, })); ASSERT_EQ( Flow::from_str("XYZ -> Z_Y xor rec[-1]"), (Flow{ .input = PauliString::from_str("XYZ"), .output = PauliString::from_str("Z_Y"), .measurements = {-1}, .observables = {}, })); ASSERT_EQ( Flow::from_str("XYZ -> Z_Y xor rec[5]"), (Flow{ .input = PauliString::from_str("XYZ"), .output = PauliString::from_str("Z_Y"), .measurements = {5}, .observables = {}, })); ASSERT_EQ( Flow::from_str("XYZ -> rec[-1]"), (Flow{ .input = PauliString::from_str("XYZ"), .output = PauliString::from_str(""), .measurements = {-1}, .observables = {}, })); ASSERT_EQ( Flow::from_str("XYZ -> Z_Y xor rec[-1] xor rec[-3]"), (Flow{ .input = PauliString::from_str("XYZ"), .output = PauliString::from_str("Z_Y"), .measurements = {-3, -1}, .observables = {}, })); ASSERT_EQ( Flow::from_str("XYZ -> ZIY xor rec[55] xor rec[-3]"), (Flow{ .input = PauliString::from_str("XYZ"), .output = PauliString::from_str("Z_Y"), .measurements = {-3, 55}, .observables = {}, })); ASSERT_EQ( Flow::from_str("XYZ -> ZIY xor rec[-3] xor rec[55]"), (Flow{ .input = PauliString::from_str("XYZ"), .output = PauliString::from_str("Z_Y"), .measurements = {-3, 55}, .observables = {}, })); ASSERT_EQ( Flow::from_str("X9 -> -Z5*Y3 xor rec[55] xor rec[-3]"), (Flow{ .input = PauliString::from_str("_________X"), .output = PauliString::from_str("-___Y_Z"), .measurements = {-3, 55}, .observables = {}, })); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, from_str_obs, { EXPECT_EQ( Flow::from_str("X9 -> obs[5]"), (Flow{ .input = PauliString::from_str("_________X"), .output = PauliString::from_str(""), .measurements = {}, .observables = {5}, })); EXPECT_EQ( Flow::from_str("X9 -> X xor obs[5] xor obs[3] xor rec[-1]"), (Flow{ .input = PauliString::from_str("_________X"), .output = PauliString::from_str("X"), .measurements = {-1}, .observables = {3, 5}, })); EXPECT_EQ( Flow::from_str("X9 -> X xor obs[5] xor rec[-1] xor obs[3]"), (Flow{ .input = PauliString::from_str("_________X"), .output = PauliString::from_str("X"), .measurements = {-1}, .observables = {3, 5}, })); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, str_and_from_str, { auto flow = Flow{ PauliString::from_str("XY"), PauliString::from_str("_Z"), {-3}, }; auto s_dense = "XY -> _Z xor rec[-3]"; auto s_sparse = "X0*Y1 -> Z1 xor rec[-3]"; ASSERT_EQ(flow.str(), s_dense); ASSERT_EQ(Flow::from_str(s_sparse), flow); ASSERT_EQ(Flow::from_str(s_dense), flow); ASSERT_EQ(Flow::from_str("1 -> rec[-1]"), (Flow{PauliString(0), PauliString(0), {-1}})); ASSERT_EQ(Flow::from_str("1 -> 1 xor rec[-1]"), (Flow{PauliString(0), PauliString(0), {-1}})); ASSERT_EQ( Flow::from_str("1 -> Z9 xor rec[55]"), (Flow{PauliString(0), PauliString("_________Z"), {55}})); ASSERT_EQ( Flow::from_str("-1 -> -X xor rec[-1] xor rec[-3]"), (Flow{PauliString::from_str("-"), PauliString::from_str("-X"), {-3, -1}})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, ordering, { ASSERT_FALSE(Flow::from_str("1 -> 1") < Flow::from_str("1 -> 1")); ASSERT_FALSE(Flow::from_str("X -> 1") < Flow::from_str("1 -> 1")); ASSERT_FALSE(Flow::from_str("1 -> X") < Flow::from_str("1 -> 1")); ASSERT_FALSE(Flow::from_str("1 -> rec[-1]") < Flow::from_str("1 -> 1")); ASSERT_TRUE(Flow::from_str("1 -> 1") < Flow::from_str("X -> 1")); ASSERT_TRUE(Flow::from_str("1 -> 1") < Flow::from_str("1 -> X")); ASSERT_TRUE(Flow::from_str("1 -> 1") < Flow::from_str("1 -> rec[-1]")); }) ================================================ FILE: src/stim/stabilizers/flow_pybind_test.py ================================================ # 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. import numpy as np import stim import pytest def test_basics(): p = stim.Flow() assert p.input_copy() == stim.PauliString(0) assert p.output_copy() == stim.PauliString(0) assert p.measurements_copy() == [] assert str(p) == "1 -> 1" assert repr(p) == 'stim.Flow("1 -> 1")' p = stim.Flow( input=stim.PauliString("XX"), output=stim.PauliString("-YYZ"), measurements=[-1, 2, 3], ) assert p.input_copy() == stim.PauliString("XX") assert p.output_copy() == stim.PauliString("-YYZ") assert p.measurements_copy() == [-1, 2, 3] assert str(p) == "XX -> -YYZ xor rec[-1] xor rec[2] xor rec[3]" assert repr(p) == 'stim.Flow("XX -> -YYZ xor rec[-1] xor rec[2] xor rec[3]")' p = stim.Flow("-X1*Z2 -> Y3 xor rec[-1]") assert p.input_copy() == stim.PauliString("-_XZ") assert p.output_copy() == stim.PauliString("___Y") assert p.measurements_copy() == [-1] assert str(p) == "-_XZ -> ___Y xor rec[-1]" assert repr(p) == 'stim.Flow("-_XZ -> ___Y xor rec[-1]")' p = stim.Flow("X20 -> Y xor rec[-1]") assert p.input_copy() == stim.PauliString("X20") assert p.output_copy() == stim.PauliString("Y") assert p.measurements_copy() == [-1] assert str(p) == "X20 -> Y0 xor rec[-1]" assert repr(p) == 'stim.Flow("X20 -> Y0 xor rec[-1]")' p = stim.Flow("X20*I21 -> Y xor rec[-1]") assert p.input_copy() == stim.PauliString("____________________X_") assert p.output_copy() == stim.PauliString("Y") assert p.measurements_copy() == [-1] assert str(p) == "____________________X_ -> Y xor rec[-1]" assert repr(p) == 'stim.Flow("____________________X_ -> Y xor rec[-1]")' p = stim.Flow("iX -> iY") assert p.input_copy() == stim.PauliString("X") assert p.output_copy() == stim.PauliString("Y") assert p.measurements_copy() == [] p = stim.Flow(input=stim.PauliString("iX"), output=stim.PauliString("iY")) assert p.input_copy() == stim.PauliString("X") assert p.output_copy() == stim.PauliString("Y") assert p.measurements_copy() == [] with pytest.raises(ValueError, match="Anti-Hermitian"): stim.Flow("iX -> Y") with pytest.raises(ValueError, match="Anti-Hermitian"): stim.Flow(input=stim.PauliString("iX"), output=stim.PauliString("Y")) def test_equality(): assert not (stim.Flow() == None) assert not (stim.Flow() == "other object") assert not (stim.Flow() == object()) assert stim.Flow() != None assert stim.Flow() != "other object" assert stim.Flow() != object() assert stim.Flow('X -> Y') == stim.Flow('X -> Y') assert stim.Flow('X -> X') != stim.Flow('X -> Y') assert not (stim.Flow('X -> Y') != stim.Flow('X -> Y')) assert not (stim.Flow('X -> Y') == stim.Flow('X -> X')) assert stim.Flow("X -> X xor rec[-1]") == stim.Flow("X -> X xor rec[-1]") assert stim.Flow("X -> X xor rec[-1]") != stim.Flow("Y -> X xor rec[-1]") assert stim.Flow("X -> X xor rec[-1]") != stim.Flow("X -> Y xor rec[-1]") assert stim.Flow("X -> X xor rec[-1]") != stim.Flow("X -> X xor rec[-2]") @pytest.mark.parametrize("value", [ stim.Flow(), stim.Flow("X -> Y xor rec[-1]"), stim.Flow("X -> 1"), stim.Flow("-X -> Y"), stim.Flow("X -> -Y"), stim.Flow("-X -> -Y"), stim.Flow("1 -> X"), stim.Flow("X__________________ -> ________Y"), ]) def test_repr(value): assert eval(repr(value), {'stim': stim}) == value assert repr(eval(repr(value), {'stim': stim})) == repr(value) def test_obs_flows(): assert stim.Circuit(""" OBSERVABLE_INCLUDE(1) X2 """).has_flow(stim.Flow("X2 -> obs[1]")) assert not stim.Circuit(""" OBSERVABLE_INCLUDE(1) !X2 """).has_flow(stim.Flow("X2 -> obs[1]")) assert stim.Circuit(""" OBSERVABLE_INCLUDE(1) !X2 """).has_flow(stim.Flow("X2 -> obs[1]"), unsigned=True) assert stim.Circuit(""" OBSERVABLE_INCLUDE(1) !X2 """).has_flow(stim.Flow("-X2 -> obs[1]")) assert not stim.Circuit(""" OBSERVABLE_INCLUDE(1) X2 """).has_flow(stim.Flow("Y2 -> obs[1]")) assert not stim.Circuit(""" OBSERVABLE_INCLUDE(1) X2 """).has_flow(stim.Flow("Y2 -> obs[1]"), unsigned=True) def test_obs_include_pauli_terms_sensitivity(): _, obs = stim.Circuit(""" OBSERVABLE_INCLUDE(0) X0 X_ERROR(0.5) 0 OBSERVABLE_INCLUDE(0) X0 """).compile_detector_sampler().sample(shots=1024, separate_observables=True) assert np.count_nonzero(obs) == 0 _, obs = stim.Circuit(""" OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 X_ERROR(0.5) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 """).compile_detector_sampler().sample(shots=1024, separate_observables=True) xs, ys, zs = np.count_nonzero(obs, axis=0) assert xs == 0 assert 256 <= ys <= 768 assert zs == ys _, obs = stim.Circuit(""" OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 Y_ERROR(0.5) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 """).compile_detector_sampler().sample(shots=1024, separate_observables=True) xs, ys, zs = np.count_nonzero(obs, axis=0) assert ys == 0 assert 256 <= xs <= 768 assert zs == xs _, obs = stim.Circuit(""" OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 Z_ERROR(0.5) 0 OBSERVABLE_INCLUDE(0) X0 OBSERVABLE_INCLUDE(1) Y0 OBSERVABLE_INCLUDE(2) Z0 """).compile_detector_sampler().sample(shots=1024, separate_observables=True) xs, ys, zs = np.count_nonzero(obs, axis=0) assert zs == 0 assert 256 <= ys <= 768 assert xs == ys def test_flow_canonicalization(): assert stim.Flow(measurements=[4, 0, 4]) == stim.Flow(measurements=[0]) assert stim.Flow(included_observables=[4, 0, 4]) == stim.Flow(included_observables=[0]) def test_flow_multiplication(): assert stim.Flow("XYZ -> 1") * stim.Flow("1 -> XYZ") == stim.Flow("XYZ -> XYZ") assert stim.Flow("XX_ -> 1") * stim.Flow("_XX -> 1") == stim.Flow("X_X -> 1") assert stim.Flow("1 -> XX_") * stim.Flow("1 -> _XX") == stim.Flow("1 -> X_X") assert stim.Flow("1 -> rec[-1] xor rec[-3]") * stim.Flow("1 -> rec[-1] xor rec[-2]") == stim.Flow("1 -> rec[-2] xor rec[-3]") assert stim.Flow("1 -> obs[1] xor obs[3]") * stim.Flow("1 -> obs[1] xor obs[2]") == stim.Flow("1 -> obs[2] xor obs[3]") assert stim.Flow("X -> X") * stim.Flow("Z -> Z") == stim.Flow("Y -> Y") assert stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ") == stim.Flow("1 -> -YY") assert stim.Flow("1 -> obs[1]") * stim.Flow("1 -> obs[1]") == stim.Flow("1 -> 1") assert stim.Flow("1 -> rec[1]") * stim.Flow("1 -> rec[1]") == stim.Flow("1 -> 1") with pytest.raises(ValueError, match="anticommute"): _ = stim.Flow("1 -> X") * stim.Flow("1 -> Y") with pytest.raises(ValueError, match="anticommute"): _ = stim.Flow("1 -> Y") * stim.Flow("1 -> X") ================================================ FILE: src/stim/stabilizers/pauli_string.h ================================================ /* * 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. */ #ifndef _STIM_STABILIZERS_PAULI_STRING_H #define _STIM_STABILIZERS_PAULI_STRING_H #include #include #include "stim/mem/simd_bits.h" #include "stim/stabilizers/pauli_string_ref.h" namespace stim { /// Converts from the xz encoding /// /// 0b00: I /// 0b01: X /// 0b10: Z /// 0b11: Y /// /// To the xyz encoding /// /// 0: I /// 1: X /// 2: Y /// 3: Z inline uint8_t pauli_xz_to_xyz(bool x, bool z) { return (uint8_t)(x ^ z) | ((uint8_t)z << 1); } /// Converts from the xyz encoding /// /// 0: I /// 1: X /// 2: Y /// 3: Z /// /// To the xz encoding /// /// 0b00: I /// 0b01: X /// 0b10: Z /// 0b11: Y inline uint8_t pauli_xyz_to_xz(uint8_t xyz) { xyz ^= xyz >> 1; return xyz; } /// A Pauli string is a product of Pauli operations (I, X, Y, Z) to apply to various qubits. /// /// In most cases, methods will take a PauliStringRef instead of a PauliString. This is because PauliStringRef can /// have contents referring into densely packed table row data (or to a PauliString or to other sources). Basically, /// PauliString is for the special somewhat-unusual case where you want to create data to back a PauliStringRef instead /// of simply passing existing data along. It's a convenience class. /// /// The template parameter, W, represents the SIMD width. template struct PauliString { /// The length of the Pauli string. size_t num_qubits; /// Whether or not the Pauli string is negated. True means -1, False means +1. Imaginary phase is not permitted. bool sign; /// The Paulis in the Pauli string, densely bit packed in a fashion enabling the use vectorized instructions. /// Paulis are xz-encoded (P=xz: I=00, X=10, Y=11, Z=01) pairwise across the two bit vectors. simd_bits xs, zs; /// Standard constructors. explicit PauliString(size_t num_qubits); PauliString(const PauliStringRef &other); // NOLINT(google-explicit-constructor) PauliString(const PauliString &other); PauliString(PauliString &&other) noexcept; PauliString &operator=(const PauliStringRef &other); PauliString &operator=(const PauliString &other); PauliString &operator=(PauliString &&other); /// Parse constructor. explicit PauliString(std::string_view text); /// Factory method for creating a PauliString whose Pauli entries are returned by a function. static PauliString from_func(bool sign, size_t num_qubits, const std::function &func); /// Factory method for creating a PauliString by parsing a string (e.g. "-XIIYZ"). static PauliString from_str(std::string_view text); /// Factory method for creating a PauliString with uniformly random sign and Pauli entries. static PauliString random(size_t num_qubits, std::mt19937_64 &rng); /// Equality. bool operator==(const PauliStringRef &other) const; bool operator==(const PauliString &other) const; /// Inequality. bool operator!=(const PauliStringRef &other) const; bool operator!=(const PauliString &other) const; bool operator<(const PauliStringRef &other) const; bool operator<(const PauliString &other) const; /// Implicit conversion to a reference. operator PauliStringRef(); /// Implicit conversion to a const reference. operator const PauliStringRef() const; /// Explicit conversion to a reference. PauliStringRef ref(); /// Explicit conversion to a const reference. const PauliStringRef ref() const; /// Returns a python-style slice of the Paulis in the Pauli string. PauliString py_get_slice(int64_t start, int64_t step, int64_t slice_length) const; /// Returns a Pauli from the pauli string, allowing python-style negative indices, using IXYZ encoding. uint8_t py_get_item(int64_t index) const; /// Returns a string describing the given Pauli string, with one character per qubit. std::string str() const; /// Grows the pauli string to be at least as large as the given number /// of qubits. /// /// Requires: /// resize_pad_factor >= 1 /// /// Args: /// min_num_qubits: A minimum number of qubits that will be needed. /// resize_pad_factor: When resizing, memory will be overallocated /// so that the pauli string can be expanded to at least this /// many times the number of requested qubits. Use this to /// avoid quadratic overheads from constant slight expansions. void ensure_num_qubits(size_t min_num_qubits, double resize_pad_factor); void mul_pauli_term(GateTarget t, bool *imag, bool right_mul); void left_mul_pauli(GateTarget t, bool *imag); void right_mul_pauli(GateTarget t, bool *imag); }; /// Writes a string describing the given Pauli string to an output stream. /// /// The template parameter, W, represents the SIMD width. template std::ostream &operator<<(std::ostream &out, const PauliString &ps); } // namespace stim #include "stim/stabilizers/pauli_string.inl" #endif ================================================ FILE: src/stim/stabilizers/pauli_string.inl ================================================ // 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. #include #include #include #include "stim/mem/simd_util.h" #include "stim/stabilizers/pauli_string.h" namespace stim { template PauliString::operator const PauliStringRef() const { return ref(); } template PauliString::operator PauliStringRef() { return ref(); } template PauliString::PauliString(size_t num_qubits) : num_qubits(num_qubits), sign(false), xs(num_qubits), zs(num_qubits) { } template PauliString::PauliString(std::string_view text) : num_qubits(0), sign(false), xs(0), zs(0) { *this = std::move(PauliString::from_str(text)); } template PauliString::PauliString(const PauliStringRef &other) : num_qubits(other.num_qubits), sign((bool)other.sign), xs(other.xs), zs(other.zs) { } template PauliString::PauliString(const PauliString &other) : num_qubits(other.num_qubits), sign((bool)other.sign), xs(other.xs), zs(other.zs) { } template PauliString::PauliString(PauliString &&other) noexcept : num_qubits(other.num_qubits), sign((bool)other.sign), xs(std::move(other.xs)), zs(std::move(other.zs)) { other.num_qubits = 0; other.sign = false; } template PauliString &PauliString::operator=(const PauliStringRef &other) { xs = other.xs; zs = other.zs; num_qubits = other.num_qubits; sign = other.sign; return *this; } template PauliString &PauliString::operator=(const PauliString &other) { xs = other.xs; zs = other.zs; num_qubits = other.num_qubits; sign = other.sign; return *this; } template PauliString &PauliString::operator=(PauliString &&other) { if (&other == this) { return *this; } xs = std::move(other.xs); zs = std::move(other.zs); num_qubits = other.num_qubits; sign = other.sign; other.num_qubits = 0; other.sign = false; return *this; } template const PauliStringRef PauliString::ref() const { size_t nw = (num_qubits + W - 1) / W; return PauliStringRef( num_qubits, // HACK: const correctness is temporarily removed, but immediately restored. bit_ref((bool *)&sign, 0), xs.word_range_ref(0, nw), zs.word_range_ref(0, nw)); } template PauliStringRef PauliString::ref() { size_t nw = (num_qubits + W - 1) / W; return PauliStringRef(num_qubits, bit_ref(&sign, 0), xs.word_range_ref(0, nw), zs.word_range_ref(0, nw)); } template std::string PauliString::str() const { return ref().str(); } template PauliString PauliString::from_func(bool sign, size_t num_qubits, const std::function &func) { PauliString result(num_qubits); result.sign = sign; for (size_t i = 0; i < num_qubits; i++) { char c = func(i); bool x; bool z; if (c == 'X') { x = true; z = false; } else if (c == 'Y') { x = true; z = true; } else if (c == 'Z') { x = false; z = true; } else if (c == '_' || c == 'I') { x = false; z = false; } else { throw std::invalid_argument("Unrecognized pauli character. " + std::to_string(c)); } result.xs.u64[i / 64] ^= (uint64_t)x << (i & 63); result.zs.u64[i / 64] ^= (uint64_t)z << (i & 63); } return result; } template PauliString PauliString::from_str(std::string_view text) { bool is_negated = text.starts_with('-'); bool is_prefixed = text.starts_with('+'); if (is_prefixed || is_negated) { text = text.substr(1); } return PauliString::from_func(is_negated, text.size(), [&](size_t i) { return text[i]; }); } template PauliString PauliString::random(size_t num_qubits, std::mt19937_64 &rng) { auto result = PauliString(num_qubits); result.xs.randomize(num_qubits, rng); result.zs.randomize(num_qubits, rng); result.sign ^= rng() & 1; return result; } template bool PauliString::operator==(const PauliStringRef &other) const { return ref() == other; } template bool PauliString::operator==(const PauliString &other) const { return ref() == other.ref(); } template bool PauliString::operator!=(const PauliStringRef &other) const { return ref() != other; } template bool PauliString::operator!=(const PauliString &other) const { return ref() != other.ref(); } template bool PauliString::operator<(const PauliString &other) const { return ref() < other.ref(); } template bool PauliString::operator<(const PauliStringRef &other) const { return ref() < other; } template void PauliString::ensure_num_qubits(size_t min_num_qubits, double resize_pad_factor) { assert(resize_pad_factor >= 1); if (min_num_qubits <= num_qubits) { return; } if (xs.num_bits_padded() >= min_num_qubits) { num_qubits = min_num_qubits; return; } size_t new_num_qubits = (size_t)(min_num_qubits * resize_pad_factor); simd_bits new_xs(new_num_qubits); simd_bits new_zs(new_num_qubits); new_xs.truncated_overwrite_from(xs, num_qubits); new_zs.truncated_overwrite_from(zs, num_qubits); xs = std::move(new_xs); zs = std::move(new_zs); num_qubits = min_num_qubits; } template void PauliString::mul_pauli_term(GateTarget t, bool *imag, bool right_mul) { auto q = t.qubit_value(); ensure_num_qubits(q + 1, 1.25); bool x2 = (bool)(t.data & TARGET_PAULI_X_BIT); bool z2 = (bool)(t.data & TARGET_PAULI_Z_BIT); if (!(x2 | z2)) { throw std::invalid_argument("Not a pauli target: " + t.str()); } bit_ref x1 = xs[q]; bit_ref z1 = zs[q]; bool old_x1 = x1; bool old_z1 = z1; x1 ^= x2; z1 ^= z2; // At each bit position: accumulate anti-commutation (+i or -i) counts. bool x1z2 = x1 & z2; bool anti_commutes = (x2 & z1) ^ x1z2; sign ^= (*imag ^ old_x1 ^ old_z1 ^ x1z2) & anti_commutes; sign ^= (bool)(t.data & TARGET_INVERTED_BIT); *imag ^= anti_commutes; sign ^= right_mul && anti_commutes; } template void PauliString::left_mul_pauli(GateTarget t, bool *imag) { mul_pauli_term(t, imag, false); } template void PauliString::right_mul_pauli(GateTarget t, bool *imag) { mul_pauli_term(t, imag, true); } template uint8_t PauliString::py_get_item(int64_t index) const { if (index < 0) { index += num_qubits; } if (index < 0 || (size_t)index >= num_qubits) { throw std::out_of_range("index"); } size_t u = (size_t)index; int x = xs[u]; int z = zs[u]; return pauli_xz_to_xyz(x, z); } template PauliString PauliString::py_get_slice(int64_t start, int64_t step, int64_t slice_length) const { assert(slice_length >= 0); assert(slice_length == 0 || start >= 0); return PauliString::from_func(false, slice_length, [&](size_t i) { int j = start + i * step; return "_XZY"[xs[j] + zs[j] * 2]; }); } template std::ostream &operator<<(std::ostream &out, const PauliString &ps) { return out << ps.ref(); } } // namespace stim ================================================ FILE: src/stim/stabilizers/pauli_string.perf.cc ================================================ // 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. #include "stim/stabilizers/pauli_string.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(PauliString_multiplication_1M) { size_t n = 1000 * 1000; PauliString p1(n); PauliString p2(n); benchmark_go([&]() { p1.ref().inplace_right_mul_returning_log_i_scalar(p2); }) .goal_micros(8.5) .show_rate("Paulis", n); } BENCHMARK(PauliString_multiplication_100K) { size_t n = 100 * 1000; PauliString p1(n); PauliString p2(n); benchmark_go([&]() { p1.ref().inplace_right_mul_returning_log_i_scalar(p2); }) .goal_nanos(700) .show_rate("Paulis", n); } BENCHMARK(PauliString_multiplication_10K) { size_t n = 10 * 1000; PauliString p1(n); PauliString p2(n); benchmark_go([&]() { p1.ref().inplace_right_mul_returning_log_i_scalar(p2); }) .goal_nanos(90) .show_rate("Paulis", n); } ================================================ FILE: src/stim/stabilizers/pauli_string.pybind.cc ================================================ // 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. #include "stim/stabilizers/pauli_string.h" #include "stim/circuit/circuit_instruction.pybind.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" #include "stim/stabilizers/pauli_string.pybind.h" #include "stim/stabilizers/pauli_string_iter.h" #include "stim/stabilizers/tableau.h" using namespace stim; using namespace stim_pybind; pybind11::object flex_pauli_string_to_unitary_matrix(const stim::FlexPauliString &ps, std::string_view endian) { bool little_endian; if (endian == "little") { little_endian = true; } else if (endian == "big") { little_endian = false; } else { throw std::invalid_argument("endian not in ['little', 'big']"); } size_t q = ps.value.num_qubits; if (q >= 32) { throw std::invalid_argument("Too many qubits."); } size_t n = 1 << q; std::complex *buffer = new std::complex[n * n]; uint64_t x = 0; uint64_t z = 0; for (size_t k = 0; k < q; k++) { x <<= 1; z <<= 1; if (little_endian) { x |= ps.value.xs[q - k - 1]; z |= ps.value.zs[q - k - 1]; } else { x |= ps.value.xs[k]; z |= ps.value.zs[k]; } } uint8_t start_phase = 0; start_phase += std::popcount(x & z); if (ps.imag) { start_phase += 1; } if (ps.value.sign) { start_phase += 2; } for (size_t col = 0; col < n; col++) { size_t row = col ^ x; uint8_t phase = start_phase; if (std::popcount(col & z) & 1) { phase += 2; } std::complex v{1, 0}; if (phase & 2) { v *= -1; } if (phase & 1) { v *= std::complex{0, 1}; } buffer[row * n + col] = v; } pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast *>(f); }); pybind11::ssize_t sn = (pybind11::ssize_t)n; pybind11::ssize_t itemsize = sizeof(std::complex); return pybind11::array_t>({sn, sn}, {sn * itemsize, itemsize}, buffer, free_when_done); } FlexPauliString &flex_pauli_string_obj_imul(FlexPauliString &lhs, const pybind11::object &rhs) { if (pybind11::isinstance(rhs)) { return lhs *= pybind11::cast(rhs); } else if (rhs.equal(pybind11::cast(std::complex{+1, 0}))) { return lhs; } else if (rhs.equal(pybind11::cast(std::complex{-1, 0}))) { return lhs *= std::complex{-1, 0}; } else if (rhs.equal(pybind11::cast(std::complex{0, 1}))) { return lhs *= std::complex{0, 1}; } else if (rhs.equal(pybind11::cast(std::complex{0, -1}))) { return lhs *= std::complex{0, -1}; } else if (pybind11::isinstance(rhs)) { pybind11::ssize_t k = pybind11::int_(rhs); if (k >= 0) { return lhs *= (pybind11::size_t)k; } } throw std::out_of_range("need isinstance(rhs, (stim.PauliString, int)) or rhs in (1, -1, 1j, -1j)"); } FlexPauliString flex_pauli_string_obj_mul(const FlexPauliString &lhs, const pybind11::object &rhs) { FlexPauliString copy = lhs; flex_pauli_string_obj_imul(copy, rhs); return copy; } size_t numpy_to_size(const pybind11::object &numpy_array, size_t expected_size) { if (pybind11::isinstance>(numpy_array)) { auto arr = pybind11::cast>(numpy_array); if (arr.ndim() == 1) { size_t max_n = arr.shape(0) * 8; size_t min_n = max_n == 0 ? 0 : max_n - 7; if (expected_size == SIZE_MAX) { throw std::invalid_argument( "Need to specify expected number of pauli terms (the `num_qubits` argument) when bit packing.\n" "A numpy array is bit packed (has dtype=np.uint8) but `num_qubits=None`."); } if (expected_size < min_n || expected_size > max_n) { std::stringstream ss; ss << "Numpy array has dtype=np.uint8 (meaning it is bit packed) and shape="; ss << arr.shape(0) << " (meaning it has between " << min_n << " and " << max_n << " bits)"; ss << " but len=" << expected_size << " is outside that range."; throw std::invalid_argument(ss.str()); } return expected_size; } } else if (pybind11::isinstance>(numpy_array)) { auto arr = pybind11::cast>(numpy_array); if (arr.ndim() == 1) { size_t num_bits = arr.shape(0); if (expected_size != SIZE_MAX && num_bits != expected_size) { std::stringstream ss; ss << "Numpy array has dtype=bool_ and shape=" << num_bits << " which is different from the given len=" << expected_size; ss << ".\nEither don't specify len (as it is not needed when using bool_ arrays) or ensure the given " "len agrees with the given array shapes."; throw std::invalid_argument(ss.str()); } return num_bits; } } throw std::invalid_argument("Bit data must be a 1-dimensional numpy array with dtype=np.uint8 or dtype=np.bool_"); } size_t numpy_pair_to_size( const pybind11::object &numpy_array1, const pybind11::object &numpy_array2, const pybind11::object &expected_size) { size_t n0 = SIZE_MAX; if (!expected_size.is_none()) { n0 = pybind11::cast(expected_size); } size_t n1 = numpy_to_size(numpy_array1, n0); size_t n2 = numpy_to_size(numpy_array2, n0); if (n1 != n2) { throw std::invalid_argument("Inconsistent array shapes."); } return n2; } FlexPauliString flex_pauli_string_from_unitary_matrix( const pybind11::object &matrix, std::string_view endian, bool ignore_sign) { bool little_endian; if (endian == "little") { little_endian = true; } else if (endian == "big") { little_endian = false; } else { throw std::invalid_argument("endian not in ['little', 'big']"); } std::complex unphase = 0; auto row_index_phase = [&](const pybind11::handle &row) -> std::tuple { size_t num_cells = 0; size_t non_zero_index = SIZE_MAX; uint8_t phase = 0; for (const auto &cell : row) { auto c = pybind11::cast>(cell); if (abs(c) < 1e-2) { num_cells++; continue; } if (unphase == std::complex{0}) { if (ignore_sign) { unphase = conj(c); } else { unphase = 1; } } c *= unphase; if (abs(c - std::complex{1, 0}) < 1e-2) { phase = 0; } else if (abs(c - std::complex{0, 1}) < 1e-2) { phase = 1; } else if (abs(c - std::complex{-1, 0}) < 1e-2) { phase = 2; } else if (abs(c - std::complex{0, -1}) < 1e-2) { phase = 3; } else { std::stringstream ss; ss << "The given unitary matrix isn't a Pauli string matrix. "; ss << "It has values besides 0, 1, -1, 1j, and -1j"; if (ignore_sign) { ss << " (up to global phase)"; } ss << '.'; throw std::invalid_argument(ss.str()); } if (non_zero_index != SIZE_MAX) { throw std::invalid_argument( "The given unitary matrix isn't a Pauli string matrix. It has two non-zero entries in the same " "row."); } non_zero_index = num_cells; num_cells++; } if (non_zero_index == SIZE_MAX) { throw std::invalid_argument( "The given unitary matrix isn't a Pauli string matrix. It has a row with no non-zero entries."); } return {non_zero_index, phase, num_cells}; }; uint64_t x = 0; uint64_t n = 0; std::vector phases; for (const auto &row : matrix) { auto row_data = row_index_phase(row); uint64_t index = std::get<0>(row_data) ^ (uint64_t)phases.size(); phases.push_back(std::get<1>(row_data)); size_t len = std::get<2>(row_data); if (phases.size() == 1) { x = index; n = len; } else { if (x != index) { throw std::invalid_argument( "The given unitary matrix isn't a Pauli string matrix. Rows disagree about which qubits are " "flipped."); } if (n != len) { throw std::invalid_argument( "The given unitary matrix isn't a Pauli string matrix. Rows have different lengths."); } } } if (phases.size() != n) { throw std::invalid_argument("The given unitary matrix isn't a Pauli string matrix. It isn't square."); } if (n == 0 || (n & (n - 1))) { throw std::invalid_argument( "The given unitary matrix isn't a Pauli string matrix. Its height isn't a power of 2."); } uint64_t z = 0; size_t q = 0; for (size_t p = n >> 1; p > 0; p >>= 1) { z <<= 1; z |= ((phases[p] - phases[0]) & 2) != 0; q++; } for (size_t k = 0; k < n; k++) { uint8_t expected_phase = phases[0]; if (std::popcount(k & z) & 1) { expected_phase += 2; } if ((expected_phase & 3) != phases[k]) { throw std::invalid_argument( "The given unitary matrix isn't a Pauli string matrix. It doesn't have consistent phase flips."); } } uint8_t leftover_phase = phases[0] + std::popcount(x & z); FlexPauliString result(q); result.imag = (leftover_phase & 1) != 0; result.value.sign = (leftover_phase & 2) != 0; auto &rx = result.value.xs.u64[0]; auto &rz = result.value.zs.u64[0]; if (little_endian) { rx = x; rz = z; } else { for (size_t k = 0; k < q; k++) { rx <<= 1; rz <<= 1; rx |= x & 1; rz |= z & 1; x >>= 1; z >>= 1; } } if (ignore_sign) { result.imag = false; result.value.sign = false; } return result; } pybind11::class_ stim_pybind::pybind_pauli_string(pybind11::module &m) { return pybind11::class_( m, "PauliString", clean_doc_string(R"DOC( A signed Pauli tensor product (e.g. "+X \u2297 X \u2297 X" or "-Y \u2297 Z". Represents a collection of Pauli operations (I, X, Y, Z) applied pairwise to a collection of qubits. Examples: >>> import stim >>> stim.PauliString("XX") * stim.PauliString("YY") stim.PauliString("-ZZ") >>> print(stim.PauliString(5)) +_____ )DOC") .data()); } static uint8_t convert_pauli_to_int(const pybind11::handle &h) { int64_t v = -1; if (pybind11::isinstance(h)) { try { v = pybind11::cast(h); } catch (const pybind11::cast_error &) { } } else if (pybind11::isinstance(h)) { std::string_view s = pybind11::cast(h); if (s == "I" || s == "_") { v = 0; } else if (s == "X" || s == "x") { v = 1; } else if (s == "Y" || s == "y") { v = 2; } else if (s == "Z" || s == "z") { v = 3; } } if (v >= 0 && v < 4) { return (uint8_t)v; } else { throw std::invalid_argument( "Don't know how to convert " + pybind11::cast(pybind11::repr(h)) + " into a pauli.\n" "Expected something from {0, 1, 2, 3, 'I', 'X', 'Y', 'Z', '_'}."); } } static FlexPauliString pauli_string_from_dict(const pybind11::dict &dict) { // Handle empty dict: if (dict.empty()) { return FlexPauliString(0); } const auto &first_entry = dict.begin(); std::vector> pauli_by_location; size_t max_index = 0; auto add_pauli_to_index = [&pauli_by_location, &max_index](pybind11::handle index, uint8_t pauli) { int64_t index_value = pybind11::cast(index); if (index_value < 0) { throw std::invalid_argument("Qubit index must be non-negative. got: " + std::to_string(index_value)); } size_t index_ = static_cast(index_value); if (index_ > max_index) { max_index = index_; } pauli_by_location.push_back(std::make_pair(index_, pauli)); }; if (pybind11::isinstance(first_entry->second) || pybind11::isinstance(first_entry->second)) { // Value is int or str -> key is qubit index: for (const auto &item : dict) { const auto &index = item.first; const auto &pauli_string = item.second; // Verify index is int for consistency: if (!pybind11::isinstance(index)) { throw std::invalid_argument( "When constructing stim.PauliString from Dict, keys must all be ints (indices) with single Pauli " "values, or Pauli keys with iterable values. Conflicting key: " + pybind11::cast(pybind11::repr(index))); } add_pauli_to_index(index, convert_pauli_to_int(pauli_string)); } } else if (pybind11::isinstance(first_entry->second)) { // Value is iterable -> key is Pauli: // Find maximum number of indices: size_t max_expected_indices = 0; for (const auto &item : dict) { const auto &indices = item.second; if (pybind11::isinstance(indices)) { max_expected_indices += static_cast(pybind11::len(indices)); } else { // In the iterable case - all values must also be iterables: throw std::invalid_argument( "When constructing stim.PauliString from Dict, with values as iterables, all values must be " "iterables. got: " + pybind11::cast(pybind11::repr(indices))); } } std::unordered_map used_indices; used_indices.reserve(max_expected_indices); auto verify_index_not_used = [&used_indices](pybind11::handle index, uint8_t pauli) -> bool { // This helper function checks if an index has been used before. It doesn't allow non-trivial // Pauli strings to collide, but it does allow collisions with trivial ("I") Pauli strings by not keeping // track of them. return true if the new index should be added to the final result. int64_t index_value = pybind11::cast(index); if (index_value < 0) { throw std::invalid_argument("Qubit index must be non-negative. got: " + std::to_string(index_value)); } size_t index_size_t = static_cast(index_value); const auto index_found = used_indices.find(index_size_t); if (index_found == used_indices.end()) { // Index has not been seed yet - add to map if non-trivial: if (pauli != 0) { used_indices.emplace(index_size_t, pauli); } } else { // Index has been seen before if (pauli == 0) { // Must not add that Pauli to final result, it will override the older non-trivial one. return false; } // Check if older index is not the same Pauli: if (index_found->second != pauli) { throw std::invalid_argument( "More than one Pauli definitions use the same qubit index. Conflict for index:" + pybind11::cast(pybind11::repr(index))); } } // Add new inde to final result: return true; }; for (const auto &item : dict) { const auto &pauli_str_or_int = item.first; const auto &indices = item.second; // Verify pauli_str_or_int is str or int for consistency: if (!(pybind11::isinstance(pauli_str_or_int) || pybind11::isinstance(pauli_str_or_int))) { throw std::invalid_argument( "When constructing stim.PauliString from Dict, keys must all be ints (indices) with single Pauli " "values, or Pauli keys with iterable values. Conflicting key: " + pybind11::cast(pybind11::repr(pauli_str_or_int))); } for (const auto &qubit_index : indices) { // Verify index is an int: if (!pybind11::isinstance(qubit_index)) { throw std::invalid_argument( "Qubit index must be an int. got:" + pybind11::cast(pybind11::repr(qubit_index))); } uint8_t pauli = convert_pauli_to_int(pauli_str_or_int); bool should_add_new_pauli = verify_index_not_used(qubit_index, pauli); if (should_add_new_pauli) { add_pauli_to_index(qubit_index, pauli); } } } } else { throw std::invalid_argument( "Don't know how to initialize a stim.PauliString from " + pybind11::cast(pybind11::repr(dict))); } // Format collected info into a FlexPauliString: FlexPauliString result(pauli_by_location.empty() ? 0 : max_index + 1); for (const auto &[key, value] : pauli_by_location) { // Conver 0-3 to x,z values (00, 01, 10, 11) uint8_t p = value; p ^= p >> 1; result.value.xs[key] = p & 1; result.value.zs[key] = (p & 2) >> 1; } return result; } void stim_pybind::pybind_pauli_string_methods(pybind11::module &m, pybind11::class_ &c) { c.def( pybind11::init( [](const pybind11::object &arg, const pybind11::object &num_qubits, const pybind11::object &text, const pybind11::object &other, const pybind11::object &pauli_indices) -> FlexPauliString { size_t count = 0; count += !arg.is_none(); count += !num_qubits.is_none(); count += !text.is_none(); count += !other.is_none(); count += !pauli_indices.is_none(); if (count > 1) { throw std::invalid_argument("Multiple arguments specified."); } if (count == 0) { return FlexPauliString(0); } const auto &num_qubits_or = pybind11::isinstance(arg) ? arg : num_qubits; if (!num_qubits_or.is_none()) { return FlexPauliString(pybind11::cast(num_qubits_or)); } pybind11::object text_or = pybind11::isinstance(arg) ? arg : text; if (!text_or.is_none()) { return FlexPauliString::from_text(pybind11::cast(text_or)); } pybind11::object other_or = pybind11::isinstance(arg) ? arg : other; if (!other_or.is_none()) { return pybind11::cast(other_or); } if (pybind11::isinstance(arg)) { return pauli_string_from_dict(pybind11::cast(arg)); } pybind11::object pauli_indices_or = pybind11::isinstance(arg) ? arg : pauli_indices; if (!pauli_indices_or.is_none()) { std::vector ps; for (const pybind11::handle &h : pauli_indices_or) { ps.push_back(convert_pauli_to_int(h)); } FlexPauliString result(ps.size()); for (size_t k = 0; k < ps.size(); k++) { uint8_t p = ps[k]; p ^= p >> 1; result.value.xs[k] = p & 1; result.value.zs[k] = p & 2; } return result; } throw std::invalid_argument( "Don't know how to initialize a stim.PauliString from " + pybind11::cast(pybind11::repr(arg))); }), pybind11::arg("arg") = pybind11::none(), pybind11::pos_only(), // These are no longer needed, and hidden from documentation, but are included to guarantee backwards // compatibility. pybind11::arg("num_qubits") = pybind11::none(), pybind11::arg("text") = pybind11::none(), pybind11::arg("other") = pybind11::none(), pybind11::arg("pauli_indices") = pybind11::none(), clean_doc_string(R"DOC( @signature def __init__(self, arg: Union[None, int, str, stim.PauliString, Iterable[Union[int, Literal["_", "I", "X", "Y", "Z"]]]] = None, /) -> None: Initializes a stim.PauliString from the given argument. When given a string, the string is parsed as a pauli string. The string can optionally start with a sign ('+', '-', 'i', '+i', or '-i'). The rest of the string should be either a dense pauli string or a sparse pauli string. A dense pauli string is made up of characters from '_IXYZ' where '_' and 'I' mean identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z. A sparse pauli string is a series of integers seperated by '*' and prefixed by 'I', 'X', 'Y', or 'Z'. Args: arg [position-only]: This can be a variety of types, including: None (default): initializes an empty Pauli string. int: initializes an identity Pauli string of the given length. str: initializes by parsing the given text. stim.PauliString: initializes a copy of the given Pauli string. Iterable: initializes by interpreting each item as a Pauli. Each item can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Dict[int, Union[int, str]]: initializes by interpreting keys as the qubit index and values as the Pauli for that index. Each value can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Dict[Union[int, str], Iterable[int]]: initializes by interpreting keys as Pauli operators and values as the qubit indices for that Pauli. Each key can be a single-qubit Pauli string (like "X"), or an integer. Integers use the convention 0=I, 1=X, 2=Y, 3=Z. Examples: >>> import stim >>> stim.PauliString("-XYZ") stim.PauliString("-XYZ") >>> stim.PauliString() stim.PauliString("+") >>> stim.PauliString(5) stim.PauliString("+_____") >>> stim.PauliString(stim.PauliString("XX")) stim.PauliString("+XX") >>> stim.PauliString([0, 1, 3, 2]) stim.PauliString("+_XZY") >>> stim.PauliString("X" for _ in range(4)) stim.PauliString("+XXXX") >>> stim.PauliString("-X2*Y6") stim.PauliString("-__X___Y") >>> stim.PauliString("X6*Y6") stim.PauliString("+i______Z") >>> stim.PauliString({0: "X", 2: "Y", 3: "X"}) stim.PauliString("+X_YX") >>> stim.PauliString({0: "X", 2: 2, 3: 1}) stim.PauliString("+X_YX") >>> stim.PauliString({"X": [1], 2: [4], "Z": [0, 3]}) stim.PauliString("+ZX_ZY") )DOC") .data()); c.def_static( "random", [](size_t num_qubits, bool allow_imaginary) { auto rng = make_py_seeded_rng(pybind11::none()); return FlexPauliString( PauliString::random(num_qubits, rng), allow_imaginary ? (rng() & 1) : false); }, pybind11::arg("num_qubits"), pybind11::kw_only(), pybind11::arg("allow_imaginary") = false, clean_doc_string(R"DOC( Samples a uniformly random Hermitian Pauli string. Args: num_qubits: The number of qubits the Pauli string should act on. allow_imaginary: Defaults to False. If True, the sign of the result may be 1j or -1j in addition to +1 or -1. In other words, setting this to True allows the result to be non-Hermitian. Examples: >>> import stim >>> p = stim.PauliString.random(5) >>> len(p) 5 >>> p.sign in [-1, +1] True >>> p2 = stim.PauliString.random(3, allow_imaginary=True) >>> len(p2) 3 >>> p2.sign in [-1, +1, 1j, -1j] True Returns: The sampled Pauli string. )DOC") .data()); c.def( "to_tableau", [](const FlexPauliString &self) { return Tableau::from_pauli_string(self.value); }, clean_doc_string(R"DOC( Creates a Tableau equivalent to this Pauli string. The tableau represents a Clifford operation that multiplies qubits by the corresponding Pauli operations from this Pauli string. The global phase of the pauli operation is lost in the conversion. Returns: The created tableau. Examples: >>> import stim >>> p = stim.PauliString("ZZ") >>> p.to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-X_"), stim.PauliString("-_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+_Z"), ], ) >>> q = stim.PauliString("YX_Z") >>> q.to_tableau() stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-X___"), stim.PauliString("+_X__"), stim.PauliString("+__X_"), stim.PauliString("-___X"), ], zs=[ stim.PauliString("-Z___"), stim.PauliString("-_Z__"), stim.PauliString("+__Z_"), stim.PauliString("+___Z"), ], ) )DOC") .data()); c.def( "to_unitary_matrix", &flex_pauli_string_to_unitary_matrix, pybind11::kw_only(), pybind11::arg("endian"), clean_doc_string(R"DOC( @signature def to_unitary_matrix(self, *, endian: Literal["little", "big"]) -> np.ndarray[np.complex64]: Converts the pauli string into a unitary matrix. Args: endian: "little": The first qubit is the least significant (corresponds to an offset of 1 in the matrix). "big": The first qubit is the most significant (corresponds to an offset of 2**(n - 1) in the matrix). Returns: A numpy array with dtype=np.complex64 and shape=(1 << len(pauli_string), 1 << len(pauli_string)). Example: >>> import stim >>> stim.PauliString("-YZ").to_unitary_matrix(endian="little") array([[0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.-1.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 0.-1.j], [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j]], dtype=complex64) )DOC") .data()); c.def_static( "from_unitary_matrix", &flex_pauli_string_from_unitary_matrix, pybind11::arg("matrix"), pybind11::kw_only(), pybind11::arg("endian") = "little", pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( @signature def from_unitary_matrix(matrix: Iterable[Iterable[Union[int, float, complex]]], *, endian: Literal["little", "big"] = 'little', unsigned: bool = False) -> stim.PauliString: Creates a stim.PauliString from the unitary matrix of a Pauli group member. Args: matrix: A unitary matrix specified as an iterable of rows, with each row is an iterable of amplitudes. The unitary matrix must correspond to a Pauli string, including global phase. endian: "little": matrix entries are in little endian order, where higher index qubits correspond to larger changes in row/col indices. "big": matrix entries are in big endian order, where higher index qubits correspond to smaller changes in row/col indices. unsigned: When False, the input must only contain the values [0, 1, -1, 1j, -1j] and the output will have the correct global phase. When True, the input is permitted to be scaled by an arbitrary unit complex value and the output will always have positive sign. False is stricter but provides more information, while True is more flexible but provides less information. Returns: The pauli string equal to the given unitary matrix. Raises: ValueError: The given matrix isn't the unitary matrix of a Pauli string. Examples: >>> import stim >>> stim.PauliString.from_unitary_matrix([ ... [1j, 0], ... [0, -1j], ... ], endian='little') stim.PauliString("+iZ") >>> stim.PauliString.from_unitary_matrix([ ... [1j**0.1, 0], ... [0, -(1j**0.1)], ... ], endian='little', unsigned=True) stim.PauliString("+Z") >>> stim.PauliString.from_unitary_matrix([ ... [0, 1, 0, 0], ... [1, 0, 0, 0], ... [0, 0, 0, -1], ... [0, 0, -1, 0], ... ], endian='little') stim.PauliString("+XZ") )DOC") .data()); c.def( "pauli_indices", [](const FlexPauliString &self, std::string_view include) { std::vector result; size_t n64 = self.value.xs.num_u64_padded(); bool keep_i = false; bool keep_x = false; bool keep_y = false; bool keep_z = false; for (char c : include) { switch (c) { case '_': case 'I': keep_i = true; break; case 'x': case 'X': keep_x = true; break; case 'y': case 'Y': keep_y = true; break; case 'z': case 'Z': keep_z = true; break; default: throw std::invalid_argument("Invalid character in include string: " + std::string(1, c)); } } for (size_t k = 0; k < n64; k++) { uint64_t x = self.value.xs.u64[k]; uint64_t z = self.value.zs.u64[k]; uint64_t u = 0; if (keep_i) { u |= ~x & ~z; } if (keep_x) { u |= x & ~z; } if (keep_y) { u |= x & z; } if (keep_z) { u |= ~x & z; } while (u) { uint8_t v = std::countr_zero(u); uint64_t q = k * 64 + v; if (q >= self.value.num_qubits) { return result; } result.push_back(q); u &= u - 1; } } return result; }, pybind11::arg("included_paulis") = "XYZ", clean_doc_string(R"DOC( @signature def pauli_indices(self, included_paulis: str = "XYZ") -> List[int]: Returns the indices of non-identity Paulis, or of specified Paulis. Args: include: A string containing the Pauli types to include. X type Pauli indices are included if "X" or "x" is in the string. Y type Pauli indices are included if "Y" or "y" is in the string. Z type Pauli indices are included if "Z" or "z" is in the string. I type Pauli indices are included if "I" or "_" is in the string. An exception is thrown if other characters are in the string. Returns: A list containing the ascending indices of matching Pauli terms. Examples: >>> import stim >>> stim.PauliString("_____X___Y____Z___").pauli_indices() [5, 9, 14] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("XZ") [5, 14] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("X") [5] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("Y") [9] >>> stim.PauliString("_____X___Y____Z___").pauli_indices("IY") [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17] >>> stim.PauliString("-X103*Y100").pauli_indices() [100, 103] )DOC") .data()); c.def( "commutes", [](const FlexPauliString &self, const FlexPauliString &other) { return self.value.ref().commutes(other.value.ref()); }, pybind11::arg("other"), clean_doc_string(R"DOC( Determines if two Pauli strings commute or not. Two Pauli strings commute if they have an even number of matched non-equal non-identity Pauli terms. Otherwise they anticommute. Args: other: The other Pauli string. Examples: >>> import stim >>> xx = stim.PauliString("XX") >>> xx.commutes(stim.PauliString("X_")) True >>> xx.commutes(stim.PauliString("XX")) True >>> xx.commutes(stim.PauliString("XY")) False >>> xx.commutes(stim.PauliString("XZ")) False >>> xx.commutes(stim.PauliString("ZZ")) True >>> xx.commutes(stim.PauliString("X_Y__")) True >>> xx.commutes(stim.PauliString("")) True Returns: True if the Pauli strings commute, False if they anti-commute. )DOC") .data()); c.def("__str__", &FlexPauliString::str, "Returns a text description."); c.def( "__repr__", [](const FlexPauliString &self) { return "stim.PauliString(\"" + self.str() + "\")"; }, "Returns valid python code evaluating to an equivalent `stim.PauliString`."); c.def_property( "sign", &FlexPauliString::get_phase, [](FlexPauliString &self, std::complex new_sign) { if (new_sign == std::complex(1)) { self.value.sign = false; self.imag = false; } else if (new_sign == std::complex(-1)) { self.value.sign = true; self.imag = false; } else if (new_sign == std::complex(0, 1)) { self.value.sign = false; self.imag = true; } else if (new_sign == std::complex(0, -1)) { self.value.sign = true; self.imag = true; } else { throw std::invalid_argument("new_sign not in [1, -1, 1j, -1j]"); } }, clean_doc_string(R"DOC( The sign of the Pauli string. Can be +1, -1, 1j, or -1j. Examples: >>> import stim >>> stim.PauliString("X").sign (1+0j) >>> stim.PauliString("-X").sign (-1+0j) >>> stim.PauliString("iX").sign 1j >>> stim.PauliString("-iX").sign (-0-1j) )DOC") .data()); c.def(pybind11::self == pybind11::self, "Determines if two Pauli strings have identical contents."); c.def(pybind11::self != pybind11::self, "Determines if two Pauli strings have non-identical contents."); c.def( "__len__", [](const FlexPauliString &self) { return self.value.num_qubits; }, clean_doc_string(R"DOC( Returns the length the pauli string; the number of qubits it operates on. Examples: >>> import stim >>> len(stim.PauliString("XY_ZZ")) 5 >>> len(stim.PauliString("X0*Z99")) 100 )DOC") .data()); c.def_property_readonly( "weight", [](const FlexPauliString &self) { return self.value.ref().weight(); }, clean_doc_string(R"DOC( Returns the number of non-identity pauli terms in the pauli string. Examples: >>> import stim >>> stim.PauliString("+___").weight 0 >>> stim.PauliString("+__X").weight 1 >>> stim.PauliString("+XYZ").weight 3 >>> stim.PauliString("-XXX___XXYZ").weight 7 )DOC") .data()); c.def( "extended_product", [](const FlexPauliString &self, const FlexPauliString &other) { return std::make_tuple(std::complex(1, 0), self * other); }, pybind11::arg("other"), clean_doc_string(R"DOC( [DEPRECATED] Use multiplication (__mul__ or *) instead. )DOC") .data()); c.def( pybind11::self + pybind11::self, pybind11::arg("rhs"), clean_doc_string(R"DOC( Returns the tensor product of two Pauli strings. Concatenates the Pauli strings and multiplies their signs. Args: rhs: A second stim.PauliString. Examples: >>> import stim >>> stim.PauliString("X") + stim.PauliString("YZ") stim.PauliString("+XYZ") >>> stim.PauliString("iX") + stim.PauliString("-X") stim.PauliString("-iXX") Returns: The tensor product. )DOC") .data()); c.def( pybind11::self += pybind11::self, pybind11::arg("rhs"), clean_doc_string(R"DOC( Performs an inplace tensor product. Concatenates the given Pauli string onto the receiving string and multiplies their signs. Args: rhs: A second stim.PauliString. Examples: >>> import stim >>> p = stim.PauliString("iX") >>> alias = p >>> p += stim.PauliString("-YY") >>> p stim.PauliString("-iXYY") >>> alias is p True Returns: The mutated pauli string. )DOC") .data()); c.def( "__mul__", &flex_pauli_string_obj_mul, pybind11::arg("rhs"), clean_doc_string(R"DOC( Right-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: rhs: The right hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term with the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply with the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> stim.PauliString("X") * 1 stim.PauliString("+X") >>> stim.PauliString("X") * -1 stim.PauliString("-X") >>> stim.PauliString("X") * 1j stim.PauliString("+iX") >>> stim.PauliString("X") * 2 stim.PauliString("+XX") >>> stim.PauliString("-X") * 2 stim.PauliString("+XX") >>> stim.PauliString("iX") * 2 stim.PauliString("-XX") >>> stim.PauliString("X") * 3 stim.PauliString("+XXX") >>> stim.PauliString("iX") * 3 stim.PauliString("-iXXX") >>> stim.PauliString("X") * stim.PauliString("Y") stim.PauliString("+iZ") >>> stim.PauliString("X") * stim.PauliString("XX_") stim.PauliString("+_X_") >>> stim.PauliString("XXXX") * stim.PauliString("_XYZ") stim.PauliString("+X_ZY") Returns: The product or tensor power. Raises: TypeError: The right hand side isn't a stim.PauliString, a non-negative integer, or a complex unit (1, -1, 1j, or -1j). )DOC") .data()); c.def( "__rmul__", [](const FlexPauliString &self, const pybind11::object &lhs) { if (pybind11::isinstance(lhs)) { return pybind11::cast(lhs) * self; } return flex_pauli_string_obj_mul(self, lhs); }, pybind11::arg("lhs"), clean_doc_string(R"DOC( Left-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: lhs: The left hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term with the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply with the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> 1 * stim.PauliString("X") stim.PauliString("+X") >>> -1 * stim.PauliString("X") stim.PauliString("-X") >>> 1j * stim.PauliString("X") stim.PauliString("+iX") >>> 2 * stim.PauliString("X") stim.PauliString("+XX") >>> 2 * stim.PauliString("-X") stim.PauliString("+XX") >>> 2 * stim.PauliString("iX") stim.PauliString("-XX") >>> 3 * stim.PauliString("X") stim.PauliString("+XXX") >>> 3 * stim.PauliString("iX") stim.PauliString("-iXXX") >>> stim.PauliString("X") * stim.PauliString("Y") stim.PauliString("+iZ") >>> stim.PauliString("X") * stim.PauliString("XX_") stim.PauliString("+_X_") >>> stim.PauliString("XXXX") * stim.PauliString("_XYZ") stim.PauliString("+X_ZY") Returns: The product. Raises: ValueError: The scalar phase factor isn't 1, -1, 1j, or -1j. )DOC") .data()); c.def( "__imul__", &flex_pauli_string_obj_imul, pybind11::arg("rhs"), clean_doc_string(R"DOC( Inplace right-multiplies the Pauli string. Can multiply by another Pauli string, a complex unit, or a tensor power. Args: rhs: The right hand side of the multiplication. This can be: - A stim.PauliString to right-multiply term-by-term into the paulis of the pauli string. - A complex unit (1, -1, 1j, -1j) to multiply into the sign of the pauli string. - A non-negative integer indicating the tensor power to raise the pauli string to (how many times to repeat it). Examples: >>> import stim >>> p = stim.PauliString("X") >>> p *= 1j >>> p stim.PauliString("+iX") >>> p = stim.PauliString("iXY_") >>> p *= 3 >>> p stim.PauliString("-iXY_XY_XY_") >>> p = stim.PauliString("X") >>> alias = p >>> p *= stim.PauliString("Y") >>> alias stim.PauliString("+iZ") >>> p = stim.PauliString("X") >>> p *= stim.PauliString("_YY") >>> p stim.PauliString("+XYY") Returns: The mutated Pauli string. )DOC") .data()); c.def( "__itruediv__", &FlexPauliString::operator/=, pybind11::is_operator(), pybind11::arg("rhs"), clean_doc_string(R"DOC( Inplace divides the Pauli string by a complex unit. Args: rhs: The divisor. Can be 1, -1, 1j, or -1j. Examples: >>> import stim >>> p = stim.PauliString("X") >>> p /= 1j >>> p stim.PauliString("-iX") Returns: The mutated Pauli string. Raises: ValueError: The divisor isn't 1, -1, 1j, or -1j. )DOC") .data()); c.def( "__truediv__", &FlexPauliString::operator/, pybind11::is_operator(), pybind11::arg("rhs"), clean_doc_string(R"DOC( Divides the Pauli string by a complex unit. Args: rhs: The divisor. Can be 1, -1, 1j, or -1j. Examples: >>> import stim >>> stim.PauliString("X") / 1j stim.PauliString("-iX") Returns: The quotient. Raises: ValueError: The divisor isn't 1, -1, 1j, or -1j. )DOC") .data()); c.def( "__neg__", [](const FlexPauliString &self) { FlexPauliString result = self; result.value.sign ^= 1; return result; }, clean_doc_string(R"DOC( Returns the negation of the pauli string. Examples: >>> import stim >>> -stim.PauliString("X") stim.PauliString("-X") >>> -stim.PauliString("-Y") stim.PauliString("+Y") >>> -stim.PauliString("iZZZ") stim.PauliString("-iZZZ") )DOC") .data()); c.def( "copy", [](const FlexPauliString &self) { FlexPauliString copy = self; return copy; }, clean_doc_string(R"DOC( Returns a copy of the pauli string. The copy is an independent pauli string with the same contents. Examples: >>> import stim >>> p1 = stim.PauliString.random(2) >>> p2 = p1.copy() >>> p2 is p1 False >>> p2 == p1 True )DOC") .data()); c.def( "to_numpy", [](const FlexPauliString &self, bool bit_packed) { return pybind11::make_tuple( simd_bits_to_numpy(self.value.xs, self.value.num_qubits, bit_packed), simd_bits_to_numpy(self.value.zs, self.value.num_qubits, bit_packed)); }, pybind11::kw_only(), pybind11::arg("bit_packed") = false, clean_doc_string(R"DOC( @signature def to_numpy(self, *, bit_packed: bool = False) -> Tuple[np.ndarray, np.ndarray]: Decomposes the contents of the pauli string into X bit and Z bit numpy arrays. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: An (xs, zs) tuple encoding the paulis from the string. The k'th Pauli from the string is encoded into k'th bit of xs and the k'th bit of zs using the "xz" encoding: P=I -> x=0 and z=0 P=X -> x=1 and z=0 P=Y -> x=1 and z=1 P=Z -> x=0 and z=1 The dtype and shape of the result depends on the bit_packed argument. If bit_packed=False: Each bit gets its own byte. xs.dtype = zs.dtype = np.bool_ xs.shape = zs.shape = len(self) xs_k = xs[k] zs_k = zs[k] If bit_packed=True: Equivalent to applying np.packbits(bitorder='little') to the result. xs.dtype = zs.dtype = np.uint8 xs.shape = zs.shape = math.ceil(len(self) / 8) xs_k = (xs[k // 8] >> (k % 8)) & 1 zs_k = (zs[k // 8] >> (k % 8)) & 1 Examples: >>> import stim >>> xs, zs = stim.PauliString("XXXXYYYZZ").to_numpy() >>> xs array([ True, True, True, True, True, True, True, False, False]) >>> zs array([False, False, False, False, True, True, True, True, True]) >>> xs, zs = stim.PauliString("XXXXYYYZZ").to_numpy(bit_packed=True) >>> xs array([127, 0], dtype=uint8) >>> zs array([240, 1], dtype=uint8) )DOC") .data()); c.def_static( "from_numpy", [](const pybind11::object &xs, const pybind11::object &zs, const pybind11::object &sign, const pybind11::object &num_qubits) -> FlexPauliString { size_t n = numpy_pair_to_size(xs, zs, num_qubits); FlexPauliString result(n); memcpy_bits_from_numpy_to_simd(n, xs, result.value.xs); memcpy_bits_from_numpy_to_simd(n, zs, result.value.zs); flex_pauli_string_obj_imul(result, sign); return result; }, pybind11::kw_only(), pybind11::arg("xs"), pybind11::arg("zs"), pybind11::arg("sign") = +1, pybind11::arg("num_qubits") = pybind11::none(), clean_doc_string(R"DOC( @signature def from_numpy(*, xs: np.ndarray, zs: np.ndarray, sign: Union[int, float, complex] = +1, num_qubits: Optional[int] = None) -> stim.PauliString: Creates a pauli string from X bit and Z bit numpy arrays, using the encoding: x=0 and z=0 -> P=I x=1 and z=0 -> P=X x=1 and z=1 -> P=Y x=0 and z=1 -> P=Z Args: xs: The X bits of the pauli string. This array can either be a 1-dimensional numpy array with dtype=np.bool_, or a bit packed 1-dimensional numpy array with dtype=np.uint8. If the dtype is np.uint8 then the array is assumed to be bit packed in little endian order and the "num_qubits" argument must be specified. When bit packed, the x bit with offset k is stored at (xs[k // 8] >> (k % 8)) & 1. zs: The Z bits of the pauli string. This array can either be a 1-dimensional numpy array with dtype=np.bool_, or a bit packed 1-dimensional numpy array with dtype=np.uint8. If the dtype is np.uint8 then the array is assumed to be bit packed in little endian order and the "num_qubits" argument must be specified. When bit packed, the x bit with offset k is stored at (xs[k // 8] >> (k % 8)) & 1. sign: Defaults to +1. Set to +1, -1, 1j, or -1j to control the sign of the returned Pauli string. num_qubits: Must be specified if xs or zs is a bit packed array. Specifies the expected length of the Pauli string. Returns: The created pauli string. Examples: >>> import stim >>> import numpy as np >>> xs = np.array([1, 1, 1, 1, 1, 1, 1, 0, 0], dtype=np.bool_) >>> zs = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1], dtype=np.bool_) >>> stim.PauliString.from_numpy(xs=xs, zs=zs, sign=-1) stim.PauliString("-XXXXYYYZZ") >>> xs = np.array([127, 0], dtype=np.uint8) >>> zs = np.array([240, 1], dtype=np.uint8) >>> stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=9) stim.PauliString("+XXXXYYYZZ") )DOC") .data()); c.def( "__pos__", [](const FlexPauliString &self) { FlexPauliString copy = self; return copy; }, clean_doc_string(R"DOC( Returns a pauli string with the same contents. Examples: >>> import stim >>> +stim.PauliString("+X") stim.PauliString("+X") >>> +stim.PauliString("-YY") stim.PauliString("-YY") >>> +stim.PauliString("iZZZ") stim.PauliString("+iZZZ") )DOC") .data()); c.def( "__setitem__", [](FlexPauliString &self, pybind11::ssize_t index, const pybind11::object &arg_new_pauli) { if (index < 0) { index += self.value.num_qubits; } if (index < 0 || (size_t)index >= self.value.num_qubits) { throw std::out_of_range("index"); } size_t u = (size_t)index; try { pybind11::ssize_t new_pauli = pybind11::cast(arg_new_pauli); if (new_pauli < 0 || new_pauli > 3) { throw std::out_of_range("Expected new_pauli in [0, 1, 2, 3, '_', 'I', 'X', 'Y', 'Z']"); } int z = (new_pauli >> 1) & 1; int x = (new_pauli & 1) ^ z; self.value.xs[u] = x; self.value.zs[u] = z; } catch (const pybind11::cast_error &) { char new_pauli = pybind11::cast(arg_new_pauli); if (new_pauli == 'X') { self.value.xs[u] = 1; self.value.zs[u] = 0; } else if (new_pauli == 'Y') { self.value.xs[u] = 1; self.value.zs[u] = 1; } else if (new_pauli == 'Z') { self.value.xs[u] = 0; self.value.zs[u] = 1; } else if (new_pauli == 'I' || new_pauli == '_') { self.value.xs[u] = 0; self.value.zs[u] = 0; } else { throw std::out_of_range("Expected new_pauli in [0, 1, 2, 3, '_', 'I', 'X', 'Y', 'Z']"); } } }, pybind11::arg("index"), pybind11::arg("new_pauli"), clean_doc_string(R"DOC( Mutates an entry in the pauli string using the encoding 0=I, 1=X, 2=Y, 3=Z. Args: index: The index of the pauli to overwrite. new_pauli: Either a character from '_IXYZ' or an integer from range(4). Examples: >>> import stim >>> p = stim.PauliString(4) >>> p[2] = 1 >>> print(p) +__X_ >>> p[0] = 3 >>> p[1] = 2 >>> p[3] = 0 >>> print(p) +ZYX_ >>> p[0] = 'I' >>> p[1] = 'X' >>> p[2] = 'Y' >>> p[3] = 'Z' >>> print(p) +_XYZ >>> p[-1] = 'Y' >>> print(p) +_XYY )DOC") .data()); c.def( "after", [](const FlexPauliString &self, const pybind11::object &operation, const pybind11::object &targets) -> FlexPauliString { PauliString result(0); if (pybind11::isinstance(operation)) { if (!targets.is_none()) { throw std::invalid_argument("Don't specify 'targets' when the operation is a stim.Circuit"); } result = self.value.ref().after(pybind11::cast(operation)); } else if (pybind11::isinstance(operation)) { if (!targets.is_none()) { throw std::invalid_argument( "Don't specify 'targets' when the operation is a stim.CircuitInstruction"); } result = self.value.ref().after(pybind11::cast(operation).as_operation_ref()); } else if (pybind11::isinstance>(operation)) { if (targets.is_none()) { throw std::invalid_argument("Must specify 'targets' when the operation is a stim.Tableau"); } std::vector raw_targets; for (const auto &e : targets) { raw_targets.push_back(pybind11::cast(e)); } result = self.value.ref().after(pybind11::cast>(operation), raw_targets); } else { throw std::invalid_argument( "Don't know how to apply " + pybind11::cast(pybind11::repr(operation))); } return FlexPauliString(result, self.imag); }, pybind11::arg("operation"), pybind11::arg("targets") = pybind11::none(), clean_doc_string(R"DOC( @overload def after(self, operation: Union[stim.Circuit, stim.CircuitInstruction]) -> stim.PauliString: @overload def after(self, operation: stim.Tableau, targets: Iterable[int]) -> stim.PauliString: @signature def after(self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None) -> stim.PauliString: Returns the result of conjugating the Pauli string by an operation. Args: operation: A circuit, tableau, or circuit instruction to conjugate the Pauli string by. Must be Clifford (e.g. if it's a circuit, the circuit can't have noise or measurements). targets: Required if and only if the operation is a tableau. Specifies which qubits to target. Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p.after(stim.CircuitInstruction("H", [1])) stim.PauliString("+_ZYZ") >>> p.after(stim.Circuit(''' ... C_XYZ 1 2 3 ... ''')) stim.PauliString("+_YZX") >>> p.after(stim.Tableau.from_named_gate('CZ'), targets=[0, 1]) stim.PauliString("+ZXYZ") Returns: The conjugated Pauli string. The Pauli string after the operation that is exactly equivalent to the given Pauli string before the operation. )DOC") .data()); c.def( "before", [](const FlexPauliString &self, const pybind11::object &operation, const pybind11::object &targets) -> FlexPauliString { PauliString result(0); if (pybind11::isinstance(operation)) { if (!targets.is_none()) { throw std::invalid_argument("Don't specify 'targets' when the operation is a stim.Circuit"); } result = self.value.ref().before(pybind11::cast(operation)); } else if (pybind11::isinstance(operation)) { if (!targets.is_none()) { throw std::invalid_argument( "Don't specify 'targets' when the operation is a stim.CircuitInstruction"); } result = self.value.ref().before(pybind11::cast(operation).as_operation_ref()); } else if (pybind11::isinstance>(operation)) { if (targets.is_none()) { throw std::invalid_argument("Must specify 'targets' when the operation is a stim.Tableau"); } std::vector raw_targets; for (const auto &e : targets) { raw_targets.push_back(pybind11::cast(e)); } result = self.value.ref().before(pybind11::cast>(operation), raw_targets); } else { throw std::invalid_argument( "Don't know how to apply " + pybind11::cast(pybind11::repr(operation))); } return FlexPauliString(result, self.imag); }, pybind11::arg("operation"), pybind11::arg("targets") = pybind11::none(), clean_doc_string(R"DOC( @overload def before(self, operation: Union[stim.Circuit, stim.CircuitInstruction]) -> stim.PauliString: @overload def before(self, operation: stim.Tableau, targets: Iterable[int]) -> stim.PauliString: @signature def before(self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None) -> stim.PauliString: Returns the result of conjugating the Pauli string by an operation. Args: operation: A circuit, tableau, or circuit instruction to anti-conjugate the Pauli string by. Must be Clifford (e.g. if it's a circuit, the circuit can't have noise or measurements). targets: Required if and only if the operation is a tableau. Specifies which qubits to target. Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p.before(stim.CircuitInstruction("H", [1])) stim.PauliString("+_ZYZ") >>> p.before(stim.Circuit(''' ... C_XYZ 1 2 3 ... ''')) stim.PauliString("+_ZXY") >>> p.before(stim.Tableau.from_named_gate('CZ'), targets=[0, 1]) stim.PauliString("+ZXYZ") Returns: The conjugated Pauli string. The Pauli string before the operation that is exactly equivalent to the given Pauli string after the operation. )DOC") .data()); c.def( "__getitem__", [](const FlexPauliString &self, const pybind11::object &index_or_slice) -> pybind11::object { pybind11::ssize_t start, step, slice_length; if (normalize_index_or_slice(index_or_slice, self.value.num_qubits, &start, &step, &slice_length)) { return pybind11::cast(FlexPauliString(self.value.py_get_slice(start, step, slice_length))); } else { return pybind11::cast(self.value.py_get_item(start)); } }, pybind11::arg("index_or_slice"), clean_doc_string(R"DOC( Returns an individual Pauli or Pauli string slice from the pauli string. @overload def __getitem__(self, index_or_slice: int) -> int: @overload def __getitem__(self, index_or_slice: slice) -> stim.PauliString: Individual Paulis are returned as an int using the encoding 0=I, 1=X, 2=Y, 3=Z. Slices are returned as a stim.PauliString (always with positive sign). Examples: >>> import stim >>> p = stim.PauliString("_XYZ") >>> p[2] 2 >>> p[-1] 3 >>> p[:2] stim.PauliString("+_X") >>> p[::-1] stim.PauliString("+ZYX_") Args: index_or_slice: The index of the pauli to return, or the slice of paulis to return. Returns: 0: Identity. 1: Pauli X. 2: Pauli Y. 3: Pauli Z. )DOC") .data()); c.def( pybind11::pickle( [](const FlexPauliString &self) -> pybind11::str { return self.str(); }, [](const pybind11::str &d) { return FlexPauliString::from_text(pybind11::cast(d)); })); c.def_static( "iter_all", [](size_t num_qubits, size_t min_weight, const pybind11::object &max_weight_obj, std::string_view allowed_paulis) -> PauliStringIterator { bool allow_x = false; bool allow_y = false; bool allow_z = false; for (char c : allowed_paulis) { switch (c) { case 'X': allow_x = true; break; case 'Y': allow_y = true; break; case 'Z': allow_z = true; break; default: throw std::invalid_argument( "allowed_paulis='" + std::string(allowed_paulis) + "' had characters other than 'X', 'Y', and 'Z'."); } } size_t max_weight = num_qubits; if (!max_weight_obj.is_none()) { int64_t v = pybind11::cast(max_weight_obj); if (v < 0) { min_weight = 1; max_weight = 0; } else { max_weight = (size_t)v; } } return PauliStringIterator( num_qubits, min_weight, max_weight, allow_x, allow_y, allow_z); }, pybind11::arg("num_qubits"), pybind11::kw_only(), pybind11::arg("min_weight") = 0, pybind11::arg("max_weight") = pybind11::none(), pybind11::arg("allowed_paulis") = "XYZ", clean_doc_string(R"DOC( Returns an iterator that iterates over all matching pauli strings. Args: num_qubits: The desired number of qubits in the pauli strings. min_weight: Defaults to 0. The minimum number of non-identity terms that must be present in each yielded pauli string. max_weight: Defaults to None (unused). The maximum number of non-identity terms that must be present in each yielded pauli string. allowed_paulis: Defaults to "XYZ". Set this to a string containing the non-identity paulis that are allowed to appear in each yielded pauli string. This argument must be a string made up of only "X", "Y", and "Z" characters. A non-identity Pauli is allowed if it appears in the string, and not allowed if it doesn't. Identity Paulis are always allowed. Returns: An Iterable[stim.PauliString] that yields the requested pauli strings. Examples: >>> import stim >>> pauli_string_iterator = stim.PauliString.iter_all( ... num_qubits=3, ... min_weight=1, ... max_weight=2, ... allowed_paulis="XZ", ... ) >>> for p in pauli_string_iterator: ... print(p) +X__ +Z__ +_X_ +_Z_ +__X +__Z +XX_ +XZ_ +ZX_ +ZZ_ +X_X +X_Z +Z_X +Z_Z +_XX +_XZ +_ZX +_ZZ )DOC") .data()); } ================================================ FILE: src/stim/stabilizers/pauli_string.pybind.h ================================================ // 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. #ifndef _STIM_STABILIZERS_PAULI_STRING_PYBIND_H #define _STIM_STABILIZERS_PAULI_STRING_PYBIND_H #include #include #include "stim/stabilizers/flex_pauli_string.h" namespace stim_pybind { pybind11::class_ pybind_pauli_string(pybind11::module &m); void pybind_pauli_string_methods(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/stabilizers/pauli_string.test.cc ================================================ // 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. #include "stim/stabilizers/pauli_string.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(pauli_string, str, { auto p1 = PauliString::from_str("+IXYZ"); ASSERT_EQ(p1.str(), "+_XYZ"); auto p2 = PauliString::from_str("X"); ASSERT_EQ(p2.str(), "+X"); auto p3 = PauliString::from_str("-XZ"); ASSERT_EQ(p3.str(), "-XZ"); auto s1 = PauliString::from_func(true, 24 * 24, [](size_t i) { return "_XYZ"[i & 3]; }); ASSERT_EQ( s1.str(), "-" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ" "_XYZ_XYZ_XYZ_XYZ_XYZ_XYZ"); }) TEST_EACH_WORD_SIZE_W(pauli_string, log_i_scalar_byproduct, { auto id = PauliString::from_str("_"); auto x = PauliString::from_str("X"); auto y = PauliString::from_str("Y"); auto z = PauliString::from_str("Z"); auto f = [](PauliString a, PauliString b) { // Note: copying is intentational. Do not change args to references. return a.ref().inplace_right_mul_returning_log_i_scalar(b); }; ASSERT_EQ(f(id, id), 0); ASSERT_EQ(f(id, x), 0); ASSERT_EQ(f(id, y), 0); ASSERT_EQ(f(id, z), 0); ASSERT_EQ(f(x, id), 0); ASSERT_EQ(f(x, x), 0); ASSERT_EQ(f(x, y), 1); ASSERT_EQ(f(x, z), 3); ASSERT_EQ(f(y, id), 0); ASSERT_EQ(f(y, x), 3); ASSERT_EQ(f(y, y), 0); ASSERT_EQ(f(y, z), 1); ASSERT_EQ(f(z, id), 0); ASSERT_EQ(f(z, x), 1); ASSERT_EQ(f(z, y), 3); ASSERT_EQ(f(z, z), 0); ASSERT_EQ(f(PauliString::from_str("XX"), PauliString::from_str("XY")), 1); ASSERT_EQ(f(PauliString::from_str("XX"), PauliString::from_str("ZY")), 0); ASSERT_EQ(f(PauliString::from_str("XX"), PauliString::from_str("YY")), 2); for (size_t n : std::vector{1, 499, 4999, 5000}) { auto all_x = PauliString::from_func(false, n, [](size_t i) { return 'X'; }); auto all_z = PauliString::from_func(false, n, [](size_t i) { return 'Z'; }); ASSERT_EQ(f(all_x, all_z), (-(int)n) & 3); } }) TEST_EACH_WORD_SIZE_W(pauli_string, equality, { ASSERT_TRUE(PauliString::from_str("") == PauliString::from_str("")); ASSERT_FALSE(PauliString::from_str("") != PauliString::from_str("")); ASSERT_FALSE(PauliString::from_str("") == PauliString::from_str("-")); ASSERT_FALSE(PauliString::from_str("X") == PauliString::from_str("")); ASSERT_TRUE(PauliString::from_str("XX") == PauliString::from_str("XX")); ASSERT_FALSE(PauliString::from_str("XX") == PauliString::from_str("XY")); ASSERT_FALSE(PauliString::from_str("XX") == PauliString::from_str("XZ")); ASSERT_FALSE(PauliString::from_str("XX") == PauliString::from_str("X_")); auto all_x1 = PauliString::from_func(false, 1000, [](size_t i) { return 'X'; }); auto all_x2 = PauliString::from_func(false, 1000, [](size_t i) { return 'X'; }); auto all_z = PauliString::from_func(false, 1000, [](size_t i) { return 'Z'; }); ASSERT_EQ(all_x1, all_x2); ASSERT_NE(all_x1, all_z); }) TEST_EACH_WORD_SIZE_W(pauli_string, multiplication, { auto x = PauliString::from_str("X"); auto y = PauliString::from_str("Y"); auto z = PauliString::from_str("Z"); auto lhs = x; uint8_t log_i = lhs.ref().inplace_right_mul_returning_log_i_scalar(y); ASSERT_EQ(log_i, 1); ASSERT_EQ(lhs, z); auto xxi = PauliString::from_str("XXI"); auto yyy = PauliString::from_str("YYY"); xxi.ref() *= yyy; ASSERT_EQ(xxi, PauliString::from_str("-ZZY")); }) TEST_EACH_WORD_SIZE_W(pauli_string, identity, { ASSERT_EQ(PauliString(5).str(), "+_____"); }) TEST_EACH_WORD_SIZE_W(pauli_string, gather, { auto p = PauliString::from_str("-____XXXXYYYYZZZZ"); auto p2 = PauliString(4); p.ref().gather_into(p2, std::vector{0, 1, 2, 3}); ASSERT_EQ(p2, PauliString::from_str("+IIII")); p.ref().gather_into(p2, std::vector{4, 7, 8, 9}); ASSERT_EQ(p2, PauliString::from_str("+XXYY")); }) TEST_EACH_WORD_SIZE_W(pauli_string, swap_with_overwrite_with, { auto a = PauliString::from_func(false, 500, [](size_t k) { return "XYZIX"[k % 5]; }); auto b = PauliString::from_func(false, 500, [](size_t k) { return "ZZYIXXY"[k % 7]; }); auto a2 = a; auto b2 = b; a2.ref().swap_with(b2); ASSERT_EQ(a2, b); ASSERT_EQ(b2, a); a2.ref() = b2; ASSERT_EQ(a2, a); ASSERT_EQ(b2, a); }) TEST_EACH_WORD_SIZE_W(pauli_string, scatter, { auto s1 = PauliString::from_str("-_XYZ"); auto s2 = PauliString::from_str("+XXZZ"); auto p = PauliString(8); s1.ref().scatter_into(p, std::vector{1, 3, 5, 7}); ASSERT_EQ(p, PauliString::from_str("-___X_Y_Z")); s1.ref().scatter_into(p, std::vector{1, 3, 5, 7}); ASSERT_EQ(p, PauliString::from_str("+___X_Y_Z")); s2.ref().scatter_into(p, std::vector{1, 3, 5, 7}); ASSERT_EQ(p, PauliString::from_str("+_X_X_Z_Z")); s2.ref().scatter_into(p, std::vector{4, 5, 6, 7}); ASSERT_EQ(p, PauliString::from_str("+_X_XXXZZ")); }) TEST_EACH_WORD_SIZE_W(pauli_string, move_copy_assignment, { PauliString x = PauliString::from_str("XYZ"); ASSERT_EQ(x, PauliString::from_str("XYZ")); // Move. PauliString z = PauliString::from_str("XXY"); x = std::move(z); ASSERT_EQ(x, PauliString::from_str("XXY")); z = PauliString::from_str("-IIX"); x = std::move(z); ASSERT_EQ(x, PauliString::from_str("-IIX")); // Copy. auto y = PauliString::from_str("ZZZ"); x = y; ASSERT_EQ(x, PauliString::from_str("ZZZ")); y = PauliString::from_str("-ZZZ"); x = y; ASSERT_EQ(x, PauliString::from_str("-ZZZ")); }) TEST_EACH_WORD_SIZE_W(pauli_string, foreign_memory, { auto rng = INDEPENDENT_TEST_RNG(); size_t bits = 2048; auto buffer = simd_bits::random(bits, rng); bool signs = false; size_t num_qubits = W * 2 - 12; auto p1 = PauliStringRef(num_qubits, bit_ref(&signs, 0), buffer.word_range_ref(0, 2), buffer.word_range_ref(4, 2)); auto p1b = new PauliStringRef(num_qubits, bit_ref(&signs, 0), buffer.word_range_ref(0, 2), buffer.word_range_ref(4, 2)); auto p2 = PauliStringRef(num_qubits, bit_ref(&signs, 1), buffer.word_range_ref(2, 2), buffer.word_range_ref(6, 2)); PauliString copy_p1 = p1; // p1 aliases p1b. ASSERT_EQ(p1, *p1b); ASSERT_EQ(p1, copy_p1); p1.inplace_right_mul_returning_log_i_scalar(p2); ASSERT_EQ(p1, *p1b); ASSERT_NE(p1, copy_p1); // Deleting p1b shouldn't delete the backing buffer. So p1 survives. copy_p1 = p1; ASSERT_EQ(p1, copy_p1); delete p1b; ASSERT_EQ(p1, copy_p1); }) TEST_EACH_WORD_SIZE_W(pauli_string, commutes, { auto f = [](const char *a, const char *b) { auto pa = PauliString::from_str(a); auto pb = PauliString::from_str(b); return pa.ref().commutes(pb); }; ASSERT_EQ(f("I", "I"), true); ASSERT_EQ(f("I", "X"), true); ASSERT_EQ(f("I", "Y"), true); ASSERT_EQ(f("I", "Z"), true); ASSERT_EQ(f("X", "I"), true); ASSERT_EQ(f("X", "X"), true); ASSERT_EQ(f("X", "Y"), false); ASSERT_EQ(f("X", "Z"), false); ASSERT_EQ(f("Y", "I"), true); ASSERT_EQ(f("Y", "X"), false); ASSERT_EQ(f("Y", "Y"), true); ASSERT_EQ(f("Y", "Z"), false); ASSERT_EQ(f("Z", "I"), true); ASSERT_EQ(f("Z", "X"), false); ASSERT_EQ(f("Z", "Y"), false); ASSERT_EQ(f("Z", "Z"), true); ASSERT_EQ(f("XX", "ZZ"), true); ASSERT_EQ(f("-XX", "ZZ"), true); ASSERT_EQ(f("XZ", "ZZ"), false); ASSERT_EQ(f("-XZ", "ZZ"), false); auto qa = PauliString::from_func(false, 5000, [](size_t k) { return k == 0 ? 'X' : 'Z'; }); auto qb = PauliString::from_func(false, 5000, [](size_t k) { return 'Z'; }); ASSERT_EQ(qa.ref().commutes(qa), true); ASSERT_EQ(qb.ref().commutes(qb), true); ASSERT_EQ(qa.ref().commutes(qb), false); ASSERT_EQ(qb.ref().commutes(qa), false); // Differing sizes. ASSERT_EQ(qa.ref().commutes(PauliString(0)), true); ASSERT_EQ(PauliString(0).ref().commutes(qa), true); }) TEST_EACH_WORD_SIZE_W(pauli_string, sparse_str, { ASSERT_EQ(PauliString::from_str("IIIII").ref().sparse_str(), "+I"); ASSERT_EQ(PauliString::from_str("-IIIII").ref().sparse_str(), "-I"); ASSERT_EQ(PauliString::from_str("IIIXI").ref().sparse_str(), "+X3"); ASSERT_EQ(PauliString::from_str("IYIXZ").ref().sparse_str(), "+Y1*X3*Z4"); ASSERT_EQ(PauliString::from_str("-IYIXZ").ref().sparse_str(), "-Y1*X3*Z4"); auto x501 = PauliString::from_func( false, 1000, [](size_t k) { return "IX"[k == 501]; }) .ref() .sparse_str(); ASSERT_EQ(x501, "+X501"); }) TEST_EACH_WORD_SIZE_W(pauli_string, ensure_num_qubits, { auto p = PauliString::from_str("IXYZ_I"); p.ensure_num_qubits(1, 1.0); ASSERT_EQ(p, PauliString::from_str("IXYZ_I")); p.ensure_num_qubits(6, 1.0); ASSERT_EQ(p, PauliString::from_str("IXYZ_I")); p.ensure_num_qubits(7, 1.0); ASSERT_EQ(p, PauliString::from_str("IXYZ_I_")); p.ensure_num_qubits(1000, 1.0); PauliString p2(1000); p2.xs[1] = true; p2.xs[2] = true; p2.zs[2] = true; p2.zs[3] = true; ASSERT_EQ(p, p2); }) TEST_EACH_WORD_SIZE_W(pauli_string, ensure_num_qubits_padded, { auto p = PauliString::from_str("IXYZ_I"); auto p2 = p; p.ensure_num_qubits(1121, 10.0); p2.ensure_num_qubits(1121, 1.0); ASSERT_GT(p.xs.num_simd_words, p2.xs.num_simd_words); ASSERT_EQ(p, p2); }) TEST(PauliString, pauli_xz_to_xyz) { ASSERT_EQ(pauli_xz_to_xyz(false, false), 0); ASSERT_EQ(pauli_xz_to_xyz(true, false), 1); ASSERT_EQ(pauli_xz_to_xyz(true, true), 2); ASSERT_EQ(pauli_xz_to_xyz(false, true), 3); } TEST(PauliString, pauli_xyz_to_xz) { uint8_t x = 1; uint8_t z = 2; ASSERT_EQ(pauli_xyz_to_xz(0), 0); ASSERT_EQ(pauli_xyz_to_xz(1), x); ASSERT_EQ(pauli_xyz_to_xz(2), x + z); ASSERT_EQ(pauli_xyz_to_xz(3), z); } TEST_EACH_WORD_SIZE_W(pauli_string, py_get_item, { auto p = PauliString::from_str("-XYZ_XYZ_XX"); ASSERT_EQ(p.py_get_item(0), 1); ASSERT_EQ(p.py_get_item(1), 2); ASSERT_EQ(p.py_get_item(2), 3); ASSERT_EQ(p.py_get_item(3), 0); ASSERT_EQ(p.py_get_item(8), 1); ASSERT_EQ(p.py_get_item(9), 1); ASSERT_EQ(p.py_get_item(-1), 1); ASSERT_EQ(p.py_get_item(-2), 1); ASSERT_EQ(p.py_get_item(-3), 0); ASSERT_EQ(p.py_get_item(-4), 3); ASSERT_EQ(p.py_get_item(-9), 2); ASSERT_EQ(p.py_get_item(-10), 1); ASSERT_ANY_THROW({ p.py_get_item(10); }); ASSERT_ANY_THROW({ p.py_get_item(-11); }); }) TEST_EACH_WORD_SIZE_W(pauli_string, py_get_slice, { auto p = PauliString::from_str("-XYZ_XYZ_YX"); ASSERT_EQ(p.py_get_slice(0, 2, 0), PauliString::from_str("")); ASSERT_EQ(p.py_get_slice(0, 2, 1), PauliString::from_str("X")); ASSERT_EQ(p.py_get_slice(0, 2, 2), PauliString::from_str("XZ")); ASSERT_EQ(p.py_get_slice(1, 2, 2), PauliString::from_str("Y_")); ASSERT_EQ(p.py_get_slice(5, 1, 4), PauliString::from_str("YZ_Y")); ASSERT_EQ(p.py_get_slice(5, -1, 4), PauliString::from_str("YX_Z")); }) TEST_EACH_WORD_SIZE_W(pauli_string, after_circuit, { auto actual = PauliString::from_str("+_XYZ").ref().after(Circuit(R"CIRCUIT( H 1 CNOT 1 2 S 2 )CIRCUIT")); ASSERT_EQ(actual, PauliString::from_str("-__XZ")); actual = PauliString::from_str("+X___").ref().after(Circuit(R"CIRCUIT( CX 0 1 1 2 2 3 )CIRCUIT")); ASSERT_EQ(actual, PauliString::from_str("+XXXX")); actual = PauliString::from_str("+X___").ref().after(Circuit(R"CIRCUIT( REPEAT 6 { CX 0 1 1 2 2 3 } )CIRCUIT")); ASSERT_EQ(actual, PauliString::from_str("+X_X_")); ASSERT_THROW( { PauliString::from_str("+X___").ref().after(Circuit(R"CIRCUIT( M(0.1) 0 )CIRCUIT")); }, std::invalid_argument); ASSERT_THROW( { PauliString::from_str("+X").ref().after(Circuit(R"CIRCUIT( Z_ERROR(0.1) 0 )CIRCUIT")); }, std::invalid_argument); ASSERT_THROW( { PauliString::from_str("+X").ref().after(Circuit(R"CIRCUIT( H 9 )CIRCUIT")); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, before_after_circuit_ignores_annotations, { auto c = Circuit(R"CIRCUIT( QUBIT_COORDS(2, 3) 5 REPEAT 5 { DETECTOR rec[-1] } H 1 TICK OBSERVABLE_INCLUDE(0) rec[-1] CNOT 1 2 S 2 SHIFT_COORDS(1, 2, 3) TICK )CIRCUIT"); auto before = PauliString::from_str("+_XYZ"); auto after = PauliString::from_str("-__XZ"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); }) TEST_EACH_WORD_SIZE_W(pauli_string, before_after_circuit_understands_avoiding_resets, { auto c = Circuit(R"CIRCUIT( R 0 MR 1 RX 2 MRX 3 RY 4 MRY 5 H 6 )CIRCUIT"); auto before = PauliString::from_str("+______X"); auto after = PauliString::from_str("+______Z"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); ASSERT_THROW({ PauliString::from_str("+Z______").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+X______").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+_Z_____").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+__Z____").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+___Z___").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+____Z__").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+_____Z_").ref().after(c); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, before_after_circuit_understands_commutation_with_m, { auto c = Circuit(R"CIRCUIT( M 0 H 1 )CIRCUIT"); auto before = PauliString::from_str("+_X"); auto after = PauliString::from_str("+_Z"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = after = PauliString::from_str("+Z_"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); ASSERT_THROW({ PauliString::from_str("+X_").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+Y_").ref().after(c); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, before_after_circuit_understands_commutation_with_mx, { auto c = Circuit(R"CIRCUIT( MX 0 H 1 )CIRCUIT"); auto before = PauliString::from_str("+_X"); auto after = PauliString::from_str("+_Z"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = after = PauliString::from_str("+X_"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); ASSERT_THROW({ PauliString::from_str("+Z_").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+Y_").ref().after(c); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, before_after_circuit_understands_commutation_with_my, { auto c = Circuit(R"CIRCUIT( MY 0 H 1 )CIRCUIT"); auto before = PauliString::from_str("+_X"); auto after = PauliString::from_str("+_Z"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = after = PauliString::from_str("+Y_"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); ASSERT_THROW({ PauliString::from_str("+X_").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+Z_").ref().after(c); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, before_after_circuit_understands_commutation_with_mpp, { auto c = Circuit(R"CIRCUIT( MPP X2*Y3*Z4 X5*X6 H 1 )CIRCUIT"); auto before = PauliString::from_str("+_X_____"); auto after = PauliString::from_str("+_Z_____"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = PauliString::from_str("+_XXYZXX"); after = PauliString::from_str("+_ZXYZXX"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = PauliString::from_str("+_XX____"); after = PauliString::from_str("+_ZX____"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = after = PauliString::from_str("+__ZX___"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = after = PauliString::from_str("+__ZZ_ZZ"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = after = PauliString::from_str("+___XYZZ"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = after = PauliString::from_str("+__XXYZZ"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); before = after = PauliString::from_str("+__X____"); ASSERT_EQ(before.ref().after(c), after); ASSERT_EQ(after.ref().before(c), before); ASSERT_THROW({ PauliString::from_str("+__XXYZX").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+_____ZX").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+__Z____").ref().after(c); }, std::invalid_argument); ASSERT_THROW({ PauliString::from_str("+__XXYXY").ref().after(c); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, before_circuit, { auto actual = PauliString::from_str("-__XZ").ref().before(Circuit(R"CIRCUIT( H 1 CNOT 1 2 S 2 )CIRCUIT")); ASSERT_EQ(actual, PauliString::from_str("+_XYZ")); actual = PauliString::from_str("+XXXX").ref().before(Circuit(R"CIRCUIT( CX 0 1 1 2 2 3 )CIRCUIT")); ASSERT_EQ(actual, PauliString::from_str("+X___")); actual = PauliString::from_str("+X_X_").ref().after(Circuit(R"CIRCUIT( REPEAT 6 { CX 0 1 1 2 2 3 } )CIRCUIT")); ASSERT_EQ(actual, PauliString::from_str("+X___")); ASSERT_THROW( { PauliString::from_str("+X___").ref().before(Circuit(R"CIRCUIT( M(0.1) 0 )CIRCUIT")); }, std::invalid_argument); ASSERT_THROW( { PauliString::from_str("+X").ref().before(Circuit(R"CIRCUIT( H 9 )CIRCUIT")); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, after_tableau, { auto actual = PauliString::from_str("+XZ_").ref().after(GATE_DATA.at("CX").tableau(), std::vector{0, 1, 1, 2}); ASSERT_EQ(actual, PauliString::from_str("-YYX")); ASSERT_THROW( { PauliString::from_str("+XZ_").ref().after(GATE_DATA.at("CX").tableau(), std::vector{0, 1, 1}); }, std::invalid_argument); ASSERT_THROW( { PauliString::from_str("+XZ_").ref().after(GATE_DATA.at("CX").tableau(), std::vector{0, 5}); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, before_tableau, { auto actual = PauliString::from_str("-YYX").ref().before(GATE_DATA.at("CX").tableau(), std::vector{0, 1, 1, 2}); ASSERT_EQ(actual, PauliString::from_str("+XZ_")); ASSERT_THROW( { PauliString::from_str("+XZ_").ref().before( GATE_DATA.at("CX").tableau(), std::vector{0, 1, 1}); }, std::invalid_argument); ASSERT_THROW( { PauliString::from_str("+XZ_").ref().before(GATE_DATA.at("CX").tableau(), std::vector{0, 5}); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(pauli_string, left_mul_pauli, { PauliString p(0); bool imag = false; p.left_mul_pauli(GateTarget::x(5), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("_____X")); p.left_mul_pauli(GateTarget::x(5), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("______")); p.left_mul_pauli(GateTarget::x(5), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("_____X")); p.left_mul_pauli(GateTarget::z(5), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("_____Y")); p.left_mul_pauli(GateTarget::z(5), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("_____X")); p.left_mul_pauli(GateTarget::y(5), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-_____Z")); p.left_mul_pauli(GateTarget::y(15), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-_____Z_________Y")); p.left_mul_pauli(GateTarget::y(15, true), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("_____Z__________")); }) TEST_EACH_WORD_SIZE_W(pauli_string, left_mul_pauli_mul_table, { PauliString p(1); bool imag = false; p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::x(0), &imag); p.left_mul_pauli(GateTarget::y(0), &imag); p.left_mul_pauli(GateTarget::z(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-I")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::z(0), &imag); p.left_mul_pauli(GateTarget::y(0), &imag); p.left_mul_pauli(GateTarget::x(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("I")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::x(0), &imag); p.left_mul_pauli(GateTarget::x(0), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("I")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::x(0), &imag); p.left_mul_pauli(GateTarget::y(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-Z")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::x(0), &imag); p.left_mul_pauli(GateTarget::z(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("Y")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::y(0), &imag); p.left_mul_pauli(GateTarget::x(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("Z")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::y(0), &imag); p.left_mul_pauli(GateTarget::y(0), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("I")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::y(0), &imag); p.left_mul_pauli(GateTarget::z(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-X")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::z(0), &imag); p.left_mul_pauli(GateTarget::x(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-Y")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::z(0), &imag); p.left_mul_pauli(GateTarget::y(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("X")); p = PauliString(1); imag = false; p.left_mul_pauli(GateTarget::z(0), &imag); p.left_mul_pauli(GateTarget::z(0), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("I")); }) TEST_EACH_WORD_SIZE_W(pauli_string, right_mul_pauli_mul_table, { PauliString p(1); bool imag = false; p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::x(0), &imag); p.right_mul_pauli(GateTarget::y(0), &imag); p.right_mul_pauli(GateTarget::z(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("+I")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::z(0), &imag); p.right_mul_pauli(GateTarget::y(0), &imag); p.right_mul_pauli(GateTarget::x(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-I")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::x(0), &imag); p.right_mul_pauli(GateTarget::x(0), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("I")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::x(0), &imag); p.right_mul_pauli(GateTarget::y(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("Z")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::x(0), &imag); p.right_mul_pauli(GateTarget::z(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-Y")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::y(0), &imag); p.right_mul_pauli(GateTarget::x(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-Z")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::y(0), &imag); p.right_mul_pauli(GateTarget::y(0), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("I")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::y(0), &imag); p.right_mul_pauli(GateTarget::z(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("X")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::z(0), &imag); p.right_mul_pauli(GateTarget::x(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("Y")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::z(0), &imag); p.right_mul_pauli(GateTarget::y(0), &imag); ASSERT_EQ(imag, 1); ASSERT_EQ(p, PauliString("-X")); p = PauliString(1); imag = false; p.right_mul_pauli(GateTarget::z(0), &imag); p.right_mul_pauli(GateTarget::z(0), &imag); ASSERT_EQ(imag, 0); ASSERT_EQ(p, PauliString("I")); }) ================================================ FILE: src/stim/stabilizers/pauli_string_iter.h ================================================ /* * 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. */ #ifndef _STIM_STABILIZERS_PAULI_STRING_ITER_H #define _STIM_STABILIZERS_PAULI_STRING_ITER_H #include "stim/mem/fixed_cap_vector.h" #include "stim/mem/span_ref.h" #include "stim/stabilizers/tableau.h" namespace stim { /// Tracks the state of a loop. struct NestedLooperLoop { /// The first index that should be iterated. uint64_t start; /// One past the last index that should be iterated. uint64_t end; /// If this is set to the index of another loop, the starting offset is shifted by that other loop's value. /// Set to UINT64_MAX to not use. /// This is used in 'append_combination_loops' to avoid repeating combinations. uint64_t offset_from_other = UINT64_MAX; /// The current value of the iteration variable for this loop. /// UINT64_MAX means 'loop is not started yet'. uint64_t cur = UINT64_MAX; }; /// A helper class for managing dynamically nested loops. struct NestedLooper { std::vector loops; uint64_t k = 0; /// Adds a series of nested loops for iterating combinations of w values from [0, n). inline void append_combination_loops(uint64_t n, uint64_t w) { if (w > 0) { loops.push_back(NestedLooperLoop{0, n - w + 1}); for (uint64_t j = 1; j < w; j++) { auto v = loops.size() - 1; loops.push_back(NestedLooperLoop{1, n - w + j + 1, v}); } } } /// Clears all loop variables and sets the loop index to the outermost loop. inline void start() { k = 0; for (auto &loop : loops) { loop.cur = UINT64_MAX; } } inline bool iter_next(const std::function &on_iter) { if (loops.empty()) { return false; } // k is the index of the loop to advance. // In the first step, k will be 0. // In later step, k is loops.size(). if (k == loops.size()) { // Drop k by 1 to advance the innermost loop. k--; } while (true) { // Start or advance the current loop. if (loops[k].cur == UINT64_MAX) { loops[k].cur = loops[k].start; if (loops[k].offset_from_other != UINT64_MAX) { loops[k].cur += loops[loops[k].offset_from_other].cur; } } else { loops[k].cur++; } // Notify the caller so they can dynamically add inner loops if needed. on_iter(k); // Check if the current loop has ended. if (loops[k].cur >= loops[k].end) { if (k == 0) { // The outermost loop ended. return false; } loops[k].cur = UINT64_MAX; k -= 1; continue; } // Move down to the next loop. k++; if (k == loops.size()) { // We're inside the innermost loop. return true; } } } }; /// Iterates over pauli strings matching specified parameters. /// /// The template parameter, W, represents the SIMD width. template struct PauliStringIterator { // Parameter storage. size_t num_qubits; /// Number of qubits in results. size_t min_weight; /// Minimum number of non-identity terms in results. size_t max_weight; /// Maximum number of non-identity terms in results. bool allow_x; /// Whether results are permitted to contain 'X' terms. bool allow_y; /// Whether results are permitted to contain 'Y' terms. bool allow_z; /// Whether results are permitted to contain 'Z' terms. // Progress storage. NestedLooper looper; /// Tracks nested loops over target weight, the target qubits, and the target paulis. PauliString result; /// When iter_next() returns true, the result is stored in this field. PauliStringIterator( size_t num_qubits, size_t min_weight, size_t max_weight, bool allow_x, bool allow_y, bool allow_z); /// Updates the `result` field to point at the next yielded pauli string. /// Returns true if this succeeded, or false if iteration has ended. bool iter_next(); // Restarts iteration. void restart(); }; } // namespace stim #include "stim/stabilizers/pauli_string_iter.inl" #endif ================================================ FILE: src/stim/stabilizers/pauli_string_iter.inl ================================================ // 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. #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/pauli_string_iter.h" namespace stim { template PauliStringIterator::PauliStringIterator( size_t num_qubits, size_t min_weight, size_t max_weight, bool allow_x, bool allow_y, bool allow_z) : num_qubits(num_qubits), min_weight(min_weight), max_weight(max_weight), allow_x(allow_x), allow_y(allow_y), allow_z(allow_z), result(num_qubits) { restart(); } template bool PauliStringIterator::iter_next() { return looper.iter_next([&](size_t loop_index) { const NestedLooperLoop &loop = looper.loops[loop_index]; if (loop_index == 0) { // Reached a new weight. Need to iterate over xs. looper.loops.resize(loop_index + 1); looper.append_combination_loops(num_qubits, loop.cur); } else if (loop_index == looper.loops[0].cur) { // Reached a new weight mask. Need to iterate over X/Z values. looper.loops.resize(loop_index + 1); result.xs.clear(); result.zs.clear(); size_t pauli_weight = allow_x + allow_y + allow_z; for (size_t j = 0; j < looper.loops[0].cur; j++) { looper.loops.push_back(NestedLooperLoop{1, 1 + pauli_weight}); } } else if (loop_index > looper.loops[0].cur) { // Iterating a pauli. Keep the results up to date as the paulis change. auto q = looper.loops[loop_index - looper.loops[0].cur].cur; auto v = loop.cur; v += !allow_x && v >= 1; v += !allow_y && v >= 2; v += !allow_z && v >= 3; bool y = (v & 1) != 0; bool z = (v & 2) != 0; result.xs[q] = y ^ z; result.zs[q] = z; } }); } template void PauliStringIterator::restart() { looper.loops.clear(); size_t clamped_max_weight = std::min(max_weight, num_qubits); if (clamped_max_weight >= min_weight) { looper.loops.push_back({min_weight, clamped_max_weight + 1, UINT64_MAX}); } looper.start(); } } // namespace stim ================================================ FILE: src/stim/stabilizers/pauli_string_iter.perf.cc ================================================ // 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. #include "stim/stabilizers/pauli_string_iter.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(pauli_iter_xz_2_to_5_of_5) { size_t c = 0; size_t n = 0; benchmark_go([&]() { PauliStringIterator iter(5, 2, 5, true, false, true); n = 0; while (iter.iter_next()) { c += iter.result.num_qubits; n += 1; } }) .goal_micros(8) .show_rate("PauliStrings", n); if (c == 0) { std::cerr << "use the output\n"; } } BENCHMARK(pauli_iter_xyz_1_of_1000) { size_t c = 0; size_t n = 0; benchmark_go([&]() { PauliStringIterator iter(1000, 1, 1, true, true, true); n = 0; while (iter.iter_next()) { c += iter.result.num_qubits; n += 1; } }) .goal_micros(55) .show_rate("PauliStrings", n); if (c == 0) { std::cerr << "use the output\n"; } } ================================================ FILE: src/stim/stabilizers/pauli_string_iter.pybind.cc ================================================ // 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. #include "stim/stabilizers/pauli_string_iter.pybind.h" #include "pauli_string.pybind.h" #include "stim/py/base.pybind.h" using namespace stim; using namespace stim_pybind; pybind11::class_> stim_pybind::pybind_pauli_string_iter(pybind11::module &m) { auto c = pybind11::class_>( m, "PauliStringIterator", clean_doc_string(R"DOC( Iterates over all pauli strings matching specified patterns. Examples: >>> import stim >>> pauli_string_iterator = stim.PauliString.iter_all( ... 2, ... min_weight=1, ... max_weight=1, ... allowed_paulis="XZ", ... ) >>> for p in pauli_string_iterator: ... print(p) +X_ +Z_ +_X +_Z )DOC") .data()); return c; } void stim_pybind::pybind_pauli_string_iter_methods( pybind11::module &m, pybind11::class_> &c) { c.def( "__iter__", [](PauliStringIterator &self) -> PauliStringIterator { PauliStringIterator copy = self; return copy; }, clean_doc_string(R"DOC( Returns an independent copy of the pauli string iterator. Since for-loops and loop-comprehensions call `iter` on things they iterate, this effectively allows the iterator to be iterated multiple times. )DOC") .data()); c.def( "__next__", [](PauliStringIterator &self) -> FlexPauliString { if (!self.iter_next()) { throw pybind11::stop_iteration(); } return FlexPauliString(self.result); }, clean_doc_string(R"DOC( Returns the next iterated pauli string. )DOC") .data()); } ================================================ FILE: src/stim/stabilizers/pauli_string_iter.pybind.h ================================================ // 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. #ifndef _STIM_STABILIZERS_PAULI_STRING_ITER_PYBIND_H #define _STIM_STABILIZERS_PAULI_STRING_ITER_PYBIND_H #include #include "stim/stabilizers/pauli_string_iter.h" namespace stim_pybind { pybind11::class_> pybind_pauli_string_iter(pybind11::module &m); void pybind_pauli_string_iter_methods( pybind11::module &m, pybind11::class_> &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/stabilizers/pauli_string_iter.test.cc ================================================ // 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. #include "stim/stabilizers/pauli_string_iter.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" using namespace stim; std::vector loop_state(const NestedLooper &looper) { std::vector state; for (const auto &e : looper.loops) { state.push_back(e.cur); } return state; } std::vector> loop_drain(NestedLooper &looper) { std::vector> state; looper.start(); while (looper.iter_next([](size_t k) { })) { state.push_back(loop_state(looper)); } return state; } TEST(pauli_string_iter, NestedLooper_simple) { NestedLooper looper; looper.loops.push_back(NestedLooperLoop{0, 3}); looper.loops.push_back(NestedLooperLoop{2, 6}); ASSERT_EQ( loop_drain(looper), (std::vector>{ {0, 2}, {0, 3}, {0, 4}, {0, 5}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {2, 2}, {2, 3}, {2, 4}, {2, 5}, })); } TEST(pauli_string_iter, NestedLooper_shifted) { NestedLooper looper; looper.loops.push_back(NestedLooperLoop{0, 3}); looper.loops.push_back(NestedLooperLoop{2, 6, 0}); ASSERT_EQ( loop_drain(looper), (std::vector>{ {0, 2}, {0, 3}, {0, 4}, {0, 5}, {1, 3}, {1, 4}, {1, 5}, {2, 4}, {2, 5}, })); } TEST(pauli_string_iter, NestedLooper_append_combination_loops) { NestedLooper looper; looper.append_combination_loops(6, 3); ASSERT_EQ( loop_drain(looper), (std::vector>{ {0, 1, 2}, {0, 1, 3}, {0, 1, 4}, {0, 1, 5}, {0, 2, 3}, {0, 2, 4}, {0, 2, 5}, {0, 3, 4}, {0, 3, 5}, {0, 4, 5}, {1, 2, 3}, {1, 2, 4}, {1, 2, 5}, {1, 3, 4}, {1, 3, 5}, {1, 4, 5}, {2, 3, 4}, {2, 3, 5}, {2, 4, 5}, {3, 4, 5}, })); looper.loops.clear(); looper.append_combination_loops(10, 9); ASSERT_EQ( loop_drain(looper), (std::vector>{ {0, 1, 2, 3, 4, 5, 6, 7, 8}, {0, 1, 2, 3, 4, 5, 6, 7, 9}, {0, 1, 2, 3, 4, 5, 6, 8, 9}, {0, 1, 2, 3, 4, 5, 7, 8, 9}, {0, 1, 2, 3, 4, 6, 7, 8, 9}, {0, 1, 2, 3, 5, 6, 7, 8, 9}, {0, 1, 2, 4, 5, 6, 7, 8, 9}, {0, 1, 3, 4, 5, 6, 7, 8, 9}, {0, 2, 3, 4, 5, 6, 7, 8, 9}, {1, 2, 3, 4, 5, 6, 7, 8, 9}, })); } TEST(pauli_string_iter, NestedLooper_inplace_edit) { NestedLooper looper; looper.loops.push_back(NestedLooperLoop{1, 3}); std::vector> state; looper.start(); while (looper.iter_next([&](size_t k) { if (k == 0 && looper.loops[0].cur == 2) { looper.loops.push_back(NestedLooperLoop{2, 4}); } })) { state.push_back(loop_state(looper)); } ASSERT_EQ( state, (std::vector>{ {1}, {2, 2}, {2, 3}, })); } template std::vector record_pauli_string(PauliStringIterator iter) { std::vector results; while (iter.iter_next()) { results.push_back(iter.result.str()); } return results; } TEST_EACH_WORD_SIZE_W(pauli_string_iter, small_cases, { // Empty. ASSERT_EQ( record_pauli_string(PauliStringIterator(3, 0, 0, true, true, true)), (std::vector{ "+___", })); // Empty or single. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 1, true, true, true)), (std::vector{ "+__", "+X_", "+Y_", "+Z_", "+_X", "+_Y", "+_Z", })); // Single. ASSERT_EQ( record_pauli_string(PauliStringIterator(3, 1, 1, true, true, true)), (std::vector{ "+X__", "+Y__", "+Z__", "+_X_", "+_Y_", "+_Z_", "+__X", "+__Y", "+__Z", })); // Full doubles. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 2, 2, true, true, true)), (std::vector{ "+XX", "+XY", "+XZ", "+YX", "+YY", "+YZ", "+ZX", "+ZY", "+ZZ", })); // All length 2. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 2, true, true, true)), (std::vector{ "+__", "+X_", "+Y_", "+Z_", "+_X", "+_Y", "+_Z", "+XX", "+XY", "+XZ", "+YX", "+YY", "+YZ", "+ZX", "+ZY", "+ZZ", })); // XY subset. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 2, false, true, true)), (std::vector{ "+__", "+Y_", "+Z_", "+_Y", "+_Z", "+YY", "+YZ", "+ZY", "+ZZ", })); // XZ subset. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 2, true, false, true)), (std::vector{ "+__", "+X_", "+Z_", "+_X", "+_Z", "+XX", "+XZ", "+ZX", "+ZZ", })); // YZ subset. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 2, true, true, false)), (std::vector{ "+__", "+X_", "+Y_", "+_X", "+_Y", "+XX", "+XY", "+YX", "+YY", })); // X subset. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 2, true, false, false)), (std::vector{ "+__", "+X_", "+_X", "+XX", })); // Y subset. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 2, false, true, false)), (std::vector{ "+__", "+Y_", "+_Y", "+YY", })); // Z subset. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 2, false, false, true)), (std::vector{ "+__", "+Z_", "+_Z", "+ZZ", })); // No pauli subset. ASSERT_EQ( record_pauli_string(PauliStringIterator(2, 0, 2, false, false, false)), (std::vector{ "+__", })); ASSERT_EQ(record_pauli_string(PauliStringIterator(2, 1, 2, false, false, false)), (std::vector{})); ASSERT_EQ(record_pauli_string(PauliStringIterator(2, 3, 6, false, false, false)), (std::vector{})); ASSERT_EQ(record_pauli_string(PauliStringIterator(2, 2, 1, false, false, false)), (std::vector{})); }) ================================================ FILE: src/stim/stabilizers/pauli_string_pybind_test.py ================================================ # 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. import itertools import numpy as np import stim import pytest def test_identity(): p = stim.PauliString(3) assert len(p) == 3 assert p[0] == p[1] == p[2] == 0 assert p.sign == +1 def test_from_str(): p = stim.PauliString("-_XYZ_ZYX") assert len(p) == 8 assert p[0] == 0 assert p[1] == 1 assert p[2] == 2 assert p[3] == 3 assert p[4] == 0 assert p[5] == 3 assert p[6] == 2 assert p[7] == 1 assert p.sign == -1 p = stim.PauliString("") assert len(p) == 0 assert p.sign == +1 p = stim.PauliString("X") assert len(p) == 1 assert p[0] == 1 assert p.sign == +1 p = stim.PauliString("+X") assert len(p) == 1 assert p[0] == 1 assert p.sign == +1 p = stim.PauliString("iX") assert len(p) == 1 assert p[0] == 1 assert p.sign == 1j p = stim.PauliString("+iX") assert len(p) == 1 assert p[0] == 1 assert p.sign == 1j p = stim.PauliString("-iX") assert len(p) == 1 assert p[0] == 1 assert p.sign == -1j assert stim.PauliString("X5*Y10") == stim.PauliString("_____X____Y") assert stim.PauliString("X5*Y5") == stim.PauliString("iZ5") def test_equality(): assert not (stim.PauliString(4) == None) assert not (stim.PauliString(4) == "other object") assert not (stim.PauliString(4) == object()) assert stim.PauliString(4) != None assert stim.PauliString(4) != "other object" assert stim.PauliString(4) != object() assert stim.PauliString(4) == stim.PauliString(4) assert stim.PauliString(3) != stim.PauliString(4) assert not (stim.PauliString(4) != stim.PauliString(4)) assert not (stim.PauliString(3) == stim.PauliString(4)) assert stim.PauliString("+X") == stim.PauliString("+X") assert stim.PauliString("+X") != stim.PauliString("-X") assert stim.PauliString("+X") != stim.PauliString("+Y") assert stim.PauliString("+X") != stim.PauliString("-Y") assert stim.PauliString("+X") != stim.PauliString("+iX") assert stim.PauliString("+X") != stim.PauliString("-iX") assert stim.PauliString("__") != stim.PauliString("_X") assert stim.PauliString("__") != stim.PauliString("X_") assert stim.PauliString("__") != stim.PauliString("XX") assert stim.PauliString("__") == stim.PauliString("__") def test_random(): p1 = stim.PauliString.random(100) p2 = stim.PauliString.random(100) assert p1 != p2 seen_signs = {stim.PauliString.random(1).sign for _ in range(200)} assert seen_signs == {1, -1} seen_signs = {stim.PauliString.random(1, allow_imaginary=True).sign for _ in range(200)} assert seen_signs == {1, -1, 1j, -1j} def test_str(): assert str(stim.PauliString(3)) == "+___" assert str(stim.PauliString("XYZ")) == "+XYZ" assert str(stim.PauliString("-XYZ")) == "-XYZ" assert str(stim.PauliString("iXYZ")) == "+iXYZ" assert str(stim.PauliString("-iXYZ")) == "-iXYZ" def test_repr(): assert repr(stim.PauliString(3)) == 'stim.PauliString("+___")' assert repr(stim.PauliString("-XYZ")) == 'stim.PauliString("-XYZ")' vs = [ stim.PauliString(""), stim.PauliString("ZXYZZ"), stim.PauliString("-XYZ"), stim.PauliString("I"), stim.PauliString("iIXYZ"), stim.PauliString("-iIXYZ"), ] for v in vs: r = repr(v) assert eval(r, {'stim': stim}) == v def test_to_tableau(): p = stim.PauliString("XZ_Y") t = p.to_tableau() assert t.x_output(0) == stim.PauliString("+X___") assert t.x_output(1) == stim.PauliString("-_X__") assert t.x_output(2) == stim.PauliString("+__X_") assert t.x_output(3) == stim.PauliString("-___X") assert t.z_output(0) == stim.PauliString("-Z___") assert t.z_output(1) == stim.PauliString("+_Z__") assert t.z_output(2) == stim.PauliString("+__Z_") assert t.z_output(3) == stim.PauliString("-___Z") p_random = stim.PauliString.random(32) p_random.sign = 1 p_random_roundtrip = p_random.to_tableau().to_pauli_string() assert p_random == p_random_roundtrip def test_commutes(): def c(a: str, b: str) -> bool: return stim.PauliString(a).commutes(stim.PauliString(b)) assert c("", "") assert c("X", "_") assert c("X", "X") assert not c("X", "Y") assert not c("X", "Z") assert c("XXXX", "YYYY") assert c("XXXX", "YYYZ") assert not c("XXXX", "XXXZ") assert not c("XXXX", "___Z") assert not c("XXXX", "Z___") assert c("XXXX", "Z_Z_") def test_product(): assert stim.PauliString("") * stim.PauliString("") == stim.PauliString("") assert stim.PauliString("i") * stim.PauliString("i") == stim.PauliString("-") assert stim.PauliString("i") * stim.PauliString("-i") == stim.PauliString("+") assert stim.PauliString("-i") * stim.PauliString("-i") == stim.PauliString("-") assert stim.PauliString("i") * stim.PauliString("-") == stim.PauliString("-i") x = stim.PauliString("X") y = stim.PauliString("Y") z = stim.PauliString("Z") assert x == +1 * x == x * +1 == +x assert x * -1 == -x == -1 * x assert (-x)[0] == 1 assert (-x).sign == -1 assert -(-x) == x assert stim.PauliString(10) * stim.PauliString(11) == stim.PauliString(11) assert x * z == stim.PauliString("-iY") assert x * x == stim.PauliString(1) assert x * y == stim.PauliString("iZ") assert y * x == stim.PauliString("-iZ") assert x * y == 1j * z assert y * x == z * -1j assert x.extended_product(y) == (1, 1j * z) assert y.extended_product(x) == (1, -1j * z) assert x.extended_product(x) == (1, stim.PauliString(1)) xx = stim.PauliString("+XX") yy = stim.PauliString("+YY") zz = stim.PauliString("+ZZ") assert xx * zz == -yy assert xx.extended_product(zz) == (1, -yy) def test_inplace_product(): p = stim.PauliString("X") alias = p p *= 1j assert alias == stim.PauliString("iX") assert alias is p p *= 1j assert alias == stim.PauliString("-X") p *= 1j assert alias == stim.PauliString("-iX") p *= 1j assert alias == stim.PauliString("+X") p *= stim.PauliString("Z") assert alias == stim.PauliString("-iY") p *= -1j assert alias == stim.PauliString("-Y") p *= -1j assert alias == stim.PauliString("iY") p *= -1j assert alias == stim.PauliString("+Y") p *= -1j assert alias == stim.PauliString("-iY") p *= stim.PauliString("i_") assert alias == stim.PauliString("+Y") p *= stim.PauliString("i_") assert alias == stim.PauliString("iY") p *= stim.PauliString("i_") assert alias == stim.PauliString("-Y") p *= stim.PauliString("i_") assert alias == stim.PauliString("-iY") p *= stim.PauliString("-i_") assert alias == stim.PauliString("-Y") p *= stim.PauliString("-i_") assert alias == stim.PauliString("iY") p *= stim.PauliString("-i_") assert alias == stim.PauliString("+Y") p *= stim.PauliString("-i_") assert alias == stim.PauliString("-iY") assert alias is p def test_imaginary_phase(): p = stim.PauliString("IXYZ") ip = stim.PauliString("iIXYZ") assert 1j * p == p * 1j == ip == -stim.PauliString("-iIXYZ") assert p.sign == 1 assert (-p).sign == -1 assert ip.sign == 1j assert (-ip).sign == -1j assert stim.PauliString("X") * stim.PauliString("Y") == 1j * stim.PauliString("Z") assert stim.PauliString("Y") * stim.PauliString("X") == -1j * stim.PauliString("Z") def test_get_set_sign(): p = stim.PauliString(2) assert p.sign == +1 p.sign = -1 assert str(p) == "-__" assert p.sign == -1 p.sign = +1 assert str(p) == "+__" assert p.sign == +1 with pytest.raises(ValueError, match="new_sign"): p.sign = 5 p.sign = 1j assert str(p) == "+i__" assert p.sign == 1j p.sign = -1j assert str(p) == "-i__" assert p.sign == -1j def test_get_set_item(): p = stim.PauliString(5) assert list(p) == [0, 0, 0, 0, 0] assert p[0] == 0 p[0] = 1 assert p[0] == 1 p[0] = 'Y' assert p[0] == 2 p[0] = 'Z' assert p[0] == 3 with pytest.raises(IndexError, match="new_pauli"): p[0] = 't' with pytest.raises(IndexError, match="new_pauli"): p[0] = 10 assert p[1] == 0 p[1] = 2 assert p[1] == 2 def test_get_slice(): p = stim.PauliString("XXXX__YYYY__ZZZZX") assert p[:7] == stim.PauliString("XXXX__Y") assert p[:-3] == stim.PauliString("XXXX__YYYY__ZZ") assert p[::2] == stim.PauliString("XX_YY_ZZX") assert p[::-1] == stim.PauliString("XZZZZ__YYYY__XXXX") assert p[-3:3] == stim.PauliString("") assert p[-6:-1] == stim.PauliString("_ZZZZ") assert p[3:5:-1] == stim.PauliString("") assert p[5:3:-1] == stim.PauliString("__") assert p[4:2:-1] == stim.PauliString("_X") assert p[2:0:-1] == stim.PauliString("XX") def test_copy(): p = stim.PauliString(3) p2 = p.copy() assert p == p2 assert p is not p2 p = stim.PauliString("-i_XYZ") p2 = p.copy() assert p == p2 assert p is not p2 def test_hash(): # stim.PauliString is mutable. It must not also be value-hashable. # Defining __hash__ requires defining a FrozenPauliString variant instead. with pytest.raises(TypeError, match="unhashable"): _ = hash(stim.PauliString(1)) def test_add(): ps = stim.PauliString assert ps(0) + ps(0) == ps(0) assert ps(3) + ps(1000) == ps(1003) assert ps(1000) + ps(3) == ps(1003) assert ps("_XYZ") + ps("_ZZZ_") == ps("_XYZ_ZZZ_") p = ps("_XYZ") p += p assert p == ps("_XYZ_XYZ") for k in range(1, 8): p += p assert p == ps("_XYZ_XYZ" * 2**k) p = ps("_XXX") p += ps("Y") assert p == ps("_XXXY") p = ps("") alias = p p += ps("X") assert alias is p assert alias == ps("X") p += p assert alias is p assert alias == ps("XX") def test_mul_different_sizes(): ps = stim.PauliString assert ps("") * ps("X" * 1000) == ps("X" * 1000) assert ps("X" * 1000) * ps("") == ps("X" * 1000) assert ps("Z" * 1000) * ps("") == ps("Z" * 1000) p = ps("Z") alias = p p *= ps("ZZZ") assert p == ps("_ZZ") p *= ps("Z") assert p == ps("ZZZ") assert alias is p def test_div(): assert stim.PauliString("+XYZ") / +1 == stim.PauliString("+XYZ") assert stim.PauliString("+XYZ") / -1 == stim.PauliString("-XYZ") assert stim.PauliString("+XYZ") / 1j == stim.PauliString("-iXYZ") assert stim.PauliString("+XYZ") / -1j == stim.PauliString("iXYZ") assert stim.PauliString("iXYZ") / 1j == stim.PauliString("XYZ") p = stim.PauliString("__") alias = p assert p / -1 == stim.PauliString("-__") assert alias == stim.PauliString("__") p /= -1 assert alias == stim.PauliString("-__") p /= 1j assert alias == stim.PauliString("i__") p /= 1j assert alias == stim.PauliString("__") p /= -1j assert alias == stim.PauliString("i__") p /= 1 assert alias == stim.PauliString("i__") def test_mul_repeat(): ps = stim.PauliString assert ps("") * 100 == ps("") assert ps("X") * 100 == ps("X" * 100) assert ps("XYZ_") * 1000 == ps("XYZ_" * 1000) assert ps("XYZ_") * 1 == ps("XYZ_") assert ps("XYZ_") * 0 == ps("") assert 100 * ps("") == ps("") assert 100 * ps("X") == ps("X" * 100) assert 1000 * ps("XYZ_") == ps("XYZ_" * 1000) assert 1 * ps("XYZ_") == ps("XYZ_") assert 0 * ps("XYZ_") == ps("") assert ps("i") * 0 == ps("+") assert ps("i") * 1 == ps("i") assert ps("i") * 2 == ps("-") assert ps("i") * 3 == ps("-i") assert ps("i") * 4 == ps("+") assert ps("i") * 5 == ps("i") assert ps("-i") * 0 == ps("+") assert ps("-i") * 1 == ps("-i") assert ps("-i") * 2 == ps("-") assert ps("-i") * 3 == ps("i") assert ps("-i") * 4 == ps("+") assert ps("-i") * 5 == ps("-i") assert ps("-") * 0 == ps("+") assert ps("-") * 1 == ps("-") assert ps("-") * 2 == ps("+") assert ps("-") * 3 == ps("-") assert ps("-") * 4 == ps("+") assert ps("-") * 5 == ps("-") p = ps("XYZ") alias = p p *= 1000 assert p == ps("XYZ" * 1000) assert alias is p def test_init_list(): assert stim.PauliString([]) == stim.PauliString(0) assert stim.PauliString([0, 1, 2, 3]) == stim.PauliString("_XYZ") with pytest.raises(ValueError, match="pauli"): _ = stim.PauliString([-1]) with pytest.raises(ValueError, match="pauli"): _ = stim.PauliString([4]) with pytest.raises(ValueError): _ = stim.PauliString([2**500]) def test_init_copy(): p = stim.PauliString("_XYZ") p2 = stim.PauliString(p) assert p is not p2 assert p == p2 p = stim.PauliString("-i_XYZ") p2 = stim.PauliString(p) assert p is not p2 assert p == p2 def test_commutes_different_lengths(): x1000 = stim.PauliString("X" * 1000) z1000 = stim.PauliString("Z" * 1000) x1 = stim.PauliString("X") z1 = stim.PauliString("Z") assert x1.commutes(x1000) assert x1000.commutes(x1) assert z1.commutes(z1000) assert z1000.commutes(z1) assert not z1.commutes(x1000) assert not x1000.commutes(z1) assert not x1.commutes(z1000) assert not z1000.commutes(x1) def test_pickle(): import pickle t = stim.PauliString.random(4) a = pickle.dumps(t) assert pickle.loads(a) == t t = stim.PauliString("i_XYZ") a = pickle.dumps(t) assert pickle.loads(a) == t def test_to_numpy(): p = stim.PauliString("_XYZ___XYXZYZ") xs, zs = p.to_numpy() assert xs.dtype == np.bool_ assert zs.dtype == np.bool_ np.testing.assert_array_equal(xs, [0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0]) np.testing.assert_array_equal(zs, [0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1]) xs, zs = p.to_numpy(bit_packed=True) assert xs.dtype == np.uint8 assert zs.dtype == np.uint8 np.testing.assert_array_equal(xs, [0x86, 0x0B]) np.testing.assert_array_equal(zs, [0x0C, 0x1D]) def test_from_numpy(): p = stim.PauliString.from_numpy( xs=np.array([0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0], dtype=np.bool_), zs=np.array([0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1], dtype=np.bool_)) assert p == stim.PauliString("_XYZ___XYXZYZ") p = stim.PauliString.from_numpy( xs=np.array([0x86, 0x0B], dtype=np.uint8), zs=np.array([0x0C, 0x1D], dtype=np.uint8), num_qubits=13) assert p == stim.PauliString("_XYZ___XYXZYZ") p = stim.PauliString.from_numpy( xs=np.array([0x86, 0x0B], dtype=np.uint8), zs=np.array([0x0C, 0x1D], dtype=np.uint8), num_qubits=15, sign=1j) assert p == stim.PauliString("i_XYZ___XYXZYZ__") def test_from_numpy_bad_bit_packed_len(): xs = np.array([0x86, 0x0B], dtype=np.uint8) zs = np.array([0x0C, 0x1D], dtype=np.uint8) with pytest.raises(ValueError, match="specify expected number"): stim.PauliString.from_numpy(xs=xs, zs=zs) with pytest.raises(ValueError, match="between 9 and 16 bits"): stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=100) with pytest.raises(ValueError, match="between 9 and 16 bits"): stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=0) with pytest.raises(ValueError, match="between 9 and 16 bits"): stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=8) with pytest.raises(ValueError, match="between 9 and 16 bits"): stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=17) with pytest.raises(ValueError, match="between 0 and 0 bits"): stim.PauliString.from_numpy(xs=xs[:0], zs=zs, num_qubits=9) with pytest.raises(ValueError, match="between 1 and 8 bits"): stim.PauliString.from_numpy(xs=xs[:1], zs=zs, num_qubits=9) with pytest.raises(ValueError, match="between 1 and 8 bits"): stim.PauliString.from_numpy(xs=xs, zs=zs[:1], num_qubits=9) with pytest.raises(ValueError, match="1-dimensional"): stim.PauliString.from_numpy(xs=np.array([xs, xs]), zs=np.array([zs, zs]), num_qubits=9) with pytest.raises(ValueError, match="uint8"): stim.PauliString.from_numpy(xs=np.array(xs, dtype=np.uint64), zs=np.array(xs, dtype=np.uint64), num_qubits=9) def test_from_numpy_bad_bool_len(): xs = np.array([0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0], dtype=np.bool_) zs = np.array([0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0], dtype=np.bool_) with pytest.raises(ValueError, match="shape=13"): stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=12) with pytest.raises(ValueError, match="shape=13"): stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=14) with pytest.raises(ValueError, match="shape=12"): stim.PauliString.from_numpy(xs=xs[:-1], zs=zs, num_qubits=13) with pytest.raises(ValueError, match="shape=12"): stim.PauliString.from_numpy(xs=xs, zs=zs[:-1], num_qubits=13) with pytest.raises(ValueError, match="Inconsistent"): stim.PauliString.from_numpy(xs=xs, zs=zs[:-1]) with pytest.raises(RuntimeError, match="Unable to cast"): stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=-1) @pytest.mark.parametrize("n", [0, 1, 41, 42, 1023, 1024, 1025]) def test_to_from_numpy_round_trip(n: int): p = stim.PauliString.random(n) xs, zs = p.to_numpy() p2 = stim.PauliString.from_numpy(xs=xs, zs=zs, sign=p.sign) assert p2 == p xs, zs = p.to_numpy(bit_packed=True) p2 = stim.PauliString.from_numpy(xs=xs, zs=zs, num_qubits=n, sign=p.sign) assert p2 == p def test_to_unitary_matrix(): np.testing.assert_array_equal( stim.PauliString("").to_unitary_matrix(endian="little"), [[1]], ) np.testing.assert_array_equal( stim.PauliString("-").to_unitary_matrix(endian="big"), [[-1]], ) np.testing.assert_array_equal( stim.PauliString("i").to_unitary_matrix(endian="big"), [[1j]], ) np.testing.assert_array_equal( stim.PauliString("-i").to_unitary_matrix(endian="big"), [[-1j]], ) np.testing.assert_array_equal( stim.PauliString("I").to_unitary_matrix(endian="little"), [[1, 0], [0, 1]], ) np.testing.assert_array_equal( stim.PauliString("X").to_unitary_matrix(endian="little"), [[0, 1], [1, 0]], ) np.testing.assert_array_equal( stim.PauliString("Y").to_unitary_matrix(endian="little"), [[0, -1j], [1j, 0]], ) np.testing.assert_array_equal( stim.PauliString("iY").to_unitary_matrix(endian="little"), [[0, 1], [-1, 0]], ) np.testing.assert_array_equal( stim.PauliString("Z").to_unitary_matrix(endian="little"), [[1, 0], [0, -1]], ) np.testing.assert_array_equal( stim.PauliString("-Z").to_unitary_matrix(endian="little"), [[-1, 0], [0, 1]], ) np.testing.assert_array_equal( stim.PauliString("YY").to_unitary_matrix(endian="little"), [[0, 0, 0, -1], [0, 0, 1, 0], [0, 1, 0, 0], [-1, 0, 0, 0]], ) np.testing.assert_array_equal( stim.PauliString("-YZ").to_unitary_matrix(endian="little"), [[0, 1j, 0, 0], [-1j, 0, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]], ) np.testing.assert_array_equal( stim.PauliString("XYZ").to_unitary_matrix(endian="little"), [ [0, 0, 0, -1j, 0, 0, 0, 0], [0, 0, -1j, 0, 0, 0, 0, 0], [0, 1j, 0, 0, 0, 0, 0, 0], [1j, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1j], [0, 0, 0, 0, 0, 0, 1j, 0], [0, 0, 0, 0, 0, -1j, 0, 0], [0, 0, 0, 0, -1j, 0, 0, 0], ]) np.testing.assert_array_equal( stim.PauliString("ZYX").to_unitary_matrix(endian="big"), [ [0, 0, 0, -1j, 0, 0, 0, 0], [0, 0, -1j, 0, 0, 0, 0, 0], [0, 1j, 0, 0, 0, 0, 0, 0], [1j, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1j], [0, 0, 0, 0, 0, 0, 1j, 0], [0, 0, 0, 0, 0, -1j, 0, 0], [0, 0, 0, 0, -1j, 0, 0, 0], ]) def test_from_unitary_matrix(): assert stim.PauliString.from_unitary_matrix( [[1]] ) == stim.PauliString("") assert stim.PauliString.from_unitary_matrix( [[-1]] ) == stim.PauliString("-") assert stim.PauliString.from_unitary_matrix( [[1j]] ) == stim.PauliString("i") assert stim.PauliString.from_unitary_matrix( [[-1j]] ) == stim.PauliString("-i") assert stim.PauliString.from_unitary_matrix( [[1, 0], [0, 1]] ) == stim.PauliString("I") assert stim.PauliString.from_unitary_matrix( [[0, 1], [1, 0]] ) == stim.PauliString("X") assert stim.PauliString.from_unitary_matrix( [[0, -1j], [1j, 0]] ) == stim.PauliString("Y") assert stim.PauliString.from_unitary_matrix( [[1, 0], [0, -1]] ) == stim.PauliString("Z") assert stim.PauliString.from_unitary_matrix( [[0, 1], [-1, 0]] ) == stim.PauliString("iY") assert stim.PauliString.from_unitary_matrix( [[0, 1j], [-1j, 0]] ) == stim.PauliString("-Y") assert stim.PauliString.from_unitary_matrix( [[1j, 0], [0, -1j]] ) == stim.PauliString("iZ") assert stim.PauliString.from_unitary_matrix( [[-1, 0], [0, 1]] ) == stim.PauliString("-Z") assert stim.PauliString.from_unitary_matrix( [[1]], unsigned=True ) == stim.PauliString("") assert stim.PauliString.from_unitary_matrix( [[-1]], unsigned=True ) == stim.PauliString("") assert stim.PauliString.from_unitary_matrix( [[0, 1], [-1, 0]], unsigned=True ) == stim.PauliString("Y") assert stim.PauliString.from_unitary_matrix( [[0, +1 * 1j**0.1], [-1 * 1j**0.1, 0]], unsigned=True ) == stim.PauliString("Y") assert stim.PauliString.from_unitary_matrix([ [0, 0, 0, -1j, 0, 0, 0, 0], [0, 0, -1j, 0, 0, 0, 0, 0], [0, 1j, 0, 0, 0, 0, 0, 0], [1j, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1j], [0, 0, 0, 0, 0, 0, 1j, 0], [0, 0, 0, 0, 0, -1j, 0, 0], [0, 0, 0, 0, -1j, 0, 0, 0], ], endian="little") == stim.PauliString("XYZ") assert stim.PauliString.from_unitary_matrix([ [0, 0, 0, -1j, 0, 0, 0, 0], [0, 0, -1j, 0, 0, 0, 0, 0], [0, 1j, 0, 0, 0, 0, 0, 0], [1j, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1j], [0, 0, 0, 0, 0, 0, 1j, 0], [0, 0, 0, 0, 0, -1j, 0, 0], [0, 0, 0, 0, -1j, 0, 0, 0], ], endian="big") == stim.PauliString("ZYX") assert stim.PauliString.from_unitary_matrix(np.array([ [0, 0, 0, -1j, 0, 0, 0, 0], [0, 0, -1j, 0, 0, 0, 0, 0], [0, 1j, 0, 0, 0, 0, 0, 0], [1j, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1j], [0, 0, 0, 0, 0, 0, 1j, 0], [0, 0, 0, 0, 0, -1j, 0, 0], [0, 0, 0, 0, -1j, 0, 0, 0], ]) * 1j**0.1, endian="big", unsigned=True) == stim.PauliString("ZYX") assert stim.PauliString.from_unitary_matrix(np.array([ [0, 0, 0, -1j, 0, 0, 0, 0], [0, 0, -1j, 0, 0, 0, 0, 0], [0, 1j, 0, 0, 0, 0, 0, 0], [1j, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1j], [0, 0, 0, 0, 0, 0, 1j, 0], [0, 0, 0, 0, 0, -1j, 0, 0], [0, 0, 0, 0, -1j, 0, 0, 0], ]) * -1, endian="big", unsigned=True) == stim.PauliString("ZYX") def test_from_unitary_matrix_detect_bad_matrix(): with pytest.raises(ValueError, match="power of 2"): stim.PauliString.from_unitary_matrix([]) with pytest.raises(ValueError, match="row with no non-zero"): stim.PauliString.from_unitary_matrix([[]]) with pytest.raises(ValueError, match="row with no non-zero"): stim.PauliString.from_unitary_matrix([[0]]) with pytest.raises(ValueError, match="values besides 0, 1,"): stim.PauliString.from_unitary_matrix([[0.5]]) with pytest.raises(ValueError, match="isn't square"): stim.PauliString.from_unitary_matrix([[1, 0]]) with pytest.raises(ValueError, match="no non-zero entries"): stim.PauliString.from_unitary_matrix([[1], [0]]) with pytest.raises(ValueError, match="different lengths"): stim.PauliString.from_unitary_matrix([[0, 1], [1]]) with pytest.raises(ValueError, match="two non-zero entries"): stim.PauliString.from_unitary_matrix([[1, 1], [0, 1]]) with pytest.raises(ValueError, match="which qubits are flipped"): stim.PauliString.from_unitary_matrix([[1, 0], [1, 0]]) with pytest.raises(ValueError, match="isn't square"): stim.PauliString.from_unitary_matrix([[1, 0, 0], [0, 1, 0]]) with pytest.raises(ValueError, match="consistent phase flips"): stim.PauliString.from_unitary_matrix([[1, 0], [0, 1j]]) with pytest.raises(ValueError, match="power of 2"): stim.PauliString.from_unitary_matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) with pytest.raises(ValueError, match="which qubits are flipped"): stim.PauliString.from_unitary_matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) with pytest.raises(ValueError, match="consistent phase flips"): stim.PauliString.from_unitary_matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]]) with pytest.raises(ValueError, match="consistent phase flips"): stim.PauliString.from_unitary_matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]) @pytest.mark.parametrize("n,endian", itertools.product(range(8), ['little', 'big'])) def test_fuzz_to_from_unitary_matrix(n: int, endian: str): p = stim.PauliString.random(n, allow_imaginary=True) u = p.to_unitary_matrix(endian=endian) r = stim.PauliString.from_unitary_matrix(u, endian=endian) assert p == r via_tableau = stim.Tableau.from_unitary_matrix(u, endian=endian).to_pauli_string() r.sign = +1 assert via_tableau == r def test_before_after(): before = stim.PauliString("XXXYYYZZZ") after = stim.PauliString("XYXYZYXZZ") assert before.after(stim.Circuit("C_XYZ 1 4 6")) == after assert before.after(stim.Circuit("C_XYZ 1 4 6")[0]) == after assert before.after(stim.Tableau.from_named_gate("C_XYZ"), targets=[1, 4, 6]) == after assert after.before(stim.Circuit("C_XYZ 1 4 6")) == before assert after.before(stim.Circuit("C_XYZ 1 4 6")[0]) == before assert after.before(stim.Tableau.from_named_gate("C_XYZ"), targets=[1, 4, 6]) == before def test_iter_small(): assert list(stim.PauliString.iter_all(0)) == [stim.PauliString(0)] assert list(stim.PauliString.iter_all(1)) == [ stim.PauliString("_"), stim.PauliString("X"), stim.PauliString("Y"), stim.PauliString("Z"), ] assert list(stim.PauliString.iter_all(1, max_weight=-1)) == [ ] assert list(stim.PauliString.iter_all(1, max_weight=0)) == [ stim.PauliString("_"), ] assert list(stim.PauliString.iter_all(1, max_weight=1)) == [ stim.PauliString("_"), stim.PauliString("X"), stim.PauliString("Y"), stim.PauliString("Z"), ] assert list(stim.PauliString.iter_all(1, min_weight=1, max_weight=1)) == [ stim.PauliString("X"), stim.PauliString("Y"), stim.PauliString("Z"), ] assert list(stim.PauliString.iter_all(2, min_weight=1, max_weight=1, allowed_paulis="XY")) == [ stim.PauliString("X_"), stim.PauliString("Y_"), stim.PauliString("_X"), stim.PauliString("_Y"), ] with pytest.raises(ValueError, match="characters other than"): stim.PauliString.iter_all(2, allowed_paulis="A") def test_iter_reusable(): v = stim.PauliString.iter_all(2) vs1 = list(v) vs2 = list(v) assert vs1 == vs2 assert len(vs1) == 4**2 def test_backwards_compatibility_init(): assert stim.PauliString() == stim.PauliString("+") assert stim.PauliString(5) == stim.PauliString("+_____") assert stim.PauliString([1, 2, 3]) == stim.PauliString("+XYZ") assert stim.PauliString("XYZ") == stim.PauliString("+XYZ") assert stim.PauliString(stim.PauliString("XYZ")) == stim.PauliString("+XYZ") assert stim.PauliString("X" for _ in range(4)) == stim.PauliString("+XXXX") # These keywords have been removed from the documentation and the .pyi, but # their functionality needs to be maintained for backwards compatibility. # noinspection PyArgumentList assert stim.PauliString(num_qubits=5) == stim.PauliString("+_____") # noinspection PyArgumentList assert stim.PauliString(pauli_indices=[1, 2, 3]) == stim.PauliString("+XYZ") # noinspection PyArgumentList assert stim.PauliString(text="XYZ") == stim.PauliString("+XYZ") # noinspection PyArgumentList assert stim.PauliString(other=stim.PauliString("XYZ")) == stim.PauliString("+XYZ") def test_pauli_indices(): assert stim.PauliString().pauli_indices() == [] assert stim.PauliString().pauli_indices("X") == [] assert stim.PauliString().pauli_indices("I") == [] assert stim.PauliString(5).pauli_indices() == [] assert stim.PauliString(5).pauli_indices("X") == [] assert stim.PauliString(5).pauli_indices("I") == [0, 1, 2, 3, 4] assert stim.PauliString("X1000").pauli_indices() == [1000] assert stim.PauliString("Y1000").pauli_indices() == [1000] assert stim.PauliString("Z1000").pauli_indices() == [1000] assert stim.PauliString("X1000").pauli_indices("YZ") == [] assert stim.PauliString("Y1000").pauli_indices("XZ") == [] assert stim.PauliString("Z1000").pauli_indices("XY") == [] assert stim.PauliString("X1000").pauli_indices("X") == [1000] assert stim.PauliString("Y1000").pauli_indices("Y") == [1000] assert stim.PauliString("Z1000").pauli_indices("Z") == [1000] assert stim.PauliString("_XYZ").pauli_indices("x") == [1] assert stim.PauliString("_XYZ").pauli_indices("X") == [1] assert stim.PauliString("_XYZ").pauli_indices("y") == [2] assert stim.PauliString("_XYZ").pauli_indices("Y") == [2] assert stim.PauliString("_XYZ").pauli_indices("z") == [3] assert stim.PauliString("_XYZ").pauli_indices("Z") == [3] assert stim.PauliString("_XYZ").pauli_indices("I") == [0] assert stim.PauliString("_XYZ").pauli_indices("_") == [0] with pytest.raises(ValueError, match="Invalid character"): assert stim.PauliString("_XYZ").pauli_indices("k") def test_before_reset(): assert stim.PauliString("Z").before(stim.Circuit("R 0")) == stim.PauliString("_") assert stim.PauliString("Z").before(stim.Circuit("MR 0")) == stim.PauliString("_") assert stim.PauliString("Z").before(stim.Circuit("M 0")) == stim.PauliString("Z") assert stim.PauliString("X").before(stim.Circuit("RX 0")) == stim.PauliString("_") assert stim.PauliString("X").before(stim.Circuit("MRX 0")) == stim.PauliString("_") assert stim.PauliString("X").before(stim.Circuit("MX 0")) == stim.PauliString("X") assert stim.PauliString("Y").before(stim.Circuit("RY 0")) == stim.PauliString("_") assert stim.PauliString("Y").before(stim.Circuit("MRY 0")) == stim.PauliString("_") assert stim.PauliString("Y").before(stim.Circuit("MY 0")) == stim.PauliString("Y") with pytest.raises(ValueError): stim.PauliString("Z").before(stim.Circuit("RX 0")) with pytest.raises(ValueError): stim.PauliString("Z").before(stim.Circuit("RY 0")) with pytest.raises(ValueError): stim.PauliString("Z").before(stim.Circuit("MRX 0")) with pytest.raises(ValueError): stim.PauliString("Z").before(stim.Circuit("MRY 0")) with pytest.raises(ValueError): stim.PauliString("Z").before(stim.Circuit("MX 0")) with pytest.raises(ValueError): stim.PauliString("Z").before(stim.Circuit("MY 0")) def test_constructor_from_dict(): # Values are single Pauli -> Key is the qubit index: assert stim.PauliString({2: "X", 4: "Z"}) == stim.PauliString("__X_Z") assert stim.PauliString({0: 1, 1: 2}) == stim.PauliString("XY") assert stim.PauliString({1: 1, 3: 2, 5: "Z"}) == stim.PauliString("_X_Y_Z") assert stim.PauliString({1: 0, 3: "I", 4: "_"}) == stim.PauliString("_____") assert stim.PauliString({0: "X", 2: "x", 4: "y"}) == stim.PauliString("X_X_Y") # Case-insensitive assert stim.PauliString({}) == stim.PauliString("") # Values are iterable -> Key is the Pauli: assert stim.PauliString({"X": [0], "Z": [1]}) == stim.PauliString("XZ") assert stim.PauliString({1: [0], 3: [1]}) == stim.PauliString("XZ") assert stim.PauliString({"X": [2], "Z": [4], "Y": [6], "I": [5]}) == stim.PauliString("__X_Z_Y") assert stim.PauliString({"X": [0], "Z": [1,2]}) == stim.PauliString("XZZ") assert stim.PauliString({"x": [0,2], "Y": [4]}) == stim.PauliString("X_X_Y") # Case-insensitive assert stim.PauliString({"I": [1,2]}) == stim.PauliString("___") assert stim.PauliString({"I": []}) == stim.PauliString("") assert stim.PauliString({"I": [9]}) == stim.PauliString(10) assert stim.PauliString({0: [9]}) == stim.PauliString(10) assert stim.PauliString({"_": [9]}) == stim.PauliString(10) # Acceptable collisions: assert stim.PauliString({"X": [2], "I": [2]}) == stim.PauliString("__X") # Trivial Pauli should not cause a conflict assert stim.PauliString({"X": [2], 1: [2]}) == stim.PauliString("__X") # Same Pauli should not cause conflict between int/str assert stim.PauliString({"X": [2], "I": [0,1,2,3], "Z": [0], 0: [0,1,2,3], "Y": [1], "_": [0,1,2,3]}) == stim.PauliString("ZYX_") # A more complex example def test_constructor_from_dict_errors(): with pytest.raises(ValueError, match="keys must all be ints"): stim.PauliString({"X": 0}) # When value is non-itetable, key must be int (index) with pytest.raises(ValueError, match="Don't know how to convert"): stim.PauliString({"A": [0]}) with pytest.raises(ValueError, match="Don't know how to convert"): stim.PauliString({0: "A"}) with pytest.raises(ValueError, match="Don't know how to convert"): stim.PauliString({0: 4}) # Paulis correspond to 0-3 with pytest.raises(ValueError, match="Don't know how to convert"): stim.PauliString({0: -1}) # Paulis correspond to 0-3 with pytest.raises(ValueError, match="Don't know how to convert"): stim.PauliString({"ZX": [0]}) # Paulis need to be single characters with pytest.raises(ValueError, match="Pauli keys with iterable values"): stim.PauliString({"X": "not an iterable"}) with pytest.raises(ValueError, match="Qubit index must be an int"): stim.PauliString({"Y": [0, "not an int"]}) with pytest.raises(ValueError, match="keys must all be ints"): stim.PauliString({"X": 0, 1: "Y"}) with pytest.raises(ValueError, match="Qubit index must be an int"): stim.PauliString({"X": [0], 1: "Y"}) with pytest.raises(ValueError, match="Qubit index must be an int"): stim.PauliString({"X": [0], 1: ["Y"]}) with pytest.raises(ValueError, match="same qubit index"): stim.PauliString({"X": [0], "Y": [0]}) # Different non-trivial Paulies can't use the same index with pytest.raises(ValueError, match="same qubit index"): stim.PauliString({"Z": [1], "Y": [4,1]}) # Different non-trivial Paulies can't use the same index with pytest.raises(ValueError, match="same qubit index"): stim.PauliString({"I": [0,1,4], "Z": [1], "Y": [4,1]}) # Different non-trivial Paulies can't use the same index with pytest.raises(ValueError, match="keys must all be ints"): stim.PauliString({(): []}) with pytest.raises(ValueError, match="keys must all be ints"): stim.PauliString({(): 0}) with pytest.raises(ValueError, match="Qubit index must be an int"): stim.PauliString({"X": [()]}) with pytest.raises(ValueError, match="Qubit index must be non-negative"): stim.PauliString({"X": [-1]}) with pytest.raises(ValueError, match="Qubit index must be non-negative"): stim.PauliString({-1: "X"}) with pytest.raises(ValueError, match="Qubit index must be non-negative"): stim.PauliString({"I": [-1]}) ================================================ FILE: src/stim/stabilizers/pauli_string_ref.h ================================================ /* * 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. */ #ifndef _STIM_STABILIZERS_PAULI_STRING_REF_H #define _STIM_STABILIZERS_PAULI_STRING_REF_H #include #include "stim/mem/bit_ref.h" #include "stim/mem/simd_bits_range_ref.h" #include "stim/mem/span_ref.h" namespace stim { template struct PauliString; struct Circuit; template struct Tableau; struct CircuitInstruction; /// A Pauli string is a product of Pauli operations (I, X, Y, Z) to apply to various qubits. /// /// A PauliStringRef is a Pauli string whose contents are backed by referenced memory, instead of memory owned by the /// class instance. For example, the memory may be a row from the densely packed bits of a stabilizer tableau. This /// avoids unnecessary copying, and allows for conveniently applying operations inplace on existing data. /// /// The template parameter, W, represents the SIMD width. template struct PauliStringRef { /// The length of the Pauli string. size_t num_qubits; /// Whether or not the Pauli string is negated. True means -1, False means +1. Imaginary phase is not permitted. bit_ref sign; /// The Paulis in the Pauli string, densely bit packed in a fashion enabling the use of vectorized instructions. /// Paulis are xz-encoded (P=xz: I=00, X=10, Y=11, Z=01) pairwise across the two bit vectors. simd_bits_range_ref xs, zs; /// Constructs a PauliStringRef pointing at the given sign, x, and z data. /// /// Requires: /// xs.num_bits_padded() == zs.num_bits_padded() /// xs.num_simd_words == ceil(num_qubits / W) PauliStringRef(size_t num_qubits, bit_ref sign, simd_bits_range_ref xs, simd_bits_range_ref zs); /// Equality. bool operator==(const PauliStringRef &other) const; /// Inequality. bool operator!=(const PauliStringRef &other) const; bool operator<(const PauliStringRef &other) const; /// Overwrite assignment. PauliStringRef &operator=(const PauliStringRef &other); /// Swap assignment. void swap_with(PauliStringRef other); /// Multiplies a commuting Pauli string into this one. /// /// If the two Pauli strings may anticommute, use `inplace_right_mul_returning_log_i_scalar` instead. /// /// ASSERTS: /// The given Pauli strings have the same size. /// The given Pauli strings commute. PauliStringRef &operator*=(const PauliStringRef &commuting_rhs); // A more general version of `*this *= rhs` which works for anti-commuting Paulis. // // Instead of updating the sign of `*this`, the base i logarithm of a scalar factor that still needs to be included // into the result is returned. For example, when multiplying XZ to get iY, the left hand side would become `Y` // and the returned value would be `1` (meaning a factor of `i**1 = i` is missing from the `Y`). // // Returns: // The logarithm, base i, of a scalar byproduct from the multiplication. // 0 if the scalar byproduct is 1. // 1 if the scalar byproduct is i. // 2 if the scalar byproduct is -1. // 3 if the scalar byproduct is -i. // // ASSERTS: // The given Pauli strings have the same size. uint8_t inplace_right_mul_returning_log_i_scalar(const PauliStringRef &rhs) noexcept; /// Overwrites the entire given Pauli string's contents with a subset of Paulis from this Pauli string. /// Does not affect the sign of the given Pauli string. /// /// Args: /// out: The Pauli string to overwrite. /// in_indices: For each qubit position in the output Pauli string, which qubit positions is read from in this /// Pauli string. void gather_into(PauliStringRef out, SpanRef in_indices) const; /// Overwrites part of the given Pauli string with the contents of this Pauli string. /// Also multiplies this Pauli string's sign into the given Pauli string's sign. /// /// Args: /// out: The Pauli string to partially overwrite. /// out_indices: For each qubit position in this Pauli string, which qubit position is overwritten in the output /// Pauli string. void scatter_into(PauliStringRef out, SpanRef out_indices) const; /// Determines if this Pauli string commutes with the given Pauli string. bool commutes(const PauliStringRef &other) const noexcept; /// Returns a string describing the given Pauli string, with one character per qubit. std::string str() const; /// Returns a string describing the given Pauli string, indexing the Paulis so that identities can be omitted. std::string sparse_str() const; /// Applies the given tableau to the pauli string, at the given targets. /// /// Args: /// tableau: The Clifford operation to apply. /// targets: The qubits to target. Broadcasting is supported. The length of the span must be a multiple of the /// tableau's size. /// inverse: When true, applies the inverse of the tableau instead of the tableau. void do_tableau(const Tableau &tableau, SpanRef targets, bool inverse); void do_circuit(const Circuit &circuit); void undo_circuit(const Circuit &circuit); void do_instruction(const CircuitInstruction &inst); void undo_instruction(const CircuitInstruction &inst); PauliString after(const Circuit &circuit) const; PauliString after(const Tableau &tableau, SpanRef indices) const; PauliString after(const CircuitInstruction &operation) const; PauliString before(const Circuit &circuit) const; PauliString before(const Tableau &tableau, SpanRef indices) const; PauliString before(const CircuitInstruction &operation) const; size_t weight() const; bool has_no_pauli_terms() const; bool intersects(PauliStringRef other) const; template void for_each_active_pauli(CALLBACK callback) const { size_t n = xs.num_u64_padded(); for (size_t w = 0; w < n; w++) { uint64_t v = xs.u64[w] | zs.u64[w]; while (v) { size_t q = w * 64 + std::countr_zero(v); v &= v - 1; callback(q); } } } private: void check_avoids_MPP(const CircuitInstruction &inst); void check_avoids_reset(const CircuitInstruction &inst); void check_avoids_measurement(const CircuitInstruction &inst); void undo_reset_xyz(const CircuitInstruction &inst); void do_single_cx(const CircuitInstruction &inst, uint32_t c, uint32_t t); void do_single_cy(const CircuitInstruction &inst, uint32_t c, uint32_t t); void do_single_cz(const CircuitInstruction &inst, uint32_t c, uint32_t t); void do_H_XZ(const CircuitInstruction &inst); void do_H_YZ(const CircuitInstruction &inst); void do_H_XY(const CircuitInstruction &inst); void do_H_NXY(const CircuitInstruction &inst); void do_H_NXZ(const CircuitInstruction &inst); void do_H_NYZ(const CircuitInstruction &inst); void do_C_XYZ(const CircuitInstruction &inst); void do_C_NXYZ(const CircuitInstruction &inst); void do_C_XNYZ(const CircuitInstruction &inst); void do_C_XYNZ(const CircuitInstruction &inst); void do_C_ZYX(const CircuitInstruction &inst); void do_C_NZYX(const CircuitInstruction &inst); void do_C_ZNYX(const CircuitInstruction &inst); void do_C_ZYNX(const CircuitInstruction &inst); void do_SQRT_X(const CircuitInstruction &inst); void do_SQRT_Y(const CircuitInstruction &inst); void do_SQRT_Z(const CircuitInstruction &inst); void do_SQRT_X_DAG(const CircuitInstruction &inst); void do_SQRT_Y_DAG(const CircuitInstruction &inst); void do_SQRT_Z_DAG(const CircuitInstruction &inst); void do_SQRT_XX(const CircuitInstruction &inst); void do_SQRT_XX_DAG(const CircuitInstruction &inst); void do_SQRT_YY(const CircuitInstruction &inst); void do_SQRT_YY_DAG(const CircuitInstruction &inst); void do_SQRT_ZZ(const CircuitInstruction &inst); void do_SQRT_ZZ_DAG(const CircuitInstruction &inst); template void do_ZCX(const CircuitInstruction &inst); template void do_ZCY(const CircuitInstruction &inst); void do_ZCZ(const CircuitInstruction &inst); template void do_SWAP(const CircuitInstruction &inst); void do_X(const CircuitInstruction &inst); void do_Y(const CircuitInstruction &inst); void do_Z(const CircuitInstruction &inst); template void do_ISWAP(const CircuitInstruction &inst); template void do_ISWAP_DAG(const CircuitInstruction &inst); template void do_CXSWAP(const CircuitInstruction &inst); template void do_CZSWAP(const CircuitInstruction &inst); template void do_SWAPCX(const CircuitInstruction &inst); void do_XCX(const CircuitInstruction &inst); template void do_XCY(const CircuitInstruction &inst); template void do_XCZ(const CircuitInstruction &inst); template void do_YCX(const CircuitInstruction &inst); void do_YCY(const CircuitInstruction &inst); template void do_YCZ(const CircuitInstruction &inst); }; /// Writes a string describing the given Pauli string to an output stream. template std::ostream &operator<<(std::ostream &out, const PauliStringRef &ps); } // namespace stim #include "stim/stabilizers/pauli_string_ref.inl" #endif ================================================ FILE: src/stim/stabilizers/pauli_string_ref.inl ================================================ // 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. #include #include #include "stim/circuit/circuit.h" #include "stim/circuit/gate_decomposition.h" #include "stim/mem/simd_util.h" #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/tableau.h" namespace stim { template PauliStringRef::PauliStringRef( size_t init_num_qubits, bit_ref init_sign, simd_bits_range_ref init_xs, simd_bits_range_ref init_zs) : num_qubits(init_num_qubits), sign(init_sign), xs(init_xs), zs(init_zs) { assert(init_xs.num_bits_padded() == init_zs.num_bits_padded()); assert(init_xs.num_simd_words == (init_num_qubits + W - 1) / W); } template std::string PauliStringRef::sparse_str() const { std::stringstream out; out << "+-"[(bool)sign]; bool first = true; for_each_active_pauli([&](size_t q) { auto p = xs[q] + 2 * zs[q]; if (!first) { out << '*'; } first = false; out << "IXZY"[p] << q; }); if (first) { out << 'I'; } return out.str(); } template void PauliStringRef::swap_with(PauliStringRef other) { assert(num_qubits == other.num_qubits); sign.swap_with(other.sign); xs.swap_with(other.xs); zs.swap_with(other.zs); } template PauliStringRef &PauliStringRef::operator=(const PauliStringRef &other) { assert(num_qubits == other.num_qubits); sign = other.sign; assert((bool)sign == (bool)other.sign); xs = other.xs; zs = other.zs; return *this; } template std::string PauliStringRef::str() const { std::stringstream ss; ss << *this; return ss.str(); } template bool PauliStringRef::operator==(const PauliStringRef &other) const { return num_qubits == other.num_qubits && sign == other.sign && xs == other.xs && zs == other.zs; } template bool PauliStringRef::operator!=(const PauliStringRef &other) const { return !(*this == other); } template bool PauliStringRef::operator<(const PauliStringRef &other) const { size_t n = std::min(num_qubits, other.num_qubits); for (size_t q = 0; q < n; q++) { uint8_t p1 = (xs[q] ^ zs[q]) + zs[q] * 2; uint8_t p2 = (other.xs[q] ^ other.zs[q]) + other.zs[q] * 2; if (p1 != p2) { return p1 < p2; } } if (num_qubits != other.num_qubits) { return num_qubits < other.num_qubits; } if (sign != other.sign) { return sign < other.sign; } return false; } template PauliStringRef &PauliStringRef::operator*=(const PauliStringRef &rhs) { uint8_t log_i = inplace_right_mul_returning_log_i_scalar(rhs); assert((log_i & 1) == 0); sign ^= log_i & 2; return *this; } template uint8_t PauliStringRef::inplace_right_mul_returning_log_i_scalar(const PauliStringRef &rhs) noexcept { assert(num_qubits >= rhs.num_qubits); // Accumulator registers for counting mod 4 in parallel across each bit position. simd_word cnt1{}; simd_word cnt2{}; rhs.xs.for_each_word( rhs.zs, xs, zs, [&cnt1, &cnt2](simd_word &x2, simd_word &z2, simd_word &x1, simd_word &z1) { // Update the left hand side Paulis. auto old_x1 = x1; auto old_z1 = z1; x1 ^= x2; z1 ^= z2; // At each bit position: accumulate anti-commutation (+i or -i) counts. auto x1z2 = old_x1 & z2; auto anti_commutes = (x2 & old_z1) ^ x1z2; cnt2 ^= (cnt1 ^ x1 ^ z1 ^ x1z2) & anti_commutes; cnt1 ^= anti_commutes; }); // Combine final anti-commutation phase tally (mod 4). auto s = (uint8_t)cnt1.popcount(); s ^= cnt2.popcount() << 1; s ^= (uint8_t)rhs.sign << 1; return s & 3; } template bool PauliStringRef::commutes(const PauliStringRef &other) const noexcept { if (num_qubits > other.num_qubits) { return other.commutes(*this); } simd_word cnt1{}; xs.for_each_word( zs, other.xs, other.zs, [&cnt1](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2) { cnt1 ^= (x1 & z2) ^ (x2 & z1); }); return (cnt1.popcount() & 1) == 0; } template void PauliStringRef::do_tableau(const Tableau &tableau, SpanRef indices, bool inverse) { if (tableau.num_qubits == 0 || indices.size() % tableau.num_qubits != 0) { throw std::invalid_argument("len(tableau) == 0 or len(indices) % len(tableau) != 0"); } for (auto e : indices) { if (e >= num_qubits) { throw std::invalid_argument("Attempted to apply a tableau past the end of the pauli string."); } } if (inverse) { auto inverse_tableau = tableau.inverse(); for (size_t k = indices.size(); k > 0;) { k -= tableau.num_qubits; inverse_tableau.apply_within(*this, {indices.ptr_start + k, indices.ptr_start + k + tableau.num_qubits}); } } else { for (size_t k = 0; k < indices.size(); k += tableau.num_qubits) { tableau.apply_within(*this, {indices.ptr_start + k, indices.ptr_start + k + tableau.num_qubits}); } } } template void PauliStringRef::undo_circuit(const Circuit &circuit) { circuit.for_each_operation_reverse([&](const CircuitInstruction &inst) { undo_instruction(inst); }); } template void PauliStringRef::do_circuit(const Circuit &circuit) { circuit.for_each_operation([&](const CircuitInstruction &inst) { do_instruction(inst); }); } template void PauliStringRef::undo_reset_xyz(const CircuitInstruction &inst) { bool x_dep, z_dep; if (inst.gate_type == GateType::R || inst.gate_type == GateType::MR) { x_dep = true; z_dep = false; } else if (inst.gate_type == GateType::RX || inst.gate_type == GateType::MRX) { x_dep = false; z_dep = true; } else if (inst.gate_type == GateType::RY || inst.gate_type == GateType::MRY) { x_dep = true; z_dep = true; } else { throw std::invalid_argument("Unrecognized measurement type: " + inst.str()); } for (const auto &t : inst.targets) { assert(t.is_qubit_target()); auto q = t.qubit_value(); if (q < num_qubits && ((xs[q] & x_dep) ^ (zs[q] & z_dep))) { std::stringstream ss; ss << "The pauli observable '" << *this; ss << "' doesn't have a well specified value before '" << inst; ss << "' because it anticommutes with the reset."; throw std::invalid_argument(ss.str()); } } for (const auto &t : inst.targets) { auto q = t.qubit_value(); xs[q] = false; zs[q] = false; } } template void PauliStringRef::check_avoids_measurement(const CircuitInstruction &inst) { bool x_dep, z_dep; if (inst.gate_type == GateType::M) { x_dep = true; z_dep = false; } else if (inst.gate_type == GateType::MX) { x_dep = false; z_dep = true; } else if (inst.gate_type == GateType::MY) { x_dep = true; z_dep = true; } else { throw std::invalid_argument("Unrecognized measurement type: " + inst.str()); } for (const auto &t : inst.targets) { assert(t.is_qubit_target()); auto q = t.qubit_value(); if (q < num_qubits && ((xs[q] & x_dep) ^ (zs[q] & z_dep))) { std::stringstream ss; ss << "The pauli observable '" << *this; ss << "' doesn't have a well specified value across '" << inst; ss << "' because it anticommutes with the measurement."; throw std::invalid_argument(ss.str()); } } } template void PauliStringRef::check_avoids_reset(const CircuitInstruction &inst) { // Only fail if the pauli string actually touches the reset. for (const auto &t : inst.targets) { assert(t.is_qubit_target()); auto q = t.qubit_value(); if (q < num_qubits && (xs[q] || zs[q])) { std::stringstream ss; ss << "The pauli observable '" << *this; ss << "' doesn't have a well specified value after '" << inst; ss << "' because the reset discards information."; throw std::invalid_argument(ss.str()); } } } template void PauliStringRef::check_avoids_MPP(const CircuitInstruction &inst) { size_t start = 0; const auto &targets = inst.targets; while (start < targets.size()) { size_t end = start + 1; bool anticommutes = false; while (true) { auto t = targets[end - 1]; auto q = t.qubit_value(); if (q < num_qubits) { anticommutes ^= zs[q] && (t.data & TARGET_PAULI_X_BIT); anticommutes ^= xs[q] && (t.data & TARGET_PAULI_Z_BIT); } if (end >= targets.size() || !targets[end].is_combiner()) { break; } end += 2; } if (anticommutes) { std::stringstream ss; ss << "The pauli observable '" << *this; ss << "' doesn't have a well specified value across '" << inst; ss << "' because it anticommutes with the measurement."; throw std::invalid_argument(ss.str()); } start = end; } } template void PauliStringRef::do_instruction(const CircuitInstruction &inst) { for (const auto &t : inst.targets) { if (t.has_qubit_value() && t.qubit_value() >= num_qubits && !(GATE_DATA[inst.gate_type].flags & GATE_HAS_NO_EFFECT_ON_QUBITS)) { std::stringstream ss; ss << "The instruction '" << inst; ss << "' targets qubits outside the pauli string '" << *this; ss << "'."; throw std::invalid_argument(ss.str()); } } const auto &gate_data = GATE_DATA[inst.gate_type]; switch (gate_data.id) { case GateType::H: do_H_XZ(inst); break; case GateType::H_YZ: do_H_YZ(inst); break; case GateType::H_XY: do_H_XY(inst); break; case GateType::H_NXY: do_H_NXY(inst); break; case GateType::H_NXZ: do_H_NXZ(inst); break; case GateType::H_NYZ: do_H_NYZ(inst); break; case GateType::C_XYZ: do_C_XYZ(inst); break; case GateType::C_NXYZ: do_C_NXYZ(inst); break; case GateType::C_XNYZ: do_C_XNYZ(inst); break; case GateType::C_XYNZ: do_C_XYNZ(inst); break; case GateType::C_ZYX: do_C_ZYX(inst); break; case GateType::C_NZYX: do_C_NZYX(inst); break; case GateType::C_ZNYX: do_C_ZNYX(inst); break; case GateType::C_ZYNX: do_C_ZYNX(inst); break; case GateType::SQRT_X: do_SQRT_X(inst); break; case GateType::SQRT_Y: do_SQRT_Y(inst); break; case GateType::S: do_SQRT_Z(inst); break; case GateType::SQRT_X_DAG: do_SQRT_X_DAG(inst); break; case GateType::SQRT_Y_DAG: do_SQRT_Y_DAG(inst); break; case GateType::S_DAG: do_SQRT_Z_DAG(inst); break; case GateType::SQRT_XX: do_SQRT_XX(inst); break; case GateType::SQRT_XX_DAG: do_SQRT_XX_DAG(inst); break; case GateType::SQRT_YY: do_SQRT_YY(inst); break; case GateType::SQRT_YY_DAG: do_SQRT_YY_DAG(inst); break; case GateType::SQRT_ZZ: do_SQRT_ZZ(inst); break; case GateType::SQRT_ZZ_DAG: do_SQRT_ZZ_DAG(inst); break; case GateType::CX: do_ZCX(inst); break; case GateType::CY: do_ZCY(inst); break; case GateType::CZ: do_ZCZ(inst); break; case GateType::SWAP: do_SWAP(inst); break; case GateType::X: do_X(inst); break; case GateType::Y: do_Y(inst); break; case GateType::Z: do_Z(inst); break; case GateType::ISWAP: do_ISWAP(inst); break; case GateType::ISWAP_DAG: do_ISWAP_DAG(inst); break; case GateType::CXSWAP: do_CXSWAP(inst); break; case GateType::CZSWAP: do_CZSWAP(inst); break; case GateType::SWAPCX: do_SWAPCX(inst); break; case GateType::XCX: do_XCX(inst); break; case GateType::XCY: do_XCY(inst); break; case GateType::XCZ: do_XCZ(inst); break; case GateType::YCX: do_YCX(inst); break; case GateType::YCY: do_YCY(inst); break; case GateType::YCZ: do_YCZ(inst); break; case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::MPAD: case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: // No effect. break; case GateType::R: case GateType::RX: case GateType::RY: case GateType::MR: case GateType::MRX: case GateType::MRY: check_avoids_reset(inst); break; case GateType::M: case GateType::MX: case GateType::MY: check_avoids_measurement(inst); break; case GateType::MPP: check_avoids_MPP(inst); break; case GateType::SPP: case GateType::SPP_DAG: decompose_spp_or_spp_dag_operation(inst, num_qubits, false, [&](CircuitInstruction sub_inst) { do_instruction(sub_inst); }); break; case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::E: case GateType::ELSE_CORRELATED_ERROR: { std::stringstream ss; ss << "The pauli string '" << *this; ss << "' doesn't have a well defined deterministic value after '" << inst; ss << "'."; throw std::invalid_argument(ss.str()); } default: throw std::invalid_argument("Not implemented in PauliStringRef::do_instruction: " + inst.str()); } } template void PauliStringRef::undo_instruction(const CircuitInstruction &inst) { for (const auto &t : inst.targets) { if (t.has_qubit_value() && t.qubit_value() >= num_qubits && !(GATE_DATA[inst.gate_type].flags & GATE_HAS_NO_EFFECT_ON_QUBITS)) { std::stringstream ss; ss << "The instruction '" << inst; ss << "' targets qubits outside the pauli string '" << *this; ss << "'."; throw std::invalid_argument(ss.str()); } } const auto &gate_data = GATE_DATA[inst.gate_type]; switch (gate_data.id) { case GateType::H: do_H_XZ(inst); break; case GateType::H_YZ: do_H_YZ(inst); break; case GateType::H_XY: do_H_XY(inst); break; case GateType::H_NXY: do_H_NXY(inst); break; case GateType::H_NXZ: do_H_NXZ(inst); break; case GateType::H_NYZ: do_H_NYZ(inst); break; case GateType::C_XYZ: do_C_ZYX(inst); break; case GateType::C_NXYZ: do_C_ZYNX(inst); break; case GateType::C_XNYZ: do_C_ZNYX(inst); break; case GateType::C_XYNZ: do_C_NZYX(inst); break; case GateType::C_ZYX: do_C_XYZ(inst); break; case GateType::C_NZYX: do_C_XYNZ(inst); break; case GateType::C_ZNYX: do_C_XNYZ(inst); break; case GateType::C_ZYNX: do_C_NXYZ(inst); break; case GateType::SQRT_X: do_SQRT_X_DAG(inst); break; case GateType::SQRT_Y: do_SQRT_Y_DAG(inst); break; case GateType::S: do_SQRT_Z_DAG(inst); break; case GateType::SQRT_X_DAG: do_SQRT_X(inst); break; case GateType::SQRT_Y_DAG: do_SQRT_Y(inst); break; case GateType::S_DAG: do_SQRT_Z(inst); break; case GateType::SQRT_XX: do_SQRT_XX_DAG(inst); break; case GateType::SQRT_XX_DAG: do_SQRT_XX(inst); break; case GateType::SQRT_YY: do_SQRT_YY_DAG(inst); break; case GateType::SQRT_YY_DAG: do_SQRT_YY(inst); break; case GateType::SQRT_ZZ: do_SQRT_ZZ_DAG(inst); break; case GateType::SQRT_ZZ_DAG: do_SQRT_ZZ(inst); break; case GateType::CX: do_ZCX(inst); break; case GateType::CY: do_ZCY(inst); break; case GateType::CZ: do_ZCZ(inst); break; case GateType::SWAP: do_SWAP(inst); break; case GateType::X: do_X(inst); break; case GateType::Y: do_Y(inst); break; case GateType::Z: do_Z(inst); break; case GateType::ISWAP: do_ISWAP_DAG(inst); break; case GateType::ISWAP_DAG: do_ISWAP(inst); break; case GateType::CXSWAP: do_SWAPCX(inst); break; case GateType::CZSWAP: do_CZSWAP(inst); break; case GateType::SWAPCX: do_CXSWAP(inst); break; case GateType::XCX: do_XCX(inst); break; case GateType::XCY: do_XCY(inst); break; case GateType::XCZ: do_XCZ(inst); break; case GateType::YCX: do_YCX(inst); break; case GateType::YCY: do_YCY(inst); break; case GateType::YCZ: do_YCZ(inst); break; case GateType::SPP: case GateType::SPP_DAG: { std::vector buf_targets; buf_targets.insert(buf_targets.end(), inst.targets.begin(), inst.targets.end()); std::reverse(buf_targets.begin(), buf_targets.end()); decompose_spp_or_spp_dag_operation( CircuitInstruction{inst.gate_type, {}, buf_targets, inst.tag}, num_qubits, false, [&](CircuitInstruction sub_inst) { undo_instruction(sub_inst); }); break; } case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::MPAD: case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: // No effect. break; case GateType::R: case GateType::RX: case GateType::RY: case GateType::MR: case GateType::MRX: case GateType::MRY: undo_reset_xyz(inst); break; case GateType::M: case GateType::MX: case GateType::MY: check_avoids_measurement(inst); break; case GateType::MPP: check_avoids_MPP(inst); break; case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::E: case GateType::ELSE_CORRELATED_ERROR: { std::stringstream ss; ss << "The pauli string '" << *this; ss << "' doesn't have a well defined deterministic value before '" << inst; ss << "'."; throw std::invalid_argument(ss.str()); } default: throw std::invalid_argument("Not implemented in PauliStringRef::undo_instruction: " + inst.str()); } } template PauliString PauliStringRef::after(const Circuit &circuit) const { PauliString result = *this; result.ref().do_circuit(circuit); return result; } template PauliString PauliStringRef::after(const Tableau &tableau, SpanRef indices) const { PauliString result = *this; result.ref().do_tableau(tableau, indices, false); return result; } template PauliString PauliStringRef::after(const CircuitInstruction &inst) const { PauliString result = *this; result.ref().do_instruction(inst); return result; } template PauliString PauliStringRef::before(const Circuit &circuit) const { PauliString result = *this; result.ref().undo_circuit(circuit); return result; } template PauliString PauliStringRef::before(const CircuitInstruction &inst) const { PauliString result = *this; result.ref().undo_instruction(inst); return result; } template PauliString PauliStringRef::before(const Tableau &tableau, SpanRef indices) const { PauliString result = *this; result.ref().do_tableau(tableau, indices, true); return result; } template void PauliStringRef::gather_into(PauliStringRef out, SpanRef in_indices) const { assert(in_indices.size() == out.num_qubits); for (size_t k_out = 0; k_out < out.num_qubits; k_out++) { size_t k_in = in_indices[k_out]; out.xs[k_out] = xs[k_in]; out.zs[k_out] = zs[k_in]; } } template void PauliStringRef::scatter_into(PauliStringRef out, SpanRef out_indices) const { assert(num_qubits == out_indices.size()); for (size_t k_in = 0; k_in < num_qubits; k_in++) { size_t k_out = out_indices[k_in]; out.xs[k_out] = xs[k_in]; out.zs[k_out] = zs[k_in]; } out.sign ^= sign; } template bool PauliStringRef::intersects(const PauliStringRef other) const { size_t n = std::min(xs.num_u64_padded(), other.xs.num_u64_padded()); uint64_t v = 0; for (size_t k = 0; k < n; k++) { v |= (xs.u64[k] | zs.u64[k]) & (other.xs.u64[k] | other.zs.u64[k]); } return v != 0; } template size_t PauliStringRef::weight() const { size_t total = 0; xs.for_each_word(zs, [&](const simd_word &w1, const simd_word &w2) { total += (w1 | w2).popcount(); }); return total; } template bool PauliStringRef::has_no_pauli_terms() const { size_t total = 0; size_t n = xs.num_u64_padded(); for (size_t k = 0; k < n; k++) { total |= xs.u64[k] | zs.u64[k]; } return total == 0; } template std::ostream &operator<<(std::ostream &out, const PauliStringRef &ps) { out << "+-"[ps.sign]; for (size_t k = 0; k < ps.num_qubits; k++) { out << "_XZY"[ps.xs[k] + 2 * ps.zs[k]]; } return out; } template void PauliStringRef::do_H_XZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q].swap_with(zs[q]); sign ^= xs[q] && zs[q]; } } template void PauliStringRef::do_H_NXY(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; zs[q] ^= xs[q]; sign ^= !xs[q] && !zs[q]; sign ^= true; } } template void PauliStringRef::do_H_NXZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q].swap_with(zs[q]); sign ^= !xs[q] && !zs[q]; sign ^= true; } } template void PauliStringRef::do_H_NYZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q] ^= zs[q]; sign ^= !xs[q] && !zs[q]; sign ^= true; } } template void PauliStringRef::do_SQRT_Y(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q].swap_with(zs[q]); sign ^= !xs[q] && zs[q]; } } template void PauliStringRef::do_SQRT_Y_DAG(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q].swap_with(zs[q]); sign ^= xs[q] && !zs[q]; } } template void PauliStringRef::do_H_XY(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; zs[q] ^= xs[q]; sign ^= !xs[q] && zs[q]; } } template void PauliStringRef::do_SQRT_Z(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; zs[q] ^= xs[q]; sign ^= xs[q] && !zs[q]; } } template void PauliStringRef::do_SQRT_Z_DAG(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; zs[q] ^= xs[q]; sign ^= xs[q] && zs[q]; } } template void PauliStringRef::do_H_YZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q] ^= zs[q]; sign ^= xs[q] && !zs[q]; } } template void PauliStringRef::do_SQRT_X(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q] ^= zs[q]; sign ^= xs[q] && zs[q]; } } template void PauliStringRef::do_SQRT_X_DAG(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q] ^= zs[q]; sign ^= !xs[q] && zs[q]; } } template void PauliStringRef::do_C_XYZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; xs[q] ^= zs[q]; zs[q] ^= xs[q]; } } template void PauliStringRef::do_C_NXYZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= xs[q]; sign ^= zs[q]; xs[q] ^= zs[q]; zs[q] ^= xs[q]; } } template void PauliStringRef::do_C_XNYZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= xs[q]; xs[q] ^= zs[q]; zs[q] ^= xs[q]; } } template void PauliStringRef::do_C_XYNZ(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= zs[q]; xs[q] ^= zs[q]; zs[q] ^= xs[q]; } } template void PauliStringRef::do_C_ZYX(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; zs[q] ^= xs[q]; xs[q] ^= zs[q]; } } template void PauliStringRef::do_C_ZYNX(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= xs[q]; zs[q] ^= xs[q]; xs[q] ^= zs[q]; } } template void PauliStringRef::do_C_ZNYX(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= zs[q]; zs[q] ^= xs[q]; xs[q] ^= zs[q]; } } template void PauliStringRef::do_C_NZYX(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= xs[q]; sign ^= zs[q]; zs[q] ^= xs[q]; xs[q] ^= zs[q]; } } template void PauliStringRef::do_X(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= zs[q]; } } template void PauliStringRef::do_Y(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= xs[q] ^ zs[q]; } } template void PauliStringRef::do_Z(const CircuitInstruction &inst) { for (auto t : inst.targets) { auto q = t.data; sign ^= xs[q]; } } template void PauliStringRef::do_single_cx(const CircuitInstruction &inst, uint32_t c, uint32_t t) { c &= ~TARGET_INVERTED_BIT; t &= ~TARGET_INVERTED_BIT; if (!((c | t) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { bit_ref x1 = xs[c], x2 = xs[t], z1 = zs[c], z2 = zs[t]; z1 ^= z2; x2 ^= x1; sign ^= x1 && z2 && (z1 == x2); } else if (t & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) { throw std::invalid_argument( "CX had a bit (" + GateTarget{t}.str() + ") as its target, instead of its control."); } else { if (zs[t]) { std::stringstream ss; ss << "The pauli observable '" << *this; ss << "' is affected by a controlled operation in '" << inst; ss << "' but the controlling measurement result isn't known."; throw std::invalid_argument(ss.str()); } } } template void PauliStringRef::do_single_cy(const CircuitInstruction &inst, uint32_t c, uint32_t t) { c &= ~TARGET_INVERTED_BIT; t &= ~TARGET_INVERTED_BIT; if (!((c | t) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { bit_ref x1 = xs[c], x2 = xs[t], z1 = zs[c], z2 = zs[t]; z1 ^= x2 ^ z2; z2 ^= x1; x2 ^= x1; sign ^= x1 && !z1 && x2 && !z2; sign ^= x1 && z1 && !x2 && z2; } else if (t & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) { throw std::invalid_argument( "CY had a bit (" + GateTarget{t}.str() + ") as its target, instead of its control."); } else { if (xs[t] ^ zs[t]) { std::stringstream ss; ss << "The pauli observable '" << *this; ss << "' is affected by a controlled operation in '" << inst; ss << "' but the controlling measurement result isn't known."; throw std::invalid_argument(ss.str()); } } } template void PauliStringRef::do_single_cz(const CircuitInstruction &inst, uint32_t c, uint32_t t) { c &= ~TARGET_INVERTED_BIT; t &= ~TARGET_INVERTED_BIT; if (!((c | t) & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT))) { bit_ref x1 = xs[c], x2 = xs[t], z1 = zs[c], z2 = zs[t]; z1 ^= x2; z2 ^= x1; sign ^= x1 && x2 && (z1 ^ z2); } else { bool bc = !(c & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) && xs[c]; bool bt = !(t & (TARGET_RECORD_BIT | TARGET_SWEEP_BIT)) && xs[t]; if (bc || bt) { std::stringstream ss; ss << "The pauli observable '" << *this; ss << "' is affected by a controlled operation in '" << inst; ss << "' but the controlling measurement result isn't known."; throw std::invalid_argument(ss.str()); } } } template template void PauliStringRef::do_ZCX(const CircuitInstruction &inst) { assert((inst.targets.size() & 1) == 0); for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; do_single_cx(inst, q1, q2); } } template template void PauliStringRef::do_ZCY(const CircuitInstruction &inst) { assert((inst.targets.size() & 1) == 0); for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; do_single_cy(inst, q1, q2); } } template void PauliStringRef::do_ZCZ(const CircuitInstruction &inst) { const auto &targets = inst.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { do_single_cz(inst, targets[k].data, targets[k + 1].data); } } template template void PauliStringRef::do_SWAP(const CircuitInstruction &inst) { const auto &targets = inst.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { size_t k2 = reverse_order ? targets.size() - 2 - k : k; size_t q1 = targets[k2].data, q2 = targets[k2 + 1].data; zs[q1].swap_with(zs[q2]); xs[q1].swap_with(xs[q2]); } } template template void PauliStringRef::do_ISWAP(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; sign ^= x1 && z1 && !x2 && !z2; sign ^= !x1 && !z1 && x2 && z2; sign ^= (x1 ^ x2) && z1 && z2; auto dx = x1 ^ x2; z1 ^= dx; z2 ^= dx; z1.swap_with(z2); x1.swap_with(x2); } } template template void PauliStringRef::do_ISWAP_DAG(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; auto dx = x1 ^ x2; z1 ^= dx; z2 ^= dx; z1.swap_with(z2); x1.swap_with(x2); sign ^= x1 && z1 && !x2 && !z2; sign ^= !x1 && !z1 && x2 && z2; sign ^= (x1 ^ x2) && z1 && z2; } } template template void PauliStringRef::do_CXSWAP(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; sign ^= x1 && z2 && (z1 == x2); z2 ^= z1; z1 ^= z2; x1 ^= x2; x2 ^= x1; } } template template void PauliStringRef::do_CZSWAP(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; z1.swap_with(z2); x1.swap_with(x2); z1 ^= x2; z2 ^= x1; sign ^= x1 && x2 && (z1 ^ z2); } } template template void PauliStringRef::do_SWAPCX(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; z1 ^= z2; z2 ^= z1; x2 ^= x1; x1 ^= x2; sign ^= x1 && z2 && (z1 == x2); } } template void PauliStringRef::do_SQRT_XX(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t q1 = inst.targets[k].data, q2 = inst.targets[k + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; sign ^= !x1 && z1 && !z2; sign ^= !x2 && !z1 && z2; auto dz = z1 ^ z2; x1 ^= dz; x2 ^= dz; } } template void PauliStringRef::do_SQRT_XX_DAG(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t q1 = inst.targets[k].data, q2 = inst.targets[k + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; auto dz = z1 ^ z2; x1 ^= dz; x2 ^= dz; sign ^= !x1 && z1 && !z2; sign ^= !x2 && !z1 && z2; } } template void PauliStringRef::do_SQRT_YY(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t q1 = inst.targets[k].data, q2 = inst.targets[k + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; sign ^= x1 && z1 && x2 && !z2; sign ^= x1 && !z1 && x2 && z2; sign ^= x1 && !z1 && !x2 && !z2; sign ^= !x1 && !z1 && x2 && !z2; auto d = x1 ^ z1 ^ x2 ^ z2; x1 ^= d; z1 ^= d; x2 ^= d; z2 ^= d; } } template void PauliStringRef::do_SQRT_YY_DAG(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t q1 = inst.targets[k].data, q2 = inst.targets[k + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; auto d = x1 ^ z1 ^ x2 ^ z2; x1 ^= d; z1 ^= d; x2 ^= d; z2 ^= d; sign ^= x1 && z1 && x2 && !z2; sign ^= x1 && !z1 && x2 && z2; sign ^= x1 && !z1 && !x2 && !z2; sign ^= !x1 && !z1 && x2 && !z2; } } template void PauliStringRef::do_SQRT_ZZ(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t q1 = inst.targets[k].data, q2 = inst.targets[k + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; auto dx = x1 ^ x2; z1 ^= dx; z2 ^= dx; sign ^= !z1 && x1 && !x2; sign ^= !z2 && !x1 && x2; } } template void PauliStringRef::do_SQRT_ZZ_DAG(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t q1 = inst.targets[k].data, q2 = inst.targets[k + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; sign ^= !z1 && x1 && !x2; sign ^= !z2 && !x1 && x2; auto dx = x1 ^ x2; z1 ^= dx; z2 ^= dx; } } template void PauliStringRef::do_XCX(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t q1 = inst.targets[k].data, q2 = inst.targets[k + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; sign ^= (x1 != x2) && z1 && z2; x1 ^= z2; x2 ^= z1; } } template template void PauliStringRef::do_XCY(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; x1 ^= x2 ^ z2; x2 ^= z1; z2 ^= z1; sign ^= !x1 && z1 && !x2 && z2; sign ^= x1 && z1 && x2 && !z2; } } template template void PauliStringRef::do_XCZ(const CircuitInstruction &inst) { const auto &targets = inst.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { size_t k2 = reverse_order ? targets.size() - 2 - k : k; size_t q1 = targets[k2].data, q2 = targets[k2 + 1].data; do_single_cx(inst, q2, q1); } } template template void PauliStringRef::do_YCX(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k; size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; x2 ^= x1 ^ z1; x1 ^= z2; z1 ^= z2; sign ^= !x2 && z2 && !x1 && z1; sign ^= x2 && z2 && x1 && !z1; } } template void PauliStringRef::do_YCY(const CircuitInstruction &inst) { for (size_t k = 0; k < inst.targets.size(); k += 2) { size_t q1 = inst.targets[k].data, q2 = inst.targets[k + 1].data; bit_ref x1 = xs[q1], z1 = zs[q1], x2 = xs[q2], z2 = zs[q2]; bool y1 = x1 ^ z1; bool y2 = x2 ^ z2; x1 ^= y2; z1 ^= y2; x2 ^= y1; z2 ^= y1; sign ^= x1 && !z1 && !x2 && z2; sign ^= !x1 && z1 && x2 && !z2; } } template template void PauliStringRef::do_YCZ(const CircuitInstruction &inst) { const auto &targets = inst.targets; assert((targets.size() & 1) == 0); for (size_t k = 0; k < targets.size(); k += 2) { size_t k2 = reverse_order ? targets.size() - 2 - k : k; size_t q1 = targets[k2].data, q2 = targets[k2 + 1].data; do_single_cy(inst, q2, q1); } } } // namespace stim ================================================ FILE: src/stim/stabilizers/pauli_string_ref.test.cc ================================================ // 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. #include "stim/stabilizers/pauli_string_ref.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/tableau_simulator.h" #include "stim/stabilizers/pauli_string.h" #include "stim/util_bot/test_util.test.h" using namespace stim; template void check_pauli_string_do_instruction_agrees_with_tableau_sim(Gate gate, TableauSimulator &sim) { std::vector targets{ GateTarget::qubit(3), GateTarget::qubit(5), GateTarget::qubit(8), GateTarget::qubit(5), }; CircuitInstruction inst{gate.id, {}, targets, ""}; std::vector> before; for (size_t k = 0; k < 16; k++) { sim.inv_state.zs[k].sign = false; before.push_back(sim.inv_state.zs[k]); } auto tableau = gate.tableau(); if (gate.flags & GATE_TARGETS_PAIRS) { sim.inv_state.inplace_scatter_append(tableau, {3, 5}); sim.inv_state.inplace_scatter_append(tableau, {8, 5}); } else { sim.inv_state.inplace_scatter_append(tableau, {3}); sim.inv_state.inplace_scatter_append(tableau, {5}); sim.inv_state.inplace_scatter_append(tableau, {8}); sim.inv_state.inplace_scatter_append(tableau, {5}); } PauliString v(0); for (size_t k = 0; k < before.size(); k++) { v = before[k]; v.ref().do_instruction(inst); if (v != sim.inv_state.zs[k]) { EXPECT_EQ(v, sim.inv_state.zs[k]) << "do_" << gate.name << "\nbefore=" << before[k]; return; } v.ref().undo_instruction(inst); if (v != before[k]) { EXPECT_EQ(v, sim.inv_state.zs[k]) << "undo_" << gate.name; return; } } } TEST_EACH_WORD_SIZE_W(pauli_string, do_instruction_agrees_with_tableau_sim, { TableauSimulator sim(INDEPENDENT_TEST_RNG(), 16); sim.inv_state = Tableau::random(sim.inv_state.num_qubits, sim.rng); for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { check_pauli_string_do_instruction_agrees_with_tableau_sim(gate, sim); } } }) TEST_EACH_WORD_SIZE_W(pauli_string, intersects, { ASSERT_FALSE((PauliString("_").ref().intersects(PauliString("_")))); ASSERT_FALSE((PauliString("_").ref().intersects(PauliString("X")))); ASSERT_FALSE((PauliString("_").ref().intersects(PauliString("Y")))); ASSERT_FALSE((PauliString("_").ref().intersects(PauliString("Z")))); ASSERT_FALSE((PauliString("X").ref().intersects(PauliString("_")))); ASSERT_TRUE((PauliString("X").ref().intersects(PauliString("X")))); ASSERT_TRUE((PauliString("X").ref().intersects(PauliString("Y")))); ASSERT_TRUE((PauliString("X").ref().intersects(PauliString("Z")))); ASSERT_FALSE((PauliString("Y").ref().intersects(PauliString("_")))); ASSERT_TRUE((PauliString("Y").ref().intersects(PauliString("X")))); ASSERT_TRUE((PauliString("Y").ref().intersects(PauliString("Y")))); ASSERT_TRUE((PauliString("Y").ref().intersects(PauliString("Z")))); ASSERT_FALSE((PauliString("Z").ref().intersects(PauliString("_")))); ASSERT_TRUE((PauliString("Z").ref().intersects(PauliString("X")))); ASSERT_TRUE((PauliString("Z").ref().intersects(PauliString("Y")))); ASSERT_TRUE((PauliString("Z").ref().intersects(PauliString("Z")))); ASSERT_TRUE((PauliString("_Z").ref().intersects(PauliString("ZZ")))); ASSERT_TRUE((PauliString("Z_").ref().intersects(PauliString("ZZ")))); ASSERT_TRUE((PauliString("ZZ").ref().intersects(PauliString("ZZ")))); ASSERT_FALSE((PauliString("ZZ").ref().intersects(PauliString("__")))); ASSERT_FALSE((PauliString("__").ref().intersects(PauliString("XZ")))); ASSERT_FALSE((PauliString("________________________________________________") .ref() .intersects(PauliString("________________________________________________")))); ASSERT_TRUE((PauliString("_______________________________________X________") .ref() .intersects(PauliString("_______________________________________X________")))); }) TEST_EACH_WORD_SIZE_W(pauli_string, weight, { ASSERT_EQ(PauliString::from_str("+").ref().weight(), 0); ASSERT_EQ(PauliString::from_str("+I").ref().weight(), 0); ASSERT_EQ(PauliString::from_str("+X").ref().weight(), 1); ASSERT_EQ(PauliString::from_str("+Y").ref().weight(), 1); ASSERT_EQ(PauliString::from_str("+Z").ref().weight(), 1); ASSERT_EQ(PauliString::from_str("+IX").ref().weight(), 1); ASSERT_EQ(PauliString::from_str("+XZ").ref().weight(), 2); ASSERT_EQ(PauliString::from_str("+YY").ref().weight(), 2); ASSERT_EQ(PauliString::from_str("+XI").ref().weight(), 1); PauliString p(1000); ASSERT_EQ(p.ref().weight(), 0); for (size_t k = 0; k < 1000; k++) { p.xs[k] = k % 3 == 1; p.zs[k] = k % 5 == 1; } ASSERT_EQ(p.ref().weight(), 333 + 199 - 66); p.sign = true; ASSERT_EQ(p.ref().weight(), 333 + 199 - 66); }) TEST_EACH_WORD_SIZE_W(pauli_string, has_no_pauli_terms, { ASSERT_EQ((PauliString::from_str("+").ref().has_no_pauli_terms()), true); ASSERT_EQ((PauliString::from_str("+I").ref().has_no_pauli_terms()), true); ASSERT_EQ((PauliString::from_str("+X").ref().has_no_pauli_terms()), false); ASSERT_EQ((PauliString::from_str("+Y").ref().has_no_pauli_terms()), false); ASSERT_EQ((PauliString::from_str("+Z").ref().has_no_pauli_terms()), false); ASSERT_EQ((PauliString::from_str("+II").ref().has_no_pauli_terms()), true); ASSERT_EQ((PauliString::from_str("+IX").ref().has_no_pauli_terms()), false); ASSERT_EQ((PauliString::from_str("+XZ").ref().has_no_pauli_terms()), false); ASSERT_EQ((PauliString::from_str("+YY").ref().has_no_pauli_terms()), false); ASSERT_EQ((PauliString::from_str("+XI").ref().has_no_pauli_terms()), false); PauliString p(1000); ASSERT_TRUE(p.ref().has_no_pauli_terms()); p.xs[700] = true; ASSERT_FALSE(p.ref().has_no_pauli_terms()); p.zs[700] = true; ASSERT_FALSE(p.ref().has_no_pauli_terms()); p.xs[700] = false; ASSERT_FALSE(p.ref().has_no_pauli_terms()); }) TEST_EACH_WORD_SIZE_W(pauli_string, for_each_active_pauli, { auto v = PauliString(500); v.zs[0] = true; v.xs[20] = true; v.xs[50] = true; v.zs[50] = true; v.xs[63] = true; v.zs[63] = true; v.zs[100] = true; v.xs[200] = true; v.xs[301] = true; v.zs[301] = true; std::vector indices; v.ref().for_each_active_pauli([&](size_t index) { indices.push_back(index); }); ASSERT_EQ(indices, (std::vector{0, 20, 50, 63, 100, 200, 301})); }) ================================================ FILE: src/stim/stabilizers/tableau.h ================================================ /* * 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. */ #ifndef _STIM_STABILIZERS_TABLEAU_H #define _STIM_STABILIZERS_TABLEAU_H #include #include #include #include "stim/mem/simd_bit_table.h" #include "stim/mem/simd_util.h" #include "stim/mem/span_ref.h" #include "stim/stabilizers/pauli_string.h" namespace stim { template struct TableauHalf { size_t num_qubits; simd_bit_table xt; simd_bit_table zt; simd_bits signs; PauliStringRef operator[](size_t input_qubit); const PauliStringRef operator[](size_t input_qubit) const; TableauHalf(size_t num_qubits); }; /// A Tableau is a stabilizer tableau representation of a Clifford operation. /// It stores, for each X and Z observable for each qubit, what is produced when /// conjugating that observable by the operation. In other words, it explains how /// to transform "input side" Pauli products into "output side" Pauli products. /// /// The memory layout used by this class is column major, meaning iterating over /// the output observable is iterating along the grain of memory. This makes /// prepending operations cheap. To append operations, use TableauTransposedRaii. /// /// The template parameter, W, represents the SIMD width. template struct Tableau { size_t num_qubits; TableauHalf xs; TableauHalf zs; explicit Tableau(size_t num_qubits); bool operator==(const Tableau &other) const; bool operator!=(const Tableau &other) const; PauliString eval_y_obs(size_t qubit) const; std::string str() const; /// Grows the size of the tableau (or leaves it the same) by adding /// new rows and columns with identity elements along the diagonal. /// /// Requires: /// new_num_qubits >= this.num_qubits /// resize_pad_factor >= 1 /// /// Args: /// new_num_qubits: The new number of qubits the tableau represents. /// resize_pad_factor: When resizing, memory will be overallocated /// so that the tableau can be expanded to at least this many /// times the number of requested qubits. Use this to avoid /// quadratic overheads from constant slight expansions. void expand(size_t new_num_qubits, double resize_pad_factor); /// Creates a Tableau representing the identity operation. static Tableau identity(size_t num_qubits); /// Creates a Tableau from a PauliString via conjugation static Tableau from_pauli_string(const PauliString &pauli_string); /// Creates a Tableau representing a randomly sampled Clifford operation from a uniform distribution. static Tableau random(size_t num_qubits, std::mt19937_64 &rng); /// Returns the inverse Tableau. /// /// Args: /// skip_signs: Instead of computing the signs, just set them all to positive. Tableau inverse(bool skip_signs = false) const; /// Returns the Tableau raised to an integer power (using repeated squaring). Tableau raised_to(int64_t exponent) const; std::vector> to_flat_unitary_matrix(bool little_endian) const; bool satisfies_invariants() const; /// If a Tableau fixes each pauli upto sign, then it is conjugation by a pauli bool is_pauli_product() const; /// If tableau is conjugation by a pauli, then return that pauli. Else throw exception. PauliString to_pauli_string() const; /// Creates a Tableau representing a single qubit gate. /// /// All observables specified using the string format accepted by `PauliString::from_str`. /// For example: "-X" or "+Y". /// /// Args: /// x: The output-side observable that the input-side X observable gets mapped to. /// z: The output-side observable that the input-side Y observable gets mapped to. static Tableau gate1(const char *x, const char *z); /// Creates a Tableau representing a two qubit gate. /// /// All observables specified using the string format accepted by `PauliString::from_str`. /// For example: "-IX" or "+YZ". /// /// Args: /// x1: The output-side observable that the input-side XI observable gets mapped to. /// z1: The output-side observable that the input-side YI observable gets mapped to. /// x2: The output-side observable that the input-side IX observable gets mapped to. /// z2: The output-side observable that the input-side IY observable gets mapped to. static Tableau gate2(const char *x1, const char *z1, const char *x2, const char *z2); /// Returns the result of applying the tableau to the given Pauli string. /// /// Args: /// p: The input-side Pauli string. /// /// Returns: /// The output-side Pauli string. /// Algebraically: $c p c^{-1}$ where $c$ is the tableau's Clifford operation. PauliString operator()(const PauliStringRef &p) const; /// Returns the result of applying the tableau to `gathered_input.scatter(scattered_indices)`. PauliString scatter_eval( const PauliStringRef &gathered_input, const std::vector &scattered_indices) const; /// Returns a tableau equivalent to the composition of two tableaus of the same size. Tableau then(const Tableau &second) const; /// Applies the Tableau inplace to a subset of a Pauli string. void apply_within(PauliStringRef &target, SpanRef target_qubits) const; /// Appends a smaller operation into this tableau's operation. /// /// The new value T' of this tableau will equal the composition T o P = PT where T is the old /// value of this tableau and P is the operation to append. /// /// Args: /// operation: The smaller operation to append into this tableau. /// target_qubits: The qubits being acted on by `operation`. void inplace_scatter_append(const Tableau &operation, const std::vector &target_qubits); /// Prepends a smaller operation into this tableau's operation. /// /// The new value T' of this tableau will equal the composition P o T = TP where T is the old /// value of this tableau and P is the operation to append. /// /// Args: /// operation: The smaller operation to prepend into this tableau. /// target_qubits: The qubits being acted on by `operation`. void inplace_scatter_prepend(const Tableau &operation, const std::vector &target_qubits); /// Applies a transpose to the X2X, X2Z, Z2X, and Z2Z bit tables within the tableau. void do_transpose_quadrants(); /// Returns the direct sum of two tableaus. Tableau operator+(const Tableau &second) const; /// Appends the other tableau onto this one, resulting in the direct sum. Tableau &operator+=(const Tableau &second); /// === Specialized vectorized methods for prepending operations onto the tableau === /// void prepend_SWAP(size_t q1, size_t q2); void prepend_X(size_t q); void prepend_Y(size_t q); void prepend_Z(size_t q); void prepend_H_XZ(size_t q); void prepend_H_YZ(size_t q); void prepend_H_XY(size_t q); void prepend_H_NXY(size_t q); void prepend_H_NXZ(size_t q); void prepend_H_NYZ(size_t q); void prepend_C_XYZ(size_t q); void prepend_C_NXYZ(size_t q); void prepend_C_XNYZ(size_t q); void prepend_C_XYNZ(size_t q); void prepend_C_ZYX(size_t q); void prepend_C_NZYX(size_t q); void prepend_C_ZNYX(size_t q); void prepend_C_ZYNX(size_t q); void prepend_SQRT_X(size_t q); void prepend_SQRT_X_DAG(size_t q); void prepend_SQRT_Y(size_t q); void prepend_SQRT_Y_DAG(size_t q); void prepend_SQRT_Z(size_t q); void prepend_SQRT_Z_DAG(size_t q); void prepend_SQRT_XX(size_t q1, size_t q2); void prepend_SQRT_XX_DAG(size_t q1, size_t q2); void prepend_SQRT_YY(size_t q1, size_t q2); void prepend_SQRT_YY_DAG(size_t q1, size_t q2); void prepend_SQRT_ZZ(size_t q1, size_t q2); void prepend_SQRT_ZZ_DAG(size_t q1, size_t q2); void prepend_ZCX(size_t control, size_t target); void prepend_ZCY(size_t control, size_t target); void prepend_ZCZ(size_t control, size_t target); void prepend_ISWAP(size_t q1, size_t q2); void prepend_ISWAP_DAG(size_t q1, size_t q2); void prepend_XCX(size_t control, size_t target); void prepend_XCY(size_t control, size_t target); void prepend_XCZ(size_t control, size_t target); void prepend_YCX(size_t control, size_t target); void prepend_YCY(size_t control, size_t target); void prepend_YCZ(size_t control, size_t target); void prepend_pauli_product(const PauliStringRef &op); /// Builds the Y output by using Y = iXZ. PauliString y_output(size_t input_index) const; /// Constant-time version of tableau.xs[input_index][output_index]. uint8_t x_output_pauli_xyz(size_t input_index, size_t output_index) const; /// Constant-time version of tableau.y_output(input_index)[output_index]. uint8_t y_output_pauli_xyz(size_t input_index, size_t output_index) const; /// Constant-time version of tableau.zs[input_index][output_index]. uint8_t z_output_pauli_xyz(size_t input_index, size_t output_index) const; /// Constant-time version of tableau.inverse().xs[input_index][output_index]. uint8_t inverse_x_output_pauli_xyz(size_t input_index, size_t output_index) const; /// Constant-time version of tableau.inverse().y_output(input_index)[output_index]. uint8_t inverse_y_output_pauli_xyz(size_t input_index, size_t output_index) const; /// Constant-time version of tableau.inverse().zs[input_index][output_index]. uint8_t inverse_z_output_pauli_xyz(size_t input_index, size_t output_index) const; /// Faster version of tableau.inverse().xs[input_index]. PauliString inverse_x_output(size_t input_index, bool skip_sign = false) const; /// Faster version of tableau.inverse().y_output(input_index). PauliString inverse_y_output(size_t input_index, bool skip_sign = false) const; /// Faster version of tableau.inverse().zs[input_index]. PauliString inverse_z_output(size_t input_index, bool skip_sign = false) const; std::vector> stabilizers(bool canonical) const; }; template std::ostream &operator<<(std::ostream &out, const Tableau &ps); } // namespace stim #include "stim/stabilizers/tableau.inl" #include "stim/stabilizers/tableau_specialized_prepend.inl" #endif ================================================ FILE: src/stim/stabilizers/tableau.inl ================================================ // 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. #include #include #include #include #include #include #include "stim/gates/gates.h" #include "stim/simulators/vector_simulator.h" #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/tableau.h" namespace stim { template void Tableau::expand(size_t new_num_qubits, double resize_pad_factor) { // If the new qubits fit inside the padding, just extend into it. assert(new_num_qubits >= num_qubits); assert(resize_pad_factor >= 1); if (new_num_qubits <= xs.xt.num_major_bits_padded()) { size_t old_num_qubits = num_qubits; num_qubits = new_num_qubits; xs.num_qubits = new_num_qubits; zs.num_qubits = new_num_qubits; // Initialize identity elements along the diagonal. for (size_t k = old_num_qubits; k < new_num_qubits; k++) { xs[k].xs[k] = true; zs[k].zs[k] = true; } return; } // Move state to temporary storage then re-allocate to make room for additional qubits. size_t old_num_simd_words = xs.xt.num_simd_words_major; size_t old_num_qubits = num_qubits; Tableau old_state = std::move(*this); *this = Tableau((size_t)(new_num_qubits * resize_pad_factor)); this->num_qubits = new_num_qubits; this->xs.num_qubits = new_num_qubits; this->zs.num_qubits = new_num_qubits; // Copy stored state back into new larger space. auto partial_copy = [=](simd_bits_range_ref dst, simd_bits_range_ref src) { dst.word_range_ref(0, old_num_simd_words) = src; }; partial_copy(xs.signs, old_state.xs.signs); partial_copy(zs.signs, old_state.zs.signs); for (size_t k = 0; k < old_num_qubits; k++) { partial_copy(xs[k].xs, old_state.xs[k].xs); partial_copy(xs[k].zs, old_state.xs[k].zs); partial_copy(zs[k].xs, old_state.zs[k].xs); partial_copy(zs[k].zs, old_state.zs[k].zs); } } template PauliStringRef TableauHalf::operator[](size_t input_qubit) { size_t nw = (num_qubits + W - 1) / W; return PauliStringRef( num_qubits, signs[input_qubit], xt[input_qubit].word_range_ref(0, nw), zt[input_qubit].word_range_ref(0, nw)); } template const PauliStringRef TableauHalf::operator[](size_t input_qubit) const { size_t nw = (num_qubits + W - 1) / W; return PauliStringRef( num_qubits, signs[input_qubit], xt[input_qubit].word_range_ref(0, nw), zt[input_qubit].word_range_ref(0, nw)); } template PauliString Tableau::eval_y_obs(size_t qubit) const { PauliString result(xs[qubit]); uint8_t log_i = result.ref().inplace_right_mul_returning_log_i_scalar(zs[qubit]); log_i++; assert((log_i & 1) == 0); if (log_i & 2) { result.sign ^= true; } return result; } template Tableau::Tableau(size_t num_qubits) : num_qubits(num_qubits), xs(num_qubits), zs(num_qubits) { for (size_t q = 0; q < num_qubits; q++) { xs.xt[q][q] = true; zs.zt[q][q] = true; } } template TableauHalf::TableauHalf(size_t num_qubits) : num_qubits(num_qubits), xt(num_qubits, num_qubits), zt(num_qubits, num_qubits), signs(num_qubits) { } template Tableau Tableau::identity(size_t num_qubits) { return Tableau(num_qubits); } template Tableau Tableau::from_pauli_string(const PauliString &pauli_string) { Tableau tableau = identity(pauli_string.num_qubits); tableau.xs.signs = pauli_string.zs; tableau.zs.signs = pauli_string.xs; return tableau; } template Tableau Tableau::gate1(const char *x, const char *z) { Tableau result(1); result.xs[0] = PauliString::from_str(x); result.zs[0] = PauliString::from_str(z); assert((bool)result.zs[0].sign == (z[0] == '-')); return result; } template Tableau Tableau::gate2(const char *x1, const char *z1, const char *x2, const char *z2) { Tableau result(2); result.xs[0] = PauliString::from_str(x1); result.zs[0] = PauliString::from_str(z1); result.xs[1] = PauliString::from_str(x2); result.zs[1] = PauliString::from_str(z2); return result; } template std::ostream &operator<<(std::ostream &out, const Tableau &t) { out << "+-"; for (size_t k = 0; k < t.num_qubits; k++) { out << 'x'; out << 'z'; out << '-'; } out << "\n|"; for (size_t k = 0; k < t.num_qubits; k++) { out << ' '; out << "+-"[t.xs[k].sign]; out << "+-"[t.zs[k].sign]; } for (size_t q = 0; q < t.num_qubits; q++) { out << "\n|"; for (size_t k = 0; k < t.num_qubits; k++) { out << ' '; auto x = t.xs[k]; auto z = t.zs[k]; out << "_XZY"[x.xs[q] + 2 * x.zs[q]]; out << "_XZY"[z.xs[q] + 2 * z.zs[q]]; } } return out; } template std::string Tableau::str() const { std::stringstream ss; ss << *this; return ss.str(); } template void Tableau::inplace_scatter_append(const Tableau &operation, const std::vector &target_qubits) { assert(operation.num_qubits == target_qubits.size()); if (&operation == this) { Tableau independent_copy(operation); inplace_scatter_append(independent_copy, target_qubits); return; } for (size_t q = 0; q < num_qubits; q++) { auto x = xs[q]; auto z = zs[q]; operation.apply_within(x, target_qubits); operation.apply_within(z, target_qubits); } } template bool truncated_bits_equals(size_t nw, const simd_bits_range_ref &t1, const simd_bits_range_ref &t2) { return t1.word_range_ref(0, nw) == t2.word_range_ref(0, nw); } template bool truncated_tableau_equals(size_t n, const simd_bit_table &t1, const simd_bit_table &t2) { size_t nw = (n + W - 1) / W; for (size_t k = 0; k < n; k++) { if (!truncated_bits_equals(nw, t1[k], t2[k])) { return false; } } return true; } template bool Tableau::operator==(const Tableau &other) const { size_t nw = (num_qubits + W - 1) / W; return num_qubits == other.num_qubits && truncated_tableau_equals(num_qubits, xs.xt, other.xs.xt) && truncated_tableau_equals(num_qubits, xs.zt, other.xs.zt) && truncated_tableau_equals(num_qubits, zs.xt, other.zs.xt) && truncated_tableau_equals(num_qubits, zs.zt, other.zs.zt) && xs.signs.word_range_ref(0, nw) == other.xs.signs.word_range_ref(0, nw) && zs.signs.word_range_ref(0, nw) == other.zs.signs.word_range_ref(0, nw); } template bool Tableau::operator!=(const Tableau &other) const { return !(*this == other); } template void Tableau::inplace_scatter_prepend(const Tableau &operation, const std::vector &target_qubits) { assert(operation.num_qubits == target_qubits.size()); if (&operation == this) { Tableau independent_copy(operation); inplace_scatter_prepend(independent_copy, target_qubits); return; } std::vector> new_x; std::vector> new_z; new_x.reserve(operation.num_qubits); new_z.reserve(operation.num_qubits); for (size_t q = 0; q < operation.num_qubits; q++) { new_x.push_back(scatter_eval(operation.xs[q], target_qubits)); new_z.push_back(scatter_eval(operation.zs[q], target_qubits)); } for (size_t q = 0; q < operation.num_qubits; q++) { xs[target_qubits[q]] = new_x[q]; zs[target_qubits[q]] = new_z[q]; } } template PauliString Tableau::scatter_eval( const PauliStringRef &gathered_input, const std::vector &scattered_indices) const { assert(gathered_input.num_qubits == scattered_indices.size()); auto result = PauliString(num_qubits); result.sign = gathered_input.sign; for (size_t k_gathered = 0; k_gathered < gathered_input.num_qubits; k_gathered++) { size_t k_scattered = scattered_indices[k_gathered]; bool x = gathered_input.xs[k_gathered]; bool z = gathered_input.zs[k_gathered]; if (x) { if (z) { // Multiply by Y using Y = i*X*Z. uint8_t log_i = 1; log_i += result.ref().inplace_right_mul_returning_log_i_scalar(xs[k_scattered]); log_i += result.ref().inplace_right_mul_returning_log_i_scalar(zs[k_scattered]); assert((log_i & 1) == 0); result.sign ^= (log_i & 2) != 0; } else { result.ref() *= xs[k_scattered]; } } else if (z) { result.ref() *= zs[k_scattered]; } } return result; } template PauliString Tableau::operator()(const PauliStringRef &p) const { if (p.num_qubits != num_qubits) { throw std::out_of_range("pauli_string.num_qubits != tableau.num_qubits"); } std::vector indices; for (size_t k = 0; k < p.num_qubits; k++) { indices.push_back(k); } return scatter_eval(p, indices); } template void Tableau::apply_within(PauliStringRef &target, SpanRef target_qubits) const { assert(num_qubits == target_qubits.size()); auto inp = PauliString(num_qubits); target.gather_into(inp, target_qubits); auto out = (*this)(inp); out.ref().scatter_into(target, target_qubits); } /// Samples a vector of bits and a permutation from a skewed distribution. /// /// Reference: /// "Hadamard-free circuits expose the structure of the Clifford group" /// Sergey Bravyi, Dmitri Maslov /// https://arxiv.org/abs/2003.09412 inline std::pair, std::vector> sample_qmallows(size_t n, std::mt19937_64 &gen) { auto uni = std::uniform_real_distribution(0, 1); std::vector hada; std::vector permutation; std::vector remaining_indices; for (size_t k = 0; k < n; k++) { remaining_indices.push_back(k); } for (size_t i = 0; i < n; i++) { auto m = remaining_indices.size(); auto u = uni(gen); auto eps = pow(4, -(int)m); auto k = (size_t)-ceil(log2(u + (1 - u) * eps)); hada.push_back(k < m); if (k >= m) { k = 2 * m - k - 1; } permutation.push_back(remaining_indices[k]); remaining_indices.erase(remaining_indices.begin() + k); } return {hada, permutation}; } /// Samples a random valid stabilizer tableau. /// /// Reference: /// "Hadamard-free circuits expose the structure of the Clifford group" /// Sergey Bravyi, Dmitri Maslov /// https://arxiv.org/abs/2003.09412 template simd_bit_table random_stabilizer_tableau_raw(size_t n, std::mt19937_64 &rng) { auto hs_pair = sample_qmallows(n, rng); const auto &hada = hs_pair.first; const auto &perm = hs_pair.second; simd_bit_table symmetric(n, n); for (size_t row = 0; row < n; row++) { symmetric[row].randomize(row + 1, rng); for (size_t col = 0; col < row; col++) { symmetric[col][row] = symmetric[row][col]; } } simd_bit_table symmetric_m(n, n); for (size_t row = 0; row < n; row++) { symmetric_m[row].randomize(row + 1, rng); symmetric_m[row][row] &= hada[row]; for (size_t col = 0; col < row; col++) { bool b = hada[row] && hada[col]; b |= hada[row] > hada[col] && perm[row] < perm[col]; b |= hada[row] < hada[col] && perm[row] > perm[col]; symmetric_m[row][col] &= b; symmetric_m[col][row] = symmetric_m[row][col]; } } auto lower = simd_bit_table::identity(n); for (size_t row = 0; row < n; row++) { lower[row].randomize(row, rng); } auto lower_m = simd_bit_table::identity(n); for (size_t row = 0; row < n; row++) { lower_m[row].randomize(row, rng); for (size_t col = 0; col < row; col++) { bool b = hada[row] < hada[col]; b |= hada[row] && hada[col] && perm[row] > perm[col]; b |= !hada[row] && !hada[col] && perm[row] < perm[col]; lower_m[row][col] &= b; } } auto prod = symmetric.square_mat_mul(lower, n); auto prod_m = symmetric_m.square_mat_mul(lower_m, n); auto inv = lower.inverse_assuming_lower_triangular(n); auto inv_m = lower_m.inverse_assuming_lower_triangular(n); inv.do_square_transpose(); inv_m.do_square_transpose(); auto fused = simd_bit_table::from_quadrants(n, lower, simd_bit_table(n, n), prod, inv); auto fused_m = simd_bit_table::from_quadrants(n, lower_m, simd_bit_table(n, n), prod_m, inv_m); simd_bit_table u(2 * n, 2 * n); // Apply permutation. for (size_t row = 0; row < n; row++) { u[row] = fused[perm[row]]; u[row + n] = fused[perm[row] + n]; } // Apply Hadamards. for (size_t row = 0; row < n; row++) { if (hada[row]) { u[row].swap_with(u[row + n]); } } return fused_m.square_mat_mul(u, 2 * n); } template Tableau Tableau::random(size_t num_qubits, std::mt19937_64 &rng) { auto raw = random_stabilizer_tableau_raw(num_qubits, rng); Tableau result(num_qubits); for (size_t row = 0; row < num_qubits; row++) { for (size_t col = 0; col < num_qubits; col++) { result.xs[row].xs[col] = raw[row][col]; result.xs[row].zs[col] = raw[row][col + num_qubits]; result.zs[row].xs[col] = raw[row + num_qubits][col]; result.zs[row].zs[col] = raw[row + num_qubits][col + num_qubits]; } } result.xs.signs.randomize(num_qubits, rng); result.zs.signs.randomize(num_qubits, rng); return result; } template bool Tableau::satisfies_invariants() const { for (size_t q1 = 0; q1 < num_qubits; q1++) { auto x1 = xs[q1]; auto z1 = zs[q1]; if (x1.commutes(z1)) { return false; } for (size_t q2 = q1 + 1; q2 < num_qubits; q2++) { auto x2 = xs[q2]; auto z2 = zs[q2]; if (!x1.commutes(x2) || !x1.commutes(z2) || !z1.commutes(x2) || !z1.commutes(z2)) { return false; } } } return true; } template bool Tableau::is_pauli_product() const { size_t pop_count = xs.xt.data.popcnt() + xs.zt.data.popcnt() + zs.xt.data.popcnt() + zs.zt.data.popcnt(); if (pop_count != 2 * num_qubits) { return false; } for (size_t q = 0; q < num_qubits; q++) { if (xs.xt[q][q] == false) return false; } for (size_t q = 0; q < num_qubits; q++) { if (zs.zt[q][q] == false) return false; } return true; } template PauliString Tableau::to_pauli_string() const { if (!is_pauli_product()) { throw std::invalid_argument("The Tableau isn't equivalent to a Pauli product."); } PauliString pauli_string(num_qubits); pauli_string.xs = zs.signs; pauli_string.zs = xs.signs; return pauli_string; } template Tableau Tableau::inverse(bool skip_signs) const { Tableau result(xs.xt.num_major_bits_padded()); result.num_qubits = num_qubits; result.xs.num_qubits = num_qubits; result.zs.num_qubits = num_qubits; // Transpose data with xx zz swap tweak. result.xs.xt.data = zs.zt.data; result.xs.zt.data = xs.zt.data; result.zs.xt.data = zs.xt.data; result.zs.zt.data = xs.xt.data; result.do_transpose_quadrants(); // Fix signs by checking for consistent round trips. if (!skip_signs) { PauliString singleton(num_qubits); for (size_t k = 0; k < num_qubits; k++) { singleton.xs[k] = true; bool x_round_trip_sign = (*this)(result(singleton)).sign; singleton.xs[k] = false; singleton.zs[k] = true; bool z_round_trip_sign = (*this)(result(singleton)).sign; singleton.zs[k] = false; result.xs[k].sign ^= x_round_trip_sign; result.zs[k].sign ^= z_round_trip_sign; } } return result; } template void Tableau::do_transpose_quadrants() { xs.xt.do_square_transpose(); xs.zt.do_square_transpose(); zs.xt.do_square_transpose(); zs.zt.do_square_transpose(); } template Tableau Tableau::then(const Tableau &second) const { assert(num_qubits == second.num_qubits); Tableau result(num_qubits); for (size_t q = 0; q < num_qubits; q++) { result.xs[q] = second(xs[q]); result.zs[q] = second(zs[q]); } return result; } template Tableau Tableau::raised_to(int64_t exponent) const { Tableau result(num_qubits); if (exponent) { Tableau square = *this; if (exponent < 0) { square = square.inverse(); exponent *= -1; } while (true) { if (exponent & 1) { result = result.then(square); } exponent >>= 1; if (exponent == 0) { break; } square = square.then(square); } } return result; } template Tableau Tableau::operator+(const Tableau &second) const { Tableau copy = *this; copy += second; return copy; } template Tableau &Tableau::operator+=(const Tableau &second) { size_t n = num_qubits; expand(n + second.num_qubits, 1.1); for (size_t i = 0; i < second.num_qubits; i++) { xs.signs[n + i] = second.xs.signs[i]; zs.signs[n + i] = second.zs.signs[i]; for (size_t j = 0; j < second.num_qubits; j++) { xs.xt[n + i][n + j] = second.xs.xt[i][j]; xs.zt[n + i][n + j] = second.xs.zt[i][j]; zs.xt[n + i][n + j] = second.zs.xt[i][j]; zs.zt[n + i][n + j] = second.zs.zt[i][j]; } } return *this; } template uint8_t Tableau::x_output_pauli_xyz(size_t input_index, size_t output_index) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } if (output_index >= num_qubits) { throw std::invalid_argument("output_index >= len(tableau)"); } PauliStringRef x = xs[input_index]; return pauli_xz_to_xyz(x.xs[output_index], x.zs[output_index]); } template uint8_t Tableau::y_output_pauli_xyz(size_t input_index, size_t output_index) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } if (output_index >= num_qubits) { throw std::invalid_argument("output_index >= len(tableau)"); } PauliStringRef x = xs[input_index]; PauliStringRef z = zs[input_index]; return pauli_xz_to_xyz(x.xs[output_index] ^ z.xs[output_index], x.zs[output_index] ^ z.zs[output_index]); } template uint8_t Tableau::z_output_pauli_xyz(size_t input_index, size_t output_index) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } if (output_index >= num_qubits) { throw std::invalid_argument("output_index >= len(tableau)"); } PauliStringRef z = zs[input_index]; return pauli_xz_to_xyz(z.xs[output_index], z.zs[output_index]); } template uint8_t Tableau::inverse_x_output_pauli_xyz(size_t input_index, size_t output_index) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } if (output_index >= num_qubits) { throw std::invalid_argument("output_index >= len(tableau)"); } return pauli_xz_to_xyz(zs[output_index].zs[input_index], xs[output_index].zs[input_index]); } template uint8_t Tableau::inverse_y_output_pauli_xyz(size_t input_index, size_t output_index) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } if (output_index >= num_qubits) { throw std::invalid_argument("output_index >= len(tableau)"); } PauliStringRef x = xs[output_index]; PauliStringRef z = zs[output_index]; return pauli_xz_to_xyz(z.zs[input_index] ^ z.xs[input_index], x.zs[input_index] ^ x.xs[input_index]); } template uint8_t Tableau::inverse_z_output_pauli_xyz(size_t input_index, size_t output_index) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } if (output_index >= num_qubits) { throw std::invalid_argument("output_index >= len(tableau)"); } return pauli_xz_to_xyz(zs[output_index].xs[input_index], xs[output_index].xs[input_index]); } template PauliString Tableau::inverse_x_output(size_t input_index, bool skip_sign) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } PauliString result(num_qubits); for (size_t k = 0; k < num_qubits; k++) { result.xs[k] = zs[k].zs[input_index]; result.zs[k] = xs[k].zs[input_index]; } if (!skip_sign) { result.sign = (*this)(result).sign; } return result; } template PauliString Tableau::inverse_y_output(size_t input_index, bool skip_sign) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } PauliString result(num_qubits); for (size_t k = 0; k < num_qubits; k++) { result.xs[k] = zs[k].zs[input_index] ^ zs[k].xs[input_index]; result.zs[k] = xs[k].zs[input_index] ^ xs[k].xs[input_index]; } if (!skip_sign) { result.sign = (*this)(result).sign; } return result; } template PauliString Tableau::inverse_z_output(size_t input_index, bool skip_sign) const { if (input_index >= num_qubits) { throw std::invalid_argument("input_index >= len(tableau)"); } PauliString result(num_qubits); for (size_t k = 0; k < num_qubits; k++) { result.xs[k] = zs[k].xs[input_index]; result.zs[k] = xs[k].xs[input_index]; } if (!skip_sign) { result.sign = (*this)(result).sign; } return result; } template std::vector> Tableau::to_flat_unitary_matrix(bool little_endian) const { std::vector> pauli_strings; size_t nw = xs[0].xs.num_simd_words; // Add X transformation stabilizers. for (size_t k = 0; k < num_qubits; k++) { PauliString p(num_qubits * 2); p.xs.word_range_ref(0, nw) = xs[k].xs; p.zs.word_range_ref(0, nw) = xs[k].zs; p.sign = xs[k].sign; p.xs[num_qubits + k] ^= true; pauli_strings.push_back(p); } // Add Z transformation stabilizers. for (size_t k = 0; k < num_qubits; k++) { PauliString p(num_qubits * 2); p.xs.word_range_ref(0, nw) = zs[k].xs; p.zs.word_range_ref(0, nw) = zs[k].zs; p.sign = zs[k].sign; p.zs[num_qubits + k] ^= true; pauli_strings.push_back(p); } for (auto &p : pauli_strings) { for (size_t q = 0; q < num_qubits - q - 1; q++) { size_t q2 = num_qubits - q - 1; if (!little_endian) { p.xs[q].swap_with(p.xs[q2]); p.zs[q].swap_with(p.zs[q2]); p.xs[q + num_qubits].swap_with(p.xs[q2 + num_qubits]); p.zs[q + num_qubits].swap_with(p.zs[q2 + num_qubits]); } } for (size_t q = 0; q < num_qubits; q++) { p.xs[q].swap_with(p.xs[q + num_qubits]); p.zs[q].swap_with(p.zs[q + num_qubits]); } } // Turn it into a vector. std::vector> refs; for (const auto &e : pauli_strings) { refs.push_back(e.ref()); } return VectorSimulator::state_vector_from_stabilizers(refs, 1 << num_qubits); } template PauliString Tableau::y_output(size_t input_index) const { uint8_t log_i = 1; PauliString result = xs[input_index]; log_i += result.ref().inplace_right_mul_returning_log_i_scalar(zs[input_index]); assert((log_i & 1) == 0); result.sign ^= (log_i & 2) != 0; return result; } template std::vector> Tableau::stabilizers(bool canonical) const { std::vector> stabilizers; for (size_t k = 0; k < num_qubits; k++) { stabilizers.push_back(zs[k]); } if (canonical) { size_t min_pivot = 0; for (size_t q = 0; q < num_qubits; q++) { for (size_t b = 0; b < 2; b++) { size_t pivot = min_pivot; while (pivot < num_qubits && !(b ? stabilizers[pivot].zs : stabilizers[pivot].xs)[q]) { pivot++; } if (pivot == num_qubits) { continue; } for (size_t s = 0; s < num_qubits; s++) { if (s != pivot && (b ? stabilizers[s].zs : stabilizers[s].xs)[q]) { stabilizers[s].ref() *= stabilizers[pivot]; } } if (min_pivot != pivot) { std::swap(stabilizers[min_pivot], stabilizers[pivot]); } min_pivot += 1; } } } return stabilizers; } } // namespace stim ================================================ FILE: src/stim/stabilizers/tableau.perf.cc ================================================ // 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. #include "stim/stabilizers/tableau.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(tableau_random_10) { size_t n = 10; Tableau t(n); std::mt19937_64 rng(0); benchmark_go([&]() { t = Tableau::random(n, rng); }).goal_micros(30); } BENCHMARK(tableau_random_100) { size_t n = 100; Tableau t(n); std::mt19937_64 rng(0); benchmark_go([&]() { t = Tableau::random(n, rng); }).goal_millis(1.1); } BENCHMARK(tableau_random_256) { size_t n = 256; Tableau t(n); std::mt19937_64 rng(0); benchmark_go([&]() { t = Tableau::random(n, rng); }).goal_millis(7.5); } BENCHMARK(tableau_random_1000) { size_t n = 1000; Tableau t(n); std::mt19937_64 rng(0); benchmark_go([&]() { t = Tableau::random(n, rng); }).goal_millis(130); } BENCHMARK(tableau_cnot_10Kqubits) { size_t n = 10 * 1000; Tableau t(n); benchmark_go([&]() { t.prepend_ZCX(5, 20); }).goal_nanos(170); } ================================================ FILE: src/stim/stabilizers/tableau.pybind.cc ================================================ // 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. #include "stim/stabilizers/tableau.pybind.h" #include "flex_pauli_string.h" #include "stim/py/base.pybind.h" #include "stim/py/numpy.pybind.h" #include "stim/simulators/tableau_simulator.h" #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/tableau.h" #include "stim/stabilizers/tableau_iter.h" #include "stim/util_top/circuit_vs_amplitudes.h" #include "stim/util_top/stabilizers_to_tableau.h" #include "stim/util_top/stabilizers_vs_amplitudes.h" using namespace stim; using namespace stim_pybind; void check_tableau_signs_shape(const pybind11::object &numpy_array, size_t n, const char *name) { if (pybind11::isinstance>(numpy_array)) { auto arr = pybind11::cast>(numpy_array); if (arr.ndim() == 1) { size_t minor = arr.shape(0); if (minor != (n + 7) / 8) { std::stringstream ss; ss << name << " had dtype=uint8 (meaning it is bit packed) "; ss << "but its shape was " << minor << " instead of "; ss << (n + 7) / 8 << "."; throw std::invalid_argument(ss.str()); } return; } } else if (pybind11::isinstance>(numpy_array)) { auto arr = pybind11::cast>(numpy_array); if (arr.ndim() == 1) { size_t minor = arr.shape(0); if (minor != n) { std::stringstream ss; ss << name << " had dtype=bool_ "; ss << "but its shape was " << minor << " instead of "; ss << n << "."; throw std::invalid_argument(ss.str()); } } return; } std::stringstream ss; ss << name << " wasn't a 1d numpy array with dtype=bool_ or dtype=uint8"; throw std::invalid_argument(ss.str()); } void check_tableau_shape(const pybind11::object &numpy_array, size_t n, const char *name) { if (pybind11::isinstance>(numpy_array)) { auto arr = pybind11::cast>(numpy_array); if (arr.ndim() == 2) { size_t major = arr.shape(0); size_t minor = arr.shape(1); if (major != n || minor != (n + 7) / 8) { std::stringstream ss; ss << name << " had dtype=uint8 (meaning it is bit packed) "; ss << "but its shape was (" << major << ", " << minor << ") instead of ("; ss << n << ", " << (n + 7) / 8 << ")."; throw std::invalid_argument(ss.str()); } return; } } else if (pybind11::isinstance>(numpy_array)) { auto arr = pybind11::cast>(numpy_array); if (arr.ndim() == 2) { size_t major = arr.shape(0); size_t minor = arr.shape(1); if (major != n || minor != n) { std::stringstream ss; ss << name << " had dtype=bool_ "; ss << "but its shape was (" << major << ", " << minor << ") instead of ("; ss << n << ", " << n << ")."; throw std::invalid_argument(ss.str()); } } return; } std::stringstream ss; ss << name << " wasn't a 2d numpy array with dtype=bool_ or dtype=uint8"; throw std::invalid_argument(ss.str()); } size_t determine_tableau_shape(const pybind11::object &numpy_array, const char *name) { size_t n = 0; if (pybind11::isinstance>(numpy_array)) { auto arr = pybind11::cast>(numpy_array); if (arr.ndim() == 2) { n = arr.shape(0); } } else if (pybind11::isinstance>(numpy_array)) { auto arr = pybind11::cast>(numpy_array); if (arr.ndim() == 2) { n = arr.shape(0); } } check_tableau_shape(numpy_array, n, name); return n; } pybind11::class_> stim_pybind::pybind_tableau(pybind11::module &m) { return pybind11::class_>( m, "Tableau", clean_doc_string(R"DOC( A stabilizer tableau. Represents a Clifford operation by explicitly storing how that operation conjugates a list of Pauli group generators into composite Pauli products. Examples: >>> import stim >>> stim.Tableau.from_named_gate("H") stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], ) >>> t = stim.Tableau.random(5) >>> t_inv = t**-1 >>> print(t * t_inv) +-xz-xz-xz-xz-xz- | ++ ++ ++ ++ ++ | XZ __ __ __ __ | __ XZ __ __ __ | __ __ XZ __ __ | __ __ __ XZ __ | __ __ __ __ XZ >>> x2z3 = t.x_output(2) * t.z_output(3) >>> t_inv(x2z3) stim.PauliString("+__XZ_") )DOC") .data()); } void stim_pybind::pybind_tableau_methods(pybind11::module &m, pybind11::class_> &c) { c.def( pybind11::init(), pybind11::arg("num_qubits"), clean_doc_string(R"DOC( Creates an identity tableau over the given number of qubits. Examples: >>> import stim >>> t = stim.Tableau(3) >>> print(t) +-xz-xz-xz- | ++ ++ ++ | XZ __ __ | __ XZ __ | __ __ XZ Args: num_qubits: The number of qubits the tableau's operation acts on. )DOC") .data()); c.def_static( "random", [](size_t num_qubits) { auto rng = make_py_seeded_rng(pybind11::none()); return Tableau::random(num_qubits, rng); }, pybind11::arg("num_qubits"), clean_doc_string(R"DOC( Samples a uniformly random Clifford operation and returns its tableau. Args: num_qubits: The number of qubits the tableau should act on. Returns: The sampled tableau. Examples: >>> import stim >>> t = stim.Tableau.random(42) References: "Hadamard-free circuits expose the structure of the Clifford group" Sergey Bravyi, Dmitri Maslov https://arxiv.org/abs/2003.09412 )DOC") .data()); c.def_static( "iter_all", [](size_t num_qubits, bool unsigned_only) -> TableauIterator { return TableauIterator(num_qubits, !unsigned_only); }, pybind11::arg("num_qubits"), pybind11::kw_only(), pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( Returns an iterator that iterates over all Tableaus of a given size. Args: num_qubits: The size of tableau to iterate over. unsigned: Defaults to False. If set to True, only tableaus where all columns have positive sign are yielded by the iterator. This substantially reduces the total number of tableaus to iterate over. Returns: An Iterable[stim.Tableau] that yields the requested tableaus. Examples: >>> import stim >>> single_qubit_gate_reprs = set() >>> for t in stim.Tableau.iter_all(1): ... single_qubit_gate_reprs.add(repr(t)) >>> len(single_qubit_gate_reprs) 24 >>> num_2q_gates_mod_paulis = 0 >>> for _ in stim.Tableau.iter_all(2, unsigned=True): ... num_2q_gates_mod_paulis += 1 >>> num_2q_gates_mod_paulis 720 )DOC") .data()); c.def( "to_unitary_matrix", [](Tableau &self, std::string_view endian) { bool little_endian; if (endian == "little") { little_endian = true; } else if (endian == "big") { little_endian = false; } else { throw std::invalid_argument("endian not in ['little', 'big']"); } auto data = self.to_flat_unitary_matrix(little_endian); std::complex *buffer = new std::complex[data.size()]; for (size_t k = 0; k < data.size(); k++) { buffer[k] = data[k]; } pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast *>(f); }); pybind11::ssize_t n = 1 << self.num_qubits; pybind11::ssize_t itemsize = sizeof(std::complex); return pybind11::array_t>({n, n}, {n * itemsize, itemsize}, buffer, free_when_done); }, pybind11::kw_only(), pybind11::arg("endian"), clean_doc_string(R"DOC( @signature def to_unitary_matrix(self, *, endian: Literal["little", "big"]) -> np.ndarray[np.complex64]: Converts the tableau into a unitary matrix. For an n-qubit tableau, this method performs O(n 4^n) work. It uses the state channel duality to transform the tableau into a list of stabilizers, then generates a random state vector and projects it into the +1 eigenspace of each stabilizer. Note that tableaus don't have a defined global phase, so the result's global phase may be different from what you expect. For example, the square of SQRT_X's unitary might equal -X instead of +X. Args: endian: "little": The first qubit is the least significant (corresponds to an offset of 1 in the state vector). "big": The first qubit is the most significant (corresponds to an offset of 2**(n - 1) in the state vector). Returns: A numpy array with dtype=np.complex64 and shape=(1 << len(tableau), 1 << len(tableau)). Example: >>> import stim >>> cnot = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("XX"), ... stim.PauliString("_X"), ... ], ... zs=[ ... stim.PauliString("Z_"), ... stim.PauliString("ZZ"), ... ], ... ) >>> cnot.to_unitary_matrix(endian='big') array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64) )DOC") .data()); c.def( "to_numpy", [](const Tableau &self, bool bit_packed) { auto n = self.num_qubits; return pybind11::make_tuple( simd_bit_table_to_numpy(self.xs.xt, n, n, bit_packed, false, pybind11::none()), simd_bit_table_to_numpy(self.xs.zt, n, n, bit_packed, false, pybind11::none()), simd_bit_table_to_numpy(self.zs.xt, n, n, bit_packed, false, pybind11::none()), simd_bit_table_to_numpy(self.zs.zt, n, n, bit_packed, false, pybind11::none()), simd_bits_to_numpy(self.xs.signs, n, bit_packed), simd_bits_to_numpy(self.zs.signs, n, bit_packed)); }, pybind11::kw_only(), pybind11::arg("bit_packed") = false, clean_doc_string(R"DOC( @signature def to_numpy(self, *, bit_packed: bool = False) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: Decomposes the contents of the tableau into six numpy arrays. The first four numpy arrays correspond to the four quadrants of the table defined in Aaronson and Gottesman's "Improved Simulation of Stabilizer Circuits" ( https://arxiv.org/abs/quant-ph/0406196 ). The last two numpy arrays are the X and Z sign bit vectors of the tableau. Args: bit_packed: Defaults to False. Determines whether the output numpy arrays use dtype=bool_ or dtype=uint8 with 8 bools packed into each byte. Returns: An (x2x, x2z, z2x, z2z, x_signs, z_signs) tuple encoding the tableau. x2x: A 2d table of whether tableau(X_i)_j is X or Y (instead of I or Z). x2z: A 2d table of whether tableau(X_i)_j is Z or Y (instead of I or X). z2x: A 2d table of whether tableau(Z_i)_j is X or Y (instead of I or Z). z2z: A 2d table of whether tableau(Z_i)_j is Z or Y (instead of I or X). x_signs: A vector of whether tableau(X_i) is negative. z_signs: A vector of whether tableau(Z_i) is negative. If bit_packed=False then: x2x.dtype = np.bool_ x2z.dtype = np.bool_ z2x.dtype = np.bool_ z2z.dtype = np.bool_ x_signs.dtype = np.bool_ z_signs.dtype = np.bool_ x2x.shape = (len(tableau), len(tableau)) x2z.shape = (len(tableau), len(tableau)) z2x.shape = (len(tableau), len(tableau)) z2z.shape = (len(tableau), len(tableau)) x_signs.shape = len(tableau) z_signs.shape = len(tableau) x2x[i, j] = tableau.x_output_pauli(i, j) in [1, 2] x2z[i, j] = tableau.x_output_pauli(i, j) in [2, 3] z2x[i, j] = tableau.z_output_pauli(i, j) in [1, 2] z2z[i, j] = tableau.z_output_pauli(i, j) in [2, 3] If bit_packed=True then: x2x.dtype = np.uint8 x2z.dtype = np.uint8 z2x.dtype = np.uint8 z2z.dtype = np.uint8 x_signs.dtype = np.uint8 z_signs.dtype = np.uint8 x2x.shape = (len(tableau), math.ceil(len(tableau) / 8)) x2z.shape = (len(tableau), math.ceil(len(tableau) / 8)) z2x.shape = (len(tableau), math.ceil(len(tableau) / 8)) z2z.shape = (len(tableau), math.ceil(len(tableau) / 8)) x_signs.shape = math.ceil(len(tableau) / 8) z_signs.shape = math.ceil(len(tableau) / 8) (x2x[i, j // 8] >> (j % 8)) & 1 = tableau.x_output_pauli(i, j) in [1, 2] (x2z[i, j // 8] >> (j % 8)) & 1 = tableau.x_output_pauli(i, j) in [2, 3] (z2x[i, j // 8] >> (j % 8)) & 1 = tableau.z_output_pauli(i, j) in [1, 2] (z2z[i, j // 8] >> (j % 8)) & 1 = tableau.z_output_pauli(i, j) in [2, 3] Examples: >>> import stim >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> print(repr(cnot)) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+ZZ"), ], ) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = cnot.to_numpy() >>> x2x array([[ True, True], [False, True]]) >>> x2z array([[False, False], [False, False]]) >>> z2x array([[False, False], [False, False]]) >>> z2z array([[ True, False], [ True, True]]) >>> x_signs array([False, False]) >>> z_signs array([False, False]) >>> t = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("-Y_ZY"), ... stim.PauliString("-Y_YZ"), ... stim.PauliString("-XXX_"), ... stim.PauliString("+ZYX_"), ... ], ... zs=[ ... stim.PauliString("-_ZZX"), ... stim.PauliString("+YZXZ"), ... stim.PauliString("+XZ_X"), ... stim.PauliString("-YYXX"), ... ], ... ) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy() >>> x2x array([[ True, False, False, True], [ True, False, True, False], [ True, True, True, False], [False, True, True, False]]) >>> x2z array([[ True, False, True, True], [ True, False, True, True], [False, False, False, False], [ True, True, False, False]]) >>> z2x array([[False, False, False, True], [ True, False, True, False], [ True, False, False, True], [ True, True, True, True]]) >>> z2z array([[False, True, True, False], [ True, True, False, True], [False, True, False, False], [ True, True, False, False]]) >>> x_signs array([ True, True, True, False]) >>> z_signs array([ True, False, False, True]) >>> x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy(bit_packed=True) >>> x2x array([[9], [5], [7], [6]], dtype=uint8) >>> x2z array([[13], [13], [ 0], [ 3]], dtype=uint8) >>> z2x array([[ 8], [ 5], [ 9], [15]], dtype=uint8) >>> z2z array([[ 6], [11], [ 2], [ 3]], dtype=uint8) >>> x_signs array([7], dtype=uint8) >>> z_signs array([9], dtype=uint8) )DOC") .data()); c.def_static( "from_numpy", [](const pybind11::object &x2x, const pybind11::object &x2z, const pybind11::object &z2x, const pybind11::object &z2z, const pybind11::object &x_signs, const pybind11::object &z_signs) { size_t n = determine_tableau_shape(x2x, "x2x"); check_tableau_shape(x2z, n, "x2z"); check_tableau_shape(z2x, n, "z2x"); check_tableau_shape(z2z, n, "z2z"); if (!x_signs.is_none()) { check_tableau_signs_shape(x_signs, n, "x_signs"); } if (!z_signs.is_none()) { check_tableau_signs_shape(z_signs, n, "z_signs"); } Tableau result(n); memcpy_bits_from_numpy_to_simd_bit_table(n, n, x2x, result.xs.xt); memcpy_bits_from_numpy_to_simd_bit_table(n, n, x2z, result.xs.zt); memcpy_bits_from_numpy_to_simd_bit_table(n, n, z2x, result.zs.xt); memcpy_bits_from_numpy_to_simd_bit_table(n, n, z2z, result.zs.zt); if (!x_signs.is_none()) { memcpy_bits_from_numpy_to_simd(n, x_signs, result.xs.signs); } if (!z_signs.is_none()) { memcpy_bits_from_numpy_to_simd(n, z_signs, result.zs.signs); } if (!result.satisfies_invariants()) { throw std::invalid_argument( "The given tableau data don't describe a valid Clifford operation.\n" "It doesn't preserve commutativity.\n" "All generator outputs must commute, except for the output of X_k anticommuting with the output of " "Z_k for each k."); } return result; }, pybind11::kw_only(), pybind11::arg("x2x"), pybind11::arg("x2z"), pybind11::arg("z2x"), pybind11::arg("z2z"), pybind11::arg("x_signs") = pybind11::none(), pybind11::arg("z_signs") = pybind11::none(), clean_doc_string(R"DOC( @signature def from_numpy(self, *, x2x: np.ndarray, x2z: np.ndarray, z2x: np.ndarray, z2z: np.ndarray, x_signs: Optional[np.ndarray] = None, z_signs: Optional[np.ndarray] = None) -> stim.Tableau: Creates a tableau from numpy arrays x2x, x2z, z2x, z2z, x_signs, and z_signs. The x2x, x2z, z2x, z2z arrays are the four quadrants of the table defined in Aaronson and Gottesman's "Improved Simulation of Stabilizer Circuits" ( https://arxiv.org/abs/quant-ph/0406196 ). Args: x2x: A 2d numpy array containing the x-to-x coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.x_output_pauli(i, j) in [1, 2] == x2x[i, j]. Bit packing must be in little endian order and only applies to the second axis. x2z: A 2d numpy array containing the x-to-z coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.x_output_pauli(i, j) in [2, 3] == x2z[i, j]. Bit packing must be in little endian order and only applies to the second axis. z2x: A 2d numpy array containing the z-to-x coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.z_output_pauli(i, j) in [1, 2] == z2x[i, j]. Bit packing must be in little endian order and only applies to the second axis. z2z: A 2d numpy array containing the z-to-z coupling bits. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). When not bit packed, the result will satisfy result.z_output_pauli(i, j) in [2, 3] == z2z[i, j]. Bit packing must be in little endian order and only applies to the second axis. x_signs: Defaults to all-positive if not specified. A 1d numpy array containing the sign bits for the X generator outputs. False means positive and True means negative. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). Bit packing must be in little endian order. z_signs: Defaults to all-positive if not specified. A 1d numpy array containing the sign bits for the Z generator outputs. False means positive and True means negative. The bits can be bit packed (dtype=uint8) or not (dtype=bool_). Bit packing must be in little endian order. Returns: The tableau created from the numpy data. Examples: >>> import stim >>> import numpy as np >>> tableau = stim.Tableau.from_numpy( ... x2x=np.array([[1, 1], [0, 1]], dtype=np.bool_), ... z2x=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... x2z=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... z2z=np.array([[1, 0], [1, 1]], dtype=np.bool_), ... ) >>> tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+Z_"), stim.PauliString("+ZZ"), ], ) >>> tableau == stim.Tableau.from_named_gate("CNOT") True >>> stim.Tableau.from_numpy( ... x2x=np.array([[9], [5], [7], [6]], dtype=np.uint8), ... x2z=np.array([[13], [13], [0], [3]], dtype=np.uint8), ... z2x=np.array([[8], [5], [9], [15]], dtype=np.uint8), ... z2z=np.array([[6], [11], [2], [3]], dtype=np.uint8), ... x_signs=np.array([7], dtype=np.uint8), ... z_signs=np.array([9], dtype=np.uint8), ... ) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-Y_ZY"), stim.PauliString("-Y_YZ"), stim.PauliString("-XXX_"), stim.PauliString("+ZYX_"), ], zs=[ stim.PauliString("-_ZZX"), stim.PauliString("+YZXZ"), stim.PauliString("+XZ_X"), stim.PauliString("-YYXX"), ], ) )DOC") .data()); c.def( "to_pauli_string", [](const Tableau &self) { return FlexPauliString(self.to_pauli_string()); }, clean_doc_string(R"DOC( Return a Pauli string equivalent to the tableau. If the tableau is equivalent to a pauli product, creates an equivalent pauli string. If not, then an error is raised. Returns: The created pauli string Raises: ValueError: The Tableau isn't equivalent to a Pauli product. Example: >>> import stim >>> t = (stim.Tableau.from_named_gate("Z") + ... stim.Tableau.from_named_gate("Y") + ... stim.Tableau.from_named_gate("I") + ... stim.Tableau.from_named_gate("X")) >>> print(t) +-xz-xz-xz-xz- | -+ -- ++ +- | XZ __ __ __ | __ XZ __ __ | __ __ XZ __ | __ __ __ XZ >>> print(t.to_pauli_string()) +ZY_X )DOC") .data()); c.def( "to_circuit", [](Tableau &self, std::string_view method) { return tableau_to_circuit(self, method); }, pybind11::arg("method") = "elimination", clean_doc_string(R"DOC( @signature def to_circuit(self, method: Literal["elimination", "graph_state"] = 'elimination') -> stim.Circuit: Synthesizes a circuit that implements the tableau's Clifford operation. The circuits returned by this method are not guaranteed to be stable from version to version, and may be produced using randomization. Args: method: The method to use when synthesizing the circuit. Available values: "elimination": Cancels off-diagonal terms using Gaussian elimination. Gate set: H, S, CX Circuit qubit count: n Circuit operation count: O(n^2) Circuit depth: O(n^2) "graph_state": Prepares the tableau's state using a graph state circuit. Gate set: RX, CZ, H, S, X, Y, Z Circuit qubit count: n Circuit operation count: O(n^2) The circuit will be made up of three layers: 1. An RX layer initializing all qubits. 2. A CZ layer coupling the qubits. (Each CZ is an edge in the graph state.) 3. A single qubit rotation layer. Note: "graph_state" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. "mpp_state": Prepares the tableau's state using MPP and feedback. Gate set: MPP, CX rec, CY rec, CZ rec Circuit qubit count: n Circuit operation count: O(n^2) The circuit will be made up of two layers: 1. An MPP layer measuring each of the tableau's stabilizers. 2. A feedback layer using the measurement results to control whether or not to apply each of the tableau's destabilizers in order to get the correct sign for each stabilizer. Note: "mpp_state" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. "mpp_state_unsigned": Prepares the tableau's state up to sign using MPP. Gate set: MPP Circuit qubit count: n Circuit operation count: O(n^2) The circuit will contain a series of MPP measurements measuring each of the tableau's stabilizers. The stabilizers are measured in the order used by the tableau (i.e. tableau.z_output(k) is the k'th stabilizer measured). Note: "mpp_state_unsigned" treats the tableau as a state instead of as a Clifford operation. It will preserve the set of stabilizers, but not the exact choice of generators. Returns: The synthesized circuit. Example: >>> import stim >>> tableau = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("+YZ__"), ... stim.PauliString("-Y_XY"), ... stim.PauliString("+___Y"), ... stim.PauliString("+YZX_"), ... ], ... zs=[ ... stim.PauliString("+XZYY"), ... stim.PauliString("-XYX_"), ... stim.PauliString("-ZXXZ"), ... stim.PauliString("+XXZ_"), ... ], ... ) >>> tableau.to_circuit() stim.Circuit(''' S 0 H 0 1 3 CX 0 1 0 2 0 3 S 1 3 H 1 3 CX 1 0 3 0 3 1 1 3 3 1 H 1 S 1 CX 1 3 H 2 3 CX 2 1 3 1 3 2 2 3 3 2 H 3 CX 2 3 S 3 H 3 0 1 2 S 0 0 1 1 2 2 H 0 1 2 S 3 3 ''') >>> tableau.to_circuit("graph_state") stim.Circuit(''' RX 0 1 2 3 TICK CZ 0 3 1 2 1 3 TICK X 0 1 Z 2 S 2 3 H 3 S 3 ''') >>> tableau.to_circuit("mpp_state_unsigned") stim.Circuit(''' MPP X0*Z1*Y2*Y3 !X0*Y1*X2 !Z0*X1*X2*Z3 X0*X1*Z2 ''') >>> tableau.to_circuit("mpp_state") stim.Circuit(''' MPP X0*Z1*Y2*Y3 !X0*Y1*X2 !Z0*X1*X2*Z3 X0*X1*Z2 CX rec[-3] 2 rec[-1] 2 CY rec[-4] 0 rec[-3] 0 rec[-3] 3 rec[-2] 3 rec[-1] 0 CZ rec[-4] 1 rec[-1] 1 ''') )DOC") .data()); c.def_static( "from_named_gate", [](const char *name) { const Gate &gate = GATE_DATA.at(name); if (!(gate.flags & GATE_IS_UNITARY)) { throw std::out_of_range("Recognized name, but not unitary: " + std::string(name)); } return gate.tableau(); }, pybind11::arg("name"), clean_doc_string(R"DOC( Returns the tableau of a named Clifford gate. Args: name: The name of the Clifford gate. Returns: The gate's tableau. Examples: >>> import stim >>> print(stim.Tableau.from_named_gate("H")) +-xz- | ++ | ZX >>> print(stim.Tableau.from_named_gate("CNOT")) +-xz-xz- | ++ ++ | XZ _Z | X_ XZ >>> print(stim.Tableau.from_named_gate("S")) +-xz- | ++ | YZ )DOC") .data()); c.def( "__len__", [](const Tableau &self) { return self.num_qubits; }, clean_doc_string(R"DOC( Returns the number of qubits operated on by the tableau. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> len(t) 2 )DOC") .data()); c.def("__str__", &Tableau::str, "Returns a text description."); c.def(pybind11::self == pybind11::self, "Determines if two tableaus have identical contents."); c.def(pybind11::self != pybind11::self, "Determines if two tableaus have non-identical contents."); c.def( "__pow__", &Tableau::raised_to, pybind11::arg("exponent"), clean_doc_string(R"DOC( Raises the tableau to an integer power. Large powers are reached efficiently using repeated squaring. Negative powers are reached by inverting the tableau. Args: exponent: The power to raise to. Can be negative, zero, or positive. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> s**0 == stim.Tableau(1) True >>> s**1 == s True >>> s**2 == stim.Tableau.from_named_gate("Z") True >>> s**-1 == s**3 == stim.Tableau.from_named_gate("S_DAG") True >>> s**5 == s True >>> s**(400000000 + 1) == s True >>> s**(-400000000 + 1) == s True )DOC") .data()); c.def( "inverse", &Tableau::inverse, pybind11::kw_only(), pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( Computes the inverse of the tableau. The inverse T^-1 of a tableau T is the unique tableau with the property that T * T^-1 = T^-1 * T = I where I is the identity tableau. Args: unsigned: Defaults to false. When set to true, skips computing the signs of the output observables and instead just set them all to be positive. This is beneficial because computing the signs takes O(n^3) time and the rest of the inverse computation is O(n^2) where n is the number of qubits in the tableau. So, if you only need the Pauli terms (not the signs), it is significantly cheaper. Returns: The inverse tableau. Examples: >>> import stim >>> # Check that the inverse agrees with hard-coded tableaus. >>> s = stim.Tableau.from_named_gate("S") >>> s_dag = stim.Tableau.from_named_gate("S_DAG") >>> s.inverse() == s_dag True >>> z = stim.Tableau.from_named_gate("Z") >>> z.inverse() == z True >>> # Check that multiplying by the inverse produces the identity. >>> t = stim.Tableau.random(10) >>> t_inv = t.inverse() >>> identity = stim.Tableau(10) >>> t * t_inv == t_inv * t == identity True >>> # Check a manual case. >>> t = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("-__Z"), ... stim.PauliString("+XZ_"), ... stim.PauliString("+_ZZ"), ... ], ... zs=[ ... stim.PauliString("-YYY"), ... stim.PauliString("+Z_Z"), ... stim.PauliString("-ZYZ") ... ], ... ) >>> print(t.inverse()) +-xz-xz-xz- | -- +- -- | XX XX YX | XZ Z_ X_ | X_ YX Y_ >>> print(t.inverse(unsigned=True)) +-xz-xz-xz- | ++ ++ ++ | XX XX YX | XZ Z_ X_ | X_ YX Y_ )DOC") .data()); c.def( "append", [](Tableau &self, const Tableau &gate, const std::vector targets) { std::vector use(self.num_qubits, false); if (targets.size() != gate.num_qubits) { throw std::invalid_argument("len(targets) != len(gate)"); } for (size_t k : targets) { if (k >= self.num_qubits) { throw std::invalid_argument("target >= len(tableau)"); } if (use[k]) { throw std::invalid_argument("target collision on qubit " + std::to_string(k)); } use[k] = true; } self.inplace_scatter_append(gate, targets); }, pybind11::arg("gate"), pybind11::arg("targets"), clean_doc_string(R"DOC( @signature def append(self, gate: stim.Tableau, targets: Sequence[int]) -> None: Appends an operation's effect into this tableau, mutating this tableau. Time cost is O(n*m*m) where n=len(self) and m=len(gate). Args: gate: The tableau of the operation being appended into this tableau. targets: The qubits being targeted by the gate. Examples: >>> import stim >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> t = stim.Tableau(2) >>> t.append(cnot, [0, 1]) >>> t.append(cnot, [1, 0]) >>> t.append(cnot, [0, 1]) >>> t == stim.Tableau.from_named_gate("SWAP") True )DOC") .data()); c.def( "then", [](const Tableau &self, const Tableau &second) { if (self.num_qubits != second.num_qubits) { throw std::invalid_argument("len(self) != len(second)"); } return self.then(second); }, pybind11::arg("second"), clean_doc_string(R"DOC( Returns the result of composing two tableaus. If the tableau T1 represents the Clifford operation with unitary C1, and the tableau T2 represents the Clifford operation with unitary C2, then the tableau T1.then(T2) represents the Clifford operation with unitary C2*C1. Args: second: The result is equivalent to applying the second tableau after the receiving tableau. Examples: >>> import stim >>> t1 = stim.Tableau.random(4) >>> t2 = stim.Tableau.random(4) >>> t3 = t1.then(t2) >>> p = stim.PauliString.random(4) >>> t3(p) == t2(t1(p)) True )DOC") .data()); c.def( "__mul__", [](const Tableau &self, const Tableau &rhs) { if (self.num_qubits != rhs.num_qubits) { throw std::invalid_argument("len(lhs) != len(rhs)"); } return rhs.then(self); }, pybind11::arg("rhs"), clean_doc_string(R"DOC( Returns the product of two tableaus. If the tableau T1 represents the Clifford operation with unitary C1, and the tableau T2 represents the Clifford operation with unitary C2, then the tableau T1*T2 represents the Clifford operation with unitary C1*C2. Args: rhs: The tableau on the right hand side of the multiplication. Examples: >>> import stim >>> t1 = stim.Tableau.random(4) >>> t2 = stim.Tableau.random(4) >>> t3 = t2 * t1 >>> p = stim.PauliString.random(4) >>> t3(p) == t2(t1(p)) True )DOC") .data()); c.def( "prepend", [](Tableau &self, const Tableau &gate, const std::vector targets) { std::vector use(self.num_qubits, false); if (targets.size() != gate.num_qubits) { throw std::invalid_argument("len(targets) != len(gate)"); } for (size_t k : targets) { if (k >= self.num_qubits) { throw std::invalid_argument("target >= len(tableau)"); } if (use[k]) { throw std::invalid_argument("target collision on qubit " + std::to_string(k)); } use[k] = true; } self.inplace_scatter_prepend(gate, targets); }, pybind11::arg("gate"), pybind11::arg("targets"), clean_doc_string(R"DOC( @signature def prepend(self, gate: stim.Tableau, targets: Sequence[int]) -> None: Prepends an operation's effect into this tableau, mutating this tableau. Time cost is O(n*m*m) where n=len(self) and m=len(gate). Args: gate: The tableau of the operation being prepended into this tableau. targets: The qubits being targeted by the gate. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("H") >>> t.prepend(stim.Tableau.from_named_gate("X"), [0]) >>> t == stim.Tableau.from_named_gate("SQRT_Y_DAG") True )DOC") .data()); c.def( "x_output", [](Tableau &self, size_t target) { if (target >= self.num_qubits) { throw std::invalid_argument("target >= len(tableau)"); } return FlexPauliString(self.xs[target]); }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns the result of conjugating a Pauli X by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli X operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.x_output(0) stim.PauliString("+Z") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.x_output(0) stim.PauliString("+XX") >>> cnot.x_output(1) stim.PauliString("+_X") )DOC") .data()); c.def( "x_sign", [](Tableau &self, pybind11::ssize_t target) -> int { if (target < 0 || (size_t)target >= self.num_qubits) { throw std::invalid_argument("not 0 <= target < len(tableau)"); } return self.xs.signs[target] ? -1 : +1; }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns just the sign of the result of conjugating an X generator. This operation runs in constant time. Args: target: The qubit the X generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("S_DAG").x_sign(0) -1 >>> stim.Tableau.from_named_gate("S").x_sign(0) 1 )DOC") .data()); c.def( "z_sign", [](Tableau &self, pybind11::ssize_t target) -> int { if (target < 0 || (size_t)target >= self.num_qubits) { throw std::invalid_argument("not 0 <= target < len(tableau)"); } return self.zs.signs[target] ? -1 : +1; }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns just the sign of the result of conjugating a Z generator. This operation runs in constant time. Args: target: The qubit the Z generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("SQRT_X_DAG").z_sign(0) 1 >>> stim.Tableau.from_named_gate("SQRT_X").z_sign(0) -1 )DOC") .data()); c.def( "y_sign", [](Tableau &self, pybind11::ssize_t target) -> int { if (target < 0 || (size_t)target >= self.num_qubits) { throw std::invalid_argument("not 0 <= target < len(tableau)"); } return self.y_output(target).sign ? -1 : +1; }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns just the sign of the result of conjugating a Y generator. Unlike x_sign and z_sign, this operation runs in linear time. The Y generator has to be computed by multiplying the X and Z outputs and the sign depends on all terms. Args: target: The qubit the Y generator applies to. Examples: >>> import stim >>> stim.Tableau.from_named_gate("S_DAG").y_sign(0) 1 >>> stim.Tableau.from_named_gate("S").y_sign(0) -1 )DOC") .data()); c.def( "y_output", [](Tableau &self, size_t target) { if (target >= self.num_qubits) { throw std::invalid_argument("target >= len(tableau)"); } return FlexPauliString(self.y_output(target)); }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns the result of conjugating a Pauli Y by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli Y operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.y_output(0) stim.PauliString("-Y") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.y_output(0) stim.PauliString("+YX") >>> cnot.y_output(1) stim.PauliString("+ZY") )DOC") .data()); c.def( "z_output", [](Tableau &self, size_t target) { if (target >= self.num_qubits) { throw std::invalid_argument("target >= len(tableau)"); } return FlexPauliString(self.zs[target]); }, pybind11::arg("target"), clean_doc_string(R"DOC( Returns the result of conjugating a Pauli Z by the tableau's Clifford operation. Args: target: The qubit targeted by the Pauli Z operation. Examples: >>> import stim >>> h = stim.Tableau.from_named_gate("H") >>> h.z_output(0) stim.PauliString("+X") >>> cnot = stim.Tableau.from_named_gate("CNOT") >>> cnot.z_output(0) stim.PauliString("+Z_") >>> cnot.z_output(1) stim.PauliString("+ZZ") )DOC") .data()); c.def( "x_output_pauli", &Tableau::x_output_pauli_xyz, pybind11::arg("input_index"), pybind11::arg("output_index"), clean_doc_string(R"DOC( Constant-time version of `tableau.x_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input X generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.x_output_pauli(0, 0) 2 >>> t.x_output_pauli(0, 1) 0 >>> t.x_output_pauli(1, 0) 2 >>> t.x_output_pauli(1, 1) 3 )DOC") .data()); c.def( "y_output_pauli", &Tableau::y_output_pauli_xyz, pybind11::arg("input_index"), pybind11::arg("output_index"), clean_doc_string(R"DOC( Constant-time version of `tableau.y_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input Y generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.y_output_pauli(0, 0) 1 >>> t.y_output_pauli(0, 1) 2 >>> t.y_output_pauli(1, 0) 0 >>> t.y_output_pauli(1, 1) 2 )DOC") .data()); c.def( "z_output_pauli", &Tableau::z_output_pauli_xyz, pybind11::arg("input_index"), pybind11::arg("output_index"), clean_doc_string(R"DOC( Constant-time version of `tableau.z_output(input_index)[output_index]` Args: input_index: Identifies the tableau column (the qubit of the input Z generator). output_index: Identifies the tableau row (the output qubit). Returns: An integer identifying Pauli at the given location in the tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ) >>> t.z_output_pauli(0, 0) 3 >>> t.z_output_pauli(0, 1) 2 >>> t.z_output_pauli(1, 0) 2 >>> t.z_output_pauli(1, 1) 1 )DOC") .data()); c.def( "inverse_x_output_pauli", &Tableau::inverse_x_output_pauli_xyz, pybind11::arg("input_index"), pybind11::arg("output_index"), clean_doc_string(R"DOC( Constant-time version of `tableau.inverse().x_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input X generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_x_output_pauli(0, 0) 2 >>> t_inv.inverse_x_output_pauli(0, 1) 0 >>> t_inv.inverse_x_output_pauli(1, 0) 2 >>> t_inv.inverse_x_output_pauli(1, 1) 3 )DOC") .data()); c.def( "inverse_y_output_pauli", &Tableau::inverse_y_output_pauli_xyz, pybind11::arg("input_index"), pybind11::arg("output_index"), clean_doc_string(R"DOC( Constant-time version of `tableau.inverse().y_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input Y generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_y_output_pauli(0, 0) 1 >>> t_inv.inverse_y_output_pauli(0, 1) 2 >>> t_inv.inverse_y_output_pauli(1, 0) 0 >>> t_inv.inverse_y_output_pauli(1, 1) 2 )DOC") .data()); c.def( "inverse_z_output_pauli", &Tableau::inverse_z_output_pauli_xyz, pybind11::arg("input_index"), pybind11::arg("output_index"), clean_doc_string(R"DOC( Constant-time version of `tableau.inverse().z_output(input_index)[output_index]` Args: input_index: Identifies the column (the qubit of the input Z generator) in the inverse tableau. output_index: Identifies the row (the output qubit) in the inverse tableau. Returns: An integer identifying Pauli at the given location in the inverse tableau: 0: I 1: X 2: Y 3: Z Examples: >>> import stim >>> t_inv = stim.Tableau.from_conjugated_generators( ... xs=[stim.PauliString("-Y_"), stim.PauliString("+YZ")], ... zs=[stim.PauliString("-ZY"), stim.PauliString("+YX")], ... ).inverse() >>> t_inv.inverse_z_output_pauli(0, 0) 3 >>> t_inv.inverse_z_output_pauli(0, 1) 2 >>> t_inv.inverse_z_output_pauli(1, 0) 2 >>> t_inv.inverse_z_output_pauli(1, 1) 1 )DOC") .data()); c.def( "inverse_x_output", [](const Tableau &self, size_t input_index, bool skip_sign) { return FlexPauliString(self.inverse_x_output(input_index, skip_sign)); }, pybind11::arg("input_index"), pybind11::kw_only(), pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( Conjugates a single-qubit X Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).x_output(input_index)`. Args: input_index: Identifies the column (the qubit of the X generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating an X generator by the inverse of the tableau. Examples: >>> import stim # Check equivalence with the inverse's x_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().x_output(0) >>> t.inverse_x_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_x_output(0, unsigned=True) == expected True )DOC") .data()); c.def( "inverse_y_output", [](const Tableau &self, size_t input_index, bool skip_sign) { return FlexPauliString(self.inverse_y_output(input_index, skip_sign)); }, pybind11::arg("input_index"), pybind11::kw_only(), pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( Conjugates a single-qubit Y Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).y_output(input_index)`. Args: input_index: Identifies the column (the qubit of the Y generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating a Y generator by the inverse of the tableau. Examples: >>> import stim # Check equivalence with the inverse's y_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().y_output(0) >>> t.inverse_y_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_y_output(0, unsigned=True) == expected True )DOC") .data()); c.def( "inverse_z_output", [](const Tableau &self, size_t input_index, bool skip_sign) { return FlexPauliString(self.inverse_z_output(input_index, skip_sign)); }, pybind11::arg("input_index"), pybind11::kw_only(), pybind11::arg("unsigned") = false, clean_doc_string(R"DOC( Conjugates a single-qubit Z Pauli generator by the inverse of the tableau. A faster version of `tableau.inverse(unsigned).z_output(input_index)`. Args: input_index: Identifies the column (the qubit of the Z generator) to return from the inverse tableau. unsigned: Defaults to false. When set to true, skips computing the result's sign and instead just sets it to positive. This is beneficial because computing the sign takes O(n^2) time whereas all other parts of the computation take O(n) time where n is the number of qubits in the tableau. Returns: The result of conjugating a Z generator by the inverse of the tableau. Examples: >>> import stim >>> import stim # Check equivalence with the inverse's z_output. >>> t = stim.Tableau.random(4) >>> expected = t.inverse().z_output(0) >>> t.inverse_z_output(0) == expected True >>> expected.sign = +1 >>> t.inverse_z_output(0, unsigned=True) == expected True )DOC") .data()); c.def( "copy", [](Tableau &self) { Tableau copy = self; return copy; }, clean_doc_string(R"DOC( Returns a copy of the tableau. An independent tableau with the same contents. Examples: >>> import stim >>> t1 = stim.Tableau.random(2) >>> t2 = t1.copy() >>> t2 is t1 False >>> t2 == t1 True )DOC") .data()); c.def_static( "from_conjugated_generators", [](const std::vector &xs, const std::vector &zs) { size_t n = xs.size(); if (n != zs.size()) { throw std::invalid_argument("len(xs) != len(zs)"); } for (const auto &p : xs) { if (p.imag) { throw std::invalid_argument("Conjugated generator can't have imaginary sign."); } if (p.value.num_qubits != n) { throw std::invalid_argument("not all(len(p) == len(xs) for p in xs)"); } } for (const auto &p : zs) { if (p.imag) { throw std::invalid_argument("Conjugated generator can't have imaginary sign."); } if (p.value.num_qubits != n) { throw std::invalid_argument("not all(len(p) == len(zs) for p in zs)"); } } Tableau result(n); for (size_t q = 0; q < n; q++) { result.xs[q] = xs[q].value; result.zs[q] = zs[q].value; } if (!result.satisfies_invariants()) { throw std::invalid_argument( "The given generator outputs don't describe a valid Clifford operation.\n" "They don't preserve commutativity.\n" "Everything must commute, except for X_k anticommuting with Z_k for each k."); } return result; }, pybind11::kw_only(), pybind11::arg("xs"), pybind11::arg("zs"), clean_doc_string(R"DOC( Creates a tableau from the given outputs for each generator. Verifies that the tableau is well formed. Args: xs: A List[stim.PauliString] with the results of conjugating X0, X1, etc. zs: A List[stim.PauliString] with the results of conjugating Z0, Z1, etc. Returns: The created tableau. Raises: ValueError: The given outputs are malformed. Their lengths are inconsistent, or they don't satisfy the required commutation relationships. Examples: >>> import stim >>> identity3 = stim.Tableau.from_conjugated_generators( ... xs=[ ... stim.PauliString("X__"), ... stim.PauliString("_X_"), ... stim.PauliString("__X"), ... ], ... zs=[ ... stim.PauliString("Z__"), ... stim.PauliString("_Z_"), ... stim.PauliString("__Z"), ... ], ... ) >>> identity3 == stim.Tableau(3) True )DOC") .data()); c.def_static( "from_unitary_matrix", [](const pybind11::object &matrix, std::string_view endian) { bool little_endian; if (endian == "little") { little_endian = true; } else if (endian == "big") { little_endian = false; } else { throw std::invalid_argument("endian not in ['little', 'big']"); } std::vector>> converted_matrix; for (const auto &row : matrix) { converted_matrix.push_back({}); for (const auto &cell : row) { converted_matrix.back().push_back(pybind11::cast>(cell)); } } return unitary_to_tableau(converted_matrix, little_endian); }, pybind11::arg("matrix"), pybind11::kw_only(), pybind11::arg("endian"), clean_doc_string(R"DOC( @signature def from_unitary_matrix(matrix: Iterable[Iterable[float]], *, endian: Literal["little", "big"] = 'little') -> stim.Tableau: Creates a tableau from the unitary matrix of a Clifford operation. Args: matrix: A unitary matrix specified as an iterable of rows, with each row is an iterable of amplitudes. The unitary matrix must correspond to a Clifford operation. endian: "little": matrix entries are in little endian order, where higher index qubits correspond to larger changes in row/col indices. "big": matrix entries are in big endian order, where higher index qubits correspond to smaller changes in row/col indices. Returns: The tableau equivalent to the given unitary matrix (up to global phase). Raises: ValueError: The given matrix isn't the unitary matrix of a Clifford operation. Examples: >>> import stim >>> stim.Tableau.from_unitary_matrix([ ... [1, 0], ... [0, 1j], ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Y"), ], zs=[ stim.PauliString("+Z"), ], ) >>> stim.Tableau.from_unitary_matrix([ ... [1, 0, 0, 0], ... [0, 1, 0, 0], ... [0, 0, 0, -1j], ... [0, 0, 1j, 0], ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XZ"), stim.PauliString("+YX"), ], zs=[ stim.PauliString("+ZZ"), stim.PauliString("+_Z"), ], ) )DOC") .data()); c.def_static( "from_circuit", [](const Circuit &circuit, bool ignore_noise, bool ignore_measurement, bool ignore_reset) { return circuit_to_tableau(circuit, ignore_noise, ignore_measurement, ignore_reset); }, pybind11::arg("circuit"), pybind11::kw_only(), pybind11::arg("ignore_noise") = false, pybind11::arg("ignore_measurement") = false, pybind11::arg("ignore_reset") = false, clean_doc_string(R"DOC( @signature def from_circuit(circuit: stim.Circuit, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False) -> stim.Tableau: Converts a circuit into an equivalent stabilizer tableau. Args: circuit: The circuit to compile into a tableau. ignore_noise: Defaults to False. When False, any noise operations in the circuit will cause the conversion to fail with an exception. When True, noise operations are skipped over as if they weren't even present in the circuit. ignore_measurement: Defaults to False. When False, any measurement operations in the circuit will cause the conversion to fail with an exception. When True, measurement operations are skipped over as if they weren't even present in the circuit. ignore_reset: Defaults to False. When False, any reset operations in the circuit will cause the conversion to fail with an exception. When True, reset operations are skipped over as if they weren't even present in the circuit. Returns: The tableau equivalent to the given circuit (up to global phase). Raises: ValueError: The circuit contains noise operations but ignore_noise=False. OR The circuit contains measurement operations but ignore_measurement=False. OR The circuit contains reset operations but ignore_reset=False. Examples: >>> import stim >>> stim.Tableau.from_circuit(stim.Circuit(''' ... H 0 ... CNOT 0 1 ... ''')) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) )DOC") .data()); c.def( "__repr__", [](const Tableau &self) { std::stringstream result; result << "stim.Tableau.from_conjugated_generators(\n xs=[\n"; for (size_t q = 0; q < self.num_qubits; q++) { result << " stim.PauliString(\"" << self.xs[q].str() << "\"),\n"; } result << " ],\n zs=[\n"; for (size_t q = 0; q < self.num_qubits; q++) { result << " stim.PauliString(\"" << self.zs[q].str() << "\"),\n"; } result << " ],\n)"; return result.str(); }, "Returns valid python code evaluating to an equal `stim.Tableau`."); c.def( "__call__", [](const Tableau &self, const FlexPauliString &pauli_string) { FlexPauliString result{self(pauli_string.value)}; if (pauli_string.imag) { result *= std::complex(0, 1); } return result; }, pybind11::arg("pauli_string"), clean_doc_string(R"DOC( Returns the equivalent PauliString after the Tableau's Clifford operation. If P is a Pauli product before a Clifford operation C, then this method returns Q = C * P * C**-1 (the conjugation of P by C). Q is the equivalent Pauli product after C. This works because: C*P = C*P * I = C*P * (C**-1 * C) = (C*P*C**-1) * C = Q*C (Keep in mind that A*B means first B is applied, then A is applied.) Args: pauli_string: The pauli string to conjugate. Returns: The new conjugated pauli string. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> p = stim.PauliString("XX") >>> result = t(p) >>> print(result) +X_ )DOC") .data()); c.def( pybind11::self + pybind11::self, pybind11::arg("rhs"), clean_doc_string(R"DOC( Returns the direct sum (diagonal concatenation) of two Tableaus. Args: rhs: A second stim.Tableau. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> cz = stim.Tableau.from_named_gate("CZ") >>> print(s + cz) +-xz-xz-xz- | ++ ++ ++ | YZ __ __ | __ XZ Z_ | __ Z_ XZ Returns: The direct sum. )DOC") .data()); c.def( pybind11::self += pybind11::self, pybind11::arg("rhs"), clean_doc_string(R"DOC( Performs an inplace direct sum (diagonal concatenation). Args: rhs: A second stim.Tableau. Examples: >>> import stim >>> s = stim.Tableau.from_named_gate("S") >>> cz = stim.Tableau.from_named_gate("CZ") >>> alias = s >>> s += cz >>> alias is s True >>> print(s) +-xz-xz-xz- | ++ ++ ++ | YZ __ __ | __ XZ Z_ | __ Z_ XZ Returns: The mutated tableau. )DOC") .data()); c.def( pybind11::pickle( [](const Tableau &self) { pybind11::dict d; std::vector xs; std::vector zs; for (size_t q = 0; q < self.num_qubits; q++) { xs.push_back(FlexPauliString(self.xs[q])); } for (size_t q = 0; q < self.num_qubits; q++) { zs.push_back(FlexPauliString(self.zs[q])); } d["xs"] = xs; d["zs"] = zs; return d; }, [](const pybind11::dict &d) { std::vector xs; std::vector zs; for (const auto &e : d["xs"]) { xs.push_back(pybind11::cast(e)); } for (const auto &e : d["zs"]) { zs.push_back(pybind11::cast(e)); } size_t n = xs.size(); bool correct_shape = zs.size() == n; for (const auto &e : xs) { correct_shape &= !e.imag; correct_shape &= e.value.num_qubits == n; } for (const auto &e : zs) { correct_shape &= !e.imag; correct_shape &= e.value.num_qubits == n; } if (!correct_shape) { throw std::invalid_argument("Invalid pickle."); } Tableau result(n); for (size_t q = 0; q < n; q++) { result.xs[q] = xs[q].value; result.zs[q] = zs[q].value; } if (!result.satisfies_invariants()) { throw std::invalid_argument("Pickled tableau was invalid. It doesn't preserve commutativity."); } return result; })); c.def_static( "from_stabilizers", [](pybind11::object &stabilizers, bool allow_redundant, bool allow_underconstrained) { std::vector> converted_stabilizers; for (const auto &stabilizer : stabilizers) { const FlexPauliString &p = pybind11::cast(stabilizer); if (p.imag) { throw std::invalid_argument("Stabilizers can't have imaginary sign."); } converted_stabilizers.push_back(p.value); } return stabilizers_to_tableau( converted_stabilizers, allow_redundant, allow_underconstrained, false); }, pybind11::arg("stabilizers"), pybind11::kw_only(), pybind11::arg("allow_redundant") = false, pybind11::arg("allow_underconstrained") = false, clean_doc_string(R"DOC( @signature def from_stabilizers(stabilizers: Iterable[stim.PauliString], *, allow_redundant: bool = False, allow_underconstrained: bool = False) -> stim.Tableau: Creates a tableau representing a state with the given stabilizers. Args: stabilizers: A list of `stim.PauliString`s specifying the stabilizers that the state must have. It is permitted for stabilizers to have different lengths. All stabilizers are padded up to the length of the longest stabilizer by appending identity terms. allow_redundant: Defaults to False. If set to False, then the given stabilizers must all be independent. If any one of them is a product of the others (including the empty product), an exception will be raised. If set to True, then redundant stabilizers are simply ignored. allow_underconstrained: Defaults to False. If set to False, then the given stabilizers must form a complete set of generators. They must exactly specify the desired stabilizer state, with no degrees of freedom left over. For an n-qubit state there must be n independent stabilizers. If set to True, then there can be leftover degrees of freedom which can be set arbitrarily. Returns: A tableau which, when applied to the all-zeroes state, produces a state with the given stabilizers. Guarantees that result.z_output(k) will be equal to the k'th independent stabilizer from the `stabilizers` argument. Raises: ValueError: A stabilizer is redundant but allow_redundant=True wasn't set. OR The given stabilizers are contradictory (e.g. "+Z" and "-Z" both specified). OR The given stabilizers anticommute (e.g. "+Z" and "+X" both specified). OR The stabilizers left behind a degree of freedom but allow_underconstrained=True wasn't set. OR A stabilizer has an imaginary sign (i or -i). Examples: >>> import stim >>> stim.Tableau.from_stabilizers([ ... stim.PauliString("XX"), ... stim.PauliString("ZZ"), ... ]) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) >>> stim.Tableau.from_stabilizers([ ... stim.PauliString("XX_"), ... stim.PauliString("ZZ_"), ... stim.PauliString("-YY_"), ... stim.PauliString(""), ... ], allow_underconstrained=True, allow_redundant=True) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z__"), stim.PauliString("+_X_"), stim.PauliString("+__X"), ], zs=[ stim.PauliString("+XX_"), stim.PauliString("+ZZ_"), stim.PauliString("+__Z"), ], ) )DOC") .data()); c.def_static( "from_state_vector", [](pybind11::object &state_vector, std::string_view endian) { bool little_endian; if (endian == "little") { little_endian = true; } else if (endian == "big") { little_endian = false; } else { throw std::invalid_argument("endian not in ['little', 'big']"); } std::vector> v; for (const auto &obj : state_vector) { v.push_back(pybind11::cast>(obj)); } return circuit_to_tableau( stabilizer_state_vector_to_circuit(v, little_endian), false, false, false); }, pybind11::arg("state_vector"), pybind11::kw_only(), pybind11::arg("endian"), clean_doc_string(R"DOC( @signature def from_state_vector(state_vector: Iterable[float], *, endian: Literal["little", "big"]) -> stim.Tableau: Creates a tableau representing the stabilizer state of the given state vector. Args: state_vector: A list of complex amplitudes specifying a superposition. The vector must correspond to a state that is reachable using Clifford operations, and can be unnormalized. endian: "little": state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A tableau which, when applied to the all-zeroes state, produces a state with the given state vector. Raises: ValueError: The given state vector isn't a list of complex values specifying a stabilizer state. OR The given endian value isn't 'little' or 'big'. Examples: >>> import stim >>> stim.Tableau.from_state_vector([ ... 0.5**0.5, ... 0.5**0.5 * 1j, ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+Y"), ], ) >>> stim.Tableau.from_state_vector([ ... 0.5**0.5, ... 0, ... 0, ... 0.5**0.5, ... ], endian='little') stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z_"), stim.PauliString("+_X"), ], zs=[ stim.PauliString("+XX"), stim.PauliString("+ZZ"), ], ) )DOC") .data()); c.def( "to_state_vector", [](const Tableau &self, std::string_view endian) { bool little_endian; if (endian == "little") { little_endian = true; } else if (endian == "big") { little_endian = false; } else { throw std::invalid_argument("endian not in ['little', 'big']"); } TableauSimulator sim(std::mt19937_64{0}, self.num_qubits); sim.inv_state = self.inverse(false); auto complex_vec = sim.to_state_vector(little_endian); std::complex *buffer = new std::complex[complex_vec.size()]; for (size_t k = 0; k < complex_vec.size(); k++) { buffer[k] = complex_vec[k]; } pybind11::capsule free_when_done(buffer, [](void *f) { delete[] reinterpret_cast *>(f); }); return pybind11::array_t>( {(pybind11::ssize_t)complex_vec.size()}, {(pybind11::ssize_t)sizeof(std::complex)}, buffer, free_when_done); }, pybind11::kw_only(), pybind11::arg("endian") = "little", clean_doc_string(R"DOC( @signature def to_state_vector(self, *, endian: Literal["little", "big"] = 'little') -> np.ndarray[np.complex64]: Returns the state vector produced by applying the tableau to the |0..0> state. This function takes O(n * 2**n) time and O(2**n) space, where n is the number of qubits. The computation is done by initialization a random state vector and iteratively projecting it into the +1 eigenspace of each stabilizer of the state. The state is then canonicalized so that zero values are actually exactly 0, and so that the first non-zero entry is positive. Args: endian: "little" (default): state vector is in little endian order, where higher index qubits correspond to larger changes in the state index. "big": state vector is in big endian order, where higher index qubits correspond to smaller changes in the state index. Returns: A `numpy.ndarray[numpy.complex64]` of computational basis amplitudes. If the result is in little endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_0, the qubit with index 1 is storing the bit b_1, etc. If the result is in big endian order then the amplitude at offset b_0 + b_1*2 + b_2*4 + ... + b_{n-1}*2^{n-1} is the amplitude for the computational basis state where the qubit with index 0 is storing the bit b_{n-1}, the qubit with index 1 is storing the bit b_{n-2}, etc. Examples: >>> import stim >>> import numpy as np >>> i2 = stim.Tableau.from_named_gate('I') >>> x = stim.Tableau.from_named_gate('X') >>> h = stim.Tableau.from_named_gate('H') >>> (x + i2).to_state_vector(endian='little') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> (i2 + x).to_state_vector(endian='little') array([0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], dtype=complex64) >>> (i2 + x).to_state_vector(endian='big') array([0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) >>> (h + h).to_state_vector(endian='little') array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], dtype=complex64) )DOC") .data()); c.def( "to_stabilizers", [](const Tableau &self, bool canonical) { auto stabilizers = self.stabilizers(canonical); std::vector result; result.reserve(stabilizers.size()); for (auto &s : stabilizers) { result.emplace_back(std::move(s), false); } return result; }, pybind11::kw_only(), pybind11::arg("canonicalize") = false, clean_doc_string(R"DOC( Returns the stabilizer generators of the tableau, optionally canonicalized. The stabilizer generators of the tableau are its Z outputs. Canonicalizing standardizes the generators, so that states that are equal will produce the same generators. For example, [ZI, IZ], [ZI, ZZ], amd [ZZ, ZI] describe equal states and all canonicalize to [ZI, IZ]. The canonical form is computed as follows: 1. Get a list of stabilizers using `tableau.z_output(k)` for each k. 2. Perform Gaussian elimination. pivoting on standard generators. 2a) Pivot on g=X0 first, then Z0, X1, Z1, X2, Z2, etc. 2b) Find a stabilizer that uses the generator g. If there are none, go to the next g. 2c) Multiply that stabilizer into all other stabilizers that use the generator g. 2d) Swap that stabilizer with the stabilizer at position `r` then increment `r`. `r` starts at 0. Args: canonicalize: Defaults to False. When False, the tableau's Z outputs are returned unchanged. When True, the Z outputs are rewritten into a standard form. Two stabilizer states have the same standard form if and only if they describe equivalent quantum states. Returns: A List[stim.PauliString] of the tableau's stabilizer generators. Examples: >>> import stim >>> t = stim.Tableau.from_named_gate("CNOT") >>> raw_stabilizers = t.to_stabilizers() >>> for e in raw_stabilizers: ... print(repr(e)) stim.PauliString("+Z_") stim.PauliString("+ZZ") >>> canonical_stabilizers = t.to_stabilizers(canonicalize=True) >>> for e in canonical_stabilizers: ... print(repr(e)) stim.PauliString("+Z_") stim.PauliString("+_Z") )DOC") .data()); } ================================================ FILE: src/stim/stabilizers/tableau.pybind.h ================================================ // 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. #ifndef _STIM_STABILIZERS_TABLEAU_PYBIND_H #define _STIM_STABILIZERS_TABLEAU_PYBIND_H #include #include "stim/stabilizers/tableau.h" namespace stim_pybind { pybind11::class_> pybind_tableau(pybind11::module &m); void pybind_tableau_methods(pybind11::module &m, pybind11::class_> &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/stabilizers/tableau.test.cc ================================================ // 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. #include "stim/stabilizers/tableau.h" #include #include "gtest/gtest.h" #include "stim/gates/gates.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/vector_simulator.h" #include "stim/stabilizers/tableau_transposed_raii.h" #include "stim/util_bot/test_util.test.h" using namespace stim; static float complex_distance(std::complex a, std::complex b) { auto d = a - b; return sqrtf(d.real() * d.real() + d.imag() * d.imag()); } TEST_EACH_WORD_SIZE_W(tableau, identity, { auto t = Tableau::identity(4); ASSERT_EQ( t.str(), "+-xz-xz-xz-xz-\n" "| ++ ++ ++ ++\n" "| XZ __ __ __\n" "| __ XZ __ __\n" "| __ __ XZ __\n" "| __ __ __ XZ"); }) TEST_EACH_WORD_SIZE_W(tableau, gate1, { auto gate1 = Tableau::gate1("+X", "+Z"); ASSERT_EQ(gate1.xs[0].str(), "+X"); ASSERT_EQ(gate1.eval_y_obs(0).str(), "+Y"); ASSERT_EQ(gate1.zs[0].str(), "+Z"); }) template bool tableau_agrees_with_unitary( const Tableau &tableau, const std::vector>> &unitary) { auto n = tableau.num_qubits; assert(unitary.size() == 1ULL << n); std::vector> basis; for (size_t x = 0; x < 2; x++) { for (size_t k = 0; k < n; k++) { basis.emplace_back(n); if (x) { basis.back().xs[k] ^= 1; } else { basis.back().zs[k] ^= 1; } } } for (const auto &input_side_obs : basis) { VectorSimulator sim(n * 2); // Create EPR pairs to test all possible inputs via state channel duality. for (size_t q = 0; q < n; q++) { sim.apply(GateType::H, q); sim.apply(GateType::CX, q, q + n); } // Apply input-side observable. sim.apply(input_side_obs, n); // Apply operation's unitary. std::vector qs; for (size_t q = 0; q < n; q++) { qs.push_back(q + n); } sim.apply(unitary, {qs}); // Apply output-side observable, which should cancel input-side. sim.apply(tableau(input_side_obs), n); // Verify that the state encodes the unitary matrix, with the // input-side and output-side observables having perfectly cancelled out. auto scale = powf(0.5f, 0.5f * n); for (size_t row = 0; row < 1u << n; row++) { for (size_t col = 0; col < 1u << n; col++) { auto a = sim.state[(row << n) | col]; auto b = unitary[row][col] * scale; if (complex_distance(a, b) > 1e-4) { return false; } } } } return true; } TEST_EACH_WORD_SIZE_W(tableau, big_not_seeing_double, { Tableau t(500); auto s = t.xs[0].str(); size_t n = 0; for (size_t k = 1; k < s.size(); k++) { if (s[k] != '_') { n += 1; } } ASSERT_EQ(n, 1) << s; }) TEST_EACH_WORD_SIZE_W(tableau, str, { ASSERT_EQ( Tableau::gate1("+X", "-Z").str(), "+-xz-\n" "| +-\n" "| XZ"); ASSERT_EQ( GATE_DATA.at("X").tableau().str(), "+-xz-\n" "| +-\n" "| XZ"); ASSERT_EQ( GATE_DATA.at("SQRT_Z").tableau().str(), "+-xz-\n" "| ++\n" "| YZ"); ASSERT_EQ( GATE_DATA.at("SQRT_Z_DAG").tableau().str(), "+-xz-\n" "| -+\n" "| YZ"); ASSERT_EQ( GATE_DATA.at("H_XZ").tableau().str(), "+-xz-\n" "| ++\n" "| ZX"); ASSERT_EQ( GATE_DATA.at("ZCX").tableau().str(), "+-xz-xz-\n" "| ++ ++\n" "| XZ _Z\n" "| X_ XZ"); Tableau t(4); t.prepend_H_XZ(0); t.prepend_H_XZ(1); t.prepend_SQRT_Z(1); t.prepend_ZCX(0, 2); t.prepend_ZCX(1, 3); t.prepend_ZCX(0, 1); t.prepend_ZCX(1, 0); ASSERT_EQ( t.inverse().str(), "+-xz-xz-xz-xz-\n" "| ++ +- ++ ++\n" "| Z_ ZY _Z _Z\n" "| ZX _X _Z __\n" "| _X __ XZ __\n" "| __ _X __ XZ"); ASSERT_EQ( t.inverse(true).str(), "+-xz-xz-xz-xz-\n" "| ++ ++ ++ ++\n" "| Z_ ZY _Z _Z\n" "| ZX _X _Z __\n" "| _X __ XZ __\n" "| __ _X __ XZ"); ASSERT_EQ( t.str(), "+-xz-xz-xz-xz-\n" "| -+ ++ ++ ++\n" "| Z_ ZX _X __\n" "| YX _X __ _X\n" "| X_ X_ XZ __\n" "| X_ __ __ XZ"); }) TEST_EACH_WORD_SIZE_W(tableau, gate_tableau_data_vs_unitary_data, { for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { EXPECT_TRUE(tableau_agrees_with_unitary(gate.tableau(), gate.unitary())) << gate.name; } } }) TEST_EACH_WORD_SIZE_W(tableau, inverse_data, { for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { auto &inv_gate = gate.inverse(); auto tab = gate.tableau(); auto inv_tab = inv_gate.tableau(); ASSERT_EQ(tab.then(inv_tab), Tableau::identity(tab.num_qubits)) << gate.name << " -> " << inv_gate.name; } } }) TEST_EACH_WORD_SIZE_W(tableau, eval, { const auto &cnot = GATE_DATA.at("ZCX").tableau(); ASSERT_EQ(cnot(PauliString::from_str("-XX")), PauliString::from_str("-XI")); ASSERT_EQ(cnot(PauliString::from_str("+XX")), PauliString::from_str("+XI")); ASSERT_EQ(cnot(PauliString::from_str("+ZZ")), PauliString::from_str("+IZ")); ASSERT_EQ(cnot(PauliString::from_str("+IY")), PauliString::from_str("+ZY")); ASSERT_EQ(cnot(PauliString::from_str("+YI")), PauliString::from_str("+YX")); ASSERT_EQ(cnot(PauliString::from_str("+YY")), PauliString::from_str("-XZ")); const auto &x2 = GATE_DATA.at("SQRT_X").tableau(); ASSERT_EQ(x2(PauliString::from_str("+X")), PauliString::from_str("+X")); ASSERT_EQ(x2(PauliString::from_str("+Y")), PauliString::from_str("+Z")); ASSERT_EQ(x2(PauliString::from_str("+Z")), PauliString::from_str("-Y")); const auto &s = GATE_DATA.at("SQRT_Z").tableau(); ASSERT_EQ(s(PauliString::from_str("+X")), PauliString::from_str("+Y")); ASSERT_EQ(s(PauliString::from_str("+Y")), PauliString::from_str("-X")); ASSERT_EQ(s(PauliString::from_str("+Z")), PauliString::from_str("+Z")); }) TEST_EACH_WORD_SIZE_W(tableau, apply_within, { const auto &cnot = GATE_DATA.at("ZCX").tableau(); auto p1 = PauliString::from_str("-XX"); PauliStringRef p1_ptr(p1); cnot.apply_within(p1_ptr, std::vector{0, 1}); ASSERT_EQ(p1, PauliString::from_str("-XI")); auto p2 = PauliString::from_str("+XX"); PauliStringRef p2_ptr(p2); cnot.apply_within(p2_ptr, std::vector{0, 1}); ASSERT_EQ(p2, PauliString::from_str("+XI")); }) TEST_EACH_WORD_SIZE_W(tableau, equality, { ASSERT_TRUE(GATE_DATA.at("SQRT_Z").tableau() == GATE_DATA.at("SQRT_Z").tableau()); ASSERT_FALSE(GATE_DATA.at("SQRT_Z").tableau() != GATE_DATA.at("SQRT_Z").tableau()); ASSERT_FALSE(GATE_DATA.at("SQRT_Z").tableau() == GATE_DATA.at("ZCX").tableau()); ASSERT_TRUE(GATE_DATA.at("SQRT_Z").tableau() != GATE_DATA.at("ZCX").tableau()); ASSERT_FALSE(GATE_DATA.at("SQRT_Z").tableau() == GATE_DATA.at("SQRT_X").tableau()); ASSERT_TRUE(GATE_DATA.at("SQRT_Z").tableau() != GATE_DATA.at("SQRT_X").tableau()); ASSERT_EQ(Tableau(1), GATE_DATA.at("I").tableau()); ASSERT_NE(Tableau(1), GATE_DATA.at("X").tableau()); }) TEST_EACH_WORD_SIZE_W(tableau, inplace_scatter_append, { auto t1 = Tableau::identity(1); t1.inplace_scatter_append(GATE_DATA.at("SQRT_Z").tableau(), {0}); ASSERT_EQ(t1, GATE_DATA.at("SQRT_Z").tableau()); t1.inplace_scatter_append(GATE_DATA.at("SQRT_Z").tableau(), {0}); ASSERT_EQ(t1, GATE_DATA.at("Z").tableau()); t1.inplace_scatter_append(GATE_DATA.at("SQRT_Z").tableau(), {0}); ASSERT_EQ(t1, GATE_DATA.at("SQRT_Z_DAG").tableau()); // Test swap decomposition into exp(i pi/2 (XX + YY + ZZ)). auto t2 = Tableau::identity(2); // ZZ^0.5 t2.inplace_scatter_append(GATE_DATA.at("SQRT_Z").tableau(), {0}); t2.inplace_scatter_append(GATE_DATA.at("SQRT_Z").tableau(), {1}); t2.inplace_scatter_append(GATE_DATA.at("ZCZ").tableau(), {0, 1}); // YY^0.5 t2.inplace_scatter_append(GATE_DATA.at("SQRT_Y").tableau(), {0}); t2.inplace_scatter_append(GATE_DATA.at("SQRT_Y").tableau(), {1}); t2.inplace_scatter_append(GATE_DATA.at("H_YZ").tableau(), {0}); t2.inplace_scatter_append(GATE_DATA.at("ZCY").tableau(), {0, 1}); t2.inplace_scatter_append(GATE_DATA.at("H_YZ").tableau(), {0}); // XX^0.5 t2.inplace_scatter_append(GATE_DATA.at("SQRT_X").tableau(), {0}); t2.inplace_scatter_append(GATE_DATA.at("SQRT_X").tableau(), {1}); t2.inplace_scatter_append(GATE_DATA.at("H_XZ").tableau(), {0}); t2.inplace_scatter_append(GATE_DATA.at("ZCX").tableau(), {0, 1}); t2.inplace_scatter_append(GATE_DATA.at("H_XZ").tableau(), {0}); ASSERT_EQ(t2, GATE_DATA.at("SWAP").tableau()); // Test order dependence. auto t3 = Tableau::identity(2); t3.inplace_scatter_append(GATE_DATA.at("H_XZ").tableau(), {0}); t3.inplace_scatter_append(GATE_DATA.at("SQRT_X").tableau(), {1}); t3.inplace_scatter_append(GATE_DATA.at("ZCX").tableau(), {0, 1}); ASSERT_EQ(t3(PauliString::from_str("XI")), PauliString::from_str("ZI")); ASSERT_EQ(t3(PauliString::from_str("ZI")), PauliString::from_str("XX")); ASSERT_EQ(t3(PauliString::from_str("IX")), PauliString::from_str("IX")); ASSERT_EQ(t3(PauliString::from_str("IZ")), PauliString::from_str("-ZY")); }) TEST_EACH_WORD_SIZE_W(tableau, inplace_scatter_prepend, { auto t1 = Tableau::identity(1); t1.inplace_scatter_prepend(GATE_DATA.at("SQRT_Z").tableau(), {0}); ASSERT_EQ(t1, GATE_DATA.at("SQRT_Z").tableau()); t1.inplace_scatter_prepend(GATE_DATA.at("SQRT_Z").tableau(), {0}); ASSERT_EQ(t1, GATE_DATA.at("Z").tableau()); t1.inplace_scatter_prepend(GATE_DATA.at("SQRT_Z").tableau(), {0}); ASSERT_EQ(t1, GATE_DATA.at("SQRT_Z_DAG").tableau()); // Test swap decomposition into exp(i pi/2 (XX + YY + ZZ)). auto t2 = Tableau::identity(2); // ZZ^0.5 t2.inplace_scatter_prepend(GATE_DATA.at("SQRT_Z").tableau(), {0}); t2.inplace_scatter_prepend(GATE_DATA.at("SQRT_Z").tableau(), {1}); t2.inplace_scatter_prepend(GATE_DATA.at("ZCZ").tableau(), {0, 1}); // YY^0.5 t2.inplace_scatter_prepend(GATE_DATA.at("SQRT_Y").tableau(), {0}); t2.inplace_scatter_prepend(GATE_DATA.at("SQRT_Y").tableau(), {1}); t2.inplace_scatter_prepend(GATE_DATA.at("H_YZ").tableau(), {0}); t2.inplace_scatter_prepend(GATE_DATA.at("ZCY").tableau(), {0, 1}); t2.inplace_scatter_prepend(GATE_DATA.at("H_YZ").tableau(), {0}); // XX^0.5 t2.inplace_scatter_prepend(GATE_DATA.at("SQRT_X").tableau(), {0}); t2.inplace_scatter_prepend(GATE_DATA.at("SQRT_X").tableau(), {1}); t2.inplace_scatter_prepend(GATE_DATA.at("H_XZ").tableau(), {0}); t2.inplace_scatter_prepend(GATE_DATA.at("ZCX").tableau(), {0, 1}); t2.inplace_scatter_prepend(GATE_DATA.at("H_XZ").tableau(), {0}); ASSERT_EQ(t2, GATE_DATA.at("SWAP").tableau()); // Test order dependence. auto t3 = Tableau::identity(2); t3.inplace_scatter_prepend(GATE_DATA.at("H_XZ").tableau(), {0}); t3.inplace_scatter_prepend(GATE_DATA.at("SQRT_X").tableau(), {1}); t3.inplace_scatter_prepend(GATE_DATA.at("ZCX").tableau(), {0, 1}); ASSERT_EQ(t3(PauliString::from_str("XI")), PauliString::from_str("ZX")); ASSERT_EQ(t3(PauliString::from_str("ZI")), PauliString::from_str("XI")); ASSERT_EQ(t3(PauliString::from_str("IX")), PauliString::from_str("IX")); ASSERT_EQ(t3(PauliString::from_str("IZ")), PauliString::from_str("-XY")); }) TEST_EACH_WORD_SIZE_W(tableau, eval_y, { ASSERT_EQ(GATE_DATA.at("H_XZ").tableau().zs[0], PauliString::from_str("+X")); ASSERT_EQ(GATE_DATA.at("SQRT_Z").tableau().zs[0], PauliString::from_str("+Z")); ASSERT_EQ(GATE_DATA.at("H_YZ").tableau().zs[0], PauliString::from_str("+Y")); ASSERT_EQ(GATE_DATA.at("SQRT_Y").tableau().zs[0], PauliString::from_str("X")); ASSERT_EQ(GATE_DATA.at("SQRT_Y_DAG").tableau().zs[0], PauliString::from_str("-X")); ASSERT_EQ(GATE_DATA.at("ZCX").tableau().zs[1], PauliString::from_str("ZZ")); ASSERT_EQ(GATE_DATA.at("H_XZ").tableau().eval_y_obs(0), PauliString::from_str("-Y")); ASSERT_EQ(GATE_DATA.at("SQRT_Z").tableau().eval_y_obs(0), PauliString::from_str("-X")); ASSERT_EQ(GATE_DATA.at("H_YZ").tableau().eval_y_obs(0), PauliString::from_str("+Z")); ASSERT_EQ(GATE_DATA.at("SQRT_Y").tableau().eval_y_obs(0), PauliString::from_str("+Y")); ASSERT_EQ(GATE_DATA.at("SQRT_Y_DAG").tableau().eval_y_obs(0), PauliString::from_str("+Y")); ASSERT_EQ(GATE_DATA.at("SQRT_X").tableau().eval_y_obs(0), PauliString::from_str("+Z")); ASSERT_EQ(GATE_DATA.at("SQRT_X_DAG").tableau().eval_y_obs(0), PauliString::from_str("-Z")); ASSERT_EQ(GATE_DATA.at("ZCX").tableau().eval_y_obs(1), PauliString::from_str("ZY")); }) template bool are_tableau_mutations_equivalent( size_t n, const std::function &t, const std::vector &)> &mutation1, const std::function &t, const std::vector &)> &mutation2) { auto rng = INDEPENDENT_TEST_RNG(); auto test_tableau_dual = Tableau::identity(2 * n); std::vector targets1; std::vector targets2; std::vector targets3; for (size_t k = 0; k < n; k++) { test_tableau_dual.inplace_scatter_append(GATE_DATA.at("H_XZ").tableau(), {k}); test_tableau_dual.inplace_scatter_append(GATE_DATA.at("ZCX").tableau(), {k, k + n}); targets1.push_back(k); targets2.push_back(k + n); targets3.push_back(k + (k % 2 == 0 ? 0 : n)); } std::vector> tableaus{ test_tableau_dual, Tableau::random(n + 10, rng), Tableau::random(n + 30, rng), }; std::vector> cases{targets1, targets2, targets3}; for (const auto &t : tableaus) { for (const auto &targets : cases) { auto t1 = t; auto t2 = t; mutation1(t1, targets); mutation2(t2, targets); if (t1 != t2) { return false; } } } return true; } template bool are_tableau_prepends_equivalent(std::string_view name, const std::function &t, size_t)> &func) { return are_tableau_mutations_equivalent( 1, [&](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at(name).tableau(), targets); }, [&](Tableau &t, const std::vector &targets) { func(t, targets[0]); }); } template bool are_tableau_prepends_equivalent( std::string_view name, const std::function &t, size_t, size_t)> &func) { return are_tableau_mutations_equivalent( 2, [&](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at(name).tableau(), targets); }, [&](Tableau &t, const std::vector &targets) { func(t, targets[0], targets[1]); }); } TEST_EACH_WORD_SIZE_W(tableau, check_invariants, { ASSERT_TRUE(Tableau::gate1("X", "Z").satisfies_invariants()); ASSERT_TRUE(Tableau::gate2("XI", "ZI", "IX", "IZ").satisfies_invariants()); ASSERT_FALSE(Tableau::gate1("X", "X").satisfies_invariants()); ASSERT_FALSE(Tableau::gate2("XI", "ZI", "XI", "ZI").satisfies_invariants()); ASSERT_FALSE(Tableau::gate2("XI", "II", "IX", "IZ").satisfies_invariants()); }) TEST_EACH_WORD_SIZE_W(tableau, is_conjugation_by_pauli, { Tableau tableau(8); ASSERT_TRUE(tableau.is_pauli_product()); tableau.xs.signs[0] = true; tableau.xs.signs[3] = true; ASSERT_TRUE(tableau.is_pauli_product()); tableau.xs.zt[0][2] = true; tableau.zs.zt[0][2] = true; ASSERT_FALSE(tableau.is_pauli_product()); }) TEST_EACH_WORD_SIZE_W(tableau, to_pauli_string, { Tableau tableau(8); tableau.xs.signs[3] = true; auto pauli_string_z = tableau.to_pauli_string(); ASSERT_EQ(pauli_string_z.str(), "+___Z____"); tableau.zs.signs[3] = true; auto pauli_string_y = tableau.to_pauli_string(); ASSERT_EQ(pauli_string_y.str(), "+___Y____"); tableau.xs.signs[3] = false; tableau.xs.signs[5] = true; auto pauli_string_xz = tableau.to_pauli_string(); ASSERT_EQ(pauli_string_xz.str(), "+___X_Z__"); tableau.xs.zt[0][1] = true; ASSERT_THROW(tableau.to_pauli_string(), std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(tableau, from_pauli_string, { auto pauli_string_empty = PauliString::from_str(""); auto tableau_empty = Tableau::from_pauli_string(pauli_string_empty); ASSERT_EQ(tableau_empty.to_pauli_string(), pauli_string_empty); auto pauli_string = PauliString::from_str("+_XZX__YZZX"); auto tableau = Tableau::from_pauli_string(pauli_string); ASSERT_EQ(tableau.to_pauli_string(), pauli_string); }) TEST_EACH_WORD_SIZE_W(tableau, random, { auto rng = INDEPENDENT_TEST_RNG(); for (size_t k = 0; k < 20; k++) { auto t = Tableau::random(1, rng); ASSERT_TRUE(t.satisfies_invariants()) << t; } for (size_t k = 0; k < 20; k++) { auto t = Tableau::random(2, rng); ASSERT_TRUE(t.satisfies_invariants()) << t; } for (size_t k = 0; k < 20; k++) { auto t = Tableau::random(3, rng); ASSERT_TRUE(t.satisfies_invariants()) << t; } for (size_t k = 0; k < 20; k++) { auto t = Tableau::random(30, rng); ASSERT_TRUE(t.satisfies_invariants()); } }) TEST_EACH_WORD_SIZE_W(tableau, specialized_operation, { EXPECT_TRUE( are_tableau_mutations_equivalent( 1, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("X").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("X").tableau(), targets); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 1, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("SQRT_Z").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("SQRT_Z").tableau(), targets); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 2, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("ZCX").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("ZCX").tableau(), targets); })); EXPECT_FALSE( are_tableau_mutations_equivalent( 1, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("H_XZ").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("SQRT_Y").tableau(), targets); })); EXPECT_FALSE( are_tableau_mutations_equivalent( 2, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("ZCX").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_prepend(GATE_DATA.at("ZCZ").tableau(), targets); })); EXPECT_TRUE(are_tableau_prepends_equivalent("H_XZ", &Tableau::prepend_H_XZ)); EXPECT_TRUE(are_tableau_prepends_equivalent("H_YZ", &Tableau::prepend_H_YZ)); EXPECT_TRUE(are_tableau_prepends_equivalent("H_XY", &Tableau::prepend_H_XY)); EXPECT_TRUE(are_tableau_prepends_equivalent("X", &Tableau::prepend_X)); EXPECT_TRUE(are_tableau_prepends_equivalent("Y", &Tableau::prepend_Y)); EXPECT_TRUE(are_tableau_prepends_equivalent("Z", &Tableau::prepend_Z)); EXPECT_TRUE(are_tableau_prepends_equivalent("C_XYZ", &Tableau::prepend_C_XYZ)); EXPECT_TRUE(are_tableau_prepends_equivalent("C_ZYX", &Tableau::prepend_C_ZYX)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_X", &Tableau::prepend_SQRT_X)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_Y", &Tableau::prepend_SQRT_Y)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_Z", &Tableau::prepend_SQRT_Z)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_X_DAG", &Tableau::prepend_SQRT_X_DAG)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_Y_DAG", &Tableau::prepend_SQRT_Y_DAG)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_Z_DAG", &Tableau::prepend_SQRT_Z_DAG)); EXPECT_TRUE(are_tableau_prepends_equivalent("SWAP", &Tableau::prepend_SWAP)); EXPECT_TRUE(are_tableau_prepends_equivalent("ZCX", &Tableau::prepend_ZCX)); EXPECT_TRUE(are_tableau_prepends_equivalent("ZCY", &Tableau::prepend_ZCY)); EXPECT_TRUE(are_tableau_prepends_equivalent("ZCZ", &Tableau::prepend_ZCZ)); EXPECT_TRUE(are_tableau_prepends_equivalent("ISWAP", &Tableau::prepend_ISWAP)); EXPECT_TRUE(are_tableau_prepends_equivalent("ISWAP_DAG", &Tableau::prepend_ISWAP_DAG)); EXPECT_TRUE(are_tableau_prepends_equivalent("XCX", &Tableau::prepend_XCX)); EXPECT_TRUE(are_tableau_prepends_equivalent("XCY", &Tableau::prepend_XCY)); EXPECT_TRUE(are_tableau_prepends_equivalent("XCZ", &Tableau::prepend_XCZ)); EXPECT_TRUE(are_tableau_prepends_equivalent("YCX", &Tableau::prepend_YCX)); EXPECT_TRUE(are_tableau_prepends_equivalent("YCY", &Tableau::prepend_YCY)); EXPECT_TRUE(are_tableau_prepends_equivalent("YCZ", &Tableau::prepend_YCZ)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_XX", &Tableau::prepend_SQRT_XX)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_XX_DAG", &Tableau::prepend_SQRT_XX_DAG)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_YY", &Tableau::prepend_SQRT_YY)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_YY_DAG", &Tableau::prepend_SQRT_YY_DAG)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_ZZ", &Tableau::prepend_SQRT_ZZ)); EXPECT_TRUE(are_tableau_prepends_equivalent("SQRT_ZZ_DAG", &Tableau::prepend_SQRT_ZZ_DAG)); EXPECT_TRUE( are_tableau_mutations_equivalent( 1, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("X").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_X(targets[0]); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 1, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("H_XZ").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_H_XZ(targets[0]); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 1, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("H_XY").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_H_XY(targets[0]); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 1, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("H_YZ").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_H_YZ(targets[0]); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 1, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("S").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_S(targets[0]); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 2, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("ZCX").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_ZCX(targets[0], targets[1]); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 2, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("ZCY").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_ZCY(targets[0], targets[1]); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 2, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("ZCZ").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_ZCZ(targets[0], targets[1]); })); EXPECT_TRUE( are_tableau_mutations_equivalent( 2, [](Tableau &t, const std::vector &targets) { t.inplace_scatter_append(GATE_DATA.at("SWAP").tableau(), targets); }, [](Tableau &t, const std::vector &targets) { TableauTransposedRaii(t).append_SWAP(targets[0], targets[1]); })); }) TEST_EACH_WORD_SIZE_W(tableau, expand, { auto rng = INDEPENDENT_TEST_RNG(); auto t = Tableau::random(4, rng); auto t2 = t; for (size_t n = 8; n < 500; n += 255) { t2.expand(n, 1.0); ASSERT_EQ(t2.num_qubits, n); for (size_t k = 0; k < n; k++) { if (k < 4) { ASSERT_EQ(t.xs[k].sign, t2.xs[k].sign); ASSERT_EQ(t.zs[k].sign, t2.zs[k].sign); } else { ASSERT_EQ(t2.xs[k].sign, false); ASSERT_EQ(t2.zs[k].sign, false); } for (size_t k2 = 0; k2 < n; k2++) { if (k < 4 && k2 < 4) { ASSERT_EQ(t.xs[k].xs[k2], t2.xs[k].xs[k2]); ASSERT_EQ(t.xs[k].zs[k2], t2.xs[k].zs[k2]); ASSERT_EQ(t.zs[k].xs[k2], t2.zs[k].xs[k2]); ASSERT_EQ(t.zs[k].zs[k2], t2.zs[k].zs[k2]); } else if (k == k2) { ASSERT_EQ(t2.xs[k].xs[k2], true); ASSERT_EQ(t2.xs[k].zs[k2], false); ASSERT_EQ(t2.zs[k].xs[k2], false); ASSERT_EQ(t2.zs[k].zs[k2], true); } else { ASSERT_EQ(t2.xs[k].xs[k2], false); ASSERT_EQ(t2.xs[k].zs[k2], false); ASSERT_EQ(t2.zs[k].xs[k2], false); ASSERT_EQ(t2.zs[k].zs[k2], false); } } } } }) TEST_EACH_WORD_SIZE_W(tableau, expand_pad, { auto rng = INDEPENDENT_TEST_RNG(); auto t = Tableau::random(4, rng); auto t2 = t; size_t n = 8; while (n < 10000) { n++; t2.expand(n, 2); } ASSERT_EQ(t2.num_qubits, n); for (size_t k = 0; k < n; k++) { if (k < 4) { ASSERT_EQ(t.xs[k].sign, t2.xs[k].sign); ASSERT_EQ(t.zs[k].sign, t2.zs[k].sign); } else { ASSERT_EQ(t2.xs[k].sign, false); ASSERT_EQ(t2.zs[k].sign, false); } for (size_t k2 = 0; k2 < n; k2++) { if (k2 == 4 && k > 8) { k2 = k - 2; } if (k2 == k + 2) { k2 = n - 1; } if (k < 4 && k2 < 4) { ASSERT_EQ(t.xs[k].xs[k2], t2.xs[k].xs[k2]); ASSERT_EQ(t.xs[k].zs[k2], t2.xs[k].zs[k2]); ASSERT_EQ(t.zs[k].xs[k2], t2.zs[k].xs[k2]); ASSERT_EQ(t.zs[k].zs[k2], t2.zs[k].zs[k2]); } else if (k == k2) { ASSERT_EQ(t2.xs[k].xs[k2], true); ASSERT_EQ(t2.xs[k].zs[k2], false); ASSERT_EQ(t2.zs[k].xs[k2], false); ASSERT_EQ(t2.zs[k].zs[k2], true); } else { ASSERT_EQ(t2.xs[k].xs[k2], false); ASSERT_EQ(t2.xs[k].zs[k2], false); ASSERT_EQ(t2.zs[k].xs[k2], false); ASSERT_EQ(t2.zs[k].zs[k2], false); } } } }) TEST_EACH_WORD_SIZE_W(tableau, expand_pad_equals, { auto rng = INDEPENDENT_TEST_RNG(); auto t = Tableau::random(15, rng); auto t2 = t; t.expand(500, 1.0); t2.expand(500, 2.0); ASSERT_EQ(t, t2); }) TEST_EACH_WORD_SIZE_W(tableau, transposed_access, { auto rng = INDEPENDENT_TEST_RNG(); size_t n = W > 256 ? 1000 : 400; Tableau t(n); auto m = t.xs.xt.data.num_bits_padded(); t.xs.xt.data.randomize(m, rng); t.xs.zt.data.randomize(m, rng); t.zs.xt.data.randomize(m, rng); t.zs.zt.data.randomize(m, rng); for (size_t inp_qubit = 0; inp_qubit < n; inp_qubit += 99) { for (size_t out_qubit = 0; out_qubit < n; out_qubit += 99) { bool bxx = t.xs.xt[inp_qubit][out_qubit]; bool bxz = t.xs.zt[inp_qubit][out_qubit]; bool bzx = t.zs.xt[inp_qubit][out_qubit]; bool bzz = t.zs.zt[inp_qubit][out_qubit]; ASSERT_EQ(t.xs[inp_qubit].xs[out_qubit], bxx) << inp_qubit << ", " << out_qubit; ASSERT_EQ(t.xs[inp_qubit].zs[out_qubit], bxz) << inp_qubit << ", " << out_qubit; ASSERT_EQ(t.zs[inp_qubit].xs[out_qubit], bzx) << inp_qubit << ", " << out_qubit; ASSERT_EQ(t.zs[inp_qubit].zs[out_qubit], bzz) << inp_qubit << ", " << out_qubit; { TableauTransposedRaii trans(t); ASSERT_EQ(t.xs.xt[out_qubit][inp_qubit], bxx); ASSERT_EQ(t.xs.zt[out_qubit][inp_qubit], bxz); ASSERT_EQ(t.zs.xt[out_qubit][inp_qubit], bzx); ASSERT_EQ(t.zs.zt[out_qubit][inp_qubit], bzz); } } } }) TEST_EACH_WORD_SIZE_W(tableau, inverse, { auto rng = INDEPENDENT_TEST_RNG(); Tableau t1(1); ASSERT_EQ(t1, t1.inverse()); t1.prepend_X(0); ASSERT_EQ(t1, GATE_DATA.at("X").tableau()); auto t2 = t1.inverse(); ASSERT_EQ(t1, GATE_DATA.at("X").tableau()); ASSERT_EQ(t2, GATE_DATA.at("X").tableau()); for (size_t k = 5; k < 20; k += 7) { t1 = Tableau::random(k, rng); t2 = t1.inverse(); ASSERT_TRUE(t2.satisfies_invariants()); auto p = PauliString::random(k, rng); auto p2 = t1(t2(p)); auto x1 = p.xs.str(); auto x2 = p2.xs.str(); auto z1 = p.zs.str(); auto z2 = p2.zs.str(); ASSERT_EQ(p, p2); ASSERT_EQ(p, t2(t1(p))); } }) TEST_EACH_WORD_SIZE_W(tableau, prepend_pauli_product, { auto rng = INDEPENDENT_TEST_RNG(); auto t = Tableau::random(6, rng); auto ref = t; t.prepend_pauli_product(PauliString::from_str("_XYZ__")); ref.prepend_X(1); ref.prepend_Y(2); ref.prepend_Z(3); ASSERT_EQ(t, ref); t.prepend_pauli_product(PauliString::from_str("Y_ZX__")); ref.prepend_X(3); ref.prepend_Y(0); ref.prepend_Z(2); ASSERT_EQ(t, ref); }) TEST_EACH_WORD_SIZE_W(tableau, then, { auto cnot = GATE_DATA.at("CNOT").tableau(); auto swap = GATE_DATA.at("SWAP").tableau(); Tableau hh(2); hh.inplace_scatter_append(GATE_DATA.at("H").tableau(), {0}); hh.inplace_scatter_append(GATE_DATA.at("H").tableau(), {1}); ASSERT_EQ(cnot.then(cnot), Tableau(2)); ASSERT_EQ(hh.then(cnot).then(hh), swap.then(cnot).then(swap)); ASSERT_EQ(cnot.then(cnot), Tableau(2)); Tableau t(2); t.inplace_scatter_append(GATE_DATA.at("SQRT_X_DAG").tableau(), {1}); t = t.then(GATE_DATA.at("CY").tableau()); t.inplace_scatter_append(GATE_DATA.at("SQRT_X").tableau(), {1}); ASSERT_EQ(t, GATE_DATA.at("CZ").tableau()); }) TEST_EACH_WORD_SIZE_W(tableau, raised_to, { auto cnot = GATE_DATA.at("CNOT").tableau(); ASSERT_EQ(cnot.raised_to(-97268202), Tableau(2)); ASSERT_EQ(cnot.raised_to(-97268201), cnot); ASSERT_EQ(cnot.raised_to(-3), cnot); ASSERT_EQ(cnot.raised_to(-2), Tableau(2)); ASSERT_EQ(cnot.raised_to(-1), cnot); ASSERT_EQ(cnot.raised_to(0), Tableau(2)); ASSERT_EQ(cnot.raised_to(1), cnot); ASSERT_EQ(cnot.raised_to(2), Tableau(2)); ASSERT_EQ(cnot.raised_to(3), cnot); ASSERT_EQ(cnot.raised_to(4), Tableau(2)); ASSERT_EQ(cnot.raised_to(97268201), cnot); ASSERT_EQ(cnot.raised_to(97268202), Tableau(2)); auto s = GATE_DATA.at("S").tableau(); auto z = GATE_DATA.at("Z").tableau(); auto s_dag = GATE_DATA.at("S_DAG").tableau(); ASSERT_EQ(s.raised_to(4 * -437829 + 0), Tableau(1)); ASSERT_EQ(s.raised_to(4 * -437829 + 1), s); ASSERT_EQ(s.raised_to(4 * -437829 + 2), z); ASSERT_EQ(s.raised_to(4 * -437829 + 3), s_dag); ASSERT_EQ(s.raised_to(-5), s_dag); ASSERT_EQ(s.raised_to(-4), Tableau(1)); ASSERT_EQ(s.raised_to(-3), s); ASSERT_EQ(s.raised_to(-2), z); ASSERT_EQ(s.raised_to(-1), s_dag); ASSERT_EQ(s.raised_to(0), Tableau(1)); ASSERT_EQ(s.raised_to(1), s); ASSERT_EQ(s.raised_to(2), z); ASSERT_EQ(s.raised_to(3), s_dag); ASSERT_EQ(s.raised_to(4), Tableau(1)); ASSERT_EQ(s.raised_to(5), s); ASSERT_EQ(s.raised_to(6), z); ASSERT_EQ(s.raised_to(7), s_dag); ASSERT_EQ(s.raised_to(4 * 437829 + 0), Tableau(1)); ASSERT_EQ(s.raised_to(4 * 437829 + 1), s); ASSERT_EQ(s.raised_to(4 * 437829 + 2), z); ASSERT_EQ(s.raised_to(4 * 437829 + 3), s_dag); Tableau p15(3); p15.inplace_scatter_append(GATE_DATA.at("SQRT_X").tableau(), {0}); p15.inplace_scatter_append(s, {2}); p15.inplace_scatter_append(cnot, {0, 1}); p15.inplace_scatter_append(cnot, {1, 2}); for (size_t k = 1; k < 15; k++) { ASSERT_NE(p15.raised_to(k), Tableau(3)); } ASSERT_EQ(p15.raised_to(15), Tableau(3)); ASSERT_EQ(p15.raised_to(15 * 47321 + 4), p15.raised_to(4)); ASSERT_EQ(p15.raised_to(15 * 47321 + 1), p15); ASSERT_EQ(p15.raised_to(15 * -47321 + 1), p15); }) TEST_EACH_WORD_SIZE_W(tableau, transposed_xz_input, { auto rng = INDEPENDENT_TEST_RNG(); auto t = Tableau::random(4, rng); PauliString x0(0); PauliString x1(0); { TableauTransposedRaii tmp(t); x0 = tmp.unsigned_x_input(0); x1 = tmp.unsigned_x_input(1); } auto tx0 = t(x0); auto tx1 = t(x1); tx0.sign = false; tx1.sign = false; ASSERT_EQ(tx0, PauliString::from_str("X___")); ASSERT_EQ(tx1, PauliString::from_str("_X__")); }) TEST_EACH_WORD_SIZE_W(tableau, direct_sum, { auto rng = INDEPENDENT_TEST_RNG(); auto t1 = Tableau::random(160, rng); auto t2 = Tableau::random(170, rng); auto t3 = t1; t3 += t2; ASSERT_EQ(t3, t1 + t2); PauliString p1 = t1.xs[5]; p1.ensure_num_qubits(160 + 170, 1.0); ASSERT_EQ(t3.xs[5], p1); std::string p2 = t2.xs[6].str(); std::string p3 = t3.xs[166].str(); ASSERT_EQ(p2[0], p3[0]); p2 = p2.substr(1); p3 = p3.substr(1); for (size_t k = 0; k < 160; k++) { ASSERT_EQ(p3[k], '_'); } for (size_t k = 0; k < 170; k++) { ASSERT_EQ(p3[160 + k], p2[k]); } }) TEST_EACH_WORD_SIZE_W(tableau, pauli_access_methods, { auto rng = INDEPENDENT_TEST_RNG(); auto t = Tableau::random(3, rng); auto t_inv = t.inverse(); for (size_t i = 0; i < 3; i++) { auto x = t.xs[i]; auto y = t.eval_y_obs(i); auto z = t.zs[i]; for (size_t j = 0; j < 3; j++) { ASSERT_EQ(t.x_output_pauli_xyz(i, j), pauli_xz_to_xyz(x.xs[j], x.zs[j])); ASSERT_EQ(t.y_output_pauli_xyz(i, j), pauli_xz_to_xyz(y.xs[j], y.zs[j])); ASSERT_EQ(t.z_output_pauli_xyz(i, j), pauli_xz_to_xyz(z.xs[j], z.zs[j])); ASSERT_EQ(t_inv.inverse_x_output_pauli_xyz(i, j), pauli_xz_to_xyz(x.xs[j], x.zs[j])); ASSERT_EQ(t_inv.inverse_y_output_pauli_xyz(i, j), pauli_xz_to_xyz(y.xs[j], y.zs[j])); ASSERT_EQ(t_inv.inverse_z_output_pauli_xyz(i, j), pauli_xz_to_xyz(z.xs[j], z.zs[j])); } } t = Tableau(3); t.xs[0] = PauliString::from_str("+XXX"); t.xs[1] = PauliString::from_str("-XZY"); t.xs[2] = PauliString::from_str("+Z_Z"); t.zs[0] = PauliString::from_str("-_XZ"); t.zs[1] = PauliString::from_str("-_X_"); t.zs[2] = PauliString::from_str("-X__"); ASSERT_EQ(t.x_output_pauli_xyz(0, 0), 1); ASSERT_EQ(t.x_output_pauli_xyz(0, 1), 1); ASSERT_EQ(t.x_output_pauli_xyz(0, 2), 1); ASSERT_EQ(t.x_output_pauli_xyz(1, 0), 1); ASSERT_EQ(t.x_output_pauli_xyz(1, 1), 3); ASSERT_EQ(t.x_output_pauli_xyz(1, 2), 2); ASSERT_EQ(t.x_output_pauli_xyz(2, 0), 3); ASSERT_EQ(t.x_output_pauli_xyz(2, 1), 0); ASSERT_EQ(t.x_output_pauli_xyz(2, 2), 3); ASSERT_EQ(t.z_output_pauli_xyz(0, 0), 0); ASSERT_EQ(t.z_output_pauli_xyz(0, 1), 1); ASSERT_EQ(t.z_output_pauli_xyz(0, 2), 3); ASSERT_EQ(t.z_output_pauli_xyz(1, 0), 0); ASSERT_EQ(t.z_output_pauli_xyz(1, 1), 1); ASSERT_EQ(t.z_output_pauli_xyz(1, 2), 0); ASSERT_EQ(t.z_output_pauli_xyz(2, 0), 1); ASSERT_EQ(t.z_output_pauli_xyz(2, 1), 0); ASSERT_EQ(t.z_output_pauli_xyz(2, 2), 0); t = t.inverse(); ASSERT_EQ(t.inverse_x_output_pauli_xyz(0, 0), 1); ASSERT_EQ(t.inverse_x_output_pauli_xyz(0, 1), 1); ASSERT_EQ(t.inverse_x_output_pauli_xyz(0, 2), 1); ASSERT_EQ(t.inverse_x_output_pauli_xyz(1, 0), 1); ASSERT_EQ(t.inverse_x_output_pauli_xyz(1, 1), 3); ASSERT_EQ(t.inverse_x_output_pauli_xyz(1, 2), 2); ASSERT_EQ(t.inverse_x_output_pauli_xyz(2, 0), 3); ASSERT_EQ(t.inverse_x_output_pauli_xyz(2, 1), 0); ASSERT_EQ(t.inverse_x_output_pauli_xyz(2, 2), 3); ASSERT_EQ(t.inverse_z_output_pauli_xyz(0, 0), 0); ASSERT_EQ(t.inverse_z_output_pauli_xyz(0, 1), 1); ASSERT_EQ(t.inverse_z_output_pauli_xyz(0, 2), 3); ASSERT_EQ(t.inverse_z_output_pauli_xyz(1, 0), 0); ASSERT_EQ(t.inverse_z_output_pauli_xyz(1, 1), 1); ASSERT_EQ(t.inverse_z_output_pauli_xyz(1, 2), 0); ASSERT_EQ(t.inverse_z_output_pauli_xyz(2, 0), 1); ASSERT_EQ(t.inverse_z_output_pauli_xyz(2, 1), 0); ASSERT_EQ(t.inverse_z_output_pauli_xyz(2, 2), 0); }) TEST_EACH_WORD_SIZE_W(tableau, inverse_pauli_string_acces_methods, { auto rng = INDEPENDENT_TEST_RNG(); auto t = Tableau::random(5, rng); auto t_inv = t.inverse(); auto y0 = t_inv.eval_y_obs(0); auto y1 = t_inv.eval_y_obs(1); auto y2 = t_inv.eval_y_obs(2); ASSERT_EQ(t.inverse_x_output(0), t_inv.xs[0]); ASSERT_EQ(t.inverse_x_output(1), t_inv.xs[1]); ASSERT_EQ(t.inverse_x_output(2), t_inv.xs[2]); ASSERT_EQ(t.inverse_y_output(0), y0); ASSERT_EQ(t.inverse_y_output(1), y1); ASSERT_EQ(t.inverse_y_output(2), y2); ASSERT_EQ(t.inverse_z_output(0), t_inv.zs[0]); ASSERT_EQ(t.inverse_z_output(1), t_inv.zs[1]); ASSERT_EQ(t.inverse_z_output(2), t_inv.zs[2]); t_inv.xs.signs.clear(); t_inv.zs.signs.clear(); y0.sign = false; y1.sign = false; y2.sign = false; ASSERT_EQ(t.inverse_x_output(0, true), t_inv.xs[0]); ASSERT_EQ(t.inverse_x_output(1, true), t_inv.xs[1]); ASSERT_EQ(t.inverse_x_output(2, true), t_inv.xs[2]); ASSERT_EQ(t.inverse_y_output(0, true), y0); ASSERT_EQ(t.inverse_y_output(1, true), y1); ASSERT_EQ(t.inverse_y_output(2, true), y2); ASSERT_EQ(t.inverse_z_output(0, true), t_inv.zs[0]); ASSERT_EQ(t.inverse_z_output(1, true), t_inv.zs[1]); ASSERT_EQ(t.inverse_z_output(2, true), t_inv.zs[2]); }) TEST_EACH_WORD_SIZE_W(tableau, unitary_little_endian, { Tableau t(1); ASSERT_EQ(t.to_flat_unitary_matrix(false), (std::vector>{1, 0, 0, 1})); t.prepend_SQRT_Y(0); auto s = sqrtf(0.5); ASSERT_EQ(t.to_flat_unitary_matrix(false), (std::vector>{s, -s, s, s})); t.prepend_SQRT_Y(0); ASSERT_EQ(t.to_flat_unitary_matrix(false), (std::vector>{0, 1, -1, 0})); t.prepend_SQRT_Y(0); ASSERT_EQ(t.to_flat_unitary_matrix(false), (std::vector>{s, s, -s, s})); t = Tableau(2); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1})); t.prepend_X(1); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0})); t.prepend_X(0); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0})); t.prepend_X(1); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0})); t.prepend_X(0); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1})); t.prepend_Z(1); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1})); t.prepend_Z(0); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1})); t.prepend_Z(1); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1})); t.prepend_Z(0); t.prepend_SQRT_Z(0); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, {0, 1}, 0, 0, 0, 0, {0, 1}})); t.prepend_SQRT_Z_DAG(0); t.prepend_H_XZ(0); t.prepend_H_XZ(1); ASSERT_EQ( t.to_flat_unitary_matrix(false), (std::vector>{ 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, })); }) TEST_EACH_WORD_SIZE_W(tableau, unitary_big_endian, { Tableau t(1); ASSERT_EQ(t.to_flat_unitary_matrix(true), (std::vector>{1, 0, 0, 1})); t.prepend_SQRT_Y(0); auto s = sqrtf(0.5); ASSERT_EQ(t.to_flat_unitary_matrix(false), (std::vector>{s, -s, s, s})); t.prepend_SQRT_Y(0); ASSERT_EQ(t.to_flat_unitary_matrix(false), (std::vector>{0, 1, -1, 0})); t.prepend_SQRT_Y(0); ASSERT_EQ(t.to_flat_unitary_matrix(false), (std::vector>{s, s, -s, s})); t = Tableau(2); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1})); t.prepend_X(1); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0})); t.prepend_X(0); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0})); t.prepend_X(1); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0})); t.prepend_X(0); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1})); t.prepend_Z(1); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1})); t.prepend_Z(0); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1})); t.prepend_Z(1); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1})); t.prepend_Z(0); t.prepend_SQRT_Z(0); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{1, 0, 0, 0, 0, {0, 1}, 0, 0, 0, 0, 1, 0, 0, 0, 0, {0, 1}})); t.prepend_SQRT_Z_DAG(0); t.prepend_H_XZ(0); t.prepend_H_XZ(1); ASSERT_EQ( t.to_flat_unitary_matrix(true), (std::vector>{ 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, })); }) TEST_EACH_WORD_SIZE_W(tableau, unitary_vs_gate_data, { for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { std::vector> flat_expected; for (const auto &row : gate.unitary()) { flat_expected.insert(flat_expected.end(), row.begin(), row.end()); } VectorSimulator v(0); v.state = std::move(flat_expected); v.canonicalize_assuming_stabilizer_state((gate.flags & stim::GATE_TARGETS_PAIRS) ? 4 : 2); EXPECT_EQ(gate.tableau().to_flat_unitary_matrix(true), v.state) << gate.name; } } }) TEST_EACH_WORD_SIZE_W(tableau, inverse_not_confused_by_size_padding, { // Create a tableau where the avoid-quadratic-overhead padding causes it to pad over a simd word size boundary. Tableau t(1); t += Tableau(500); // Check that inverting it doesn't produce garbage. Tableau t_inv = t.inverse(); ASSERT_EQ(t_inv, t); }) ================================================ FILE: src/stim/stabilizers/tableau_iter.h ================================================ /* * 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. */ #ifndef _STIM_STABILIZERS_TABLEAU_ITER_H #define _STIM_STABILIZERS_TABLEAU_ITER_H #include "stim/mem/fixed_cap_vector.h" #include "stim/mem/span_ref.h" #include "stim/stabilizers/tableau.h" namespace stim { /// Iterates over Pauli strings that match commutators and anticommutators. /// /// The template parameter, W, represents the SIMD width. template struct CommutingPauliStringIterator { // Fields defining the pauli strings that will be iterated. size_t num_qubits; SpanRef> cur_desired_commutators; SpanRef> cur_desired_anticommutators; // Fields tracking progress of the iteration. PauliString current; // The current Pauli string being considered. size_t next_output_index; // Next thing to return from buffer. size_t filled_output; // Number of used entries in buffer. std::vector> output_buf; // Pre-allocated buffer. CommutingPauliStringIterator(size_t num_qubits); /// Restarts iteration, and changes the target commutators/anticommutators. void restart_iter(SpanRef> commutators, SpanRef> anticommutators); void restart_iter_same_constraints(); /// Yields the next iterated Pauli string (or nullptr if iteration over). const PauliString *iter_next(); /// Checks whether the given Pauli string (versus) commutes with 64 variants /// of `current` created by varying its first three Paulis. Assumes that the /// first three Paulis of current are set to I. /// /// Args: /// versus: The Pauli string to compare to (to see if we commute or /// anticommute. /// /// Returns: /// A 64 bit integer where the k'th bit corresponds to whether the k'th /// variant of the current Pauli string commuted or not. The bits of k /// (where k is the index of a bit in the result) are x0, x1, x2, z0, /// z1, z2 in little endian order. uint64_t mass_anticommute_check(const PauliStringRef versus); /// Internal method used for refilling a buffer of results. void load_more(); }; /// Iterates over tableaus of a given size. /// /// The template parameter, W, represents the SIMD width. template struct TableauIterator { bool also_iter_signs; // If false, only unsigned tableaus are yielded. Tableau result; // Pre-allocated result storage. std::vector> tableau_column_refs; // Quick access to tableau columns. // Fields tracking the progress of iteration. size_t cur_k; std::vector> pauli_string_iterators; TableauIterator(size_t num_qubits, bool also_iter_signs); TableauIterator(const TableauIterator &); TableauIterator &operator=(const TableauIterator &); TableauIterator &operator=(TableauIterator &&) = delete; /// Updates the `result` field to point at the next yielded tableau. /// Returns true if this succeeded, or false if iteration has ended. bool iter_next(); // Restarts iteration. void restart(); std::pair>, SpanRef>> constraints_for_pauli_iterator( size_t k) const; }; } // namespace stim #include "stim/stabilizers/tableau_iter.inl" #endif ================================================ FILE: src/stim/stabilizers/tableau_iter.inl ================================================ // 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. #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/tableau_iter.h" namespace stim { template CommutingPauliStringIterator::CommutingPauliStringIterator(size_t num_qubits) : num_qubits(num_qubits), cur_desired_commutators(), cur_desired_anticommutators(), current(num_qubits), next_output_index(0), filled_output(0), output_buf() { if (num_qubits < 1) { throw std::invalid_argument("Too few qubits (num_qubits < 1)."); } if (num_qubits >= 64) { throw std::invalid_argument("Too many qubits to iterate tableaus (num_qubits > 64)."); } while (output_buf.size() < 64) { output_buf.push_back(PauliString(num_qubits)); } } template uint64_t CommutingPauliStringIterator::mass_anticommute_check(const PauliStringRef versus) { constexpr uint64_t x0 = 0xAAAAAAAAAAAAAAAAUL; constexpr uint64_t x1 = 0xCCCCCCCCCCCCCCCCUL; constexpr uint64_t x2 = 0xF0F0F0F0F0F0F0F0UL; constexpr uint64_t z0 = 0xFF00FF00FF00FF00UL; constexpr uint64_t z1 = 0xFFFF0000FFFF0000UL; constexpr uint64_t z2 = 0xFFFFFFFF00000000UL; uint64_t result = 0; if (versus.zs[0]) result ^= x0; if (versus.zs[1]) result ^= x1; if (versus.zs[2]) result ^= x2; if (versus.xs[0]) result ^= z0; if (versus.xs[1]) result ^= z1; if (versus.xs[2]) result ^= z2; if (versus.num_qubits > 3 && !versus.commutes(current)) { result ^= -1; } return result; } template void CommutingPauliStringIterator::restart_iter_same_constraints() { current.xs.u64[0] = 0; current.zs.u64[0] = 0; next_output_index = 0; filled_output = 0; } template void CommutingPauliStringIterator::restart_iter( SpanRef> commutators, SpanRef> anticommutators) { restart_iter_same_constraints(); cur_desired_commutators = commutators; cur_desired_anticommutators = anticommutators; } template const PauliString *CommutingPauliStringIterator::iter_next() { if (next_output_index >= filled_output) { load_more(); } if (next_output_index >= filled_output) { return nullptr; } return &output_buf[next_output_index++]; } template void CommutingPauliStringIterator::load_more() { next_output_index = 0; filled_output = 0; uint64_t start_pass_mask = -1; if (num_qubits < 2) { // Drop bits corresponding to cases where x2 or z2 or x1 or z1 are set. start_pass_mask &= 0x0000000000000303UL; } else if (num_qubits < 3) { // Drop bits corresponding to cases where x2 or z2 are set. start_pass_mask &= 0x000000000F0F0F0FUL; } uint64_t num_bit_strings = 1 << num_qubits; while (filled_output == 0 && current.zs.u64[0] < num_bit_strings) { uint64_t pass_mask = start_pass_mask; if (current.xs.u64[0] == 0 && current.zs.u64[0] == 0) { pass_mask &= ~1; // Don't search the identity. } for (const auto &p : cur_desired_commutators) { pass_mask &= ~mass_anticommute_check(p); } for (const auto &p : cur_desired_anticommutators) { pass_mask &= mass_anticommute_check(p); } if (pass_mask) { for (size_t b = 0; b < 64; b++) { if ((pass_mask >> b) & 1) { output_buf[filled_output] = current; output_buf[filled_output].xs.u64[0] |= b & 7; output_buf[filled_output].zs.u64[0] |= (b >> 3) & 7; filled_output++; } } } current.xs.u64[0] += 8; if (current.xs.u64[0] >= num_bit_strings) { current.xs.u64[0] = 0; current.zs.u64[0] += 8; } } } template TableauIterator::TableauIterator(size_t num_qubits, bool also_iter_signs) : also_iter_signs(also_iter_signs), result(num_qubits), cur_k(0) { for (size_t k = 0; k < num_qubits; k++) { // Iterator for X_k's output. pauli_string_iterators.push_back(CommutingPauliStringIterator(num_qubits)); tableau_column_refs.push_back(result.xs[k]); // Iterator for Z_k's output. pauli_string_iterators.push_back(CommutingPauliStringIterator(num_qubits)); tableau_column_refs.push_back(result.zs[k]); } for (size_t k = 0; k < 2 * num_qubits; k++) { auto constraints = constraints_for_pauli_iterator(k); pauli_string_iterators[k].cur_desired_commutators = constraints.first; pauli_string_iterators[k].cur_desired_anticommutators = constraints.second; } } template TableauIterator::TableauIterator(const TableauIterator &other) : result(0) { *this = other; } template TableauIterator &TableauIterator::operator=(const TableauIterator &other) { also_iter_signs = other.also_iter_signs; result = other.result; cur_k = other.cur_k; pauli_string_iterators = other.pauli_string_iterators; tableau_column_refs.clear(); for (size_t k = 0; k < result.num_qubits; k++) { tableau_column_refs.push_back(result.xs[k]); tableau_column_refs.push_back(result.zs[k]); } for (size_t k = 0; k < 2 * result.num_qubits; k++) { auto constraints = constraints_for_pauli_iterator(k); pauli_string_iterators[k].cur_desired_commutators = constraints.first; pauli_string_iterators[k].cur_desired_anticommutators = constraints.second; } return *this; } template std::pair>, SpanRef>> TableauIterator::constraints_for_pauli_iterator(size_t k) const { const PauliStringRef *tab_obs_start = &tableau_column_refs[0]; SpanRef> commute_rng = {tab_obs_start, tab_obs_start + k}; SpanRef> anticommute_rng; if (k & 1) { anticommute_rng.ptr_end = commute_rng.ptr_end; commute_rng.ptr_end--; anticommute_rng.ptr_start = commute_rng.ptr_end; } return {commute_rng, anticommute_rng}; } template bool TableauIterator::iter_next() { if (result.num_qubits == 0) { if (cur_k == 0) { cur_k = 1; return true; } return false; } if (result.xs.signs.u64[0] > 0) { result.xs.signs.u64[0]--; return true; } if (result.zs.signs.u64[0] > 0) { result.zs.signs.u64[0]--; result.xs.signs.u64[0] = (uint64_t{1} << result.num_qubits) - uint64_t{1}; return true; } while (cur_k != SIZE_MAX) { const PauliString *out = pauli_string_iterators[cur_k].iter_next(); if (out == nullptr) { // Exhausted all Paulis strings at this level; go back a level. cur_k--; // At 0 this underflows to SIZE_MAX, exiting the loop. continue; } tableau_column_refs[cur_k] = *out; cur_k++; if (cur_k == 2 * result.num_qubits) { cur_k--; if (also_iter_signs) { // Okay, look, I admit this is technically wrong. It fails if num_qubits > 64 because // not all sign variations are explored. But no one is actually going to be able to finish // iterating the tableaus that are actually yielded, so good look demonstrating it's wrong. // :P result.xs.signs.u64[0] = (uint64_t{1} << result.num_qubits) - uint64_t{1}; result.zs.signs.u64[0] = (uint64_t{1} << result.num_qubits) - uint64_t{1}; } return true; } pauli_string_iterators[cur_k].restart_iter_same_constraints(); } return false; } template void TableauIterator::restart() { cur_k = 0; pauli_string_iterators[0].restart_iter({}, {}); result.xs.signs.clear(); result.zs.signs.clear(); } } // namespace stim ================================================ FILE: src/stim/stabilizers/tableau_iter.perf.cc ================================================ // 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. #include "stim/stabilizers/tableau_iter.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(tableau_iter_unsigned_3q) { size_t c = 0; benchmark_go([&]() { TableauIterator iter(3, false); while (iter.iter_next()) { c += iter.result.num_qubits; } }) .goal_millis(200) .show_rate("Tableaus", 1451520); if (c == 0) { std::cerr << "use the output\n"; } } BENCHMARK(tableau_iter_all_3q) { size_t c = 0; benchmark_go([&]() { TableauIterator iter(3, true); while (iter.iter_next()) { c += iter.result.num_qubits; } }) .goal_millis(420) .show_rate("Tableaus", 92897280); if (c == 0) { std::cerr << "use the output\n"; } } ================================================ FILE: src/stim/stabilizers/tableau_iter.pybind.cc ================================================ // 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. #include "stim/stabilizers/tableau_iter.pybind.h" #include "stim/py/base.pybind.h" using namespace stim; using namespace stim_pybind; pybind11::class_> stim_pybind::pybind_tableau_iter(pybind11::module &m) { auto c = pybind11::class_>( m, "TableauIterator", clean_doc_string(R"DOC( Iterates over all stabilizer tableaus of a specified size. Examples: >>> import stim >>> tableau_iterator = stim.Tableau.iter_all(1) >>> n = 0 >>> for single_qubit_clifford in tableau_iterator: ... n += 1 >>> n 24 )DOC") .data()); return c; } void stim_pybind::pybind_tableau_iter_methods( pybind11::module &m, pybind11::class_> &c) { c.def( "__iter__", [](TableauIterator &self) -> TableauIterator { TableauIterator copy = self; return copy; }, clean_doc_string(R"DOC( Returns an independent copy of the tableau iterator. Since for-loops and loop-comprehensions call `iter` on things they iterate, this effectively allows the iterator to be iterated multiple times. )DOC") .data()); c.def( "__next__", [](TableauIterator &self) -> Tableau { if (!self.iter_next()) { throw pybind11::stop_iteration(); } return self.result; }, clean_doc_string(R"DOC( Returns the next iterated tableau. )DOC") .data()); } ================================================ FILE: src/stim/stabilizers/tableau_iter.pybind.h ================================================ // 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. #ifndef _STIM_STABILIZERS_TABLEAU_ITER_PYBIND_H #define _STIM_STABILIZERS_TABLEAU_ITER_PYBIND_H #include #include "stim/stabilizers/tableau_iter.h" namespace stim_pybind { pybind11::class_> pybind_tableau_iter(pybind11::module &m); void pybind_tableau_iter_methods( pybind11::module &m, pybind11::class_> &c); } // namespace stim_pybind #endif ================================================ FILE: src/stim/stabilizers/tableau_iter.test.cc ================================================ // 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. #include "stim/stabilizers/tableau_iter.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(tableau_iter, CommutingPauliStringIterator_1, { CommutingPauliStringIterator iter(1); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+X")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Z")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Y")); ASSERT_EQ(iter.iter_next(), nullptr); iter.restart_iter({}, {}); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+X")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Z")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Y")); ASSERT_EQ(iter.iter_next(), nullptr); auto ps = PauliString::from_str("X"); PauliStringRef r = ps; iter.restart_iter({&r}, {}); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+X")); ASSERT_EQ(iter.iter_next(), nullptr); iter.restart_iter({}, {&r}); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Z")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Y")); ASSERT_EQ(iter.iter_next(), nullptr); }) TEST_EACH_WORD_SIZE_W(tableau_iter, CommutingPauliStringIterator_2, { std::vector> coms; std::vector> refs; coms.push_back(PauliString::from_str("+Z_")); refs.push_back(coms[0]); std::vector> anti_coms; std::vector> anti_refs; anti_coms.push_back(PauliString::from_str("+XX")); anti_refs.push_back(anti_coms[0]); CommutingPauliStringIterator iter(2); iter.restart_iter(refs, anti_refs); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Z_")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+ZX")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+_Z")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+_Y")); ASSERT_EQ(iter.iter_next(), nullptr); }) TEST_EACH_WORD_SIZE_W(tableau_iter, CommutingPauliStringIterator_4, { CommutingPauliStringIterator iter(4); iter.restart_iter({}, {}); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+X___")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+_X__")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+XX__")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+__X_")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+X_X_")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+_XX_")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+XXX_")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Z___")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+Y___")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+ZX__")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+YX__")); std::vector> coms; std::vector> refs; coms.push_back(PauliString::from_str("+Z___")); coms.push_back(PauliString::from_str("+_Z__")); coms.push_back(PauliString::from_str("+___Z")); refs.push_back(coms[0]); refs.push_back(coms[1]); refs.push_back(coms[2]); std::vector> anti_coms; std::vector> anti_refs; anti_coms.push_back(PauliString::from_str("+X___")); anti_coms.push_back(PauliString::from_str("+_X__")); anti_coms.push_back(PauliString::from_str("+___X")); anti_refs.push_back(anti_coms[0]); anti_refs.push_back(anti_coms[1]); anti_refs.push_back(anti_coms[2]); iter.restart_iter(refs, anti_refs); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+ZZ_Z")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+ZZXZ")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+ZZZZ")); ASSERT_EQ(*iter.iter_next(), PauliString::from_str("+ZZYZ")); ASSERT_EQ(iter.iter_next(), nullptr); iter.restart_iter(&refs[0], &refs[0]); ASSERT_EQ(iter.iter_next(), nullptr); }) TEST_EACH_WORD_SIZE_W(tableau_iter, iter_tableau, { TableauIterator iter1(1, false); TableauIterator iter1_signs(1, true); TableauIterator iter2(2, false); TableauIterator iter3(3, false); int n1 = 0; while (iter1.iter_next()) { n1++; } ASSERT_EQ(n1, 6); int s1 = 0; while (iter1_signs.iter_next()) { s1++; } ASSERT_EQ(s1, 24); int n2 = 0; while (iter2.iter_next()) { n2++; } ASSERT_EQ(n2, 720); // Note: disabled because it takes 2-3 seconds. // int n3 = 0; // while (iter3.iter_next()) { // n3++; // } // ASSERT_EQ(n3, 1451520); }) TEST_EACH_WORD_SIZE_W(tableau_iter, iter_tableau_distinct, { std::set seen; TableauIterator iter2_signs(2, true); while (iter2_signs.iter_next()) { seen.insert(iter2_signs.result.str()); } ASSERT_EQ(seen.size(), 11520); }) TEST_EACH_WORD_SIZE_W(tableau_iter, iter_tableau_copy, { TableauIterator iter2(2, true); { TableauIterator iter1(3, false); iter2 = iter1; } ASSERT_TRUE(iter2.iter_next()); { TableauIterator iter1(3, false); for (size_t k = 0; k < 100; k++) { iter2 = iter1; ASSERT_EQ(iter1.iter_next(), iter2.iter_next()); ASSERT_EQ(iter2.result, iter1.result); } for (size_t k = 0; k < 1000; k++) { ASSERT_EQ(iter1.iter_next(), iter2.iter_next()); ASSERT_EQ(iter2.result, iter1.result); } } }) ================================================ FILE: src/stim/stabilizers/tableau_pybind_test.py ================================================ # 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. import random import re import numpy as np import stim import pytest def test_init_equality(): assert stim.Tableau(3) != None assert stim.Tableau(3) != object() assert stim.Tableau(3) != "another type" assert not (stim.Tableau(3) == None) assert not (stim.Tableau(3) == object()) assert not (stim.Tableau(3) == "another type") assert stim.Tableau(3) == stim.Tableau(3) assert not (stim.Tableau(3) != stim.Tableau(3)) assert stim.Tableau(3) != stim.Tableau(4) assert not (stim.Tableau(3) == stim.Tableau(4)) assert stim.Tableau.from_named_gate("S") == stim.Tableau.from_named_gate("S") assert stim.Tableau.from_named_gate("S") != stim.Tableau.from_named_gate("S_DAG") assert stim.Tableau.from_named_gate("S") != stim.Tableau.from_named_gate("H") assert stim.Tableau.from_named_gate("S_DAG") == stim.Tableau.from_named_gate("S_DAG") def test_from_named_gate(): assert str(stim.Tableau.from_named_gate("H")).strip() == """ +-xz- | ++ | ZX """.strip() assert str(stim.Tableau.from_named_gate("I")).strip() == """ +-xz- | ++ | XZ """.strip() assert stim.Tableau.from_named_gate("H_XZ") == stim.Tableau.from_named_gate("h") with pytest.raises(IndexError, match="not found"): stim.Tableau.from_named_gate("not a gate") with pytest.raises(IndexError, match="not unitary"): stim.Tableau.from_named_gate("X_ERROR") def test_from_state_vector_fuzz(): for n in range(1, 7): t = stim.Tableau.random(n) v = t.to_state_vector() * (random.random() + 1j*random.random()) t2 = stim.Tableau.from_state_vector(v, endian='little') np.testing.assert_array_equal(t.to_stabilizers(canonicalize=True), t2.to_stabilizers(canonicalize=True)) def test_identity(): t = stim.Tableau(3) assert len(t) == 3 assert t.x_output(0) == stim.PauliString("X__") assert t.x_output(1) == stim.PauliString("_X_") assert t.x_output(2) == stim.PauliString("__X") assert t.z_output(0) == stim.PauliString("Z__") assert t.z_output(1) == stim.PauliString("_Z_") assert t.z_output(2) == stim.PauliString("__Z") assert t.y_output(0) == stim.PauliString("Y__") assert t.y_output(1) == stim.PauliString("_Y_") assert t.y_output(2) == stim.PauliString("__Y") def test_pauli_output(): h = stim.Tableau.from_named_gate("H") assert h.x_output(0) == stim.PauliString("Z") assert h.y_output(0) == stim.PauliString("-Y") assert h.z_output(0) == stim.PauliString("X") s = stim.Tableau.from_named_gate("S") assert s.x_output(0) == stim.PauliString("Y") assert s.y_output(0) == stim.PauliString("-X") assert s.z_output(0) == stim.PauliString("Z") s_dag = stim.Tableau.from_named_gate("S_DAG") assert s_dag.x_output(0) == stim.PauliString("-Y") assert s_dag.y_output(0) == stim.PauliString("X") assert s_dag.z_output(0) == stim.PauliString("Z") cz = stim.Tableau.from_named_gate("CZ") assert cz.x_output(0) == stim.PauliString("XZ") assert cz.y_output(0) == stim.PauliString("YZ") assert cz.z_output(0) == stim.PauliString("Z_") assert cz.x_output(1) == stim.PauliString("ZX") assert cz.y_output(1) == stim.PauliString("ZY") assert cz.z_output(1) == stim.PauliString("_Z") def test_random(): t = stim.Tableau.random(10) assert len(t) == 10 assert t != stim.Tableau.random(10) def test_str(): assert str(stim.Tableau.from_named_gate("cnot")).strip() == """ +-xz-xz- | ++ ++ | XZ _Z | X_ XZ """.strip() def test_append(): t = stim.Tableau(2) with pytest.raises(ValueError, match=re.escape("len(targets) != len(gate)")): t.append(stim.Tableau.from_named_gate("CY"), [0]) with pytest.raises(ValueError, match="collision"): t.append(stim.Tableau.from_named_gate("CY"), [0, 0]) with pytest.raises(ValueError, match=re.escape("target >= len(tableau)")): t.append(stim.Tableau.from_named_gate("CY"), [1, 2]) t.append(stim.Tableau.from_named_gate("SQRT_X_DAG"), [1]) t.append(stim.Tableau.from_named_gate("CY"), [0, 1]) t.append(stim.Tableau.from_named_gate("SQRT_X"), [1]) assert t == stim.Tableau.from_named_gate("CZ") t = stim.Tableau(2) t.append(stim.Tableau.from_named_gate("SQRT_X"), [1]) t.append(stim.Tableau.from_named_gate("CY"), [0, 1]) t.append(stim.Tableau.from_named_gate("SQRT_X_DAG"), [1]) assert t != stim.Tableau.from_named_gate("CZ") def test_prepend(): t = stim.Tableau(2) with pytest.raises(ValueError, match=re.escape("len(targets) != len(gate)")): t.prepend(stim.Tableau.from_named_gate("CY"), [0]) with pytest.raises(ValueError, match="collision"): t.prepend(stim.Tableau.from_named_gate("CY"), [0, 0]) with pytest.raises(ValueError, match=re.escape("target >= len(tableau)")): t.prepend(stim.Tableau.from_named_gate("CY"), [1, 2]) t.prepend(stim.Tableau.from_named_gate("SQRT_X_DAG"), [1]) t.prepend(stim.Tableau.from_named_gate("CY"), [0, 1]) t.prepend(stim.Tableau.from_named_gate("SQRT_X"), [1]) assert t != stim.Tableau.from_named_gate("CZ") t = stim.Tableau(2) t.prepend(stim.Tableau.from_named_gate("SQRT_X"), [1]) t.prepend(stim.Tableau.from_named_gate("CY"), [0, 1]) t.prepend(stim.Tableau.from_named_gate("SQRT_X_DAG"), [1]) assert t == stim.Tableau.from_named_gate("CZ") def test_from_conjugated_generators(): assert stim.Tableau(3) == stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X__"), stim.PauliString("_X_"), stim.PauliString("__X"), ], zs=[ stim.PauliString("Z__"), stim.PauliString("_Z_"), stim.PauliString("__Z"), ], ) assert stim.Tableau.from_named_gate("S") == stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("Y"), ], zs=[ stim.PauliString("Z"), ], ) assert stim.Tableau.from_named_gate("S_DAG") == stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-Y"), ], zs=[ stim.PauliString("Z"), ], ) assert stim.Tableau(2) == stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), stim.PauliString("_X"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("_Z"), ], ) with pytest.raises(ValueError, match=re.escape("len(p) == len(zs)")): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), stim.PauliString("_X"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("_Z_"), ], ) with pytest.raises(ValueError, match="imag"): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("iX_"), stim.PauliString("_X"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("_Z"), ], ) with pytest.raises(ValueError, match="imag"): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), stim.PauliString("_X"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("i_Z"), ], ) with pytest.raises(ValueError, match=re.escape("len(p) == len(xs)")): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), stim.PauliString("_X_"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("_Z"), ], ) with pytest.raises(ValueError, match=re.escape("len(xs) != len(zs)")): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("_Z"), ], ) with pytest.raises(ValueError, match=re.escape("len(xs) != len(zs)")): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), stim.PauliString("_X"), ], zs=[ stim.PauliString("Z_"), ], ) with pytest.raises(ValueError, match="commutativity"): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), stim.PauliString("_Z"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("_Z"), ], ) with pytest.raises(ValueError, match="commutativity"): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), stim.PauliString("Z_"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("X_"), ], ) with pytest.raises(ValueError, match="commutativity"): stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("X_"), stim.PauliString("X_"), ], zs=[ stim.PauliString("Z_"), stim.PauliString("Z_"), ], ) def test_repr(): v = stim.Tableau.from_named_gate("H") r = repr(v) assert r == """stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+Z"), ], zs=[ stim.PauliString("+X"), ], )""" assert eval(r, {"stim": stim}) == v def test_call(): t = stim.Tableau.from_named_gate("CNOT") assert t(stim.PauliString("__")) == stim.PauliString("__") assert t(stim.PauliString("-__")) == stim.PauliString("-__") assert t(stim.PauliString("i__")) == stim.PauliString("i__") assert t(stim.PauliString("-i__")) == stim.PauliString("-i__") assert t(stim.PauliString("X_")) == stim.PauliString("XX") assert t(stim.PauliString("Y_")) == stim.PauliString("YX") assert t(stim.PauliString("Z_")) == stim.PauliString("Z_") assert t(stim.PauliString("_X")) == stim.PauliString("_X") assert t(stim.PauliString("_Y")) == stim.PauliString("ZY") assert t(stim.PauliString("_Z")) == stim.PauliString("ZZ") assert t(stim.PauliString("YY")) == stim.PauliString("-XZ") assert t(stim.PauliString("-YY")) == stim.PauliString("XZ") def test_pow(): s = stim.Tableau.from_named_gate("S") s_dag = stim.Tableau.from_named_gate("S_DAG") z = stim.Tableau.from_named_gate("Z") assert stim.Tableau(1) == s**0 == s**4 == s**-4 assert s == s**1 == s**5 == s**-3 == s**(40000 + 1) == s**(-40000 + 1) assert s_dag == s**-1 == s**3 == s**7 == s**(40000 + 3) == s**(-40000 + 3) assert z == s**2 == s**6 == s**-2 == s**(40000 + 2) == s**(-40000 + 2) def test_aliasing(): t = stim.Tableau.random(4) t2 = t**1 t.append(t2, range(4)) t2.append(t2, range(4)) assert t == t2 t = stim.Tableau.random(4) t2 = t**1 t.prepend(t2, range(4)) t2.prepend(t2, range(4)) assert t == t2 def test_composition(): assert stim.Tableau(0) * stim.Tableau(0) == stim.Tableau(0) assert stim.Tableau(0).then(stim.Tableau(0)) == stim.Tableau(0) assert stim.Tableau(1) * stim.Tableau(1) == stim.Tableau(1) assert stim.Tableau(1).then(stim.Tableau(1)) == stim.Tableau(1) t = stim.Tableau.random(4) t2 = stim.Tableau.random(4) t3 = t.then(t2) assert t3 == t2 * t p = stim.PauliString.random(4) assert t2(t(p)) == t3(p) with pytest.raises(ValueError, match="!= len"): _ = stim.Tableau(3) * stim.Tableau(4) with pytest.raises(ValueError, match="!= len"): _ = stim.Tableau(3).then(stim.Tableau(4)) def test_copy(): t = stim.Tableau(3) t2 = t.copy() assert t == t2 assert t is not t2 def test_hash(): # stim.Tableau is mutable. It must not also be value-hashable. # Defining __hash__ requires defining a FrozenTableau variant instead. with pytest.raises(TypeError, match="unhashable"): _ = hash(stim.Tableau(1)) def test_add(): h = stim.Tableau.from_named_gate("H") swap = stim.Tableau.from_named_gate("SWAP") cnot = stim.Tableau.from_named_gate("CNOT") combo = h + swap + cnot assert str(combo).strip() == """ +-xz-xz-xz-xz-xz- | ++ ++ ++ ++ ++ | ZX __ __ __ __ | __ __ XZ __ __ | __ XZ __ __ __ | __ __ __ XZ _Z | __ __ __ X_ XZ """.strip() alias = h h += swap h += cnot assert h == combo h += stim.Tableau(0) assert h == combo assert h is alias assert h is not combo assert swap == stim.Tableau.from_named_gate("SWAP") assert stim.Tableau(0) + stim.Tableau(0) == stim.Tableau(0) assert stim.Tableau(1) + stim.Tableau(2) == stim.Tableau(3) assert stim.Tableau(100) + stim.Tableau(500) == stim.Tableau(600) assert stim.Tableau(0) + cnot + stim.Tableau(0) == cnot x = stim.Tableau.from_named_gate("X") y = stim.Tableau.from_named_gate("Y") z = stim.Tableau.from_named_gate("Z") assert str(y + y).strip() == """ +-xz-xz- | -- -- | XZ __ | __ XZ """.strip() assert str(x + x).strip() == """ +-xz-xz- | +- +- | XZ __ | __ XZ """.strip() assert str(z + z).strip() == """ +-xz-xz- | -+ -+ | XZ __ | __ XZ """.strip() assert str(x + z).strip() == """ +-xz-xz- | +- -+ | XZ __ | __ XZ """.strip() assert str(z + x).strip() == """ +-xz-xz- | -+ +- | XZ __ | __ XZ """.strip() def test_xyz_output_pauli(): t = stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-__Z"), stim.PauliString("+XZ_"), stim.PauliString("+_ZZ"), ], zs=[ stim.PauliString("-YYY"), stim.PauliString("+Z_Z"), stim.PauliString("-ZYZ"), ], ) assert t.x_output_pauli(0, 0) == 0 assert t.x_output_pauli(0, 1) == 0 assert t.x_output_pauli(0, 2) == 3 assert t.x_output_pauli(1, 0) == 1 assert t.x_output_pauli(1, 1) == 3 assert t.x_output_pauli(1, 2) == 0 assert t.x_output_pauli(2, 0) == 0 assert t.x_output_pauli(2, 1) == 3 assert t.x_output_pauli(2, 2) == 3 assert t.y_output_pauli(0, 0) == 2 assert t.y_output_pauli(0, 1) == 2 assert t.y_output_pauli(0, 2) == 1 assert t.y_output_pauli(1, 0) == 2 assert t.y_output_pauli(1, 1) == 3 assert t.y_output_pauli(1, 2) == 3 assert t.y_output_pauli(2, 0) == 3 assert t.y_output_pauli(2, 1) == 1 assert t.y_output_pauli(2, 2) == 0 assert t.z_output_pauli(0, 0) == 2 assert t.z_output_pauli(0, 1) == 2 assert t.z_output_pauli(0, 2) == 2 assert t.z_output_pauli(1, 0) == 3 assert t.z_output_pauli(1, 1) == 0 assert t.z_output_pauli(1, 2) == 3 assert t.z_output_pauli(2, 0) == 3 assert t.z_output_pauli(2, 1) == 2 assert t.z_output_pauli(2, 2) == 3 with pytest.raises(TypeError): t.x_output_pauli(-1, 0) with pytest.raises(ValueError): t.x_output_pauli(3, 0) with pytest.raises(TypeError): t.x_output_pauli(0, -1) with pytest.raises(ValueError): t.x_output_pauli(0, 3) with pytest.raises(TypeError): t.y_output_pauli(-1, 0) with pytest.raises(ValueError): t.y_output_pauli(3, 0) with pytest.raises(TypeError): t.y_output_pauli(0, -1) with pytest.raises(ValueError): t.y_output_pauli(0, 3) with pytest.raises(TypeError): t.z_output_pauli(-1, 0) with pytest.raises(ValueError): t.z_output_pauli(3, 0) with pytest.raises(TypeError): t.z_output_pauli(0, -1) with pytest.raises(ValueError): t.z_output_pauli(0, 3) def test_inverse_xyz_output_pauli(): t = stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-__Z"), stim.PauliString("+XZ_"), stim.PauliString("+_ZZ"), ], zs=[ stim.PauliString("-YYY"), stim.PauliString("+Z_Z"), stim.PauliString("-ZYZ"), ], ).inverse() assert t.inverse_x_output_pauli(0, 0) == 0 assert t.inverse_x_output_pauli(0, 1) == 0 assert t.inverse_x_output_pauli(0, 2) == 3 assert t.inverse_x_output_pauli(1, 0) == 1 assert t.inverse_x_output_pauli(1, 1) == 3 assert t.inverse_x_output_pauli(1, 2) == 0 assert t.inverse_x_output_pauli(2, 0) == 0 assert t.inverse_x_output_pauli(2, 1) == 3 assert t.inverse_x_output_pauli(2, 2) == 3 assert t.inverse_y_output_pauli(0, 0) == 2 assert t.inverse_y_output_pauli(0, 1) == 2 assert t.inverse_y_output_pauli(0, 2) == 1 assert t.inverse_y_output_pauli(1, 0) == 2 assert t.inverse_y_output_pauli(1, 1) == 3 assert t.inverse_y_output_pauli(1, 2) == 3 assert t.inverse_y_output_pauli(2, 0) == 3 assert t.inverse_y_output_pauli(2, 1) == 1 assert t.inverse_y_output_pauli(2, 2) == 0 assert t.inverse_z_output_pauli(0, 0) == 2 assert t.inverse_z_output_pauli(0, 1) == 2 assert t.inverse_z_output_pauli(0, 2) == 2 assert t.inverse_z_output_pauli(1, 0) == 3 assert t.inverse_z_output_pauli(1, 1) == 0 assert t.inverse_z_output_pauli(1, 2) == 3 assert t.inverse_z_output_pauli(2, 0) == 3 assert t.inverse_z_output_pauli(2, 1) == 2 assert t.inverse_z_output_pauli(2, 2) == 3 with pytest.raises(TypeError): t.inverse_x_output_pauli(-1, 0) with pytest.raises(ValueError): t.inverse_x_output_pauli(3, 0) with pytest.raises(TypeError): t.inverse_x_output_pauli(0, -1) with pytest.raises(ValueError): t.inverse_x_output_pauli(0, 3) with pytest.raises(TypeError): t.inverse_y_output_pauli(-1, 0) with pytest.raises(ValueError): t.inverse_y_output_pauli(3, 0) with pytest.raises(TypeError): t.inverse_y_output_pauli(0, -1) with pytest.raises(ValueError): t.inverse_y_output_pauli(0, 3) with pytest.raises(TypeError): t.inverse_z_output_pauli(-1, 0) with pytest.raises(ValueError): t.inverse_z_output_pauli(3, 0) with pytest.raises(TypeError): t.inverse_z_output_pauli(0, -1) with pytest.raises(ValueError): t.inverse_z_output_pauli(0, 3) def test_inverse_xyz_output(): t = stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-__Z"), stim.PauliString("+XZ_"), stim.PauliString("+_ZZ"), ], zs=[ stim.PauliString("-YYY"), stim.PauliString("+Z_Z"), stim.PauliString("-ZYZ"), ], ) t_inv = t.inverse() for k in range(3): assert t_inv.inverse_x_output(k) == t.x_output(k) assert t_inv.inverse_y_output(k) == t.y_output(k) assert t_inv.inverse_z_output(k) == t.z_output(k) assert t_inv.inverse_x_output(k, unsigned=True) == t.x_output(k) / t.x_output(k).sign assert t_inv.inverse_y_output(k, unsigned=True) == t.y_output(k) / t.y_output(k).sign assert t_inv.inverse_z_output(k, unsigned=True) == t.z_output(k) / t.z_output(k).sign with pytest.raises(TypeError): t.inverse_x_output(-1) with pytest.raises(ValueError): t.inverse_x_output(3) with pytest.raises(TypeError): t.inverse_y_output(-1) with pytest.raises(ValueError): t.inverse_y_output(3) with pytest.raises(TypeError): t.inverse_z_output(-1) with pytest.raises(ValueError): t.inverse_z_output(3) def test_inverse(): t = stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XXX"), stim.PauliString("-XZY"), stim.PauliString("+Z_Z"), ], zs=[ stim.PauliString("-_XZ"), stim.PauliString("-_X_"), stim.PauliString("-X__"), ], ) assert t.inverse() == t.inverse(unsigned=False) == stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-__Z"), stim.PauliString("-_Z_"), stim.PauliString("+XZZ"), ], zs=[ stim.PauliString("+ZZX"), stim.PauliString("+YX_"), stim.PauliString("+ZZ_"), ], ) assert t.inverse(unsigned=True) == stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+__Z"), stim.PauliString("+_Z_"), stim.PauliString("+XZZ"), ], zs=[ stim.PauliString("+ZZX"), stim.PauliString("+YX_"), stim.PauliString("+ZZ_"), ], ) def test_pickle(): import pickle t = stim.Tableau.random(4) a = pickle.dumps(t) assert pickle.loads(a) == t def test_unitary(): swap = stim.Tableau.from_named_gate("SWAP") np.testing.assert_array_equal(swap.to_unitary_matrix(endian='big'), [ [1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1], ]) def test_to_pauli_string(): assert stim.PauliString(0).to_tableau() == stim.Tableau(0) p = stim.PauliString("+YX_Z") assert p.to_tableau().to_pauli_string() == p cnot = stim.Tableau.from_named_gate("CNOT") with pytest.raises(ValueError, match="The Tableau isn't equivalent to a Pauli product."): cnot.to_pauli_string() def test_iter_0q(): assert list(stim.Tableau.iter_all(0, unsigned=True)) == [stim.Tableau(0)] assert list(stim.Tableau.iter_all(0, unsigned=False)) == [stim.Tableau(0)] def test_iter_1q(): r = stim.Tableau.iter_all(1, unsigned=True) assert len(set(repr(e) for e in r)) == 6 assert len(set(repr(e) for e in r)) == 6 # Can re-iterate. assert sum(1 for _ in stim.Tableau.iter_all(1)) == 24 def test_iter_2q(): u2 = stim.Tableau.iter_all(2, unsigned=True) assert sum(1 for _ in u2) == 720 assert sum(1 for _ in stim.Tableau.iter_all(2, unsigned=False)) == 11520 assert len(set(repr(e) for e in u2)) == 720 def test_iter_3q(): n = 0 for _ in stim.Tableau.iter_all(3, unsigned=True): n += 1 assert n == 1451520 def test_from_unitary_matrix(): s = 0.5**0.5 t = stim.Tableau.from_unitary_matrix([ [s, s], [s, -s] ], endian='little') assert t == stim.Tableau.from_named_gate("H") with pytest.raises(ValueError, match="Clifford operation"): stim.Tableau.from_unitary_matrix([ [1, 0], [0, 0], ], endian='little') def test_to_circuit_vs_from_circuit(): t = stim.Tableau.random(4) assert t.to_circuit() is not None c = t.to_circuit(method="elimination") sim = stim.TableauSimulator() sim.do_circuit(c) assert sim.current_inverse_tableau().inverse() == t assert stim.Tableau.from_circuit(c) == t def test_to_numpy(): t = stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+_XXY"), stim.PauliString("+X_ZX"), stim.PauliString("+_X__"), stim.PauliString("-XXXY"), ], zs=[ stim.PauliString("+Z_ZY"), stim.PauliString("+_X_Y"), stim.PauliString("-_ZXZ"), stim.PauliString("-Y_X_"), ], ) x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy() np.testing.assert_array_equal(x_signs, [0, 0, 0, 1]) np.testing.assert_array_equal(z_signs, [0, 0, 1, 1]) np.testing.assert_array_equal(x2x, [ [0, 1, 1, 1], [1, 0, 0, 1], [0, 1, 0, 0], [1, 1, 1, 1], ]) np.testing.assert_array_equal(x2z, [ [0, 0, 0, 1], [0, 0, 1, 0], [0, 0, 0, 0], [0, 0, 0, 1], ]) np.testing.assert_array_equal(z2x, [ [0, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 0], [1, 0, 1, 0], ]) np.testing.assert_array_equal(z2z, [ [1, 0, 1, 1], [0, 0, 0, 1], [0, 1, 0, 1], [1, 0, 0, 0], ]) x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy(bit_packed=True) np.testing.assert_array_equal(x_signs, [8]) np.testing.assert_array_equal(z_signs, [12]) np.testing.assert_array_equal(x2x, [ [14], [9], [2], [15], ]) np.testing.assert_array_equal(x2z, [ [8], [4], [0], [8], ]) np.testing.assert_array_equal(z2x, [ [8], [10], [4], [5], ]) np.testing.assert_array_equal(z2z, [ [13], [8], [10], [1], ]) def test_from_numpy(): expected = stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+_XXY"), stim.PauliString("+X_ZX"), stim.PauliString("+_X__"), stim.PauliString("-XXXY"), ], zs=[ stim.PauliString("+Z_ZY"), stim.PauliString("+_X_Y"), stim.PauliString("-_ZXZ"), stim.PauliString("-Y_X_"), ], ) assert stim.Tableau.from_numpy( x_signs=np.array([0, 0, 0, 1], dtype=np.bool_), z_signs=np.array([0, 0, 1, 1], dtype=np.bool_), x2x=np.array([ [0, 1, 1, 1], [1, 0, 0, 1], [0, 1, 0, 0], [1, 1, 1, 1], ], dtype=np.bool_), x2z=np.array([ [0, 0, 0, 1], [0, 0, 1, 0], [0, 0, 0, 0], [0, 0, 0, 1], ], dtype=np.bool_), z2x=np.array([ [0, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 0], [1, 0, 1, 0], ], dtype=np.bool_), z2z=np.array([ [1, 0, 1, 1], [0, 0, 0, 1], [0, 1, 0, 1], [1, 0, 0, 0], ], dtype=np.bool_), ) == expected @pytest.mark.parametrize("n", [0, 1, 15, 16, 17, 301]) def test_to_from_numpy_fuzz(n: int): t = stim.Tableau.random(n) x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy() t1 = stim.Tableau.from_numpy(x2x=x2x, x2z=x2z, z2x=z2x, z2z=z2z, x_signs=x_signs, z_signs=z_signs) assert t1 == t x2x, x2z, z2x, z2z, x_signs, z_signs = t.to_numpy(bit_packed=True) t2 = stim.Tableau.from_numpy(x2x=x2x, x2z=x2z, z2x=z2x, z2z=z2z, x_signs=x_signs, z_signs=z_signs) assert t2 == t def test_signs(): t = stim.Tableau.from_named_gate("S") assert t.x_sign(0) == +1 assert t.y_sign(0) == -1 assert t.z_sign(0) == +1 t = stim.Tableau.from_named_gate("S_DAG") assert t.x_sign(0) == -1 assert t.y_sign(0) == +1 assert t.z_sign(0) == +1 t = stim.Tableau.from_named_gate("SQRT_X") assert t.x_sign(0) == +1 assert t.y_sign(0) == +1 assert t.z_sign(0) == -1 t = stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-XX"), stim.PauliString("-ZZ"), ], zs=[ stim.PauliString("+IZ"), stim.PauliString("-XI"), ], ) assert t.x_sign(0) == -1 == t.x_output(0).sign assert t.x_sign(1) == -1 == t.x_output(1).sign assert t.y_sign(0) == -1 == t.y_output(0).sign assert t.y_sign(1) == -1 == t.y_output(1).sign assert t.z_sign(0) == +1 == t.z_output(0).sign assert t.z_sign(1) == -1 == t.z_output(1).sign with pytest.raises(ValueError, match="target"): _ = t.x_sign(-1) with pytest.raises(ValueError, match="target"): _ = t.y_sign(-1) with pytest.raises(ValueError, match="target"): _ = t.z_sign(-1) with pytest.raises(ValueError, match="target"): _ = t.x_sign(2) with pytest.raises(ValueError, match="target"): _ = t.y_sign(2) with pytest.raises(ValueError, match="target"): _ = t.z_sign(2) def test_to_stabilizers(): t = stim.Tableau.from_stabilizers([ stim.PauliString("XXXX"), stim.PauliString("YYYY"), stim.PauliString("YYZZ"), stim.PauliString("XXZZ"), ]) assert t.to_stabilizers() == [ stim.PauliString("XXXX"), stim.PauliString("YYYY"), stim.PauliString("YYZZ"), stim.PauliString("XXZZ"), ] assert t.to_stabilizers(canonicalize=True) == [ stim.PauliString("-XX__"), stim.PauliString("-ZZ__"), stim.PauliString("-__XX"), stim.PauliString("-__ZZ"), ] def test_to_circuit_graph_state_preserves_stabilizers(): t = stim.Tableau.random(10) c = t.to_circuit("graph_state") c = stim.Circuit(str(c).replace('RX', 'H')) original = t.to_stabilizers(canonicalize=True) reconstructed = c.to_tableau().to_stabilizers(canonicalize=True) assert original == reconstructed def test_to_circuit_mpp_preserves_stabilizers(): t = stim.Tableau.random(10) original = t.to_stabilizers(canonicalize=True) sim = stim.TableauSimulator() sim.do_circuit(t.to_circuit("mpp_state")) reconstructed = sim.canonical_stabilizers() assert original == reconstructed def test_to_circuit_mpp_unsigned_preserves_stabilizers(): t = stim.Tableau.random(10) original = t.to_stabilizers(canonicalize=True) sim = stim.TableauSimulator() sim.do_circuit(t.to_circuit("mpp_state_unsigned")) reconstructed = sim.canonical_stabilizers() for e in original: e.sign = +1 for e in reconstructed: e.sign = +1 assert original == reconstructed def test_from_stabilizers_error_messages(): with pytest.raises(ValueError, match="anticommute"): stim.Tableau.from_stabilizers([ stim.PauliString("Z"), stim.PauliString("X"), ]) with pytest.raises(ValueError, match="anticommute"): stim.Tableau.from_stabilizers([ stim.PauliString("Z"), stim.PauliString("X" + "_"*500), ]) with pytest.raises(ValueError, match="contradict"): stim.Tableau.from_stabilizers([ stim.PauliString("Z_"), stim.PauliString("-_Z"), stim.PauliString("Z" + "_"*500 + "X"), stim.PauliString("ZZ"), ]) with pytest.raises(ValueError, match="redundant"): stim.Tableau.from_stabilizers([ stim.PauliString("-Z_"), stim.PauliString("Z" + "_"*500 + "X"), stim.PauliString("-__Z"), stim.PauliString("_Z_"), stim.PauliString("Z_Z"), ]) ================================================ FILE: src/stim/stabilizers/tableau_specialized_prepend.inl ================================================ // 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. #include #include "stim/stabilizers/tableau.h" namespace stim { template void Tableau::prepend_X(size_t q) { zs[q].sign ^= 1; } template void Tableau::prepend_Y(size_t q) { xs[q].sign ^= 1; zs[q].sign ^= 1; } template void Tableau::prepend_Z(size_t q) { xs[q].sign ^= 1; } template void Tableau::prepend_pauli_product(const PauliStringRef &op) { assert(op.num_qubits == num_qubits); zs.signs ^= op.xs; xs.signs ^= op.zs; } template struct IgnoreAntiCommute { PauliStringRef rhs; IgnoreAntiCommute(PauliStringRef rhs) : rhs(rhs) { } }; template void operator*=(PauliStringRef lhs, const IgnoreAntiCommute &rhs) { lhs.sign ^= 2 & lhs.inplace_right_mul_returning_log_i_scalar(rhs.rhs); } template void Tableau::prepend_H_XZ(const size_t q) { xs[q].swap_with(zs[q]); } template void Tableau::prepend_H_YZ(const size_t q) { zs[q] *= IgnoreAntiCommute(xs[q]); prepend_Z(q); } template void Tableau::prepend_H_XY(const size_t q) { xs[q] *= IgnoreAntiCommute(zs[q]); prepend_Y(q); } template void Tableau::prepend_H_NXY(const size_t q) { xs[q] *= IgnoreAntiCommute(zs[q]); prepend_X(q); } template void Tableau::prepend_H_NXZ(const size_t q) { xs[q].swap_with(zs[q]); prepend_Y(q); } template void Tableau::prepend_H_NYZ(const size_t q) { zs[q] *= IgnoreAntiCommute(xs[q]); prepend_Y(q); } template void Tableau::prepend_C_XYZ(const size_t q) { PauliStringRef x = xs[q]; PauliStringRef z = zs[q]; z *= IgnoreAntiCommute(x); x.swap_with(z); } template void Tableau::prepend_C_NXYZ(const size_t q) { PauliStringRef x = xs[q]; PauliStringRef z = zs[q]; z *= IgnoreAntiCommute(x); x.swap_with(z); prepend_Y(q); } template void Tableau::prepend_C_XNYZ(const size_t q) { PauliStringRef x = xs[q]; PauliStringRef z = zs[q]; z *= IgnoreAntiCommute(x); x.swap_with(z); prepend_Z(q); } template void Tableau::prepend_C_XYNZ(const size_t q) { PauliStringRef x = xs[q]; PauliStringRef z = zs[q]; z *= IgnoreAntiCommute(x); x.swap_with(z); prepend_X(q); } template void Tableau::prepend_C_ZYX(const size_t q) { PauliStringRef x = xs[q]; PauliStringRef z = zs[q]; x.swap_with(z); z *= IgnoreAntiCommute(x); prepend_X(q); } template void Tableau::prepend_C_ZYNX(const size_t q) { PauliStringRef x = xs[q]; PauliStringRef z = zs[q]; x.swap_with(z); z *= IgnoreAntiCommute(x); prepend_Y(q); } template void Tableau::prepend_C_ZNYX(const size_t q) { PauliStringRef x = xs[q]; PauliStringRef z = zs[q]; x.swap_with(z); z *= IgnoreAntiCommute(x); } template void Tableau::prepend_C_NZYX(const size_t q) { PauliStringRef x = xs[q]; PauliStringRef z = zs[q]; x.swap_with(z); z *= IgnoreAntiCommute(x); prepend_Z(q); } template void Tableau::prepend_SQRT_X(size_t q) { prepend_SQRT_X_DAG(q); prepend_X(q); } template void Tableau::prepend_SQRT_X_DAG(size_t q) { zs[q] *= IgnoreAntiCommute(xs[q]); } template void Tableau::prepend_SQRT_Y(size_t q) { PauliStringRef z = zs[q]; z.sign ^= 1; xs[q].swap_with(z); } template void Tableau::prepend_SQRT_Y_DAG(size_t q) { PauliStringRef z = zs[q]; xs[q].swap_with(z); z.sign ^= 1; } template void Tableau::prepend_SQRT_Z(size_t q) { prepend_SQRT_Z_DAG(q); prepend_Z(q); } template void Tableau::prepend_SQRT_Z_DAG(size_t q) { xs[q] *= IgnoreAntiCommute(zs[q]); } template void Tableau::prepend_SWAP(size_t q1, size_t q2) { zs[q1].swap_with(zs[q2]); xs[q1].swap_with(xs[q2]); } template void Tableau::prepend_ISWAP(size_t q1, size_t q2) { prepend_SWAP(q1, q2); prepend_ZCZ(q1, q2); prepend_SQRT_Z(q1); prepend_SQRT_Z(q2); } template void Tableau::prepend_ISWAP_DAG(size_t q1, size_t q2) { prepend_SWAP(q1, q2); prepend_ZCZ(q1, q2); prepend_SQRT_Z_DAG(q1); prepend_SQRT_Z_DAG(q2); } template void Tableau::prepend_ZCX(size_t control, size_t target) { zs[target] *= zs[control]; xs[control] *= xs[target]; } template void Tableau::prepend_ZCY(size_t control, size_t target) { prepend_H_YZ(target); prepend_ZCZ(control, target); prepend_H_YZ(target); } template void Tableau::prepend_ZCZ(size_t control, size_t target) { xs[target] *= zs[control]; xs[control] *= zs[target]; } template void Tableau::prepend_XCX(size_t control, size_t target) { zs[target] *= xs[control]; zs[control] *= xs[target]; } template void Tableau::prepend_SQRT_XX(size_t q1, size_t q2) { prepend_SQRT_XX_DAG(q1, q2); prepend_X(q1); prepend_X(q2); } template void Tableau::prepend_SQRT_XX_DAG(size_t q1, size_t q2) { zs[q1] *= IgnoreAntiCommute(xs[q1]); zs[q1] *= IgnoreAntiCommute(xs[q2]); zs[q2] *= IgnoreAntiCommute(xs[q1]); zs[q2] *= IgnoreAntiCommute(xs[q2]); } template void Tableau::prepend_SQRT_YY(size_t q1, size_t q2) { prepend_SQRT_YY_DAG(q1, q2); prepend_Y(q1); prepend_Y(q2); } template void Tableau::prepend_SQRT_YY_DAG(size_t q1, size_t q2) { auto z1 = zs[q1]; auto z2 = zs[q2]; auto x1 = xs[q1]; auto x2 = xs[q2]; x1 *= IgnoreAntiCommute(z1); z1 *= IgnoreAntiCommute(z2); z1 *= IgnoreAntiCommute(x2); x2 *= IgnoreAntiCommute(x1); z2 *= IgnoreAntiCommute(x1); x1 *= IgnoreAntiCommute(z1); x1.swap_with(z1); x2.swap_with(z2); prepend_Z(q2); } template void Tableau::prepend_SQRT_ZZ(size_t q1, size_t q2) { prepend_SQRT_ZZ_DAG(q1, q2); prepend_Z(q1); prepend_Z(q2); } template void Tableau::prepend_SQRT_ZZ_DAG(size_t q1, size_t q2) { xs[q1] *= IgnoreAntiCommute(zs[q1]); xs[q1] *= IgnoreAntiCommute(zs[q2]); xs[q2] *= IgnoreAntiCommute(zs[q1]); xs[q2] *= IgnoreAntiCommute(zs[q2]); } template void Tableau::prepend_XCY(size_t control, size_t target) { prepend_H_XY(target); prepend_XCX(control, target); prepend_H_XY(target); } template void Tableau::prepend_XCZ(size_t control, size_t target) { prepend_ZCX(target, control); } template void Tableau::prepend_YCX(size_t control, size_t target) { prepend_XCY(target, control); } template void Tableau::prepend_YCY(size_t control, size_t target) { prepend_H_YZ(control); prepend_H_YZ(target); prepend_ZCZ(control, target); prepend_H_YZ(target); prepend_H_YZ(control); } template void Tableau::prepend_YCZ(size_t control, size_t target) { prepend_ZCY(target, control); } } // namespace stim ================================================ FILE: src/stim/stabilizers/tableau_transposed_raii.h ================================================ /* * 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. */ #ifndef _STIM_STABILIZERS_TABLEAU_TRANSPOSED_RAII_H #define _STIM_STABILIZERS_TABLEAU_TRANSPOSED_RAII_H #include #include #include "stim/mem/simd_bit_table.h" #include "stim/mem/simd_util.h" #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/tableau.h" namespace stim { /// When this class is constructed, it transposes the tableau given to it. /// The transpose is undone on deconstruction. /// /// This is useful when appending operations to the tableau, since otherwise /// the append would be working against the grain of memory. /// /// The template parameter, W, represents the SIMD width. template struct TableauTransposedRaii { Tableau &tableau; explicit TableauTransposedRaii(Tableau &tableau); ~TableauTransposedRaii(); TableauTransposedRaii() = delete; TableauTransposedRaii(const TableauTransposedRaii &) = delete; TableauTransposedRaii(TableauTransposedRaii &&) = delete; PauliString unsigned_x_input(size_t q) const; void append_H_XZ(size_t q); void append_H_XY(size_t q); void append_H_YZ(size_t q); void append_S(size_t q); void append_ZCX(size_t control, size_t target); void append_ZCY(size_t control, size_t target); void append_ZCZ(size_t control, size_t target); void append_X(size_t q); void append_SWAP(size_t q1, size_t q2); }; } // namespace stim #include "stim/stabilizers/tableau_transposed_raii.inl" #endif ================================================ FILE: src/stim/stabilizers/tableau_transposed_raii.inl ================================================ // 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. #include #include #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/tableau_transposed_raii.h" namespace stim { template TableauTransposedRaii::TableauTransposedRaii(Tableau &tableau) : tableau(tableau) { tableau.do_transpose_quadrants(); } template TableauTransposedRaii::~TableauTransposedRaii() { tableau.do_transpose_quadrants(); } /// Iterates over the Paulis in a row of the tableau. /// /// Args: /// trans: The transposed tableau (where rows are contiguous in memory and so operations can be done efficiently). /// q: The row to iterate over. /// body: A function taking X, Z, and SIGN words. /// The X and Z words are chunks of xz-encoded Paulis from the row. /// The SIGN word is the corresponding chunk of sign bits from the sign row. template inline void for_each_trans_obs(TableauTransposedRaii &trans, size_t q, FUNC body) { for (size_t k = 0; k < 2; k++) { TableauHalf &h = k == 0 ? trans.tableau.xs : trans.tableau.zs; PauliStringRef p = h[q]; p.xs.for_each_word(p.zs, h.signs, body); } } template inline void for_each_trans_obs(TableauTransposedRaii &trans, size_t q1, size_t q2, FUNC body) { for (size_t k = 0; k < 2; k++) { TableauHalf &h = k == 0 ? trans.tableau.xs : trans.tableau.zs; PauliStringRef p1 = h[q1]; PauliStringRef p2 = h[q2]; p1.xs.for_each_word(p1.zs, p2.xs, p2.zs, h.signs, body); } } template void TableauTransposedRaii::append_ZCX(size_t control, size_t target) { for_each_trans_obs( *this, control, target, [](simd_word &cx, simd_word &cz, simd_word &tx, simd_word &tz, simd_word &s) { s ^= (cz ^ tx).andnot(cx & tz); cz ^= tz; tx ^= cx; }); } template void TableauTransposedRaii::append_ZCY(size_t control, size_t target) { for_each_trans_obs( *this, control, target, [](simd_word &cx, simd_word &cz, simd_word &tx, simd_word &tz, simd_word &s) { cz ^= tx; s ^= cx & cz & (tx ^ tz); cz ^= tz; tx ^= cx; tz ^= cx; }); } template void TableauTransposedRaii::append_ZCZ(size_t control, size_t target) { for_each_trans_obs( *this, control, target, [](simd_word &cx, simd_word &cz, simd_word &tx, simd_word &tz, simd_word &s) { s ^= cx & tx & (cz ^ tz); cz ^= tx; tz ^= cx; }); } template void TableauTransposedRaii::append_SWAP(size_t q1, size_t q2) { for_each_trans_obs( *this, q1, q2, [](simd_word &x1, simd_word &z1, simd_word &x2, simd_word &z2, simd_word &s) { std::swap(x1, x2); std::swap(z1, z2); }); } template void TableauTransposedRaii::append_H_XY(size_t target) { for_each_trans_obs(*this, target, [](simd_word &x, simd_word &z, simd_word &s) { s ^= x.andnot(z); z ^= x; }); } template void TableauTransposedRaii::append_H_YZ(size_t target) { for_each_trans_obs(*this, target, [](simd_word &x, simd_word &z, simd_word &s) { s ^= z.andnot(x); x ^= z; }); } template void TableauTransposedRaii::append_S(size_t target) { for_each_trans_obs(*this, target, [](simd_word &x, simd_word &z, simd_word &s) { s ^= x & z; z ^= x; }); } template void TableauTransposedRaii::append_H_XZ(size_t q) { for_each_trans_obs(*this, q, [](simd_word &x, simd_word &z, simd_word &s) { std::swap(x, z); s ^= x & z; }); } template void TableauTransposedRaii::append_X(size_t target) { for_each_trans_obs(*this, target, [](simd_word &x, simd_word &z, simd_word &s) { s ^= z; }); } template PauliString TableauTransposedRaii::unsigned_x_input(size_t q) const { PauliString result(tableau.num_qubits); result.xs = tableau.zs[q].zs; result.zs = tableau.xs[q].zs; return result; } } // namespace stim ================================================ FILE: src/stim/util_bot/arg_parse.cc ================================================ // 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. #include "stim/util_bot/arg_parse.h" #include #include #include #include #include #include #include #include #include using namespace stim; std::string SubCommandHelp::str_help() const { std::stringstream ss; write_help(ss); return ss.str(); } void write_indented(std::string_view s, std::ostream &out, size_t indent) { bool was_new_line = true; for (char c : s) { if (was_new_line && c != '\n') { for (size_t k = 0; k < indent; k++) { out.put(' '); } } out.put(c); was_new_line = c == '\n'; } } void SubCommandHelp::write_help(std::ostream &out) const { std::vector flags_copy = flags; std::sort(flags_copy.begin(), flags_copy.end(), [](const SubCommandHelpFlag &f1, const SubCommandHelpFlag &f2) { return f1.flag_name < f2.flag_name; }); out << "NAME\n"; out << " stim " << subcommand_name << "\n\n"; out << "SYNOPSIS\n"; out << " stim " << subcommand_name; for (const auto &flag : flags_copy) { out << " \\\n "; bool allows_none = std::find(flag.allowed_values.begin(), flag.allowed_values.end(), "[none]") != flag.allowed_values.end(); bool allows_empty = std::find(flag.allowed_values.begin(), flag.allowed_values.end(), "[switch]") != flag.allowed_values.end(); if (allows_none) { out << "["; } out << flag.flag_name; if (flag.type != "bool") { out << " "; if (allows_empty) { out << "["; } out << flag.type; if (allows_empty) { out << "]"; } } if (allows_none) { out << "]"; } } out << "\n\n"; out << "DESCRIPTION\n"; write_indented(description, out, 4); out << "\n\n"; if (!flags_copy.empty()) { out << "OPTIONS\n"; for (const auto &f : flags_copy) { out << " " << f.flag_name << "\n"; write_indented(f.description, out, 8); out << "\n\n"; } } if (!examples.empty()) { out << "EXAMPLES\n"; for (size_t k = 0; k < examples.size(); k++) { if (k) { out << "\n\n"; } out << " Example #" << (k + 1) << "\n"; write_indented(examples[k], out, 8); } } } const char *stim::require_find_argument(const char *name_c_str, int argc, const char **argv) { const char *result = find_argument(name_c_str, argc, argv); if (result == 0) { std::stringstream msg; msg << "\033[31mMissing command line argument: '" << name_c_str << "'"; throw std::invalid_argument(msg.str()); } return result; } const char *stim::find_argument(const char *name_c_str, int argc, const char **argv) { // Respect that the "--" argument terminates flags. size_t flag_count = 1; while (flag_count < (size_t)argc && strcmp(argv[flag_count], "--") != 0) { flag_count++; } // Search for the desired flag. size_t n = strlen(name_c_str); for (size_t i = 1; i < flag_count; i++) { // Check if argument starts with expected flag. const char *loc = strstr(argv[i], name_c_str); if (loc != argv[i] || (loc[n] != '\0' && loc[n] != '=')) { continue; } // If the flag is alone and followed by the end or another flag, no // argument was provided. Return the empty string to indicate this. if (loc[n] == '\0' && ((int)i == argc - 1 || (argv[i + 1][0] == '-' && !isdigit(argv[i + 1][1])))) { return argv[i] + n; } // If the flag value is specified inline with '=', return a pointer to // the start of the value within the flag string. if (loc[n] == '=') { // Argument provided inline. return loc + n + 1; } // The argument value is specified by the next command line argument. return argv[i + 1]; } // Not found. return 0; } void stim::check_for_unknown_arguments( const std::vector &known_arguments, const std::vector &known_but_deprecated_arguments, const char *for_mode, int argc, const char **argv) { for (int i = 1; i < argc; i++) { if (for_mode != nullptr && i == 1 && strcmp(argv[i], for_mode) == 0) { continue; } // Respect that the "--" argument terminates flags. if (!strcmp(argv[i], "--")) { break; } // Check if there's a matching command line argument. int matched = 0; std::array *, 2> both{&known_arguments, &known_but_deprecated_arguments}; for (const auto &knowns : both) { for (const auto &known : *knowns) { const char *loc = strstr(argv[i], known); size_t n = strlen(known); if (loc == argv[i] && (loc[n] == '\0' || loc[n] == '=')) { // Skip words that are values for a previous flag. if (loc[n] == '\0' && i < argc - 1 && argv[i + 1][0] != '-') { i++; } matched = 1; break; } } } // Print error and exit if flag is not recognized. if (!matched) { std::stringstream msg; if (for_mode == nullptr) { msg << "Unrecognized command line argument " << argv[i] << ".\n"; msg << "Recognized command line arguments:\n"; } else { msg << "Unrecognized command line argument " << argv[i] << " for `stim " << for_mode << "`.\n"; msg << "Recognized command line arguments for `stim " << for_mode << "`:\n"; } std::set known_sorted; for (const auto &v : known_arguments) { known_sorted.insert(v); } for (const auto &v : known_sorted) { msg << " " << v << "\n"; } throw std::invalid_argument(msg.str()); } } } bool stim::find_bool_argument(const char *name_c_str, int argc, const char **argv) { const char *text = find_argument(name_c_str, argc, argv); if (text == nullptr) { return false; } if (text[0] == '\0') { return true; } std::stringstream msg; msg << "Got non-empty value '" << text << "' for boolean flag '" << name_c_str << "'."; throw std::invalid_argument(msg.str()); } bool stim::parse_int64(std::string_view data, int64_t *out) { if (data.empty()) { return false; } bool negate = false; if (data.starts_with("-")) { negate = true; data = data.substr(1); } else if (data.starts_with("+")) { data = data.substr(1); } uint64_t accumulator = 0; for (char c : data) { if (!(c >= '0' && c <= '9')) { return false; } uint64_t digit = c - '0'; uint64_t next = accumulator * 10 + digit; if (accumulator != (next - digit) / 10) { return false; // Overflow. } accumulator = next; } if (negate && accumulator == (uint64_t)INT64_MAX + uint64_t{1}) { *out = INT64_MIN; return true; } if (accumulator > INT64_MAX) { return false; } *out = (int64_t)accumulator; if (negate) { *out *= -1; } return true; } int64_t stim::find_int64_argument( const char *name_c_str, int64_t default_value, int64_t min_value, int64_t max_value, int argc, const char **argv) { const char *text = find_argument(name_c_str, argc, argv); if (text == nullptr || text[0] == '\0') { if (default_value < min_value || default_value > max_value) { std::stringstream msg; msg << "Must specify a value for int flag '" << name_c_str << "'."; throw std::invalid_argument(msg.str()); } return default_value; } // Attempt to parse. int64_t i; if (!parse_int64(text, &i)) { std::stringstream msg; msg << "Got non-int64 value '" << text << "' for int64 flag '" << name_c_str << "'."; throw std::invalid_argument(msg.str()); } // In range? if (i < min_value || i > max_value) { std::stringstream msg; msg << "Integer value '" << text << "' for flag '" << name_c_str << "' doesn't satisfy " << min_value << " <= " << i << " <= " << max_value << "."; throw std::invalid_argument(msg.str()); } return i; } float stim::find_float_argument( const char *name_c_str, float default_value, float min_value, float max_value, int argc, const char **argv) { const char *text = find_argument(name_c_str, argc, argv); if (text == nullptr) { if (default_value < min_value || default_value > max_value) { std::stringstream msg; msg << "Must specify a value for float flag '" << name_c_str << "'."; throw std::invalid_argument(msg.str()); } return default_value; } // Attempt to parse. char *processed; float f = strtof(text, &processed); if (*processed != '\0') { std::stringstream msg; msg << "Got non-float value '" << text << "' for float flag '" << name_c_str << "'."; throw std::invalid_argument(msg.str()); } // In range? if (f < min_value || f > max_value || f != f) { std::stringstream msg; msg << "Float value '" << text << "' for flag '" << name_c_str << "' doesn't satisfy " << min_value << " <= " << f << " <= " << max_value << "."; throw std::invalid_argument(msg.str()); } return f; } FILE *stim::find_open_file_argument( const char *name_c_str, FILE *default_file, const char *mode, int argc, const char **argv) { const char *path_c_str = find_argument(name_c_str, argc, argv); if (path_c_str == nullptr) { if (default_file == nullptr) { std::stringstream msg; msg << "Missing command line argument: '" << name_c_str << "'"; throw std::invalid_argument(msg.str()); } return default_file; } if (*path_c_str == '\0') { std::stringstream msg; msg << "Command line argument '" << name_c_str << "' can't be empty. It's supposed to be a file path."; throw std::invalid_argument(msg.str()); } FILE *file = fopen(path_c_str, mode); if (file == nullptr) { std::stringstream msg; msg << "Failed to open '" << path_c_str << "'"; throw std::invalid_argument(msg.str()); } return file; } ostream_else_cout::ostream_else_cout(std::unique_ptr &&held) : held(std::move(held)) { } std::ostream &ostream_else_cout::stream() { if (held) { return *held; } else { return std::cout; } } ostream_else_cout stim::find_output_stream_argument( const char *name_c_str, bool default_std_out, int argc, const char **argv) { const char *path_c_str = find_argument(name_c_str, argc, argv); if (path_c_str == nullptr) { if (!default_std_out) { std::stringstream msg; msg << "Missing command line argument: '" << name_c_str << "'"; throw std::invalid_argument(msg.str()); } return ostream_else_cout(nullptr); } if (*path_c_str == '\0') { std::stringstream msg; msg << "Command line argument '" << name_c_str << "' can't be empty. It's supposed to be a file path."; throw std::invalid_argument(msg.str()); } std::unique_ptr f(new std::ofstream(path_c_str)); if (f->fail()) { std::stringstream msg; msg << "Failed to open '" << path_c_str << "'"; throw std::invalid_argument(msg.str()); } return ostream_else_cout(std::move(f)); } std::vector stim::split_view(char splitter, std::string_view text) { std::vector result; size_t start = 0; for (size_t k = 0; k < text.size(); k++) { if (text[k] == splitter) { result.push_back(text.substr(start, k - start)); start = k + 1; } } result.push_back(text.substr(start, text.size() - start)); return result; } static double parse_exact_double_from_null_terminated(const char *c, size_t size) { char *end = nullptr; double d = strtod(c, &end); if (size > 0 && !isspace(*c)) { if (end == c + size && !std::isinf(d) && !std::isnan(d)) { return d; } } std::stringstream ss; ss << "Not an exact finite double: '" << c << "'"; throw std::invalid_argument(ss.str()); } double stim::parse_exact_double_from_string(std::string_view text) { if (text.size() + 1 < 15) { char buf[16]; memcpy(buf, text.data(), text.size()); buf[text.size()] = '\0'; return parse_exact_double_from_null_terminated(&buf[0], text.size()); } else { std::string s(text); return parse_exact_double_from_null_terminated(s.c_str(), text.size()); } } static bool try_parse_exact_uint64_t_from_string(std::string_view text, uint64_t *out) { if (text.empty()) { return false; } if (text[0] == '-') { return false; } size_t k = 0; if (text[k] == '+') { k += 1; } uint64_t acc = 0; while (k < text.size()) { char c = text[k]; if (c < '0' || c > '9') { return false; } if (acc > UINT64_MAX / 10) { return false; } acc *= 10; uint8_t d = c - '0'; if (acc > UINT64_MAX - d) { return false; } acc += d; k++; } *out = acc; return true; } uint64_t stim::parse_exact_uint64_t_from_string(std::string_view text) { uint64_t result = 0; if (try_parse_exact_uint64_t_from_string(text, &result)) { return result; } std::stringstream ss; ss << "Not an exact integer that can be stored in a uint64_t: '" << text << "'"; throw std::invalid_argument(ss.str()); } ================================================ FILE: src/stim/util_bot/arg_parse.h ================================================ /* * 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. */ #ifndef _STIM_UTIL_BOT_ARG_PARSE_H #define _STIM_UTIL_BOT_ARG_PARSE_H #include #include #include #include #include #include namespace stim { struct SubCommandHelpFlag { std::string flag_name; std::string type; std::string default_value; std::vector allowed_values; std::string description; }; struct SubCommandHelp { std::string subcommand_name; std::string description; std::vector examples; std::vector flags; void write_help(std::ostream &out) const; std::string str_help() const; }; /// Searches through command line flags for a particular flag's argument. /// /// Args: /// name: The flag's name, including any hyphens. For example, "-mode". /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Returns: /// A pointer to the command line flag's value string, or else 0 if the flag /// is not specified. Flags that are set without specifying a value will /// cause the method to return a pointer to an empty string. const char *find_argument(const char *name, int argc, const char **argv); /// Searches through command line flags for a particular flag's argument. /// /// Args: /// name: The flag's name, including any hyphens. For example, "-mode". /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Returns: /// A pointer to the command line flag's value string. Flags that are set /// without specifying a value will cause the method to return a pointer to /// an empty string. /// /// Raises: /// std::invalid_argument: The argument isn't present. const char *require_find_argument(const char *name, int argc, const char **argv); /// Checks that all command line arguments are recognized. /// /// Args: /// known_arguments: Names of known arguments. /// for_mode: Can be set to nullptr. Modifies error message. /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Raises: /// std::invalid_argument: Unknown arguments are present. void check_for_unknown_arguments( const std::vector &known_arguments, const std::vector &known_but_deprecated_arguments, const char *for_mode, int argc, const char **argv); /// Returns a floating point value that can be modified using command line arguments. /// /// If default_value is smaller than min_value or larger than max_value, the argument is required. /// /// If the specified value is invalid, the program exits with a non-zero return code and prints /// a message describing the problem. /// /// Args: /// name: The name of the float flag. /// default_value: The value to use if the flag is not specified. If this value is less than /// min_value or larger than max_value, the flag is required. /// min_value: Values less than this are rejected. /// max_value: Values more than this are rejected. /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Returns: /// The floating point value. /// /// Raises: /// std::invalid_argument: /// The command line flag was specified but failed to parse into a float in range. /// std::invalid_argument: /// The command line flag was not specified and default_value was not in range. float find_float_argument( const char *name, float default_value, float min_value, float max_value, int argc, const char **argv); /// Returns an integer value that can be modified using command line arguments. /// /// If default_value is smaller than min_value or larger than max_value, the argument is required. /// /// If the specified value is invalid, the program exits with a non-zero return code and prints /// a message describing the problem. /// /// Args: /// name: The name of the int flag. /// default_value: The value to use if the flag is not specified. If this value is less than /// min_value or larger than max_value, the flag is required. /// min_value: Values less than this are rejected. /// max_value: Values more than this are rejected. /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Returns: /// The integer value. /// /// Raises: /// std::invalid_argument: /// The command line flag was specified but failed to parse into an int in range. /// std::invalid_argument: /// The command line flag was not specified and default_value was not in range. int64_t find_int64_argument( const char *name, int64_t default_value, int64_t min_value, int64_t max_value, int argc, const char **argv); /// /// Returns a boolean value that can be enabled using a command line argument. /// /// If the specified value is invalid, the program exits with a non-zero return code and prints /// a message describing the problem. /// /// Args: /// name: The name of the boolean flag. /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Returns: /// The boolean value. /// bool find_bool_argument(const char *name, int argc, const char **argv); /// Returns the index of an argument value within an enumerated list of allowed values. /// /// Args: /// name: The name of the enumerated flag. /// default_key: The default value of the flag. Set to a key that's not in the map to make the /// flag required. /// known_values: A map from allowed keys to returned values. /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Returns: /// The chosen value. /// /// Raises: /// std::invalid_argument: /// The command line flag is specified but its key is not in the map. /// std::invalid_argument: /// The command line flag is not specified and the default key is not in the map. template const T &find_enum_argument( const char *name, const char *default_key, const std::map &values, int argc, const char **argv) { const char *text = find_argument(name, argc, argv); if (text == nullptr) { if (default_key == nullptr) { std::stringstream msg; msg << "\033[31mMust specify a value for enum flag '" << name << "'.\n"; throw std::invalid_argument(msg.str()); } return values.at(default_key); } if (!values.contains(text)) { std::stringstream msg; msg << "\033[31mUnrecognized value '" << text << "' for enum flag '" << name << "'.\n"; msg << "Recognized values are:\n"; for (const auto &kv : values) { msg << " '" << kv.first << "'"; if (default_key != nullptr && kv.first == default_key) { msg << " (default)"; } msg << "\n"; } msg << "\033[0m"; throw std::invalid_argument(msg.str()); } return values.at(text); } /// Returns an opened file from a command line argument. /// /// Args: /// name: The name of the file flag that will specify the file path. /// default_file: The file pointer to return if the flag isn't specified (e.g. stdin). Set to /// nullptr to make the argument required. /// mode: The mode to open the file path in. /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Returns: /// The default file pointer or the opened file. /// /// Raises: /// std::invalid_argument: /// Failed to open the filepath. /// std::invalid_argument: /// No argument specified and default file is nullptr. FILE *find_open_file_argument(const char *name, FILE *default_file, const char *mode, int argc, const char **argv); /// Exposes an owned ostream or else falls back to std::cout. struct ostream_else_cout { private: std::unique_ptr held; public: explicit ostream_else_cout(std::unique_ptr &&held); std::ostream &stream(); }; /// Returns an opened ostream from a command line argument. /// /// Args: /// name: The name of the command line flag that will specify the file path. /// default_std_out: If true, defaults to stdout when the command line argument isn't given. Otherwise exits with /// failure when the command line argument isn't given. /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// /// Returns: /// The default file pointer or the opened file. /// /// Raises: /// std::invalid_argument: /// Failed to open the filepath. /// std::invalid_argument: /// Command line argument isn't present and default_std_out is false. ostream_else_cout find_output_stream_argument(const char *name, bool default_std_out, int argc, const char **argv); std::vector split_view(char splitter, std::string_view text); double parse_exact_double_from_string(std::string_view text); uint64_t parse_exact_uint64_t_from_string(std::string_view text); bool parse_int64(std::string_view data, int64_t *out); } // namespace stim #endif ================================================ FILE: src/stim/util_bot/arg_parse.test.cc ================================================ // 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. #include "stim/util_bot/arg_parse.h" #include #include #include "gtest/gtest.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(arg_parse, check_for_unknown_arguments_recognize_arguments) { std::vector known{ "--mode", "--test", "--other", }; const char *argv[] = {"skipped", "--mode", "2", "--test", "5"}; check_for_unknown_arguments(known, {}, nullptr, sizeof(argv) / sizeof(char *), argv); check_for_unknown_arguments({}, known, nullptr, sizeof(argv) / sizeof(char *), argv); } TEST(arg_parse, check_for_unknown_arguments_bad_arguments) { std::vector known{ "--mode", "--test", }; const char *argv[] = {"skipped", "--mode", "2", "--unknown", "5"}; ASSERT_THROW( { check_for_unknown_arguments(known, {}, nullptr, sizeof(argv) / sizeof(char *), argv); }, std::invalid_argument); ASSERT_THROW( { check_for_unknown_arguments({}, known, nullptr, sizeof(argv) / sizeof(char *), argv); }, std::invalid_argument); } TEST(arg_parse, check_for_unknown_arguments_terminator) { std::vector known{ "--mode", }; const char *argv[] = {"skipped", "--mode", "2", "--", "--unknown"}; check_for_unknown_arguments(known, {}, nullptr, sizeof(argv) / sizeof(char *), argv); } TEST(arg_parse, find_argument) { const char *argv[] = { "skipped", "--mode", "2", "--test", "aba", "--a", "-b", }; int n = sizeof(argv) / sizeof(char *); ASSERT_TRUE(find_argument("--mode", n, argv) == argv[2]); ASSERT_TRUE(find_argument("-mode", n, argv) == 0); ASSERT_TRUE(find_argument("mode", n, argv) == 0); ASSERT_TRUE(find_argument("--test", n, argv) == argv[4]); ASSERT_TRUE(find_argument("--a", n, argv) == argv[5] + 3); ASSERT_TRUE(find_argument("-a", n, argv) == 0); ASSERT_TRUE(find_argument("-b", n, argv) == argv[6] + 2); ASSERT_TRUE(find_argument("--b", n, argv) == 0); ASSERT_TRUE(!strcmp(find_argument("--a", n, argv), "")); ASSERT_TRUE(!strcmp(find_argument("-b", n, argv), "")); } TEST(arg_parse, require_find_argument) { const char *argv[] = { "skipped", "--mode", "2", "--test", "aba", "--a", "-b", }; int n = sizeof(argv) / sizeof(char *); assert(require_find_argument("--mode", n, argv) == argv[2]); assert(require_find_argument("--test", n, argv) == argv[4]); assert(require_find_argument("--a", n, argv) == argv[5] + 3); assert(require_find_argument("-b", n, argv) == argv[6] + 2); assert(!strcmp(require_find_argument("--a", n, argv), "")); assert(!strcmp(require_find_argument("-b", n, argv), "")); ASSERT_THROW({ require_find_argument("-mode", n, argv); }, std::invalid_argument); ASSERT_THROW({ require_find_argument("-a", n, argv); }, std::invalid_argument); ASSERT_THROW({ require_find_argument("--b", n, argv); }, std::invalid_argument); } TEST(arg_parse, find_bool_argument) { const char *argv[] = { "", "-other", "2", "do", "-not", "-be", "activate", "-par", "--", "-okay", }; int n = sizeof(argv) / sizeof(char *); ASSERT_EQ(find_bool_argument("-activate", n, argv), 0); ASSERT_EQ(find_bool_argument("-okay", n, argv), 0); ASSERT_EQ(find_bool_argument("-not", n, argv), 1); ASSERT_EQ(find_bool_argument("-par", n, argv), 1); ASSERT_THROW({ find_bool_argument("-be", n, argv); }, std::invalid_argument); ASSERT_THROW({ find_bool_argument("-other", n, argv); }, std::invalid_argument); } template std::string catch_msg_helper(std::function func, std::string expected_substring) { try { func(); return "Expected an exception with message '" + expected_substring + "', but no exception was thrown."; } catch (const TEx &ex) { std::string s = ex.what(); if (s.find(expected_substring) == std::string::npos) { return "Didn't find '" + expected_substring + "' in '" + std::string(ex.what()) + "'."; } return ""; } } #define ASSERT_THROW_MSG(body, type, msg) ASSERT_EQ("", catch_msg_helper([&]() body, msg)) TEST(arg_parse, find_int_argument) { const char *argv[] = { "", "-small=-23", "-empty", "-text", "abc", "-zero", "0", "-large", "50", "--", "-okay", }; int n = sizeof(argv) / sizeof(char *); ASSERT_EQ(find_int64_argument("-missing", 5, -100, +100, n, argv), 5); ASSERT_EQ(find_int64_argument("-small", 5, -100, +100, n, argv), -23); ASSERT_EQ(find_int64_argument("-large", 5, -100, +100, n, argv), 50); ASSERT_EQ(find_int64_argument("-zero", 5, -100, +100, n, argv), 0); ASSERT_THROW_MSG({ find_int64_argument("-large", 0, 0, 49, n, argv); }, std::invalid_argument, "50 <= 49"); ASSERT_THROW_MSG({ find_int64_argument("-large", 100, 51, 100, n, argv); }, std::invalid_argument, "51 <= 50"); ASSERT_THROW_MSG({ find_int64_argument("-text", 0, 0, 0, n, argv); }, std::invalid_argument, "non-int"); ASSERT_THROW_MSG({ find_int64_argument("-missing", -1, 0, 10, n, argv); }, std::invalid_argument, "Must specify"); ASSERT_THROW_MSG({ find_int64_argument("-missing", 11, 0, 10, n, argv); }, std::invalid_argument, "Must specify"); ASSERT_THROW_MSG( { find_int64_argument("-missing", -101, -100, 100, n, argv); }, std::invalid_argument, "Must specify"); std::vector args; args = {"", "-val", "99999999999999999999999999999999999999999999999999"}; ASSERT_THROW_MSG( { find_int64_argument("-val", 0, INT64_MIN, INT64_MAX, args.size(), args.data()); }, std::invalid_argument, "non-int64"); args = {"", "-val", "9223372036854775807"}; ASSERT_EQ(find_int64_argument("-val", 0, INT64_MIN, INT64_MAX, args.size(), args.data()), INT64_MAX); args = {"", "-val", "9223372036854775808"}; ASSERT_THROW_MSG( { find_int64_argument("-val", 0, INT64_MIN, INT64_MAX, args.size(), args.data()); }, std::invalid_argument, "non-int64"); args = {"", "-val", "-9223372036854775808"}; ASSERT_EQ(find_int64_argument("-val", 0, INT64_MIN, INT64_MAX, args.size(), args.data()), INT64_MIN); args = {"", "-val", "-9223372036854775809"}; ASSERT_THROW_MSG( { find_int64_argument("-val", 0, INT64_MIN, INT64_MAX, args.size(), args.data()); }, std::invalid_argument, "non-int64"); } TEST(arg_parse, find_float_argument) { const char *argv[] = { "", "-small=-23.5", "-empty", "-text", "abc", "-inf", "inf", "-nan", "nan", "-zero", "0", "-large", "50", "--", "-okay", }; int n = sizeof(argv) / sizeof(char *); ASSERT_EQ(find_float_argument("-missing", 5.5, -100, +100, n, argv), 5.5); ASSERT_EQ(find_float_argument("-small", 5, -100, +100, n, argv), -23.5); ASSERT_EQ(find_float_argument("-large", 5, -100, +100, n, argv), 50); ASSERT_EQ(find_float_argument("-large", 5, -100, +100, n, argv), 50); ASSERT_EQ(find_float_argument("-zero", 5, -100, +100, n, argv), 0); ASSERT_THROW_MSG({ find_float_argument("-large", 0, 0, 49, n, argv); }, std::invalid_argument, "0 <= 49"); ASSERT_THROW_MSG({ find_float_argument("-nan", 0, -100, 100, n, argv); }, std::invalid_argument, "nan <= 100"); ASSERT_THROW_MSG({ find_float_argument("-large", 100, 51, 100, n, argv); }, std::invalid_argument, "<= 50"); ASSERT_THROW_MSG({ find_float_argument("-text", 0, 0, 0, n, argv); }, std::invalid_argument, "non-float"); ASSERT_THROW_MSG({ find_float_argument("-missing", -1, 0, 10, n, argv); }, std::invalid_argument, "Must specify"); ASSERT_THROW_MSG({ find_float_argument("-missing", -1, 11, 10, n, argv); }, std::invalid_argument, "Must specify"); ASSERT_THROW_MSG( { find_float_argument("-missing", -101, -100, 100, n, argv); }, std::invalid_argument, "Must specify"); } TEST(arg_parse, find_enum_argument) { std::vector args{ "", "-a=test", "-b", "-c=rest", }; std::map enums{ {"", 10}, {"test", 20}, {"rest", 30}, }; ASSERT_EQ(find_enum_argument("-a", nullptr, enums, args.size(), args.data()), 20); ASSERT_EQ(find_enum_argument("-b", nullptr, enums, args.size(), args.data()), 10); ASSERT_EQ(find_enum_argument("-c", nullptr, enums, args.size(), args.data()), 30); ASSERT_EQ(find_enum_argument("-d", "test", enums, args.size(), args.data()), 20); ASSERT_EQ(find_enum_argument("-d", "rest", enums, args.size(), args.data()), 30); ASSERT_THROW_MSG( { find_enum_argument("-d", nullptr, enums, args.size(), args.data()); }, std::invalid_argument, "specify a value"); enums.erase("test"); ASSERT_THROW_MSG( { find_enum_argument("-a", nullptr, enums, args.size(), args.data()); }, std::invalid_argument, "Unrecognized value"); } TEST(arg_parse, find_open_file_argument) { std::vector args; FILE *tmp = tmpfile(); args = {""}; ASSERT_THROW_MSG( { find_open_file_argument("-arg", nullptr, "rb", args.size(), args.data()); }, std::invalid_argument, "Missing"); args = {""}; ASSERT_EQ(find_open_file_argument("-arg", tmp, "rb", args.size(), args.data()), tmp); args = {"", "-arg"}; ASSERT_THROW_MSG( { find_open_file_argument("-arg", nullptr, "rb", args.size(), args.data()); }, std::invalid_argument, "empty"); args = {"", "-arg"}; ASSERT_THROW_MSG( { find_open_file_argument("-arg", tmp, "rb", args.size(), args.data()); }, std::invalid_argument, "empty"); RaiiTempNamedFile f; FILE *f2 = fdopen(f.descriptor, "wb"); putc('x', f2); fclose(f2); args = {"", "-arg", f.path.data()}; f2 = find_open_file_argument("-arg", nullptr, "rb", args.size(), args.data()); ASSERT_NE(f2, nullptr); ASSERT_EQ(getc(f2), 'x'); fclose(f2); remove(f.path.c_str()); args = {"", "-arg", f.path.data()}; ASSERT_THROW_MSG( { find_open_file_argument("-arg", nullptr, "rb", args.size(), args.data()); }, std::invalid_argument, "Failed to open"); f2 = find_open_file_argument("-arg", nullptr, "wb", args.size(), args.data()); fclose(f2); fclose(tmp); } TEST(arg_parse, split_view) { ASSERT_EQ(split_view(',', ""), (std::vector{""})); ASSERT_EQ(split_view(',', "abc"), (std::vector{"abc"})); ASSERT_EQ(split_view(',', ","), (std::vector{"", ""})); ASSERT_EQ(split_view(',', "abc,"), (std::vector{"abc", ""})); ASSERT_EQ(split_view(',', ",abc"), (std::vector{"", "abc"})); ASSERT_EQ(split_view(',', "abc,def,ghi"), (std::vector{"abc", "def", "ghi"})); ASSERT_EQ(split_view(',', "abc,def,ghi,"), (std::vector{"abc", "def", "ghi", ""})); } TEST(arg_parse, parse_exact_double_from_string) { ASSERT_THROW({ parse_exact_double_from_string(""); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string("a"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string(" 1"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string("\t1"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string("1 "); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string("banana"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string("1.2.3"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string("INFINITY"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string("inf"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_double_from_string("nan"); }, std::invalid_argument); ASSERT_EQ(parse_exact_double_from_string("0"), 0); ASSERT_EQ(parse_exact_double_from_string("1"), 1); ASSERT_EQ(parse_exact_double_from_string("+1"), 1); ASSERT_EQ(parse_exact_double_from_string("-1"), -1); ASSERT_EQ(parse_exact_double_from_string("1e3"), 1000); ASSERT_EQ(parse_exact_double_from_string("1.5"), 1.5); ASSERT_EQ(parse_exact_double_from_string("-1.5"), -1.5); } TEST(arg_parse, parse_exact_uint64_t_from_string) { ASSERT_THROW({ parse_exact_uint64_t_from_string(""); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("a"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string(" 1"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("\t1"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("1 "); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("banana"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("1.2.3"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("INFINITY"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("inf"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("nan"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("1e3"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("1.5"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("-1"); }, std::invalid_argument); ASSERT_THROW({ parse_exact_uint64_t_from_string("18446744073709551616"); }, std::invalid_argument); ASSERT_EQ(parse_exact_uint64_t_from_string("0"), 0); ASSERT_EQ(parse_exact_uint64_t_from_string("1"), 1); ASSERT_EQ(parse_exact_uint64_t_from_string("+1"), 1); ASSERT_EQ(parse_exact_uint64_t_from_string("2"), 2); ASSERT_EQ(parse_exact_uint64_t_from_string("13"), 13); ASSERT_EQ(parse_exact_uint64_t_from_string("18446744073709551615"), UINT64_MAX); } TEST(arg_parse, parse_int64) { int64_t x = 0; ASSERT_TRUE(parse_int64("+0", &x)); ASSERT_EQ(x, 0); ASSERT_TRUE(parse_int64("-0", &x)); ASSERT_EQ(x, 0); ASSERT_TRUE(parse_int64("0", &x)); ASSERT_EQ(x, 0); ASSERT_TRUE(parse_int64("1", &x)); ASSERT_EQ(x, 1); ASSERT_TRUE(parse_int64("-1", &x)); ASSERT_EQ(x, -1); ASSERT_FALSE(parse_int64("i", &x)); ASSERT_FALSE(parse_int64("1i", &x)); ASSERT_FALSE(parse_int64("i1", &x)); ASSERT_FALSE(parse_int64("1e2", &x)); ASSERT_FALSE(parse_int64("12i1", &x)); ASSERT_FALSE(parse_int64("12 ", &x)); ASSERT_FALSE(parse_int64(" 12", &x)); ASSERT_TRUE(parse_int64("0123", &x)); ASSERT_EQ(x, 123); ASSERT_TRUE(parse_int64("-0123", &x)); ASSERT_EQ(x, -123); ASSERT_FALSE(parse_int64("-9223372036854775809", &x)); ASSERT_TRUE(parse_int64("-9223372036854775808", &x)); ASSERT_EQ(x, INT64_MIN); ASSERT_FALSE(parse_int64("9223372036854775808", &x)); ASSERT_TRUE(parse_int64("9223372036854775807", &x)); ASSERT_EQ(x, INT64_MAX); } ================================================ FILE: src/stim/util_bot/error_decomp.cc ================================================ #include "stim/util_bot/error_decomp.h" #include #include #include #include using namespace stim; void stim::independent_to_disjoint_xyz_errors( double x, double y, double z, double *out_x, double *out_y, double *out_z) { if (x < 0 || y < 0 || z < 0 || x > 1 || y > 1 || z > 1) { throw std::invalid_argument("x < 0 || y < 0 || z < 0 || x > 1 || y > 1 || z > 1"); } double ab = x * y; double ac = x * z; double bc = y * z; double a_i = 1.0 - x; double b_i = 1.0 - y; double c_i = 1.0 - z; double ab_i = a_i * b_i; double ac_i = a_i * c_i; double bc_i = b_i * c_i; *out_x = x * bc_i + a_i * bc; *out_y = y * ac_i + b_i * ac; *out_z = z * ab_i + c_i * ab; } bool stim::try_disjoint_to_independent_xyz_errors_approx( double x, double y, double z, double *out_x, double *out_y, double *out_z, size_t max_steps) { if (x < 0 || y < 0 || z < 0 || x + y + z > 1) { throw std::invalid_argument("x < 0 || y < 0 || z < 0 || x + y + z > 1"); } // Re-arrange the problem so identity is the most likely case. double i = std::max(0.0, 1.0 - x - y - z); if (i < x) { auto result = try_disjoint_to_independent_xyz_errors_approx(i, z, y, out_x, out_y, out_z, max_steps); *out_x = 1 - *out_x; return result; } if (i < y) { auto result = try_disjoint_to_independent_xyz_errors_approx(z, i, x, out_x, out_y, out_z, max_steps); *out_y = 1 - *out_y; return result; } if (i < z) { auto result = try_disjoint_to_independent_xyz_errors_approx(y, x, i, out_x, out_y, out_z, max_steps); *out_z = 1 - *out_z; return result; } // Solve analytically if an exact solution exists. if (x + z < 0.5 && x + y < 0.5 && y + z < 0.5) { double s_xz = sqrt(1 - 2 * x - 2 * z); double s_xy = sqrt(1 - 2 * x - 2 * y); double s_yz = sqrt(1 - 2 * y - 2 * z); double a = 0.5 - 0.5 * s_xz * s_xy / s_yz; double b = 0.5 - 0.5 * s_xy * s_yz / s_xz; double c = 0.5 - 0.5 * s_xz * s_yz / s_xy; if (a >= 0 && b >= 0 && c >= 0) { *out_x = a; *out_y = b; *out_z = c; return true; } } // If no exact solution exists, resort to approximations. double a = x; double b = y; double c = z; for (size_t step = 0; step < max_steps; step++) { // Compute current error. double ab = a * b; double ac = a * c; double bc = b * c; double a_i = 1.0 - a; double b_i = 1.0 - b; double c_i = 1.0 - c; double ab_i = a_i * b_i; double ac_i = a_i * c_i; double bc_i = b_i * c_i; double x2 = a * bc_i + a_i * bc; double y2 = b * ac_i + b_i * ac; double z2 = c * ab_i + c_i * ab; double dx = x2 - x; double dy = y2 - y; double dz = z2 - z; double err = fabs(dx) + fabs(dy) + fabs(dz); if (err < 1e-14) { // Good enough. *out_x = a; *out_y = b; *out_z = c; return true; } // Make a Newton-Raphson step towards the solution. double da = bc_i - bc; double db = ac_i - ac; double dc = ab_i - ac; a -= dx / da; b -= dy / db; c -= dz / dc; a = std::max(a, 0.0); b = std::max(b, 0.0); c = std::max(c, 0.0); } *out_x = a; *out_y = b; *out_z = c; return false; } double stim::depolarize1_probability_to_independent_per_channel_probability(double p) { if (p > 0.75) { throw std::invalid_argument( "depolarize1_probability_to_independent_per_channel_probability with p>0.75; p=" + std::to_string(p)); } return 0.5 - 0.5 * sqrt(1 - (4 * p) / 3); } double stim::depolarize2_probability_to_independent_per_channel_probability(double p) { if (p > 0.9375) { throw std::invalid_argument( "depolarize2_probability_to_independent_per_channel_probability with p>15.0/16.0; p=" + std::to_string(p)); } return 0.5 - 0.5 * pow(1 - (16 * p) / 15, 0.125); } double stim::independent_per_channel_probability_to_depolarize1_probability(double p) { double q = 1.0 - 2.0 * p; q *= q; return 3.0 / 4.0 * (1.0 - q); } double stim::independent_per_channel_probability_to_depolarize2_probability(double p) { double q = 1.0 - 2.0 * p; q *= q; q *= q; q *= q; return 15.0 / 16.0 * (1.0 - q); } ================================================ FILE: src/stim/util_bot/error_decomp.h ================================================ #ifndef _STIM_UTIL_BOT_ERROR_DECOM_H #define _STIM_UTIL_BOT_ERROR_DECOM_H #include namespace stim { void independent_to_disjoint_xyz_errors(double x, double y, double z, double *out_x, double *out_y, double *out_z); bool try_disjoint_to_independent_xyz_errors_approx( double x, double y, double z, double *out_x, double *out_y, double *out_z, size_t max_steps = 50); double depolarize1_probability_to_independent_per_channel_probability(double p); double depolarize2_probability_to_independent_per_channel_probability(double p); double independent_per_channel_probability_to_depolarize1_probability(double p); double independent_per_channel_probability_to_depolarize2_probability(double p); } // namespace stim #endif ================================================ FILE: src/stim/util_bot/error_decomp.perf.cc ================================================ #include "stim/util_bot/error_decomp.h" #include #include "stim/perf.perf.h" #include "stim/util_bot/str_util.h" using namespace stim; BENCHMARK(disjoint_to_independent_xyz_errors_approx_exact) { double w = 0; benchmark_go([&]() { double a; double b; double c; try_disjoint_to_independent_xyz_errors_approx(0.1, 0.2, 0.15, &a, &b, &c); w += a; w += b; w += c; }).goal_nanos(11); if (w == 0) { std::cout << "data dependence"; } } BENCHMARK(disjoint_to_independent_xyz_errors_approx_p10) { double w = 0; benchmark_go([&]() { double a; double b; double c; try_disjoint_to_independent_xyz_errors_approx(0.1, 0.2, 0.0, &a, &b, &c); w += a; w += b; w += c; }).goal_nanos(550); if (w == 0) { std::cout << "data dependence"; } } BENCHMARK(disjoint_to_independent_xyz_errors_approx_p100) { double w = 0; benchmark_go([&]() { double a; double b; double c; try_disjoint_to_independent_xyz_errors_approx(0.01, 0.02, 0.0, &a, &b, &c); w += a; w += b; w += c; }).goal_nanos(550); if (w == 0) { std::cout << "data dependence"; } } BENCHMARK(independent_to_disjoint_xyz_errors) { double w = 0; benchmark_go([&]() { double a; double b; double c; independent_to_disjoint_xyz_errors(0.1, 0.2, 0.3, &a, &b, &c); w += a; w += b; w += c; }).goal_nanos(4); if (w == 0) { std::cout << "data dependence"; } } ================================================ FILE: src/stim/util_bot/error_decomp.test.cc ================================================ #include "stim/util_bot/error_decomp.h" #include "gtest/gtest.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(conversions, independent_to_disjoint_xyz_errors) { double out_x; double out_y; double out_z; independent_to_disjoint_xyz_errors(0.5, 0.5, 0.5, &out_x, &out_y, &out_z); ASSERT_NEAR(out_x, 1 / 4.0, 1e-6); ASSERT_NEAR(out_y, 1 / 4.0, 1e-6); ASSERT_NEAR(out_z, 1 / 4.0, 1e-6); independent_to_disjoint_xyz_errors(0.1, 0, 0, &out_x, &out_y, &out_z); ASSERT_NEAR(out_x, 0.1, 1e-6); ASSERT_NEAR(out_y, 0, 1e-6); ASSERT_NEAR(out_z, 0, 1e-6); independent_to_disjoint_xyz_errors(0, 0.2, 0, &out_x, &out_y, &out_z); ASSERT_NEAR(out_x, 0, 1e-6); ASSERT_NEAR(out_y, 0.2, 1e-6); ASSERT_NEAR(out_z, 0, 1e-6); independent_to_disjoint_xyz_errors(0, 0, 0.05, &out_x, &out_y, &out_z); ASSERT_NEAR(out_x, 0, 1e-6); ASSERT_NEAR(out_y, 0, 1e-6); ASSERT_NEAR(out_z, 0.05, 1e-6); independent_to_disjoint_xyz_errors(0.1, 0.1, 0, &out_x, &out_y, &out_z); ASSERT_NEAR(out_x, 0.1 - 0.01, 1e-6); ASSERT_NEAR(out_y, 0.1 - 0.01, 1e-6); ASSERT_NEAR(out_z, 0.01, 1e-6); } TEST(conversions, disjoint_to_independent_xyz_errors_approx) { double out_x; double out_y; double out_z; ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0.4, 0.0, 0.0, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0.4, 1e-6); ASSERT_NEAR(out_y, 0.0, 1e-6); ASSERT_NEAR(out_z, 0.0, 1e-6); ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0.5, 0.0, 0.0, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0.5, 1e-6); ASSERT_NEAR(out_y, 0.0, 1e-6); ASSERT_NEAR(out_z, 0.0, 1e-6); ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0.6, 0.0, 0.0, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0.6, 1e-6); ASSERT_NEAR(out_y, 0.0, 1e-6); ASSERT_NEAR(out_z, 0.0, 1e-6); ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0.25, 0.25, 0.25, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0.5, 1e-6); ASSERT_NEAR(out_y, 0.5, 1e-6); ASSERT_NEAR(out_z, 0.5, 1e-6); ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0.1, 0, 0, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0.1, 1e-6); ASSERT_NEAR(out_y, 0, 1e-6); ASSERT_NEAR(out_z, 0, 1e-6); ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0, 0.2, 0, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0, 1e-6); ASSERT_NEAR(out_y, 0.2, 1e-6); ASSERT_NEAR(out_z, 0, 1e-6); ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0, 0, 0.05, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0, 1e-6); ASSERT_NEAR(out_y, 0, 1e-6); ASSERT_NEAR(out_z, 0.05, 1e-6); ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0.1 - 0.01, 0.1 - 0.01, 0.01, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0.1, 1e-6); ASSERT_NEAR(out_y, 0.1, 1e-6); ASSERT_NEAR(out_z, 0, 1e-6); ASSERT_FALSE(try_disjoint_to_independent_xyz_errors_approx(0.2, 0.2, 0, &out_x, &out_y, &out_z)); independent_to_disjoint_xyz_errors(out_x, out_y, out_z, &out_x, &out_y, &out_z); ASSERT_NEAR(out_x, 0.2, 1e-6); ASSERT_NEAR(out_y, 0.2, 1e-6); ASSERT_LT(out_z, 0.08); ASSERT_FALSE(try_disjoint_to_independent_xyz_errors_approx(0.2, 0.1, 0, &out_x, &out_y, &out_z)); independent_to_disjoint_xyz_errors(out_x, out_y, out_z, &out_x, &out_y, &out_z); ASSERT_NEAR(out_x, 0.2, 1e-6); ASSERT_NEAR(out_y, 0.1, 1e-6); ASSERT_LT(out_z, 0.03); ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(0.3 * 0.6, 0.4 * 0.7, 0.3 * 0.4, &out_x, &out_y, &out_z)); ASSERT_NEAR(out_x, 0.3, 1e-6); ASSERT_NEAR(out_y, 0.4, 1e-6); ASSERT_NEAR(out_z, 0.0, 1e-6); independent_to_disjoint_xyz_errors(out_x, out_y, out_z, &out_x, &out_y, &out_z); ASSERT_NEAR(out_x, 0.3 * 0.6, 1e-6); ASSERT_NEAR(out_y, 0.4 * 0.7, 1e-6); ASSERT_NEAR(out_z, 0.3 * 0.4, 1e-6); } TEST(conversions, fuzz_depolarize1_consistency) { auto rng = INDEPENDENT_TEST_RNG(); std::uniform_real_distribution dis(0, 1); for (size_t k = 0; k < 10; k++) { double p = dis(rng) * 0.75; double p2 = depolarize1_probability_to_independent_per_channel_probability(p); double x2, y2, z2; ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(p / 3, p / 3, p / 3, &x2, &y2, &z2)); ASSERT_NEAR(x2, p2, 1e-6) << "p=" << p; ASSERT_NEAR(y2, p2, 1e-6) << "p=" << p; ASSERT_NEAR(z2, p2, 1e-6) << "p=" << p; double p3 = independent_per_channel_probability_to_depolarize1_probability(p2); double x3, y3, z3; independent_to_disjoint_xyz_errors(x2, y2, z2, &x3, &y3, &z3); ASSERT_NEAR(x3, p / 3, 1e-6) << "p=" << p; ASSERT_NEAR(y3, p / 3, 1e-6) << "p=" << p; ASSERT_NEAR(z3, p / 3, 1e-6) << "p=" << p; ASSERT_NEAR(p3, p, 1e-6) << "p=" << p; } } TEST(conversions, fuzz_depolarize2_consistency) { auto rng = INDEPENDENT_TEST_RNG(); std::uniform_real_distribution dis(0, 1); for (size_t k = 0; k < 10; k++) { double p = dis(rng) * 0.75; double p2 = depolarize2_probability_to_independent_per_channel_probability(p); double p3 = independent_per_channel_probability_to_depolarize2_probability(p2); ASSERT_NEAR(p3, p, 1e-6) << "p=" << p; } } TEST(conversions, independent_vs_disjoint_xyz_errors_round_trip_fuzz) { auto rng = INDEPENDENT_TEST_RNG(); std::uniform_real_distribution dis(0, 1); for (size_t k = 0; k < 25; k++) { double x; double y; double z; do { x = dis(rng); y = dis(rng); z = dis(rng); } while (x + y + z >= 0.999); double x2, y2, z2; independent_to_disjoint_xyz_errors(x, y, z, &x2, &y2, &z2); double x3, y3, z3; ASSERT_TRUE(try_disjoint_to_independent_xyz_errors_approx(x2, y2, z2, &x3, &y3, &z3)); ASSERT_NEAR(x, x3, 1e-6) << "x=" << x << ",y=" << y << ",z=" << z; ASSERT_NEAR(y, y3, 1e-6) << "x=" << x << ",y=" << y << ",z=" << z; ASSERT_NEAR(z, z3, 1e-6) << "x=" << x << ",y=" << y << ",z=" << z; } } ================================================ FILE: src/stim/util_bot/probability_util.cc ================================================ // 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. #include "stim/util_bot/probability_util.h" #include #include "stim/util_bot/arg_parse.h" using namespace stim; RareErrorIterator::RareErrorIterator(float probability) : next_candidate(0), is_one(probability == 1), dist(probability) { if (!(probability >= 0 && probability <= 1)) { throw std::out_of_range("Invalid probability: " + std::to_string(probability)); } } size_t RareErrorIterator::next(std::mt19937_64 &rng) { size_t result = next_candidate + (is_one ? 0 : dist(rng)); next_candidate = result + 1; return result; } std::vector stim::sample_hit_indices(float probability, size_t attempts, std::mt19937_64 &rng) { std::vector result; RareErrorIterator::for_samples(probability, attempts, rng, [&](size_t s) { result.push_back(s); }); return result; } std::mt19937_64 stim::externally_seeded_rng() { #if defined(__linux) && defined(__GLIBCXX__) && __GLIBCXX__ >= 20200128 // Workaround for https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94087 // See https://github.com/quantumlib/Stim/issues/26 std::random_device d("/dev/urandom"); #else std::random_device d; #endif std::seed_seq seq{d(), d(), d(), d(), d(), d(), d(), d()}; std::mt19937_64 result(seq); return result; } std::mt19937_64 stim::optionally_seeded_rng(int argc, const char **argv) { if (find_argument("--seed", argc, argv) == nullptr) { return externally_seeded_rng(); } uint64_t seed = (uint64_t)find_int64_argument("--seed", 0, 0, INT64_MAX, argc, argv); return std::mt19937_64(seed ^ INTENTIONAL_VERSION_SEED_INCOMPATIBILITY); } void stim::biased_randomize_bits(float probability, uint64_t *start, uint64_t *end, std::mt19937_64 &rng) { if (probability > 0.5) { // Recurse and invert for probabilities larger than 0.5. biased_randomize_bits(1 - probability, start, end, rng); while (start != end) { *start ^= UINT64_MAX; start++; } } else if (probability == 0.5) { // For the 50/50 case, just copy the bits directly into the buffer. while (start != end) { *start = rng(); start++; } } else if (probability < 0.02) { // For small probabilities, sample gaps using a geometric distribution. size_t n = (end - start) << 6; memset(start, 0, n >> 3); RareErrorIterator::for_samples(probability, n, rng, [&](size_t s) { start[s >> 6] |= uint64_t{1} << (s & 63); }); } else { // To generate a bit with probability p < 1, you can flip a coin until (after k flips) you // get heads, and then return the k'th fractional bit in the binary representation of p. // // Alternatively, you can flip a coin at most N times until you either fail to get any heads // or you get your first heads after k flips. If you got a heads, return the k'th fractional // bit in the binary representation of p. Account for the leftover probability (from the // bits that couldn't be reached due to clamping the coin flip count) by OR'ing with a low // entropy bit that's been generated with just the right probability to fix the difference. constexpr size_t COIN_FLIPS = 8; constexpr float BUCKETS = (float)(1 << COIN_FLIPS); float raised = probability * BUCKETS; float raised_floor = floorf(raised); float raised_leftover = raised - raised_floor; float p_truncated = raised_floor / BUCKETS; float p_leftover = raised_leftover / BUCKETS; uint64_t p_top_bits = (uint64_t)raised_floor; // Flip coins, using the position of the first HEADS result to // select a bit from the probability's binary representation. for (uint64_t *cur = start; cur != end; cur++) { uint64_t alive = rng(); uint64_t result = 0; for (size_t k_bit = COIN_FLIPS - 1; k_bit--;) { uint64_t shoot = rng(); result ^= shoot & alive & -((p_top_bits >> k_bit) & 1); alive &= ~shoot; } *cur = result; } // Correct distortion from truncation. size_t n = (end - start) << 6; RareErrorIterator::for_samples(p_leftover / (1 - p_truncated), n, rng, [&](size_t s) { start[s >> 6] |= uint64_t{1} << (s & 63); }); } } ================================================ FILE: src/stim/util_bot/probability_util.h ================================================ /* * 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. */ #ifndef _STIM_UTIL_BOT_PROBABILITY_UTIL_H #define _STIM_UTIL_BOT_PROBABILITY_UTIL_H #include #include #include #include "stim/mem/span_ref.h" namespace stim { // Change this number from time to time to ensure people don't rely on seeds across versions. constexpr uint64_t INTENTIONAL_VERSION_SEED_INCOMPATIBILITY = 0xDEADBEEF124CULL; /// Yields the indices of hits sampled from a Bernoulli distribution. /// Gets more efficient as the hit probability drops. struct RareErrorIterator { size_t next_candidate; bool is_one = false; std::geometric_distribution dist; RareErrorIterator(float probability); size_t next(std::mt19937_64 &rng); template inline static void for_samples(double p, size_t n, std::mt19937_64 &rng, BODY body) { if (p == 0) { return; } RareErrorIterator skipper((float)p); while (true) { size_t s = skipper.next(rng); if (s >= n) { break; } body(s); } } template inline static void for_samples(double p, const SpanRef &vals, std::mt19937_64 &rng, BODY body) { if (p == 0) { return; } RareErrorIterator skipper((float)p); while (true) { size_t s = skipper.next(rng); if (s >= vals.size()) { break; } body(vals[s]); } } }; std::vector sample_hit_indices(float probability, size_t attempts, std::mt19937_64 &rng); /// Create a fresh random number generator seeded by entropy from the operating system. std::mt19937_64 externally_seeded_rng(); /// Create a random number generator either seeded by a --seed argument, or else by entropy from the operating system. std::mt19937_64 optionally_seeded_rng(int argc, const char **argv); /// Overwrite the given span with random data where bits are set with the given probability. /// /// Args: /// probability: The chance that each bit will be on. /// start: Inclusive start of the memory span to overwrite. /// end: Exclusive end of the memory span to overwrite. /// rng: The random number generator to use to generate entropy. void biased_randomize_bits(float probability, uint64_t *start, uint64_t *end, std::mt19937_64 &rng); } // namespace stim #endif ================================================ FILE: src/stim/util_bot/probability_util.perf.cc ================================================ // 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. #include "stim/util_bot/probability_util.h" #include "stim/mem/simd_bits.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(biased_random_1024_0point1percent) { std::mt19937_64 rng(0); float p = 0.001; size_t n = 1024; simd_bits data(n); benchmark_go([&]() { biased_randomize_bits(p, data.u64, data.u64 + data.num_u64_padded(), rng); }) .goal_nanos(70) .show_rate("bits", n); } BENCHMARK(biased_random_1024_0point01percent) { std::mt19937_64 rng(0); float p = 0.0001; size_t n = 1024; simd_bits data(n); benchmark_go([&]() { biased_randomize_bits(p, data.u64, data.u64 + data.num_u64_padded(), rng); }) .goal_nanos(35) .show_rate("bits", n); } BENCHMARK(biased_random_1024_1percent) { std::mt19937_64 rng(0); float p = 0.01; size_t n = 1024; simd_bits data(n); benchmark_go([&]() { biased_randomize_bits(p, data.u64, data.u64 + data.num_u64_padded(), rng); }) .goal_nanos(250) .show_rate("bits", n); } BENCHMARK(biased_random_1024_40percent) { std::mt19937_64 rng(0); float p = 0.4; size_t n = 1024; simd_bits data(n); benchmark_go([&]() { biased_randomize_bits(p, data.u64, data.u64 + data.num_u64_padded(), rng); }) .goal_nanos(420) .show_rate("bits", n); } BENCHMARK(biased_random_1024_50percent) { std::mt19937_64 rng(0); float p = 0.5; size_t n = 1024; simd_bits data(n); benchmark_go([&]() { biased_randomize_bits(p, data.u64, data.u64 + data.num_u64_padded(), rng); }) .goal_nanos(40) .show_rate("bits", n); } BENCHMARK(biased_random_1024_90percent) { std::mt19937_64 rng(0); float p = 0.9; size_t n = 1024; simd_bits data(n); benchmark_go([&]() { biased_randomize_bits(p, data.u64, data.u64 + data.num_u64_padded(), rng); }) .goal_nanos(450) .show_rate("bits", n); } BENCHMARK(biased_random_1024_99percent) { std::mt19937_64 rng(0); float p = 0.99; size_t n = 1024; simd_bits data(n); benchmark_go([&]() { biased_randomize_bits(p, data.u64, data.u64 + data.num_u64_padded(), rng); }) .goal_nanos(260) .show_rate("bits", n); } ================================================ FILE: src/stim/util_bot/probability_util.test.cc ================================================ // 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. #include "stim/util_bot/probability_util.h" #include "gtest/gtest.h" #include "stim/mem/simd_bits.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(probability_util, sample_hit_indices_corner_cases) { auto rng = INDEPENDENT_TEST_RNG(); ASSERT_EQ(sample_hit_indices(0, 100000, rng), (std::vector{})); ASSERT_EQ(sample_hit_indices(1, 10, rng), (std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})); } TEST(probability_util, sample_hit_indices) { auto rng = INDEPENDENT_TEST_RNG(); size_t num_buckets = 10000; size_t num_samples = 100000; double p = 0.001; std::vector buckets(num_buckets, 0); for (size_t k = 0; k < num_samples; k++) { for (auto bucket : sample_hit_indices(p, num_buckets, rng)) { buckets[bucket] += 1; } } size_t total = 0; for (auto b : buckets) { total += b; } ASSERT_TRUE(abs(total / (double)(num_buckets * num_samples) - p) <= 0.0001); for (auto b : buckets) { ASSERT_TRUE(abs(b / (double)num_samples - p) <= 0.01); } } TEST_EACH_WORD_SIZE_W(probability_util, biased_random, { auto rng = INDEPENDENT_TEST_RNG(); std::vector probs{0, 0.01, 0.03, 0.1, 0.4, 0.49, 0.5, 0.6, 0.9, 0.99, 0.999, 1}; simd_bits data(1000000); size_t n = data.num_bits_padded(); for (auto p : probs) { biased_randomize_bits(p, data.u64, data.u64 + data.num_u64_padded(), rng); size_t t = 0; for (size_t k = 0; k < data.num_u64_padded(); k++) { t += std::popcount(data.u64[k]); } float dev = sqrtf(p * (1 - p) * n); float min_expected = n * p - dev * 5; float max_expected = n * p + dev * 5; ASSERT_TRUE(min_expected >= 0 && max_expected <= n) << min_expected << ", " << max_expected; EXPECT_TRUE(min_expected <= t && t <= max_expected) << min_expected / n << " < " << t / (float)n << " < " << max_expected / n << " for p=" << p; } }) ================================================ FILE: src/stim/util_bot/str_util.h ================================================ /* * 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. */ #ifndef _STIM_STR_UTIL_H #define _STIM_STR_UTIL_H #include #include #include namespace stim { /// Wraps an iterable object so that its values are printed with comma separators. template struct CommaSep; /// A wrapper indicating a range of values should be printed with comma separators. template struct CommaSep { const TIter &iter; const char *sep; std::string str() const { std::stringstream out; out << *this; return out.str(); } }; template CommaSep comma_sep(const TIter &v, const char *sep = ", ") { return CommaSep{v, sep}; } template std::ostream &operator<<(std::ostream &out, const CommaSep &v) { bool first = true; for (const auto &t : v.iter) { if (first) { first = false; } else { out << v.sep; } out << t; } return out; } } // namespace stim #endif ================================================ FILE: src/stim/util_bot/str_util.test.cc ================================================ // 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. #include "stim/util_bot/str_util.h" #include "gtest/gtest.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(str_util, comma_sep) { std::vector v{1, 2, 3}; std::stringstream out; out << comma_sep(v); ASSERT_EQ(out.str(), "1, 2, 3"); ASSERT_EQ(comma_sep(v).str(), "1, 2, 3"); ASSERT_EQ(comma_sep(std::vector{}).str(), ""); ASSERT_EQ(comma_sep(std::vector{4}).str(), "4"); ASSERT_EQ(comma_sep(std::vector{5, 6}).str(), "5, 6"); } ================================================ FILE: src/stim/util_bot/test_util.test.cc ================================================ #include "stim/util_bot/test_util.test.h" #include #include #include "stim/util_bot/probability_util.h" using namespace stim; static bool shared_test_rng_initialized; static std::mt19937_64 shared_test_rng; std::string resolve_test_file(std::string_view name) { std::vector prefixes{ "testdata/", "../testdata/", }; for (const auto &prefix : prefixes) { std::string full_path = prefix + std::string(name); FILE *f = fopen(full_path.c_str(), "rb"); if (f != nullptr) { fclose(f); return full_path; } } for (const auto &prefix : prefixes) { std::string full_path = prefix + std::string(name); FILE *f = fopen(full_path.c_str(), "wb"); if (f != nullptr) { fclose(f); return full_path; } } throw std::invalid_argument("Run tests from the repo root so they can find the testdata/ directory."); } void expect_string_is_identical_to_saved_file(std::string_view actual, std::string_view key) { auto path = resolve_test_file(key); FILE *f = fopen(path.c_str(), "rb"); auto expected = rewind_read_close(f); if (expected != actual) { auto dot = key.rfind('.'); std::string new_path; if (dot == std::string::npos) { new_path = path + ".new"; } else { dot += path.size() - key.size(); new_path = path.substr(0, dot) + ".new" + path.substr(dot); } std::ofstream out; out.open(new_path); out << actual; out.close(); EXPECT_TRUE(false) << "Diagram didn't agree.\n" << " key=" << key << "\n" << " expected: file://" << std::filesystem::weakly_canonical(std::filesystem::path(path)).c_str() << "\n" << " actual: file://" << std::filesystem::weakly_canonical(std::filesystem::path(new_path)).c_str() << "\n"; } } std::mt19937_64 INDEPENDENT_TEST_RNG() { if (!shared_test_rng_initialized) { shared_test_rng = externally_seeded_rng(); shared_test_rng_initialized = true; } std::seed_seq seq{shared_test_rng(), shared_test_rng(), shared_test_rng(), shared_test_rng()}; return std::mt19937_64(seq); } std::string rewind_read_close(FILE *f) { rewind(f); std::string result; while (true) { int c = getc(f); if (c == EOF) { fclose(f); return result; } result.push_back((char)c); } } static void init_path(RaiiTempNamedFile &self) { char tmp_stdin_filename[] = "/tmp/stim_test_named_file_XXXXXX"; self.descriptor = mkstemp(tmp_stdin_filename); if (self.descriptor == -1) { throw std::runtime_error("Failed to create temporary file."); } self.path = std::string(tmp_stdin_filename); } RaiiTempNamedFile::RaiiTempNamedFile() { init_path(*this); } RaiiTempNamedFile::RaiiTempNamedFile(std::string_view contents) { init_path(*this); write_contents(contents); } RaiiTempNamedFile::~RaiiTempNamedFile() { if (!path.empty()) { remove(path.c_str()); path = ""; } } std::string RaiiTempNamedFile::read_contents() { FILE *f = fopen(path.c_str(), "rb"); if (f == nullptr) { throw std::runtime_error("Failed to open temp named file " + path); } std::string result; while (true) { int c = getc(f); if (c == EOF) { break; } result.push_back(c); } fclose(f); return result; } void RaiiTempNamedFile::write_contents(std::string_view contents) { FILE *f = fopen(path.c_str(), "wb"); if (f == nullptr) { throw std::runtime_error("Failed to open temp named file " + path); } for (char c : contents) { putc(c, f); } fclose(f); } ================================================ FILE: src/stim/util_bot/test_util.test.h ================================================ /* * 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. */ #ifndef _STIM_TEST_UTIL_TEST_H #define _STIM_TEST_UTIL_TEST_H #include #include "gtest/gtest.h" std::mt19937_64 INDEPENDENT_TEST_RNG(); std::string rewind_read_close(FILE *f); std::string resolve_test_file(std::string_view name); void expect_string_is_identical_to_saved_file(std::string_view actual, std::string_view key); struct RaiiTempNamedFile { int descriptor; std::string path; RaiiTempNamedFile(); ~RaiiTempNamedFile(); RaiiTempNamedFile(std::string_view contents); std::string read_contents(); void write_contents(std::string_view contents); }; #endif ================================================ FILE: src/stim/util_bot/twiddle.h ================================================ /* * 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. */ #ifndef _STIM_UTIL_BOT_TWIDDLE_H #define _STIM_UTIL_BOT_TWIDDLE_H #include #include #include #include namespace stim { inline uint8_t is_power_of_2(size_t value) { // Note: would use std::has_single_bit here, but as of March 2024 that method is missing when building with // emscripten. return std::popcount(value) == 1; } inline uint8_t floor_lg2(size_t value) { return sizeof(value) * 8 - 1 - std::countl_zero(value); } inline size_t first_set_bit(size_t value, size_t min_result) { value >>= min_result; if (!value) { throw std::invalid_argument("No matching set bit."); } return std::countr_zero(value) + min_result; } } // namespace stim #endif ================================================ FILE: src/stim/util_bot/twiddle.test.cc ================================================ // 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. #include "stim/util_bot/twiddle.h" #include #include "gtest/gtest.h" using namespace stim; TEST(twiddle, is_power_of_2) { ASSERT_EQ(is_power_of_2(0), 0); ASSERT_EQ(is_power_of_2(1), 1); ASSERT_EQ(is_power_of_2(2), 1); ASSERT_EQ(is_power_of_2(3), 0); ASSERT_EQ(is_power_of_2(4), 1); ASSERT_EQ(is_power_of_2(5), 0); ASSERT_EQ(is_power_of_2(6), 0); ASSERT_EQ(is_power_of_2(7), 0); ASSERT_EQ(is_power_of_2(8), 1); ASSERT_EQ(is_power_of_2(9), 0); } TEST(twiddle, floor_lg2) { ASSERT_EQ(floor_lg2(1), 0); ASSERT_EQ(floor_lg2(2), 1); ASSERT_EQ(floor_lg2(3), 1); ASSERT_EQ(floor_lg2(4), 2); ASSERT_EQ(floor_lg2(5), 2); ASSERT_EQ(floor_lg2(6), 2); ASSERT_EQ(floor_lg2(7), 2); ASSERT_EQ(floor_lg2(8), 3); ASSERT_EQ(floor_lg2(9), 3); } TEST(twiddle, first_set_bit) { ASSERT_EQ(first_set_bit(0b0000111001000, 0), 3); ASSERT_EQ(first_set_bit(0b0000111001000, 1), 3); ASSERT_EQ(first_set_bit(0b0000111001000, 2), 3); ASSERT_EQ(first_set_bit(0b0000111001000, 3), 3); ASSERT_EQ(first_set_bit(0b0000111001000, 4), 6); ASSERT_EQ(first_set_bit(0b0000111001000, 5), 6); ASSERT_EQ(first_set_bit(0b0000111001000, 6), 6); ASSERT_EQ(first_set_bit(0b0000111001000, 7), 7); ASSERT_EQ(first_set_bit(0b0000111001000, 8), 8); ASSERT_EQ(first_set_bit(0b0000111001001, 0), 0); ASSERT_EQ(first_set_bit(1 << 20, 0), 20); } ================================================ FILE: src/stim/util_top/circuit_flow_generators.h ================================================ #ifndef _STIM_UTIL_TOP_CIRCUIT_FLOW_GENERATORS_H #define _STIM_UTIL_TOP_CIRCUIT_FLOW_GENERATORS_H #include #include "stim/circuit/circuit.h" #include "stim/mem/simd_bits.h" namespace stim { /// Finds a basis of flows for the circuit. template std::vector> circuit_flow_generators(const Circuit &circuit); /// Finds measurements that explain each given flow for the circuit. /// /// Args: /// flows: A list of flows to solve measurements for. /// /// Returns: /// A list of results, one for each flow in the flows argument. /// When no solution exists for the circuit for a flow, its result is the empty optional. /// Otherwise a flow's result is a list of measurement indices. template std::vector>> solve_for_flow_measurements( const Circuit &circuit, std::span> flows); template struct CircuitFlowGeneratorSolver { std::vector> table; simd_bits imag_bits; size_t num_qubits; size_t num_measurements; size_t num_measurements_in_past; std::vector> measurements_only_table; std::vector buf_for_rows_with; std::vector buf_for_xor_merge; std::vector buf_targets; std::vector buf_targets_2; explicit CircuitFlowGeneratorSolver(CircuitStats stats); static CircuitFlowGeneratorSolver solver_with_circuit_generators( const Circuit &circuit, uint32_t min_num_qubits); Flow &add_row(); void add_1q_measure_terms(CircuitInstruction inst, bool x, bool z); void add_2q_measure_terms(CircuitInstruction inst, bool x, bool z); void remove_single_qubit_reset_terms(CircuitInstruction inst); void handle_anticommutations(std::span anticommutation_set); void check_for_2q_anticommutations(CircuitInstruction inst, bool x, bool z); void check_for_1q_anticommutations(CircuitInstruction inst, bool x, bool z); void mult_row_into(size_t src_row, size_t dst_row); void undo_mrb(CircuitInstruction inst, bool x, bool z); void undo_mb(CircuitInstruction inst, bool x, bool z); void undo_rb(CircuitInstruction inst, bool x, bool z); void undo_2q_m(CircuitInstruction inst, bool x, bool z); void undo_instruction(CircuitInstruction inst); void elimination_step(std::span elimination_set, size_t &num_eliminated, size_t num_available_rows); void eliminate_input_xz_terms(size_t &num_eliminated, size_t num_available_rows); void eliminate_output_xz_terms(size_t &num_eliminated, size_t num_available_rows); void eliminate_measurement_terms(size_t &num_eliminated, size_t num_available_rows); void canonicalize_over_qubits(size_t num_available_rows); void final_canonicalize_into_table(); void undo_feedback_capable_instruction(CircuitInstruction inst, bool x, bool z); std::span rows_anticommuting_with(uint32_t q, bool x, bool z); template std::span rows_with(PREDICATE predicate); }; } // namespace stim #include "stim/util_top/circuit_flow_generators.inl" #endif ================================================ FILE: src/stim/util_top/circuit_flow_generators.inl ================================================ #include "stim/util_top/circuit_inverse_qec.h" namespace stim { template CircuitFlowGeneratorSolver::CircuitFlowGeneratorSolver(CircuitStats stats) : table(), imag_bits(stats.num_qubits), num_qubits(stats.num_qubits), num_measurements(stats.num_measurements), num_measurements_in_past(stats.num_measurements), measurements_only_table(), buf_for_rows_with(), buf_for_xor_merge() { if (num_measurements_in_past > INT32_MAX) { throw std::invalid_argument("Circuit is too large. Max flow measurement index is " + std::to_string(INT32_MAX)); } } template Flow &CircuitFlowGeneratorSolver::add_row() { table.push_back( Flow{ .input = PauliString(num_qubits), .output = PauliString(num_qubits), .measurements = {}, }); return table.back(); } template void CircuitFlowGeneratorSolver::add_1q_measure_terms(CircuitInstruction inst, bool x, bool z) { for (size_t k = inst.targets.size(); k--;) { num_measurements_in_past--; auto t = inst.targets[k]; if (!t.is_qubit_target()) { throw std::invalid_argument("Bad target in " + inst.str()); } uint32_t q = t.qubit_value(); auto &row = add_row(); row.measurements.push_back(num_measurements_in_past); row.input.xs[q] = x; row.input.zs[q] = z; row.input.sign ^= t.is_inverted_result_target(); } } template void CircuitFlowGeneratorSolver::add_2q_measure_terms(CircuitInstruction inst, bool x, bool z) { size_t k = inst.targets.size(); while (k > 0) { k -= 2; num_measurements_in_past--; auto t1 = inst.targets[k]; auto t2 = inst.targets[k + 1]; if (!t1.is_qubit_target() || !t2.is_qubit_target()) { throw std::invalid_argument("Bad target in " + inst.str()); } uint32_t q1 = t1.qubit_value(); uint32_t q2 = t2.qubit_value(); auto &row = add_row(); row.measurements.push_back(num_measurements_in_past); row.input.xs[q1] = x; row.input.zs[q1] = z; row.input.xs[q2] = x; row.input.zs[q2] = z; row.input.sign ^= t1.is_inverted_result_target(); row.input.sign ^= t2.is_inverted_result_target(); } } template void CircuitFlowGeneratorSolver::remove_single_qubit_reset_terms(CircuitInstruction inst) { for (auto t : inst.targets) { if (!t.is_qubit_target()) { throw std::invalid_argument("Bad target in " + inst.str()); } uint32_t q = t.qubit_value(); for (auto &row : table) { row.input.xs[q] = 0; row.input.zs[q] = 0; } } } template void CircuitFlowGeneratorSolver::handle_anticommutations(std::span anticommutation_set) { if (anticommutation_set.empty()) { return; } // Sacrifice the first anticommutation to save the others. for (size_t k = 1; k < buf_for_rows_with.size(); k++) { mult_row_into(anticommutation_set[0], anticommutation_set[k]); } table.erase(table.begin() + anticommutation_set[0]); } template void CircuitFlowGeneratorSolver::check_for_2q_anticommutations(CircuitInstruction inst, bool x, bool z) { size_t k = inst.targets.size(); while (k > 0) { k -= 2; auto t1 = inst.targets[k]; auto t2 = inst.targets[k + 1]; if (!t1.is_qubit_target() || !t2.is_qubit_target()) { throw std::invalid_argument("Bad target in " + inst.str()); } uint32_t q1 = t1.qubit_value(); uint32_t q2 = t2.qubit_value(); auto anticommutations = rows_with([&](const Flow &flow) { bool anticommutes = false; anticommutes ^= flow.input.xs[q1] & z; anticommutes ^= flow.input.zs[q1] & x; anticommutes ^= flow.input.xs[q2] & z; anticommutes ^= flow.input.zs[q2] & x; return anticommutes; }); handle_anticommutations(anticommutations); } } template void CircuitFlowGeneratorSolver::check_for_1q_anticommutations(CircuitInstruction inst, bool x, bool z) { for (auto t : inst.targets) { if (!t.is_qubit_target()) { throw std::invalid_argument("Bad target in " + inst.str()); } uint32_t q = t.qubit_value(); handle_anticommutations(rows_anticommuting_with(q, x, z)); } } template void CircuitFlowGeneratorSolver::mult_row_into(size_t src_row, size_t dst_row) { auto &src = table[src_row]; auto &dst = table[dst_row]; // Combine the pauli strings. uint8_t log_i = 0; log_i += dst.input.ref().inplace_right_mul_returning_log_i_scalar(src.input); log_i -= dst.output.ref().inplace_right_mul_returning_log_i_scalar(src.output); if (log_i & 1) { imag_bits[dst_row] ^= 1; } if (log_i & 2) { dst.input.sign ^= 1; } // Xor-merge-sort the measurement indices. buf_for_xor_merge.resize(std::max(buf_for_xor_merge.size(), dst.measurements.size() + src.measurements.size() + 1)); const int32_t *end = xor_merge_sort( SpanRef(dst.measurements), SpanRef(src.measurements), buf_for_xor_merge.data()); size_t n = end - buf_for_xor_merge.data(); dst.measurements.resize(n); if (n > 0) { memcpy(dst.measurements.data(), buf_for_xor_merge.data(), n * sizeof(int32_t)); } } template void CircuitFlowGeneratorSolver::undo_mrb(CircuitInstruction inst, bool x, bool z) { check_for_1q_anticommutations(inst, x, z); remove_single_qubit_reset_terms(inst); add_1q_measure_terms(inst, x, z); } template void CircuitFlowGeneratorSolver::undo_mb(CircuitInstruction inst, bool x, bool z) { check_for_1q_anticommutations(inst, x, z); add_1q_measure_terms(inst, x, z); } template void CircuitFlowGeneratorSolver::undo_rb(CircuitInstruction inst, bool x, bool z) { check_for_1q_anticommutations(inst, x, z); remove_single_qubit_reset_terms(inst); } template void CircuitFlowGeneratorSolver::undo_2q_m(CircuitInstruction inst, bool x, bool z) { check_for_2q_anticommutations(inst, x, z); add_2q_measure_terms(inst, x, z); } template void CircuitFlowGeneratorSolver::undo_feedback_capable_instruction(CircuitInstruction inst, bool x, bool z) { size_t k = inst.targets.size(); while (k > 0) { k -= 2; auto t1 = inst.targets[k]; auto t2 = inst.targets[k + 1]; bool m1 = t1.is_measurement_record_target(); bool m2 = t2.is_measurement_record_target(); bool f1 = t1.is_qubit_target(); bool f2 = t2.is_qubit_target(); if ((m1 && f2) || (m2 && f1)) { uint32_t q = f1 ? t1.qubit_value() : t2.qubit_value(); int32_t t = (m1 ? t1.value() : t2.value()) + num_measurements_in_past; if (t < 0) { throw std::invalid_argument("Referred to measurement before start of time in " + inst.str()); } for (auto r : rows_anticommuting_with(q, x, z)) { xor_item_into_sorted_vec(t, table[r].measurements); } } else if (f1 && f2) { CircuitInstruction sub_inst = CircuitInstruction{inst.gate_type, {}, inst.targets.sub(k, k + 2), inst.tag}; for (auto &row : table) { row.input.ref().undo_instruction(sub_inst); } } } } template void CircuitFlowGeneratorSolver::undo_instruction(CircuitInstruction inst) { if (table.size() > num_qubits * 3) { canonicalize_over_qubits(table.size()); } switch (inst.gate_type) { case GateType::MRX: case GateType::MRY: case GateType::MR: undo_mrb(inst, inst.gate_type != GateType::MR, inst.gate_type != GateType::MRX); break; case GateType::MX: case GateType::MY: case GateType::M: undo_mb(inst, inst.gate_type != GateType::M, inst.gate_type != GateType::MX); break; case GateType::RX: case GateType::RY: case GateType::R: undo_rb(inst, inst.gate_type != GateType::R, inst.gate_type != GateType::RX); break; case GateType::MXX: case GateType::MYY: case GateType::MZZ: undo_2q_m(inst, inst.gate_type != GateType::MZZ, inst.gate_type != GateType::MXX); break; case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::PAULI_CHANNEL_1: case GateType::PAULI_CHANNEL_2: case GateType::E: case GateType::ELSE_CORRELATED_ERROR: // Ignored. break; case GateType::HERALDED_ERASE: case GateType::HERALDED_PAULI_CHANNEL_1: // Heralds. for (auto t : inst.targets) { num_measurements_in_past--; if (!t.is_qubit_target()) { throw std::invalid_argument("Bad target in " + inst.str()); } auto &row = add_row(); row.measurements.push_back(num_measurements_in_past); } break; case GateType::MPAD: // Pads. for (auto t : inst.targets) { num_measurements_in_past--; if (!t.is_qubit_target()) { throw std::invalid_argument("Bad target in " + inst.str()); } auto &row = add_row(); row.measurements.push_back(num_measurements_in_past); if (t.qubit_value()) { row.output.sign = 1; } } break; case GateType::CX: case GateType::XCZ: undo_feedback_capable_instruction(inst, true, false); break; case GateType::YCZ: case GateType::CY: undo_feedback_capable_instruction(inst, true, true); break; case GateType::CZ: undo_feedback_capable_instruction(inst, false, true); break; case GateType::XCX: case GateType::XCY: case GateType::YCX: case GateType::YCY: case GateType::H: case GateType::H_XY: case GateType::H_YZ: case GateType::H_NXY: case GateType::H_NXZ: case GateType::H_NYZ: case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: case GateType::X: case GateType::Y: case GateType::Z: case GateType::C_XYZ: case GateType::C_NXYZ: case GateType::C_XNYZ: case GateType::C_XYNZ: case GateType::C_ZYX: case GateType::C_NZYX: case GateType::C_ZNYX: case GateType::C_ZYNX: case GateType::SQRT_X: case GateType::SQRT_X_DAG: case GateType::SQRT_Y: case GateType::SQRT_Y_DAG: case GateType::S: case GateType::S_DAG: case GateType::SQRT_XX: case GateType::SQRT_XX_DAG: case GateType::SQRT_YY: case GateType::SQRT_YY_DAG: case GateType::SQRT_ZZ: case GateType::SQRT_ZZ_DAG: case GateType::SPP: case GateType::SPP_DAG: case GateType::SWAP: case GateType::ISWAP: case GateType::CXSWAP: case GateType::SWAPCX: case GateType::CZSWAP: case GateType::ISWAP_DAG: for (auto &row : table) { row.input.ref().undo_instruction(inst); } break; case GateType::MPP: buf_targets.clear(); buf_targets.insert(buf_targets.end(), inst.targets.begin(), inst.targets.end()); std::reverse(buf_targets.begin(), buf_targets.end()); decompose_mpp_operation( CircuitInstruction{inst.gate_type, {}, buf_targets, inst.tag}, num_qubits, [&](CircuitInstruction sub_inst) { buf_targets_2.clear(); buf_targets_2.insert(buf_targets_2.end(), sub_inst.targets.begin(), sub_inst.targets.end()); if (sub_inst.gate_type == GateType::M) { std::reverse(buf_targets_2.begin(), buf_targets_2.end()); } undo_instruction(CircuitInstruction{sub_inst.gate_type, sub_inst.args, buf_targets_2, sub_inst.tag}); }); break; default: throw std::invalid_argument("Not handled by circuit flow generators method: " + inst.str()); } } template std::span CircuitFlowGeneratorSolver::rows_anticommuting_with(uint32_t q, bool x, bool z) { return rows_with([&](const Flow &flow) { bool anticommutes = false; anticommutes ^= flow.input.xs[q] & z; anticommutes ^= flow.input.zs[q] & x; return anticommutes; }); } template template std::span CircuitFlowGeneratorSolver::rows_with(PREDICATE predicate) { buf_for_rows_with.clear(); for (size_t r = 0; r < table.size(); r++) { if (predicate(table[r])) { buf_for_rows_with.push_back(r); } } return buf_for_rows_with; } template void CircuitFlowGeneratorSolver::elimination_step(std::span elimination_set, size_t &num_eliminated, size_t num_available_rows) { size_t pivot = SIZE_MAX; for (auto p : elimination_set) { if (p >= num_eliminated && p < num_available_rows) { pivot = p; break; } } if (pivot == SIZE_MAX) { return; } for (auto p : elimination_set) { if (p != pivot) { mult_row_into(pivot, p); } } std::swap(table[pivot], table[num_eliminated]); num_eliminated++; } template void CircuitFlowGeneratorSolver::canonicalize_over_qubits(size_t num_available_rows) { size_t num_eliminated = 0; for (size_t q = 0; q < num_qubits; q++) { elimination_step( rows_with([&](const Flow &flow) { return flow.input.xs[q]; }), num_eliminated, num_available_rows); elimination_step( rows_with([&](const Flow &flow) { return flow.input.zs[q]; }), num_eliminated, num_available_rows); elimination_step( rows_with([&](const Flow &flow) { return flow.output.xs[q]; }), num_eliminated, num_available_rows); elimination_step( rows_with([&](const Flow &flow) { return flow.output.zs[q]; }), num_eliminated, num_available_rows); } for (size_t r = 0; r < table.size(); r++) { if (table[r].input.ref().has_no_pauli_terms() && table[r].output.ref().has_no_pauli_terms()) { measurements_only_table.push_back(std::move(table[r])); std::swap(table[r], table.back()); table.pop_back(); r--; } } } template void CircuitFlowGeneratorSolver::eliminate_input_xz_terms(size_t &num_eliminated, size_t num_available_rows) { for (size_t q = 0; q < num_qubits; q++) { elimination_step( rows_with([&](const Flow &flow) { return flow.input.xs[q]; }), num_eliminated, num_available_rows); elimination_step( rows_with([&](const Flow &flow) { return flow.input.zs[q]; }), num_eliminated, num_available_rows); } } template void CircuitFlowGeneratorSolver::eliminate_output_xz_terms(size_t &num_eliminated, size_t num_available_rows) { for (size_t q = 0; q < num_qubits; q++) { elimination_step( rows_with([&](const Flow &flow) { return flow.output.xs[q]; }), num_eliminated, num_available_rows); elimination_step( rows_with([&](const Flow &flow) { return flow.output.zs[q]; }), num_eliminated, num_available_rows); } } template void CircuitFlowGeneratorSolver::eliminate_measurement_terms(size_t &num_eliminated, size_t num_available_rows) { for (size_t m = 0; m < num_measurements; m++) { elimination_step( rows_with([&](const Flow &flow) { return std::find(flow.measurements.begin(), flow.measurements.end(), (int32_t)m) != flow.measurements.end(); }), num_eliminated, num_available_rows); } } template void CircuitFlowGeneratorSolver::final_canonicalize_into_table() { for (auto &row : measurements_only_table) { table.push_back(std::move(row)); } size_t num_eliminated = 0; eliminate_input_xz_terms(num_eliminated, table.size()); eliminate_output_xz_terms(num_eliminated, table.size()); eliminate_measurement_terms(num_eliminated, table.size()); for (auto &row : table) { row.output.sign ^= row.input.sign; row.input.sign = 0; if (row.input.ref().has_no_pauli_terms()) { row.input.xs.destructive_resize(0); row.input.zs.destructive_resize(0); row.input.num_qubits = 0; } if (row.output.ref().has_no_pauli_terms()) { row.output.xs.destructive_resize(0); row.output.zs.destructive_resize(0); row.output.num_qubits = 0; } } std::sort(table.begin(), table.end()); } template CircuitFlowGeneratorSolver CircuitFlowGeneratorSolver::solver_with_circuit_generators(const Circuit &circuit, uint32_t min_num_qubits) { auto stats = circuit.compute_stats(); stats.num_qubits = std::max(stats.num_qubits, min_num_qubits); CircuitFlowGeneratorSolver solver(stats); for (size_t q = 0; q < solver.num_qubits; q++) { auto &x = solver.add_row(); x.output.xs[q] = 1; x.input.xs[q] = 1; auto &z = solver.add_row(); z.output.zs[q] = 1; z.input.zs[q] = 1; } circuit.for_each_operation_reverse([&](CircuitInstruction inst) { solver.undo_instruction(inst); }); return solver; } template std::vector> circuit_flow_generators(const Circuit &circuit) { CircuitFlowGeneratorSolver solver = CircuitFlowGeneratorSolver::solver_with_circuit_generators(circuit, 0); if (solver.imag_bits.not_zero()) { throw std::invalid_argument("Unexpected anticommutation while solving for flow generators."); } solver.final_canonicalize_into_table(); return solver.table; } template std::vector>> solve_for_flow_measurements(const Circuit &circuit, std::span> flows) { size_t num_flow_qubits = 0; for (const auto &flow : flows) { num_flow_qubits = std::max(num_flow_qubits, flow.input.num_qubits); num_flow_qubits = std::max(num_flow_qubits, flow.output.num_qubits); if (flow.input.ref().has_no_pauli_terms() && flow.output.ref().has_no_pauli_terms()) { throw std::invalid_argument( "Given a 1 -> 1 flow (empty input, empty output). " "Only solving non-empty flows is supported by this method."); } } CircuitFlowGeneratorSolver solver = CircuitFlowGeneratorSolver::solver_with_circuit_generators(circuit, num_flow_qubits); // Copy pauli terms from flows into the table. size_t num_circuit_flows = solver.table.size(); for (size_t k = 0; k < flows.size(); k++) { const auto &flow = flows[k]; auto &dst = solver.add_row(); dst.input.xs.word_range_ref(0, flow.input.xs.num_simd_words) = flow.input.xs; dst.input.zs.word_range_ref(0, flow.input.zs.num_simd_words) = flow.input.zs; dst.output.xs.word_range_ref(0, flow.output.xs.num_simd_words) = flow.output.xs; dst.output.zs.word_range_ref(0, flow.output.zs.num_simd_words) = flow.output.zs; } // Eliminate the pauli terms. size_t num_eliminated = 0; solver.eliminate_input_xz_terms(num_eliminated, num_circuit_flows); solver.eliminate_output_xz_terms(num_eliminated, num_circuit_flows); // Greedily attempt to reduce measurement counts. // This avoids bad scenarios like stability experiments putting the global measurement set into local flows. for (size_t k = num_eliminated; k < num_circuit_flows; k++) { if (solver.table[k].input.ref().has_no_pauli_terms() && solver.table[k].output.ref().has_no_pauli_terms()) { const auto &src = solver.table[k].measurements; for (size_t k2 = num_circuit_flows; k2 < solver.table.size(); k2++) { auto &dst = solver.table[k2].measurements; if (dst.size() >= src.size() * 2) { continue; } solver.buf_for_xor_merge.resize(std::max(solver.buf_for_xor_merge.size(), src.size() + dst.size() + 1)); const int32_t *end = xor_merge_sort( SpanRef(dst), SpanRef(src), solver.buf_for_xor_merge.data()); size_t n = end - solver.buf_for_xor_merge.data(); if (n < dst.size()) { memcpy(dst.data(), solver.buf_for_xor_merge.data(), n * sizeof(int32_t)); dst.resize(n); } } } } // Collect the measurements, checking the pauli terms were actually cleared. std::vector>> result; for (size_t k = 0; k < flows.size(); k++) { Flow &solved = solver.table[k + num_circuit_flows]; if (solver.imag_bits[k] || !solved.input.ref().has_no_pauli_terms() || !solved.output.ref().has_no_pauli_terms()) { result.push_back(std::optional>{}); continue; } result.emplace_back(std::move(solved.measurements)); } return result; } } // namespace stim ================================================ FILE: src/stim/util_top/circuit_flow_generators.test.cc ================================================ #include "stim/util_top/circuit_flow_generators.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/gen/gen_surface_code.h" #include "stim/mem/simd_word.test.h" #include "stim/util_top/circuit_inverse_qec.h" #include "stim/util_top/has_flow.h" using namespace stim; TEST_EACH_WORD_SIZE_W(circuit_flow_generators, various, { EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( )CIRCUIT")), (std::vector>{})); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( X 0 )CIRCUIT")), (std::vector>{ Flow::from_str("X -> X"), Flow::from_str("Z -> -Z"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( H 0 )CIRCUIT")), (std::vector>{ Flow::from_str("X -> Z"), Flow::from_str("Z -> X"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( M 0 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> Z xor rec[0]"), Flow::from_str("Z -> rec[0]"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( M 0 0 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> rec[0] xor rec[1]"), Flow::from_str("1 -> Z xor rec[1]"), Flow::from_str("Z -> rec[1]"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( MXX 2 0 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> X_X xor rec[0]"), Flow::from_str("__X -> __X"), Flow::from_str("_X_ -> _X_"), Flow::from_str("_Z_ -> _Z_"), Flow::from_str("X__ -> __X xor rec[0]"), Flow::from_str("Z_Z -> Z_Z"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( MYY 3 1 2 3 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> __YY xor rec[1]"), Flow::from_str("1 -> _Y_Y xor rec[0]"), Flow::from_str("___Y -> ___Y"), Flow::from_str("__Y_ -> ___Y xor rec[1]"), Flow::from_str("_XZZ -> _ZZX xor rec[0]"), Flow::from_str("_ZZZ -> _ZZZ"), Flow::from_str("X___ -> X___"), Flow::from_str("Z___ -> Z___"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( MZZ 3 1 2 3 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> __ZZ xor rec[1]"), Flow::from_str("1 -> _Z_Z xor rec[0]"), Flow::from_str("___Z -> ___Z"), Flow::from_str("__Z_ -> ___Z xor rec[1]"), Flow::from_str("_XXX -> _XXX"), Flow::from_str("_Z__ -> ___Z xor rec[0]"), Flow::from_str("X___ -> X___"), Flow::from_str("Z___ -> Z___"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( ISWAP 3 1 2 3 )CIRCUIT")), (std::vector>{ Flow::from_str("___X -> _YZ_"), Flow::from_str("___Z -> _Z__"), Flow::from_str("__X_ -> __ZY"), Flow::from_str("__Z_ -> ___Z"), Flow::from_str("_X__ -> -_ZXZ"), Flow::from_str("_Z__ -> __Z_"), Flow::from_str("X___ -> X___"), Flow::from_str("Z___ -> Z___"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( S 0 )CIRCUIT")), (std::vector>{ Flow::from_str("X -> Y"), Flow::from_str("Z -> Z"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( S_DAG 0 )CIRCUIT")), (std::vector>{ Flow::from_str("X -> -Y"), Flow::from_str("Z -> Z"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( SPP Z0 )CIRCUIT")), (std::vector>{ Flow::from_str("X -> Y"), Flow::from_str("Z -> Z"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( SQRT_X 0 S 0 )CIRCUIT")), (std::vector>{ Flow::from_str("X -> Y"), Flow::from_str("Z -> X"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( SPP X0 Z0 )CIRCUIT")), (std::vector>{ Flow::from_str("X -> Y"), Flow::from_str("Z -> X"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( SPP X0*X1 )CIRCUIT")), (std::vector>{ Flow::from_str("_X -> _X"), Flow::from_str("_Z -> -XY"), Flow::from_str("X_ -> X_"), Flow::from_str("Z_ -> -YX"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( SPP_DAG Z0 )CIRCUIT")), (std::vector>{ Flow::from_str("X -> -Y"), Flow::from_str("Z -> Z"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( M 0 CX rec[-1] 0 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> Z"), Flow::from_str("Z -> rec[0]"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( R 0 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> Z"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( MR 0 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> Z"), Flow::from_str("Z -> rec[0]"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( M 0 XCZ 0 rec[-1] )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> Z"), Flow::from_str("Z -> rec[0]"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( MPAD 0 1 1 0 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> rec[0]"), Flow::from_str("1 -> rec[3]"), Flow::from_str("1 -> -rec[1]"), Flow::from_str("1 -> -rec[2]"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( M 0 CY rec[-1] 1 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> Z_ xor rec[0]"), Flow::from_str("_X -> _X xor rec[0]"), Flow::from_str("_Z -> _Z xor rec[0]"), Flow::from_str("Z_ -> rec[0]"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( HERALDED_ERASE(0.04) 1 HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.03, 0.04) 1 TICK MPP X0*Y1*Z2 Z0*Z1 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> rec[0]"), Flow::from_str("1 -> rec[1]"), Flow::from_str("1 -> XYZ xor rec[2]"), Flow::from_str("1 -> ZZ_ xor rec[3]"), Flow::from_str("__Z -> __Z"), Flow::from_str("_ZX -> _ZX"), Flow::from_str("XXX -> _ZY xor rec[2]"), Flow::from_str("Z_X -> _ZX xor rec[3]"), })); EXPECT_EQ( circuit_flow_generators(Circuit(R"CIRCUIT( MPP Y0*Y1 Y2*Y3 )CIRCUIT")), (std::vector>{ Flow::from_str("1 -> __YY xor rec[1]"), Flow::from_str("1 -> YY__ xor rec[0]"), Flow::from_str("___Y -> ___Y"), Flow::from_str("__XZ -> __ZX xor rec[1]"), Flow::from_str("__ZZ -> __ZZ"), Flow::from_str("_Y__ -> _Y__"), Flow::from_str("XZ__ -> ZX__ xor rec[0]"), Flow::from_str("ZZ__ -> ZZ__"), })); }) TEST_EACH_WORD_SIZE_W(circuit_flow_generators, all_operations, { auto circuit = generate_test_circuit_with_all_operations(); auto generators = circuit_flow_generators(circuit); auto rng = externally_seeded_rng(); auto passes = sample_if_circuit_has_stabilizer_flows(256, rng, circuit, generators); for (size_t k = 0; k < passes.size(); k++) { EXPECT_TRUE(passes[k]) << k << ": " << generators[k]; } }) TEST_EACH_WORD_SIZE_W(solve_for_flow_measurements, empty, { EXPECT_EQ( solve_for_flow_measurements( Circuit(R"CIRCUIT( )CIRCUIT"), (std::vector>{})), (std::vector>>{})); }) TEST_EACH_WORD_SIZE_W(solve_for_flow_measurements, simple, { EXPECT_EQ( solve_for_flow_measurements( Circuit(R"CIRCUIT( MX 0 )CIRCUIT"), (std::vector>{ Flow::from_str("1 -> X0"), })), (std::vector>>{ {std::vector{0}}, })); EXPECT_EQ( solve_for_flow_measurements( Circuit(R"CIRCUIT( MX 0 )CIRCUIT"), (std::vector>{ Flow::from_str("1 -> Y0"), })), (std::vector>>{ {}, })); EXPECT_EQ( solve_for_flow_measurements( Circuit(R"CIRCUIT( MX 0 )CIRCUIT"), (std::vector>{ Flow::from_str("1 -> X0"), Flow::from_str("Y0 -> Y0"), Flow::from_str("X0 -> 1"), Flow::from_str("X0 -> Z0"), Flow::from_str("Y1 -> Y1"), })), (std::vector>>{ {std::vector{0}}, {}, {std::vector{0}}, {}, {std::vector{}}, })); EXPECT_THROW( { solve_for_flow_measurements(Circuit(), (std::vector>{Flow::from_str("1 -> 1")})); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(solve_for_flow_measurements, rep_code, { EXPECT_EQ( solve_for_flow_measurements( Circuit(R"CIRCUIT( R 1 3 CX 0 1 2 3 CX 4 3 2 1 M 1 3 )CIRCUIT"), (std::vector>{ Flow::from_str("Z0*Z2 -> 1"), Flow::from_str("1 -> Z2*Z4"), Flow::from_str("1 -> Z0*Z4"), Flow::from_str("Z0*Z4 -> Z0*Z2"), Flow::from_str("Z0 -> Z0"), Flow::from_str("Z0 -> Z1"), Flow::from_str("Z0 -> Z2"), Flow::from_str("X0*X2*X4 -> X0*X2*X4"), Flow::from_str("X0 -> X0"), Flow::from_str("X0 -> Z0"), })), (std::vector>>{ {std::vector{0}}, {std::vector{1}}, {std::vector{0, 1}}, {std::vector{1}}, {std::vector{}}, {}, {std::vector{0}}, {std::vector{}}, {}, {}, })); }) ================================================ FILE: src/stim/util_top/circuit_flow_generators_test.py ================================================ import stim def test_solve_flow_measurements(): assert stim.Circuit(""" M 2 """).solve_flow_measurements([ stim.Flow("X2 -> X2"), ]) == [None] assert stim.Circuit(""" M 2 """).solve_flow_measurements([ stim.Flow("X2 -> X2"), stim.Flow("Y2 -> Y2"), stim.Flow("Z2 -> Z2"), stim.Flow("Z2 -> 1"), ]) == [None, None, [], [0]] assert stim.Circuit(""" MXX 0 1 """).solve_flow_measurements([ stim.Flow("YY -> ZZ"), stim.Flow("YY -> YY"), stim.Flow("YZ -> ZY"), ]) == [[0], [], [0]] def test_solve_flow_generators_measurements_multi_target(): assert stim.Circuit(""" M 1 2 """).flow_generators() == [ stim.Flow("1 -> __Z xor rec[1]"), stim.Flow("1 -> _Z_ xor rec[0]"), stim.Flow("__Z -> rec[1]"), stim.Flow("_Z_ -> rec[0]"), stim.Flow("X__ -> X__"), stim.Flow("Z__ -> Z__"), ] assert stim.Circuit(""" MX 1 2 """).flow_generators() == [ stim.Flow("1 -> __X xor rec[1]"), stim.Flow("1 -> _X_ xor rec[0]"), stim.Flow("__X -> rec[1]"), stim.Flow("_X_ -> rec[0]"), stim.Flow("X__ -> X__"), stim.Flow("Z__ -> Z__"), ] assert stim.Circuit(""" MYY 1 2 3 4 """).flow_generators() == [ stim.Flow("1 -> ___YY xor rec[1]"), stim.Flow("1 -> _YY__ xor rec[0]"), stim.Flow("____Y -> ____Y"), stim.Flow("___XZ -> ___ZX xor rec[1]"), stim.Flow("___ZZ -> ___ZZ"), stim.Flow("__Y__ -> __Y__"), stim.Flow("_XZ__ -> _ZX__ xor rec[0]"), stim.Flow("_ZZ__ -> _ZZ__"), stim.Flow("X____ -> X____"), stim.Flow("Z____ -> Z____"), ] assert stim.Circuit(""" MPP Y1*Y2 Y3*Y4 """).flow_generators() == [ stim.Flow("1 -> ___YY xor rec[1]"), stim.Flow("1 -> _YY__ xor rec[0]"), stim.Flow("____Y -> ____Y"), stim.Flow("___XZ -> ___ZX xor rec[1]"), stim.Flow("___ZZ -> ___ZZ"), stim.Flow("__Y__ -> __Y__"), stim.Flow("_XZ__ -> _ZX__ xor rec[0]"), stim.Flow("_ZZ__ -> _ZZ__"), stim.Flow("X____ -> X____"), stim.Flow("Z____ -> Z____"), ] def test_solve_flow_measurements_multi_target(): assert stim.Circuit(""" M 1 2 """).solve_flow_measurements([ stim.Flow("Z1 -> 1"), ]) == [[0]] assert stim.Circuit(""" MX 1 2 """).solve_flow_measurements([ stim.Flow("X1 -> 1"), ]) == [[0]] assert stim.Circuit(""" MYY 1 2 3 4 """).solve_flow_measurements([ stim.Flow("Y1*Y2 -> 1"), ]) == [[0]] assert stim.Circuit(""" MPP Y1*Y2 Y3*Y4 """).solve_flow_measurements([ stim.Flow("Y1*Y2 -> 1"), ]) == [[0]] def test_solve_flow_measurements_fewer_measurements_heuristic(): assert stim.Circuit(""" MPP Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8 M 0 1 2 3 4 5 6 7 8 """).solve_flow_measurements([ stim.Flow("1 -> Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8"), ]) == [[0]] assert stim.Circuit(""" MPP Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8 M 0 1 2 3 4 5 6 7 8 """).solve_flow_measurements([ stim.Flow("Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8 -> 1"), ]) == [[0]] assert stim.Circuit(""" M 0 1 2 3 4 5 6 7 8 MPP Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8 """).solve_flow_measurements([ stim.Flow("1 -> Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8"), ]) == [[9]] assert stim.Circuit(""" M 0 1 2 3 4 5 6 7 8 MPP Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8 """).solve_flow_measurements([ stim.Flow("Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8 -> 1"), ]) == [[9]] ================================================ FILE: src/stim/util_top/circuit_inverse_qec.cc ================================================ #include "stim/util_top/circuit_inverse_qec.h" using namespace stim; using namespace stim::internal; CircuitFlowReverser::CircuitFlowReverser(CircuitStats stats, bool dont_turn_measurements_into_resets) : stats(stats), dont_turn_measurements_into_resets(dont_turn_measurements_into_resets), rev(stats.num_qubits, stats.num_measurements, stats.num_detectors, true), qubit_workspace(stats.num_qubits), num_new_measurements(0) { } void CircuitFlowReverser::recompute_active_terms() { active_terms.clear(); for (const auto &ds : rev.xs) { for (const auto &t : ds) { active_terms.insert(t); } } for (const auto &ds : rev.zs) { for (const auto &t : ds) { active_terms.insert(t); } } for (const auto &d : rev.rec_bits) { for (const auto &e : d.second) { active_terms.insert(e); } } } void CircuitFlowReverser::do_rp_mrp_instruction(const CircuitInstruction &inst) { Gate g = GATE_DATA[inst.gate_type]; for_each_disjoint_target_segment_in_instruction_reversed(inst, qubit_workspace, [&](CircuitInstruction segment) { // Each reset effect becomes a measurement effect in the inverted circuit. Index these // measurements. for (size_t k = inst.targets.size(); k-- > 0;) { auto q = inst.targets[k].qubit_value(); for (auto d : rev.xs[q]) { d2ms[d].insert(num_new_measurements); } for (auto d : rev.zs[q]) { d2ms[d].insert(num_new_measurements); } num_new_measurements++; } // Undo the gate, ignoring measurement noise. rev.undo_gate(segment); inverted_circuit.safe_append_reversed_targets( CircuitInstruction(g.best_candidate_inverse_id, {}, segment.targets, inst.tag), false); // Measurement noise becomes noise-after-reset in the reversed circuit. if (!inst.args.empty()) { GateType ejected_noise; if (inst.gate_type == GateType::MRX) { ejected_noise = GateType::Z_ERROR; } else if (inst.gate_type == GateType::MRY) { ejected_noise = GateType::Z_ERROR; } else if (inst.gate_type == GateType::MR) { ejected_noise = GateType::X_ERROR; } else { throw std::invalid_argument("Don't know how to invert " + inst.str()); } inverted_circuit.safe_append_reversed_targets( CircuitInstruction(ejected_noise, segment.args, segment.targets, inst.tag), false); } }); } void CircuitFlowReverser::do_m2r_instruction(const CircuitInstruction &inst) { // Figure out the type of reset each measurement might be turned into. GateType reset; if (inst.gate_type == GateType::MX) { reset = GateType::RX; } else if (inst.gate_type == GateType::MY) { reset = GateType::RY; } else if (inst.gate_type == GateType::M) { reset = GateType::R; } else { throw std::invalid_argument("Don't know how to invert " + inst.str()); } Gate g = GATE_DATA[inst.gate_type]; for (size_t k = inst.targets.size(); k-- > 0;) { GateTarget t = inst.targets[k]; auto q = t.qubit_value(); if (!dont_turn_measurements_into_resets && rev.xs[q].empty() && rev.zs[q].empty() && rev.rec_bits.contains(rev.num_measurements_in_past - 1) && inst.args.empty()) { // Noiseless measurements with past-dependence and no future-dependence become resets. inverted_circuit.safe_append(CircuitInstruction(reset, inst.args, &t, inst.tag)); } else { // Measurements that aren't turned into resets need to be re-indexed. auto f = rev.rec_bits.find(rev.num_measurements_in_past - 1); if (f != rev.rec_bits.end()) { for (auto &dem_target : f->second) { d2ms[dem_target].insert(num_new_measurements); } } num_new_measurements++; inverted_circuit.safe_append(CircuitInstruction(g.best_candidate_inverse_id, inst.args, &t, inst.tag)); } rev.undo_gate(CircuitInstruction{g.id, {}, &t, inst.tag}); } } void CircuitFlowReverser::do_measuring_instruction(const CircuitInstruction &inst) { Gate g = GATE_DATA[inst.gate_type]; auto m = inst.count_measurement_results(); // Re-index the measurements for the reversed detectors. for (size_t k = 0; k < m; k++) { auto f = rev.rec_bits.find(rev.num_measurements_in_past - k - 1); if (f != rev.rec_bits.end()) { for (auto &dem_target : f->second) { d2ms[dem_target].insert(num_new_measurements); } } num_new_measurements++; } inverted_circuit.safe_append_reversed_targets( CircuitInstruction(g.best_candidate_inverse_id, inst.args, inst.targets, inst.tag), g.flags & GATE_TARGETS_PAIRS); rev.undo_gate(inst); } void CircuitFlowReverser::do_feedback_capable_instruction(const CircuitInstruction &inst) { for (GateTarget t : inst.targets) { if (t.is_measurement_record_target()) { throw std::invalid_argument( "Time-reversing feedback isn't supported yet. Found feedback in: " + inst.str()); } } do_simple_instruction(inst); } void CircuitFlowReverser::do_simple_instruction(const CircuitInstruction &inst) { Gate g = GATE_DATA[inst.gate_type]; rev.undo_gate(inst); inverted_circuit.safe_append_reversed_targets( CircuitInstruction(g.best_candidate_inverse_id, inst.args, inst.targets, inst.tag), g.flags & GATE_TARGETS_PAIRS); } void CircuitFlowReverser::flush_detectors_and_observables() { recompute_active_terms(); terms_to_erase.clear(); for (auto d : d2ms) { SpanRef out_args{}; GateType out_gate = GateType::DETECTOR; double id = 0; if (d.first.is_observable_id()) { if (d.first.raw_id() >= stats.num_observables) { continue; } id = d.first.raw_id(); out_args = &id; out_gate = GateType::OBSERVABLE_INCLUDE; } else if (active_terms.contains(d.first)) { continue; } else { out_args = d2coords[d.first]; } buf.clear(); for (auto e : d.second) { buf.push_back(GateTarget::rec((int32_t)e - (int32_t)num_new_measurements)); } inverted_circuit.safe_append(CircuitInstruction(out_gate, out_args, buf, d2tag[d.first])); terms_to_erase.push_back(d.first); } for (auto e : terms_to_erase) { d2coords.erase(e); d2ms.erase(e); d2tag.erase(e); } } void CircuitFlowReverser::do_instruction(const CircuitInstruction &inst) { switch (inst.gate_type) { case GateType::DETECTOR: { rev.undo_gate(inst); d2tag[DemTarget::relative_detector_id(rev.num_detectors_in_past)] = inst.tag; auto &v = d2coords[DemTarget::relative_detector_id(rev.num_detectors_in_past)]; for (size_t k = 0; k < inst.args.size(); k++) { v.push_back(inst.args[k] + (k < coord_shifts.size() ? coord_shifts[k] : 0)); } break; } case GateType::OBSERVABLE_INCLUDE: { for (const auto &t : inst.targets) { if (t.is_pauli_target()) { inverted_circuit.target_buf.append_tail(t); } } if (inverted_circuit.target_buf.tail.empty()) { d2tag[DemTarget::observable_id((uint64_t)inst.args[0])] = inst.tag; } else { inverted_circuit.operations.push_back( CircuitInstruction{ GateType::OBSERVABLE_INCLUDE, inverted_circuit.arg_buf.take_copy(inst.args), inverted_circuit.target_buf.commit_tail(), inverted_circuit.tag_buf.take_copy(inst.tag), }); } rev.undo_gate(inst); break; } case GateType::TICK: case GateType::I: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: case GateType::X: case GateType::Y: case GateType::Z: case GateType::C_XYZ: case GateType::C_NXYZ: case GateType::C_XNYZ: case GateType::C_XYNZ: case GateType::C_ZYX: case GateType::C_NZYX: case GateType::C_ZNYX: case GateType::C_ZYNX: case GateType::SQRT_X: case GateType::SQRT_X_DAG: case GateType::SQRT_Y: case GateType::SQRT_Y_DAG: case GateType::S: case GateType::S_DAG: case GateType::SQRT_XX: case GateType::SQRT_XX_DAG: case GateType::SQRT_YY: case GateType::SQRT_YY_DAG: case GateType::SQRT_ZZ: case GateType::SQRT_ZZ_DAG: case GateType::SPP: case GateType::SPP_DAG: case GateType::SWAP: case GateType::ISWAP: case GateType::CXSWAP: case GateType::SWAPCX: case GateType::CZSWAP: case GateType::ISWAP_DAG: case GateType::XCX: case GateType::XCY: case GateType::YCX: case GateType::YCY: case GateType::H: case GateType::H_XY: case GateType::H_YZ: case GateType::H_NXZ: case GateType::H_NXY: case GateType::H_NYZ: case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::PAULI_CHANNEL_1: case GateType::PAULI_CHANNEL_2: case GateType::E: case GateType::HERALDED_ERASE: case GateType::HERALDED_PAULI_CHANNEL_1: do_simple_instruction(inst); return; case GateType::XCZ: case GateType::YCZ: case GateType::CX: case GateType::CY: case GateType::CZ: do_feedback_capable_instruction(inst); break; case GateType::MRX: case GateType::MRY: case GateType::MR: case GateType::RX: case GateType::RY: case GateType::R: do_rp_mrp_instruction(inst); flush_detectors_and_observables(); break; case GateType::MX: case GateType::MY: case GateType::M: do_m2r_instruction(inst); flush_detectors_and_observables(); break; case GateType::MPAD: case GateType::MPP: case GateType::MXX: case GateType::MYY: case GateType::MZZ: do_measuring_instruction(inst); flush_detectors_and_observables(); break; case GateType::QUBIT_COORDS: for (size_t k = 0; k < inst.args.size(); k++) { qubit_coords_circuit.arg_buf.append_tail( inst.args[k] + (k < coord_shifts.size() ? coord_shifts[k] : 0)); } qubit_coords_circuit.operations.push_back( CircuitInstruction{ inst.gate_type, qubit_coords_circuit.arg_buf.commit_tail(), qubit_coords_circuit.target_buf.take_copy(inst.targets), inst.tag, }); break; case GateType::SHIFT_COORDS: vec_pad_add_mul(coord_shifts, inst.args); break; case GateType::ELSE_CORRELATED_ERROR: default: throw std::invalid_argument("Don't know how to invert " + inst.str()); } } Circuit &&CircuitFlowReverser::build_and_move_final_inverted_circuit() { if (qubit_coords_circuit.operations.empty()) { return std::move(inverted_circuit); } std::reverse(qubit_coords_circuit.operations.begin(), qubit_coords_circuit.operations.end()); qubit_coords_circuit += inverted_circuit; return std::move(qubit_coords_circuit); } ================================================ FILE: src/stim/util_top/circuit_inverse_qec.h ================================================ #ifndef _STIM_UTIL_TOP_CIRCUIT_INVERSE_QEC_H #define _STIM_UTIL_TOP_CIRCUIT_INVERSE_QEC_H #include "stim/circuit/circuit.h" #include "stim/simulators/sparse_rev_frame_tracker.h" #include "stim/stabilizers/flow.h" namespace stim { template std::pair>> circuit_inverse_qec( const Circuit &circuit, std::span> flows, bool dont_turn_measurements_into_resets = false); namespace internal { struct CircuitFlowReverser { CircuitStats stats; bool dont_turn_measurements_into_resets; SparseUnsignedRevFrameTracker rev; simd_bits<64> qubit_workspace; size_t num_new_measurements; Circuit inverted_circuit; std::map d2tag; std::map> d2coords; std::vector coord_buf; std::vector coord_shifts; Circuit qubit_coords_circuit; std::vector buf; std::map> d2ms; std::set active_terms; std::vector terms_to_erase; CircuitFlowReverser(CircuitStats stats, bool dont_turn_measurements_into_resets); void recompute_active_terms(); void do_rp_mrp_instruction(const CircuitInstruction &inst); void do_m2r_instruction(const CircuitInstruction &inst); void do_measuring_instruction(const CircuitInstruction &inst); void do_simple_instruction(const CircuitInstruction &inst); void do_feedback_capable_instruction(const CircuitInstruction &inst); void flush_detectors_and_observables(); void do_instruction(const CircuitInstruction &inst); template void xor_pauli_string_into_tracker_as_target(const PauliString &pauli_string, DemTarget target); template void xor_flow_ends_into_tracker(std::span> flows); template void xor_flow_measurements_into_tracker(std::span> flows); template void xor_flow_starts_into_tracker(std::span> flows); template void verify_flow_observables_disappeared(std::span> flows); template std::vector> build_inverted_flows(std::span> flows); Circuit &&build_and_move_final_inverted_circuit(); }; } // namespace internal } // namespace stim #include "stim/util_top/circuit_inverse_qec.inl" #endif ================================================ FILE: src/stim/util_top/circuit_inverse_qec.inl ================================================ #include "stim/stabilizers/flow.h" #include "stim/util_top/circuit_inverse_qec.h" namespace stim { namespace internal { template void CircuitFlowReverser::xor_pauli_string_into_tracker_as_target( const PauliString &pauli_string, DemTarget target) { pauli_string.ref().for_each_active_pauli([&](size_t q) { bool x = pauli_string.xs[q]; bool z = pauli_string.zs[q]; if (x) { rev.xs[q].xor_item(target); } if (z) { rev.zs[q].xor_item(target); } }); } template void CircuitFlowReverser::xor_flow_ends_into_tracker(std::span> flows) { for (size_t k = 0; k < flows.size(); k++) { const auto &flow = flows[k]; DemTarget flow_target = DemTarget::observable_id(stats.num_observables + k); xor_pauli_string_into_tracker_as_target(flow.output, flow_target); } } template void CircuitFlowReverser::xor_flow_measurements_into_tracker(std::span> flows) { for (size_t k = 0; k < flows.size(); k++) { const auto &flow = flows[k]; DemTarget flow_target = DemTarget::observable_id(stats.num_observables + k); for (int32_t m : flow.measurements) { if (m < 0) { m += stats.num_measurements; } if (m < 0 || (uint64_t)m >= stats.num_measurements) { std::stringstream ss; ss << "Out of range measurement in one of the flows: " << flow; throw std::invalid_argument(ss.str()); } rev.rec_bits[m].sorted_items.push_back(flow_target); } } } template void CircuitFlowReverser::xor_flow_starts_into_tracker(std::span> flows) { for (size_t k = 0; k < flows.size(); k++) { const auto &flow = flows[k]; DemTarget flow_target = DemTarget::observable_id(stats.num_observables + k); xor_pauli_string_into_tracker_as_target(flow.input, flow_target); } } template void CircuitFlowReverser::verify_flow_observables_disappeared(std::span> flows) { bool failed = false; DemTarget example{}; for (size_t q = 0; q < stats.num_qubits; q++) { for (auto &e : rev.xs[q]) { failed = true; example = e; } for (auto &e : rev.zs[q]) { failed = true; example = e; } } if (failed) { if (example.is_relative_detector_id() || (example.is_observable_id() && example.raw_id() < stats.num_observables)) { std::stringstream ss; ss << "The detecting region of " << example << " reached the start of the circuit.\n"; ss << "Only flows given as arguments are permitted to touch the start or end of the circuit.\n"; ss << "There are four potential ways to fix this issue, depending on what's wrong:\n"; ss << "- If " + example.str() + " was relying on implicit initialization into |0> at the start of the circuit, add explicit " "resets to the circuit.\n"; ss << "- If " + example.str() + " shouldn't be reaching the start of the circuit, fix its declaration.\n"; ss << "- If " + example.str() + " isn't needed, delete it from the circuit.\n"; ss << "- If the given circuit is a partial circuit, and " << example << " is reaching outside of it, refactor " << example << "into a flow argument."; throw std::invalid_argument(ss.str()); } else { std::stringstream ss; const auto &flow = flows[example.raw_id() - stats.num_observables]; ss << "The circuit didn't satisfy one of the given flows (ignoring sign): "; ss << flow; auto v = rev.current_error_sensitivity_for(example); v.xs ^= flow.input.xs; v.zs ^= flow.input.zs; ss << "\nChanging the flow to '" << Flow{.input = v, .output = flow.output, .measurements = flow.measurements} << "' would make it a valid flow."; throw std::invalid_argument(ss.str()); } } } template std::vector> CircuitFlowReverser::build_inverted_flows(std::span> flows) { std::vector> inverted_flows; for (size_t k = 0; k < flows.size(); k++) { const auto &f = flows[k]; inverted_flows.push_back( Flow{ .input = f.output, .output = f.input, .measurements = {}, }); auto &f2 = inverted_flows.back(); f2.input.sign = false; f2.output.sign = false; for (auto &m : d2ms[DemTarget::observable_id(k + stats.num_observables)]) { f2.measurements.push_back((int32_t)m - (int32_t)num_new_measurements); } } return inverted_flows; } } // namespace internal template std::pair>> circuit_inverse_qec( const Circuit &circuit, std::span> flows, bool dont_turn_measurements_into_resets) { size_t max_flow_qubit = 0; for (const auto &flow : flows) { max_flow_qubit = std::max(max_flow_qubit, flow.input.num_qubits); max_flow_qubit = std::max(max_flow_qubit, flow.output.num_qubits); } if (max_flow_qubit >= UINT32_MAX) { throw std::invalid_argument("Flow qubit is too large. Not supported."); } CircuitStats stats = circuit.compute_stats(); stats.num_qubits = std::max(stats.num_qubits, (uint32_t)max_flow_qubit + 1); internal::CircuitFlowReverser reverser(stats, dont_turn_measurements_into_resets); reverser.xor_flow_ends_into_tracker(flows); reverser.xor_flow_measurements_into_tracker(flows); circuit.for_each_operation_reverse([&](const CircuitInstruction &inst) { reverser.do_instruction(inst); }); reverser.xor_flow_starts_into_tracker(flows); reverser.verify_flow_observables_disappeared(flows); auto inverted_flows = reverser.build_inverted_flows(flows); Circuit inverted_circuit = reverser.build_and_move_final_inverted_circuit(); return {inverted_circuit, inverted_flows}; } } // namespace stim ================================================ FILE: src/stim/util_top/circuit_inverse_qec.test.cc ================================================ #include "stim/util_top/circuit_inverse_qec.h" #include "gtest/gtest.h" #include "stim/gen/gen_surface_code.h" #include "stim/mem/simd_word.test.h" #include "stim/util_top/circuit_to_detecting_regions.h" using namespace stim; TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, unitary, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( H 0 ISWAP 0 1 1 2 3 2 S 0 3 4 )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( S_DAG 4 3 0 ISWAP_DAG 3 2 1 2 0 1 H 0 )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, r_m_det, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( R 0 M 0 DETECTOR rec[-1] )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( R 0 M 0 DETECTOR rec[-1] )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, r_m_det_keep_m, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( R 0 M 0 DETECTOR rec[-1] )CIRCUIT"), {}, true); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( M 0 M 0 DETECTOR rec[-2] rec[-1] )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, two_to_one, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( R 0 1 CX 0 1 M 0 1 DETECTOR rec[-1] rec[-2] )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( R 1 0 CX 0 1 M 1 0 DETECTOR rec[-2] )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, pass_through, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( R 0 M 0 MR 0 DETECTOR rec[-1] )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( MR 0 M 0 M 0 DETECTOR rec[-1] )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, anticommute, { ASSERT_THROW( { circuit_inverse_qec( Circuit(R"CIRCUIT( R 0 MX 0 MR 0 DETECTOR rec[-1] )CIRCUIT"), {}); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, noisy_mr, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( MR(0.125) 0 1 2 0 2 4 MRX(0.25) 0 MRY(0.375) 0 )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( MRY 0 Z_ERROR(0.375) 0 MRX 0 Z_ERROR(0.25) 0 MR 4 2 0 X_ERROR(0.125) 4 2 0 MR 2 1 0 X_ERROR(0.125) 2 1 0 )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, noisy_m, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( M(0.125) 0 1 2 0 2 4 MX(0.25) 0 MY(0.375) 0 )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( MY(0.375) 0 MX(0.25) 0 M(0.125) 4 2 0 2 1 0 )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, noisy_mr_det, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( MR(0.125) 0 TICK MR(0.25) 0 MR(0.375) 0 DETECTOR rec[-1] )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( MR 0 X_ERROR(0.375) 0 MR 0 X_ERROR(0.25) 0 DETECTOR rec[-1] TICK MR 0 X_ERROR(0.125) 0 )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, m_det, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( R 0 1 2 TICK M 0 1 2 TICK M 0 1 2 DETECTOR(2) rec[-1] DETECTOR(1) rec[-2] )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( R 2 1 M 0 TICK M 2 1 0 TICK M 2 1 0 DETECTOR(2) rec[-3] DETECTOR(1) rec[-2] )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, mzz, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( MRY 0 1 M 0 TICK MZZ(0.125) 0 1 2 3 TICK M 1 MRY 0 1 DETECTOR rec[-3] rec[-5] rec[-6] )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( MRY 1 0 R 1 TICK MZZ(0.125) 2 3 0 1 TICK M 0 DETECTOR rec[-2] rec[-1] MRY 1 0 )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, mpp, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( MPP !X0*X1 Y0*Y1 Z0*Z1 DETECTOR rec[-1] rec[-2] rec[-3] )CIRCUIT"), {}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( MPP Z1*Z0 Y1*Y0 X1*!X0 DETECTOR rec[-3] rec[-2] rec[-1] )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, flow_reverse, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( M 0 )CIRCUIT"), {std::vector>{Flow::from_str("1 -> Z0 xor rec[-1]")}}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( M 0 )CIRCUIT")); ASSERT_EQ(actual.second, (std::vector>{Flow::from_str("Z0 -> rec[-1]")})); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, flow_through_mzz, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( MZZ 0 1 )CIRCUIT"), {std::vector>{ Flow::from_str("X0*X1 -> Y0*Y1 xor rec[-1]"), Flow::from_str("X0*X1 -> X0*X1"), Flow::from_str("Z0 -> Z1 xor rec[-1]"), Flow::from_str("Z0 -> Z0"), }}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( MZZ 0 1 )CIRCUIT")); ASSERT_EQ( actual.second, (std::vector>{ Flow::from_str("Y0*Y1 -> X0*X1 xor rec[-1]"), Flow::from_str("X0*X1 -> X0*X1"), Flow::from_str("Z1 -> Z0 xor rec[-1]"), Flow::from_str("Z0 -> Z0"), })); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, flow_through_mzz_h_cx_s, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( MZZ 0 1 H 0 CX 0 1 S 1 )CIRCUIT"), {std::vector>{ Flow::from_str("X0*X1 -> X0*Z1 xor rec[-1]"), Flow::from_str("X0*X1 -> Z0*Y1"), Flow::from_str("Z0 -> Z0*Z1 xor rec[-1]"), Flow::from_str("Z0 -> X0*Y1"), }}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( S_DAG 1 CX 0 1 H 0 MZZ 0 1 )CIRCUIT")); ASSERT_EQ( actual.second, (std::vector>{ Flow::from_str("X0*Z1 -> X0*X1 xor rec[-1]"), Flow::from_str("Z0*Y1 -> X0*X1"), Flow::from_str("Z0*Z1 -> Z0 xor rec[-1]"), Flow::from_str("X0*Y1 -> Z0"), })); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, flow_flip, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( MY 0 MRX 0 MR 1 R 0 )CIRCUIT"), {std::vector>{ Flow::from_str("Y0*Z1 -> rec[-3] xor rec[-1]"), Flow::from_str("1 -> Z0*Z1"), Flow::from_str("1 -> Z1"), Flow::from_str("1 -> Z0"), }}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( M 0 MR 1 MRX 0 RY 0 )CIRCUIT")); ASSERT_EQ( actual.second, (std::vector>{ Flow::from_str("1 -> Y0*Z1"), Flow::from_str("Z0*Z1 -> rec[-3] xor rec[-2]"), Flow::from_str("Z1 -> rec[-2]"), Flow::from_str("Z0 -> rec[-3]"), })); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, flow_past_end_of_circuit, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( H 0 )CIRCUIT"), {std::vector>{ Flow::from_str("X300*Z0 -> X300*X0"), }}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( H 0 )CIRCUIT")); ASSERT_EQ( actual.second, (std::vector>{ Flow::from_str("X300*X0 -> X300*Z0"), })); }) TEST_EACH_WORD_SIZE_W(circuit_inverse_qec, obs_include_pauli, { auto actual = circuit_inverse_qec( Circuit(R"CIRCUIT( RX 1 OBSERVABLE_INCLUDE[test](1) X1 )CIRCUIT"), {std::vector>{}}); ASSERT_EQ(actual.first, Circuit(R"CIRCUIT( OBSERVABLE_INCLUDE[test](1) X1 MX 1 OBSERVABLE_INCLUDE(1) rec[-1] )CIRCUIT")); ASSERT_EQ(actual.second, (std::vector>{})); }) ================================================ FILE: src/stim/util_top/circuit_inverse_qec_test.py ================================================ import pytest import stim def test_inv_circuit(): inv_circuit, inv_flows = stim.Circuit().time_reversed_for_flows([]) assert inv_circuit == stim.Circuit() assert inv_flows == [] inv_circuit, inv_flows = stim.Circuit(""" R 0 H 0 MX 0 DETECTOR rec[-1] """).time_reversed_for_flows([]) assert inv_circuit == stim.Circuit(""" RX 0 H 0 M 0 DETECTOR rec[-1] """) assert inv_flows == [] inv_circuit, inv_flows = stim.Circuit(""" M 0 """).time_reversed_for_flows([stim.Flow('Z -> rec[-1]')]) assert inv_circuit == stim.Circuit(""" R 0 """) assert inv_flows == [stim.Flow('1 -> Z')] assert inv_circuit.has_all_flows(inv_flows, unsigned=True) inv_circuit, inv_flows = stim.Circuit(""" R 0 """).time_reversed_for_flows([stim.Flow('1 -> Z')]) assert inv_circuit == stim.Circuit(""" M 0 """) assert inv_flows == [stim.Flow('Z -> rec[-1]')] inv_circuit, inv_flows = stim.Circuit(""" M 0 """).time_reversed_for_flows([stim.Flow('1 -> Z xor rec[-1]')]) assert inv_circuit == stim.Circuit(""" M 0 """) assert inv_flows == [stim.Flow('Z -> rec[-1]')] inv_circuit, inv_flows = stim.Circuit(""" M 0 """).time_reversed_for_flows( flows=[stim.Flow('Z -> rec[-1]')], dont_turn_measurements_into_resets=True, ) assert inv_circuit == stim.Circuit(""" M 0 """) assert inv_flows == [stim.Flow('1 -> Z xor rec[-1]')] inv_circuit, inv_flows = stim.Circuit(""" MR(0.125) 0 """).time_reversed_for_flows([]) assert inv_circuit == stim.Circuit(""" MR 0 X_ERROR(0.125) 0 """) assert inv_flows == [] def test_inv_circuit_surface_code(): circuit = stim.Circuit.generated( "surface_code:rotated_memory_x", distance=3, rounds=2, ) det_regions = circuit.detecting_regions() inv_circuit, _ = circuit.time_reversed_for_flows([]) keys = det_regions.keys() inv_det_regions = inv_circuit.detecting_regions() assert inv_det_regions.keys() == keys # Check observable is time reversed. num_ticks = circuit.num_ticks l0 = stim.target_logical_observable_id(0) assert inv_circuit.num_ticks == num_ticks assert {num_ticks - t - 1: v for t, v in det_regions[l0].items()} == inv_det_regions[l0] # Check all regions are time reversed (though may be indexed differently) original_region_sets_reversed = set() for k, v in det_regions.items(): original_region_sets_reversed.add(frozenset({num_ticks - t - 1: str(p) for t, p in v.items()}.items())) inv_region_sets = set() for k, v in inv_det_regions.items(): inv_region_sets.add(frozenset({t: str(p) for t, p in v.items()}.items())) assert len(original_region_sets_reversed) == len(det_regions) assert original_region_sets_reversed == inv_region_sets def test_more_flow_qubits_than_circuit_qubits(): flows = [ stim.Flow("X300 -> X300"), stim.Flow("X2*Z301 -> Z2*Z301"), ] circuit = stim.Circuit("H 2") assert circuit.has_flow(flows[0]) assert circuit.has_flow(flows[1]) assert circuit.has_all_flows(flows) new_circuit, new_flows = circuit.time_reversed_for_flows(flows=flows) assert new_circuit == circuit assert new_flows == [ stim.Flow("X300 -> X300"), stim.Flow("Z2*Z301 -> X2*Z301"), ] def test_measurement_ordering(): circuit = stim.Circuit(""" M 0 1 """) flows = [ stim.Flow("I -> Z0 xor rec[-2]"), stim.Flow("I -> Z1 xor rec[-1]"), ] assert circuit.has_all_flows(flows, unsigned=True) new_circuit, new_flows = circuit.time_reversed_for_flows(flows) assert new_circuit.has_all_flows(new_flows, unsigned=True) def test_measurement_ordering_2(): circuit = stim.Circuit(""" MZZ 0 1 2 3 """) flows = [ stim.Flow("I -> Z0*Z1 xor rec[-2]"), stim.Flow("I -> Z2*Z3 xor rec[-1]"), ] assert circuit.has_all_flows(flows, unsigned=True) new_circuit, new_flows = circuit.time_reversed_for_flows(flows) assert new_circuit.has_all_flows(new_flows, unsigned=True) def test_measurement_ordering_3(): circuit = stim.Circuit(""" MR 0 1 """) flows = [ stim.Flow("Z0 -> rec[-2]"), stim.Flow("Z1 -> rec[-1]"), stim.Flow("I -> Z0"), stim.Flow("I -> Z1"), ] assert circuit.has_all_flows(flows, unsigned=True) new_circuit, new_flows = circuit.time_reversed_for_flows(flows) assert new_circuit.has_all_flows(new_flows, unsigned=True) def test_feedback(): c = stim.Circuit(""" R 1 M 1 CX rec[-1] 0 """) with pytest.raises(ValueError): c.time_reversed_for_flows([stim.Flow("Z0 -> Z0")]) # TODO: once feedback is supported verify the inv flow is correct def test_obs_include_paulis(): c = stim.Circuit(""" RX 0 OBSERVABLE_INCLUDE[test1](2) X0 OBSERVABLE_INCLUDE[test2](3) Y1 MY 1 OBSERVABLE_INCLUDE(3) rec[-1] """) assert c.time_reversed_for_flows([]) == (stim.Circuit(""" RY 1 OBSERVABLE_INCLUDE[test2](3) Y1 OBSERVABLE_INCLUDE[test1](2) X0 MX 0 OBSERVABLE_INCLUDE(2) rec[-1] """), []) ================================================ FILE: src/stim/util_top/circuit_inverse_unitary.cc ================================================ #include "stim/util_top/circuit_inverse_unitary.h" using namespace stim; Circuit stim::circuit_inverse_unitary(const Circuit &unitary_circuit) { Circuit inverted; unitary_circuit.for_each_operation_reverse([&](const CircuitInstruction &op) { const auto &gate_data = GATE_DATA[op.gate_type]; if (!(gate_data.flags & GATE_IS_UNITARY)) { throw std::invalid_argument("Not unitary: " + op.str()); } size_t step = (gate_data.flags & GATE_TARGETS_PAIRS) ? 2 : 1; auto s = op.targets.ptr_start; const auto &inv_gate = gate_data.inverse(); for (size_t k = op.targets.size(); k > 0; k -= step) { inverted.safe_append(CircuitInstruction(inv_gate.id, op.args, {s + k - step, s + k}, op.tag)); } }); return inverted; } ================================================ FILE: src/stim/util_top/circuit_inverse_unitary.h ================================================ #ifndef _STIM_UTIL_TOP_CIRCUIT_INVERSE_UNITARY_H #define _STIM_UTIL_TOP_CIRCUIT_INVERSE_UNITARY_H #include "stim/circuit/circuit.h" namespace stim { /// Inverts the given circuit, as long as it only contains unitary operations. Circuit circuit_inverse_unitary(const Circuit &unitary_circuit); } // namespace stim #endif ================================================ FILE: src/stim/util_top/circuit_inverse_unitary.test.cc ================================================ #include "stim/util_top/circuit_inverse_unitary.h" #include "gtest/gtest.h" using namespace stim; TEST(conversions, circuit_inverse_unitary) { ASSERT_EQ( circuit_inverse_unitary(Circuit(R"CIRCUIT( H 0 ISWAP 0 1 1 2 3 2 S 0 3 4 )CIRCUIT")), Circuit(R"CIRCUIT( S_DAG 4 3 0 ISWAP_DAG 3 2 1 2 0 1 H 0 )CIRCUIT")); ASSERT_THROW({ circuit_inverse_unitary(Circuit("M 0")); }, std::invalid_argument); } ================================================ FILE: src/stim/util_top/circuit_to_dem.h ================================================ #ifndef _STIM_UTIL_TOP_CIRCUIT_TO_DEM_H #define _STIM_UTIL_TOP_CIRCUIT_TO_DEM_H #include "stim/simulators/error_analyzer.h" namespace stim { struct DemOptions { bool decompose_errors = false; bool flatten_loops = true; bool allow_gauge_detectors = false; double approximate_disjoint_errors_threshold = 0; bool ignore_decomposition_failures = false; bool block_decomposition_from_introducing_remnant_edges = false; }; inline DetectorErrorModel circuit_to_dem(const Circuit &circuit, DemOptions options = {}) { return ErrorAnalyzer::circuit_to_detector_error_model( circuit, options.decompose_errors, !options.flatten_loops, options.allow_gauge_detectors, options.approximate_disjoint_errors_threshold, options.ignore_decomposition_failures, options.block_decomposition_from_introducing_remnant_edges); } } // namespace stim #endif ================================================ FILE: src/stim/util_top/circuit_to_dem.test.cc ================================================ #include "stim/util_top/circuit_to_dem.h" #include "gtest/gtest.h" using namespace stim; TEST(circuit_to_dem, heralded_noise_basis) { ASSERT_EQ( circuit_to_dem(Circuit(R"CIRCUIT( MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0.25, 0, 0, 0) 0 MXX 0 1 MZZ 0 1 DETECTOR(2) rec[-3] DETECTOR(3) rec[-2] rec[-5] DETECTOR(5) rec[-1] rec[-4] )CIRCUIT")), DetectorErrorModel(R"DEM( error(0.25) D0 detector(2) D0 detector(3) D1 detector(5) D2 )DEM")); ASSERT_EQ( circuit_to_dem(Circuit(R"CIRCUIT( MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0, 0.25, 0, 0) 0 MXX 0 1 MZZ 0 1 DETECTOR(2) rec[-3] DETECTOR(3) rec[-2] rec[-5] DETECTOR(5) rec[-1] rec[-4] )CIRCUIT")), DetectorErrorModel(R"DEM( error(0.25) D0 D2 detector(2) D0 detector(3) D1 detector(5) D2 )DEM")); ASSERT_EQ( circuit_to_dem(Circuit(R"CIRCUIT( MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0, 0, 0.25, 0) 0 MXX 0 1 MZZ 0 1 DETECTOR(2) rec[-3] DETECTOR(3) rec[-2] rec[-5] DETECTOR(5) rec[-1] rec[-4] )CIRCUIT")), DetectorErrorModel(R"DEM( error(0.25) D0 D1 D2 detector(2) D0 detector(3) D1 detector(5) D2 )DEM")); ASSERT_EQ( circuit_to_dem(Circuit(R"CIRCUIT( MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0, 0, 0, 0.25) 0 MXX 0 1 MZZ 0 1 DETECTOR(2) rec[-3] DETECTOR(3) rec[-2] rec[-5] DETECTOR(5) rec[-1] rec[-4] )CIRCUIT")), DetectorErrorModel(R"DEM( error(0.25) D0 D1 detector(2) D0 detector(3) D1 detector(5) D2 )DEM")); ASSERT_EQ( circuit_to_dem( Circuit(R"CIRCUIT( MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0.125, 0, 0.25, 0) 0 MXX 0 1 MZZ 0 1 DETECTOR(2) rec[-3] DETECTOR(3) rec[-2] rec[-5] DETECTOR(5) rec[-1] rec[-4] )CIRCUIT"), {.approximate_disjoint_errors_threshold = 1}), DetectorErrorModel(R"DEM( error(0.125) D0 error(0.25) D0 D1 D2 detector(2) D0 detector(3) D1 detector(5) D2 )DEM")); ASSERT_THROW( { circuit_to_dem(Circuit(R"CIRCUIT( MXX 0 1 MZZ 0 1 HERALDED_PAULI_CHANNEL_1(0.125, 0, 0.25, 0) 0 MXX 0 1 MZZ 0 1 DETECTOR(2) rec[-3] DETECTOR(3) rec[-2] rec[-5] DETECTOR(5) rec[-1] rec[-4] )CIRCUIT")); }, std::invalid_argument); } ================================================ FILE: src/stim/util_top/circuit_to_detecting_regions.cc ================================================ #include "stim/util_top/circuit_to_detecting_regions.h" #include "stim/simulators/sparse_rev_frame_tracker.h" using namespace stim; std::map> stim::circuit_to_detecting_regions( const Circuit &circuit, std::set included_targets, std::set included_ticks, bool ignore_anticommutation_errors) { CircuitStats stats = circuit.compute_stats(); uint64_t tick_index = stats.num_ticks; SparseUnsignedRevFrameTracker tracker( stats.num_qubits, stats.num_measurements, stats.num_detectors, !ignore_anticommutation_errors); std::map> result; circuit.for_each_operation_reverse([&](const CircuitInstruction &inst) { if (inst.gate_type == GateType::TICK) { tick_index -= 1; if (included_ticks.contains(tick_index)) { for (size_t q = 0; q < stats.num_qubits; q++) { for (auto target : tracker.xs[q]) { if (included_targets.contains(target)) { auto &m = result[target]; if (!m.contains(tick_index)) { m.insert({tick_index, FlexPauliString(stats.num_qubits)}); } m.at(tick_index).value.xs[q] ^= 1; } } for (auto target : tracker.zs[q]) { if (included_targets.contains(target)) { auto &m = result[target]; if (!m.contains(tick_index)) { m.insert({tick_index, FlexPauliString(stats.num_qubits)}); } m.at(tick_index).value.zs[q] ^= 1; } } } } } tracker.undo_gate(inst); }); tracker.undo_implicit_RZs_at_start_of_circuit(); return result; } ================================================ FILE: src/stim/util_top/circuit_to_detecting_regions.h ================================================ #ifndef _STIM_UTIL_TOP_CIRCUIT_TO_DETECTING_REGIONS #define _STIM_UTIL_TOP_CIRCUIT_TO_DETECTING_REGIONS #include "stim/circuit/circuit.h" #include "stim/dem/dem_instruction.h" #include "stim/stabilizers/flex_pauli_string.h" namespace stim { std::map> circuit_to_detecting_regions( const Circuit &circuit, std::set included_targets, std::set included_ticks, bool ignore_anticommutation_errors); } // namespace stim #endif ================================================ FILE: src/stim/util_top/circuit_to_detecting_regions.test.cc ================================================ #include "stim/util_top/circuit_to_detecting_regions.h" #include "gtest/gtest.h" using namespace stim; TEST(circuit_to_detecting_regions, simple) { Circuit circuit(R"CIRCUIT( H 0 TICK CX 0 1 TICK MXX 0 1 DETECTOR rec[-1] )CIRCUIT"); auto actual = circuit_to_detecting_regions(circuit, {DemTarget::relative_detector_id(0)}, {0, 1}, false); std::map> expected{ {DemTarget::relative_detector_id(0), { {0, FlexPauliString::from_text("X_")}, {1, FlexPauliString::from_text("XX")}, }}, }; } ================================================ FILE: src/stim/util_top/circuit_to_detecting_regions_test.py ================================================ import pytest import stim def test_detecting_regions_fails_on_anticommutations_at_start_of_circuit(): c = stim.Circuit(""" TICK R 0 TICK MX 0 DETECTOR rec[-1] """) assert 'magenta' in str(c.diagram('detslice-with-ops-svg')) with pytest.raises(ValueError, match="anticommutation"): c.detecting_regions() c = stim.Circuit(""" R 0 TICK MX 0 DETECTOR rec[-1] """) assert 'magenta' in str(c.diagram('detslice-with-ops-svg')) with pytest.raises(ValueError, match="anticommutation"): c.detecting_regions() c = stim.Circuit(""" MX 0 DETECTOR rec[-1] """) assert 'magenta' in str(c.diagram('detslice-with-ops-svg')) with pytest.raises(ValueError, match="anticommutation"): c.detecting_regions() ================================================ FILE: src/stim/util_top/circuit_vs_amplitudes.cc ================================================ #include "stim/util_top/circuit_vs_amplitudes.h" #include "stim/simulators/tableau_simulator.h" #include "stim/simulators/vector_simulator.h" #include "stim/util_bot/twiddle.h" #include "stim/util_top/circuit_inverse_unitary.h" using namespace stim; inline static size_t biggest_index(const std::vector> &state_vector) { size_t best_index = 0; float best_size = std::norm(state_vector[0]); for (size_t k = 1; k < state_vector.size(); k++) { float size = std::norm(state_vector[k]); if (size > best_size) { best_size = size; best_index = k; } } return best_index; } inline static size_t compute_occupation(const std::vector> &state_vector) { size_t c = 0; for (const auto &v : state_vector) { if (v != std::complex{0, 0}) { c++; } } return c; } Circuit stim::stabilizer_state_vector_to_circuit( const std::vector> &state_vector, bool little_endian) { if (!is_power_of_2(state_vector.size())) { std::stringstream ss; ss << "Expected number of amplitudes to be a power of 2."; ss << " The given state vector had " << state_vector.size() << " amplitudes."; throw std::invalid_argument(ss.str()); } uint8_t num_qubits = floor_lg2(state_vector.size()); VectorSimulator sim(num_qubits); sim.state = state_vector; Circuit recorded; auto apply = [&](GateType gate_type, uint32_t target) { sim.apply(gate_type, target); recorded.safe_append(CircuitInstruction( gate_type, {}, std::vector{ GateTarget::qubit(little_endian ? target : (num_qubits - target - 1)), }, "")); }; auto apply2 = [&](GateType gate_type, uint32_t target, uint32_t target2) { sim.apply(gate_type, target, target2); recorded.safe_append(CircuitInstruction( gate_type, {}, std::vector{ GateTarget::qubit(little_endian ? target : (num_qubits - target - 1)), GateTarget::qubit(little_endian ? target2 : (num_qubits - target2 - 1)), }, "")); }; // Move biggest amplitude to start of state vector. size_t pivot = biggest_index(state_vector); for (size_t q = 0; q < num_qubits; q++) { if ((pivot >> q) & 1) { apply(GateType::X, q); } } sim.smooth_stabilizer_state(sim.state[0]); size_t occupation = compute_occupation(sim.state); if (!is_power_of_2(occupation)) { throw std::invalid_argument("State vector isn't a stabilizer state."); } // Repeatedly cancel amplitudes while (occupation > 1) { size_t k = 1; for (; k < state_vector.size(); k++) { if (sim.state[k].real() || sim.state[k].imag()) { break; } } if (k == state_vector.size()) { break; } size_t base_qubit = SIZE_MAX; for (size_t q = 0; q < num_qubits; q++) { if ((k >> q) & 1) { if (base_qubit == SIZE_MAX) { base_qubit = q; } else { apply2(GateType::CX, base_qubit, q); } } } auto s = sim.state[1 << base_qubit]; assert(s != (std::complex{0, 0})); if (s == std::complex{-1, 0}) { apply(GateType::Z, base_qubit); } else if (s == std::complex{0, 1}) { apply(GateType::S_DAG, base_qubit); } else if (s == std::complex{0, -1}) { apply(GateType::S, base_qubit); } apply(GateType::H, base_qubit); sim.smooth_stabilizer_state(sim.state[0]); if (compute_occupation(sim.state) * 2 != occupation) { throw std::invalid_argument("State vector isn't a stabilizer state."); } occupation >>= 1; } recorded = circuit_inverse_unitary(recorded); if (recorded.count_qubits() < num_qubits) { recorded.safe_append_u("I", {(uint32_t)(num_qubits - 1)}); } return recorded; } std::vector> stim::circuit_to_output_state_vector(const Circuit &circuit, bool little_endian) { Tableau<64> result(circuit.count_qubits()); TableauSimulator<64> sim(std::mt19937_64(0), circuit.count_qubits()); circuit.for_each_operation([&](const CircuitInstruction &op) { const auto &flags = GATE_DATA[op.gate_type].flags; if (flags & GATE_IS_UNITARY) { sim.do_gate(op); } else if (flags & (GATE_IS_NOISY | GATE_IS_RESET | GATE_PRODUCES_RESULTS)) { throw std::invalid_argument( "The circuit has no well-defined tableau because it contains noisy or dissipative operations.\n" "The first such operation is: " + op.str()); } else { // Operation should be an annotation like TICK or DETECTOR. } }); return sim.to_state_vector(little_endian); } ================================================ FILE: src/stim/util_top/circuit_vs_amplitudes.h ================================================ #ifndef _STIM_UTIL_TOP_CIRCUIT_VS_AMPLITUDES_H #define _STIM_UTIL_TOP_CIRCUIT_VS_AMPLITUDES_H #include "stim/circuit/circuit.h" namespace stim { /// Synthesizes a circuit to generate the given state vector. /// /// Args: /// stabilizer_state_vector: The vector of amplitudes to produce using a circuit. Does not need to be a unit vector, /// but must be non-zero. /// little_endian: Whether the vector is using little endian or big endian ordering. /// inverted_circuit: If false, returns a circuit that sends |000...0> to the state vector. /// If true, returns a circuit that sends the state vector to |000...0> instead of a cir. /// /// Returns: /// A circuit that outputs the given state vector (up to global phase). /// /// Throws: /// std::invalid_argument: The given state vector cannot be produced by a stabilizer circuit. Circuit stabilizer_state_vector_to_circuit( const std::vector> &stabilizer_state_vector, bool little_endian); /// Simulates the given circuit and outputs a state vector. /// /// Args: /// circuit: The circuit to simulate. Cannot contain noisy or dissipative operations. /// little_endian: Whether the returned vector uses little endian or big endian qubit order. /// /// Returns: /// The state vector, using the requested endianness. std::vector> circuit_to_output_state_vector(const Circuit &circuit, bool little_endian); } // namespace stim #endif ================================================ FILE: src/stim/util_top/circuit_vs_amplitudes.test.cc ================================================ #include "stim/util_top/circuit_vs_amplitudes.h" #include "gtest/gtest.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST(conversions, stabilizer_state_vector_to_circuit_basic) { ASSERT_THROW(stabilizer_state_vector_to_circuit({}, false), std::invalid_argument); ASSERT_THROW( stabilizer_state_vector_to_circuit( { {0}, }, false), std::invalid_argument); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {1}, }, false), stim::Circuit(R"CIRCUIT( )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {-1}, }, false), stim::Circuit(R"CIRCUIT( )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {0, 1}, {0}, }, false), stim::Circuit(R"CIRCUIT( I 0 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {0}, {1}, }, false), stim::Circuit(R"CIRCUIT( X 0 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {sqrtf(0.5)}, {sqrtf(0.5)}, }, false), stim::Circuit(R"CIRCUIT( H 0 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {0}, {1}, {0}, {0}, }, false), stim::Circuit(R"CIRCUIT( X 1 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {0}, {0}, {1}, {0}, }, false), stim::Circuit(R"CIRCUIT( X 0 I 1 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {0}, {1}, {0}, {0}, }, true), stim::Circuit(R"CIRCUIT( X 0 I 1 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {0}, {0}, {1}, {0}, }, true), stim::Circuit(R"CIRCUIT( X 1 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {sqrtf(0.5)}, {0, sqrtf(0.5)}, }, true), stim::Circuit(R"CIRCUIT( H 0 S 0 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {sqrtf(0.5)}, {0, -sqrtf(0.5)}, }, true), stim::Circuit(R"CIRCUIT( H 0 S_DAG 0 )CIRCUIT")); ASSERT_EQ( stabilizer_state_vector_to_circuit( { {sqrtf(0.5)}, {-sqrtf(0.5)}, }, true), stim::Circuit(R"CIRCUIT( H 0 Z 0 )CIRCUIT")); } TEST(conversions, stabilizer_state_vector_to_circuit_fuzz_round_trip) { auto rng = INDEPENDENT_TEST_RNG(); for (const auto &little_endian : std::vector{false, true}) { for (size_t n = 0; n < 5; n++) { // Pick a random stabilizer state. TableauSimulator<64> sim(INDEPENDENT_TEST_RNG(), n); sim.inv_state = Tableau<64>::random(n, rng); auto desired_vec = sim.to_state_vector(little_endian); // Round trip through a circuit. auto circuit = stabilizer_state_vector_to_circuit(desired_vec, little_endian); auto actual_vec = circuit_to_output_state_vector(circuit, little_endian); ASSERT_EQ(actual_vec, desired_vec) << "little_endian=" << little_endian << ", n=" << n; } } } TEST(conversions, stabilizer_state_vector_to_circuit_unnormalized_fuzz_round_trip) { auto rng = INDEPENDENT_TEST_RNG(); auto little_endian = true; for (size_t i = 0; i < 100; i++) { // Pick a random stabilizer state. size_t n = i % 5; TableauSimulator<64> sim(INDEPENDENT_TEST_RNG(), n); sim.inv_state = Tableau<64>::random(n, rng); auto desired_vec = sim.to_state_vector(little_endian); // Unnormalize by multiplying by a random non-zero factor. auto scaled_vec = desired_vec; std::uniform_real_distribution dist(-1000.0, +1000.0); std::complex scale = {dist(rng), dist(rng)}; while (std::norm(scale) < 0.01) { scale = {dist(rng), dist(rng)}; } for (auto &c : scaled_vec) { c *= scale; } // Round trip through a circuit. auto circuit = stabilizer_state_vector_to_circuit(scaled_vec, little_endian); auto actual_vec = circuit_to_output_state_vector(circuit, little_endian); ASSERT_EQ(actual_vec, desired_vec) << " scale=" << scale; } } TEST(conversions, circuit_to_output_state_vector) { ASSERT_EQ(circuit_to_output_state_vector(Circuit(""), false), (std::vector>{{1}})); ASSERT_EQ( circuit_to_output_state_vector(Circuit("H 0 1"), false), (std::vector>{{0.5}, {0.5}, {0.5}, {0.5}})); ASSERT_EQ( circuit_to_output_state_vector(Circuit("X 1"), false), (std::vector>{{0}, {1}, {0}, {0}})); ASSERT_EQ( circuit_to_output_state_vector(Circuit("X 1"), true), (std::vector>{{0}, {0}, {1}, {0}})); } ================================================ FILE: src/stim/util_top/circuit_vs_tableau.h ================================================ #ifndef _STIM_UTIL_TOP_CIRCUIT_VS_TABLEAU_H #define _STIM_UTIL_TOP_CIRCUIT_VS_TABLEAU_H #include "stim/circuit/circuit.h" #include "stim/stabilizers/tableau.h" namespace stim { /// Compiles the given circuit into a tableau. /// /// Args: /// circuit: The circuit to compile. Should only contain unitary operations. /// ignore_noise: If the circuit contains noise channels, ignore them instead of raising an exception. /// ignore_measurement: If the circuit contains measurements, ignore them instead of raising an exception. /// ignore_reset: If the circuit contains resets, ignore them instead of raising an exception. /// inverse: The last step of the implementation is to invert the tableau. Setting this argument /// to true will skip this inversion, saving time but returning the inverse tableau. /// /// Returns: /// A tableau encoding the given circuit's Clifford operation. template Tableau circuit_to_tableau( const Circuit &circuit, bool ignore_noise, bool ignore_measurement, bool ignore_reset, bool inverse = false); /// Synthesizes a circuit that implements the given tableau's Clifford operation. /// /// This method is allowed to output different circuits, from call to call or version /// to version, for the same input tableau. /// /// Args: /// tableau: The tableau to synthesize into a circuit. /// method: The method to use when synthesizing the circuit. Available values: /// "elimination": Cancels off-diagonal terms using Gaussian elimination. /// Gate set: H, S, CX /// Circuit qubit count: n /// Circuit operation count: O(n^2) /// Circuit depth: O(n^2) /// "graph_state": Prepares the tableau's state using a graph state circuit. /// Gate set: RX, CZ, H, S, X, Y, Z /// Circuit qubit count: n /// Circuit operation count: O(n^2) /// /// The circuit will be made up of three layers: /// 1. An RX layer initializing all qubits. /// 2. A CZ layer coupling the qubits. /// an edge in the graph state.) /// 3. A single qubit rotation layer. /// /// Note: "graph_state" treats the tableau as a state instead of as a /// Clifford operation. It will preserve the set of stabilizers, but /// not the exact choice of generators. /// /// Returns: /// The synthesized circuit. template Circuit tableau_to_circuit(const Tableau &tableau, std::string_view method); template Circuit tableau_to_circuit_graph_method(const Tableau &tableau); template Circuit tableau_to_circuit_mpp_method(const Tableau &tableau, bool skip_sign); template Circuit tableau_to_circuit_elimination_method(const Tableau &tableau); } // namespace stim #include "stim/util_top/circuit_vs_tableau.inl" #endif ================================================ FILE: src/stim/util_top/circuit_vs_tableau.inl ================================================ #include "stim/simulators/graph_simulator.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_top/circuit_vs_tableau.h" namespace stim { template Tableau circuit_to_tableau( const Circuit &circuit, bool ignore_noise, bool ignore_measurement, bool ignore_reset, bool inverse) { Tableau result(circuit.count_qubits()); TableauSimulator sim(std::mt19937_64(0), circuit.count_qubits()); circuit.for_each_operation([&](const CircuitInstruction &op) { const auto &flags = GATE_DATA[op.gate_type].flags; if (!ignore_measurement && (flags & GATE_PRODUCES_RESULTS)) { throw std::invalid_argument( "The circuit has no well-defined tableau because it contains measurement operations.\n" "To ignore measurement operations, pass the argument ignore_measurement=True.\n" "The first measurement operation is: " + op.str()); } if (!ignore_reset && (flags & GATE_IS_RESET)) { throw std::invalid_argument( "The circuit has no well-defined tableau because it contains reset operations.\n" "To ignore reset operations, pass the argument ignore_reset=True.\n" "The first reset operation is: " + op.str()); } if (!ignore_noise && (flags & GATE_IS_NOISY)) { for (const auto &f : op.args) { if (f > 0) { throw std::invalid_argument( "The circuit has no well-defined tableau because it contains noisy operations.\n" "To ignore noisy operations, pass the argument ignore_noise=True.\n" "The first noisy operation is: " + op.str()); } } } if (flags & GATE_IS_UNITARY) { sim.do_gate(op); } }); if (!inverse) { return sim.inv_state.inverse(); } return sim.inv_state; } template Circuit tableau_to_circuit(const Tableau &tableau, std::string_view method) { if (method == "elimination") { return tableau_to_circuit_elimination_method(tableau); } else if (method == "graph_state") { return tableau_to_circuit_graph_method(tableau); } else if (method == "mpp_state") { return tableau_to_circuit_mpp_method(tableau, false); } else if (method == "mpp_state_unsigned") { return tableau_to_circuit_mpp_method(tableau, true); } else { std::stringstream ss; ss << "Unknown method: '" << method << "'. Known methods:\n"; ss << " - 'elimination'\n"; ss << " - 'graph_state'\n"; ss << " - 'mpp_state'\n"; ss << " - 'mpp_state_unsigned'\n"; throw std::invalid_argument(ss.str()); } } template Circuit tableau_to_circuit_graph_method(const Tableau &tableau) { GraphSimulator sim(tableau.num_qubits); sim.do_circuit(tableau_to_circuit_elimination_method(tableau)); return sim.to_circuit(true); } template Circuit tableau_to_circuit_mpp_method(const Tableau &tableau, bool skip_sign) { Circuit result; std::vector targets; size_t n = tableau.num_qubits; // Measure each stabilizer with MPP. for (size_t k = 0; k < n; k++) { const auto &stabilizer = tableau.zs[k]; bool need_sign = stabilizer.sign; for (size_t q = 0; q < n; q++) { bool x = stabilizer.xs[q]; bool z = stabilizer.zs[q]; if (x || z) { targets.push_back(GateTarget::pauli_xz(q, x, z, need_sign)); targets.push_back(GateTarget::combiner()); need_sign = false; } } assert(!targets.empty()); targets.pop_back(); result.safe_append(CircuitInstruction(GateType::MPP, {}, targets, "")); targets.clear(); } if (!skip_sign) { // Correct each stabilizer's sign with feedback. std::vector targets_x; std::vector targets_y; std::vector targets_z; std::array *, 4> targets_ptrs = {nullptr, &targets_x, &targets_z, &targets_y}; for (size_t k = 0; k < n; k++) { const auto &destabilizer = tableau.xs[k]; for (size_t q = 0; q < n; q++) { bool x = destabilizer.xs[q]; bool z = destabilizer.zs[q]; auto *out = targets_ptrs[x + z * 2]; if (out != nullptr) { out->push_back(GateTarget::rec(-(int32_t)(n - k))); out->push_back(GateTarget::qubit(q)); } } } if (!targets_x.empty()) { result.safe_append(CircuitInstruction(GateType::CX, {}, targets_x, "")); } if (!targets_y.empty()) { result.safe_append(CircuitInstruction(GateType::CY, {}, targets_y, "")); } if (!targets_z.empty()) { result.safe_append(CircuitInstruction(GateType::CZ, {}, targets_z, "")); } } return result; } template Circuit tableau_to_circuit_elimination_method(const Tableau &tableau) { Tableau remaining = tableau.inverse(); Circuit recorded_circuit; auto apply = [&](GateType gate_type, uint32_t target) { remaining.inplace_scatter_append(GATE_DATA[gate_type].tableau(), {target}); recorded_circuit.safe_append( CircuitInstruction(gate_type, {}, std::vector{GateTarget::qubit(target)}, "")); }; auto apply2 = [&](GateType gate_type, uint32_t target, uint32_t target2) { remaining.inplace_scatter_append(GATE_DATA[gate_type].tableau(), {target, target2}); recorded_circuit.safe_append(CircuitInstruction( gate_type, {}, std::vector{GateTarget::qubit(target), GateTarget::qubit(target2)}, "")); }; auto x_out = [&](size_t inp, size_t out) { const auto &p = remaining.xs[inp]; return p.xs[out] + 2 * p.zs[out]; }; auto z_out = [&](size_t inp, size_t out) { const auto &p = remaining.zs[inp]; return p.xs[out] + 2 * p.zs[out]; }; size_t n = remaining.num_qubits; for (size_t col = 0; col < n; col++) { // Find a cell with an anti-commuting pair of Paulis. size_t pivot_row; for (pivot_row = col; pivot_row < n; pivot_row++) { int px = x_out(col, pivot_row); int pz = z_out(col, pivot_row); if (px && pz && px != pz) { break; } } assert(pivot_row < n); // Ensured by unitarity of the tableau. // Move the pivot to the diagonal. if (pivot_row != col) { apply2(GateType::CX, pivot_row, col); apply2(GateType::CX, col, pivot_row); apply2(GateType::CX, pivot_row, col); } // Transform the pivot to XZ. if (z_out(col, col) == 3) { apply(GateType::S, col); } if (z_out(col, col) != 2) { apply(GateType::H, col); } if (x_out(col, col) != 1) { apply(GateType::S, col); } // Use the pivot to remove all other terms in the X observable. for (size_t row = col + 1; row < n; row++) { if (x_out(col, row) == 3) { apply(GateType::S, row); } } for (size_t row = col + 1; row < n; row++) { if (x_out(col, row) == 2) { apply(GateType::H, row); } } for (size_t row = col + 1; row < n; row++) { if (x_out(col, row)) { apply2(GateType::CX, col, row); } } // Use the pivot to remove all other terms in the Z observable. for (size_t row = col + 1; row < n; row++) { if (z_out(col, row) == 3) { apply(GateType::S, row); } } for (size_t row = col + 1; row < n; row++) { if (z_out(col, row) == 1) { apply(GateType::H, row); } } for (size_t row = col + 1; row < n; row++) { if (z_out(col, row)) { apply2(GateType::CX, row, col); } } } // Fix pauli signs. simd_bits signs_copy = remaining.zs.signs; for (size_t col = 0; col < n; col++) { if (signs_copy[col]) { apply(GateType::H, col); } } for (size_t col = 0; col < n; col++) { if (signs_copy[col]) { apply(GateType::S, col); apply(GateType::S, col); } } for (size_t col = 0; col < n; col++) { if (signs_copy[col]) { apply(GateType::H, col); } } for (size_t col = 0; col < n; col++) { if (remaining.xs.signs[col]) { apply(GateType::S, col); apply(GateType::S, col); } } if (recorded_circuit.count_qubits() < n) { apply(GateType::H, n - 1); apply(GateType::H, n - 1); } return recorded_circuit; } } // namespace stim ================================================ FILE: src/stim/util_top/circuit_vs_tableau.test.cc ================================================ #include "stim/util_top/circuit_vs_tableau.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(conversions, circuit_to_tableau_ignoring_gates, { Circuit unitary(R"CIRCUIT( I 0 X 0 Y 0 Z 0 C_XYZ 0 C_ZYX 0 H 0 H_XY 0 H_XZ 0 H_YZ 0 S 0 SQRT_X 0 SQRT_X_DAG 0 SQRT_Y 0 SQRT_Y_DAG 0 SQRT_Z 0 SQRT_Z_DAG 0 S_DAG 0 CNOT 0 1 CX 0 1 CY 0 1 CZ 0 1 ISWAP 0 1 ISWAP_DAG 0 1 SQRT_XX 0 1 SQRT_XX_DAG 0 1 SQRT_YY 0 1 SQRT_YY_DAG 0 1 SQRT_ZZ 0 1 SQRT_ZZ_DAG 0 1 SWAP 0 1 XCX 0 1 XCY 0 1 XCZ 0 1 YCX 0 1 YCY 0 1 YCZ 0 1 ZCX 0 1 ZCY 0 1 ZCZ 0 1 )CIRCUIT"); Circuit noise(R"CIRCUIT( CORRELATED_ERROR(0.1) X0 DEPOLARIZE1(0.1) 0 DEPOLARIZE2(0.1) 0 1 E(0.1) X0 ELSE_CORRELATED_ERROR(0.1) Y1 PAULI_CHANNEL_1(0.1,0.2,0.3) 0 PAULI_CHANNEL_2(0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01) 0 1 X_ERROR(0.1) 0 Y_ERROR(0.1) 0 Z_ERROR(0.1) 0 )CIRCUIT"); Circuit measure(R"CIRCUIT( M 0 MPP X0 MX 0 MY 0 MZ 0 )CIRCUIT"); Circuit reset(R"CIRCUIT( R 0 RX 0 RY 0 RZ 0 )CIRCUIT"); Circuit measure_reset(R"CIRCUIT( MR 0 MRX 0 MRY 0 MRZ 0 )CIRCUIT"); Circuit annotations(R"CIRCUIT( REPEAT 10 { I 0 } DETECTOR(1, 2) OBSERVABLE_INCLUDE(1) QUBIT_COORDS(0,1,2) 0 SHIFT_COORDS(2, 3, 4) TICK )CIRCUIT"); ASSERT_EQ(circuit_to_tableau(unitary, false, false, false).num_qubits, 2); ASSERT_THROW({ circuit_to_tableau(noise, false, false, false); }, std::invalid_argument); ASSERT_THROW({ circuit_to_tableau(noise, false, true, true); }, std::invalid_argument); ASSERT_EQ(circuit_to_tableau(noise, true, false, false), Tableau(2)); ASSERT_THROW({ circuit_to_tableau(measure, false, false, false); }, std::invalid_argument); ASSERT_THROW({ circuit_to_tableau(measure, true, false, true); }, std::invalid_argument); ASSERT_EQ(circuit_to_tableau(measure, false, true, false), Tableau(1)); ASSERT_THROW({ circuit_to_tableau(reset, false, false, false); }, std::invalid_argument); ASSERT_THROW({ circuit_to_tableau(reset, true, true, false); }, std::invalid_argument); ASSERT_EQ(circuit_to_tableau(reset, false, false, true), Tableau(1)); ASSERT_THROW({ circuit_to_tableau(measure_reset, false, false, false); }, std::invalid_argument); ASSERT_THROW({ circuit_to_tableau(measure_reset, true, false, true); }, std::invalid_argument); ASSERT_THROW({ circuit_to_tableau(measure_reset, true, true, false); }, std::invalid_argument); ASSERT_EQ(circuit_to_tableau(measure_reset, false, true, true), Tableau(1)); ASSERT_EQ(circuit_to_tableau(annotations, false, false, false), Tableau(1)); ASSERT_EQ( circuit_to_tableau(annotations + measure_reset + measure + reset + unitary + noise, true, true, true) .num_qubits, 2); }) TEST_EACH_WORD_SIZE_W(conversions, circuit_to_tableau, { ASSERT_EQ( circuit_to_tableau( Circuit(R"CIRCUIT( )CIRCUIT"), false, false, false), Tableau(0)); ASSERT_EQ( circuit_to_tableau( Circuit(R"CIRCUIT( REPEAT 10 { X 0 TICK } )CIRCUIT"), false, false, false), Tableau(1)); ASSERT_EQ( circuit_to_tableau( Circuit(R"CIRCUIT( REPEAT 11 { X 0 TICK } )CIRCUIT"), false, false, false), GATE_DATA.at("X").tableau()); ASSERT_EQ( circuit_to_tableau( Circuit(R"CIRCUIT( S 0 )CIRCUIT"), false, false, false), GATE_DATA.at("S").tableau()); ASSERT_EQ( circuit_to_tableau( Circuit(R"CIRCUIT( SQRT_Y_DAG 1 CZ 0 1 SQRT_Y 1 )CIRCUIT"), false, false, false), GATE_DATA.at("CX").tableau()); ASSERT_EQ( circuit_to_tableau( Circuit(R"CIRCUIT( R 0 X_ERROR(0.1) 0 SQRT_Y_DAG 1 CZ 0 1 SQRT_Y 1 M 0 )CIRCUIT"), true, true, true), GATE_DATA.at("CX").tableau()); }) TEST_EACH_WORD_SIZE_W(conversions, tableau_to_circuit_fuzz_vs_circuit_to_tableau, { auto rng = INDEPENDENT_TEST_RNG(); for (size_t n = 0; n < 10; n++) { auto desired = Tableau::random(n, rng); Circuit circuit = tableau_to_circuit(desired, "elimination"); auto actual = circuit_to_tableau(circuit, false, false, false); ASSERT_EQ(actual, desired); for (const auto &op : circuit.operations) { ASSERT_TRUE(op.gate_type == GateType::S || op.gate_type == GateType::H || op.gate_type == GateType::CX) << op; } } }) TEST_EACH_WORD_SIZE_W(conversions, tableau_to_circuit, { ASSERT_EQ(tableau_to_circuit(GATE_DATA.at("I").tableau(), "elimination"), Circuit(R"CIRCUIT( H 0 H 0 )CIRCUIT")); ASSERT_EQ(tableau_to_circuit(GATE_DATA.at("X").tableau(), "elimination"), Circuit(R"CIRCUIT( H 0 S 0 S 0 H 0 )CIRCUIT")); ASSERT_EQ(tableau_to_circuit(GATE_DATA.at("S").tableau(), "elimination"), Circuit(R"CIRCUIT( S 0 )CIRCUIT")); ASSERT_EQ(tableau_to_circuit(GATE_DATA.at("ISWAP").tableau(), "elimination"), Circuit(R"CIRCUIT( CX 1 0 0 1 1 0 S 0 H 1 CX 0 1 H 1 S 1 )CIRCUIT")); }) TEST_EACH_WORD_SIZE_W(conversions, fuzz_mpp_circuit_produces_correct_state, { auto rng = INDEPENDENT_TEST_RNG(); auto tableau = Tableau::random(10, rng); auto circuit = tableau_to_circuit_mpp_method(tableau, false); TableauSimulator sim(std::move(rng), 10); sim.safe_do_circuit(circuit); auto expected = tableau.stabilizers(true); auto actual = sim.canonical_stabilizers(); ASSERT_EQ(actual, expected); }) TEST_EACH_WORD_SIZE_W(conversions, fuzz_mpp_circuit_produces_correct_state_unsigned, { auto rng = INDEPENDENT_TEST_RNG(); auto tableau = Tableau::random(10, rng); auto circuit = tableau_to_circuit_mpp_method(tableau, true); TableauSimulator sim(std::move(rng), 10); sim.safe_do_circuit(circuit); auto expected = tableau.stabilizers(true); auto actual = sim.canonical_stabilizers(); for (auto &e : expected) { e.sign = false; } for (auto &e : actual) { e.sign = false; } ASSERT_EQ(actual, expected); }) TEST_EACH_WORD_SIZE_W(conversions, perfect_code_mpp_circuit, { Tableau tableau(5); tableau.zs[0] = PauliString("XZZX_"); tableau.zs[1] = PauliString("_XZZX"); tableau.zs[2] = PauliString("X_XZZ"); tableau.zs[3] = PauliString("ZX_XZ"); tableau.zs[4] = PauliString("ZZZZZ"); tableau.xs[0] = PauliString("Z_Z__"); tableau.xs[1] = PauliString("ZZZZ_"); tableau.xs[2] = PauliString("ZZ_ZZ"); tableau.xs[3] = PauliString("_Z__Z"); tableau.xs[4] = PauliString("XXXXX"); ASSERT_TRUE(tableau.satisfies_invariants()); ASSERT_EQ(tableau_to_circuit_mpp_method(tableau, true), Circuit(R"CIRCUIT( MPP X0*Z1*Z2*X3 X1*Z2*Z3*X4 X0*X2*Z3*Z4 Z0*X1*X3*Z4 Z0*Z1*Z2*Z3*Z4 )CIRCUIT")); ASSERT_EQ(tableau_to_circuit_mpp_method(tableau, false), Circuit(R"CIRCUIT( MPP X0*Z1*Z2*X3 X1*Z2*Z3*X4 X0*X2*Z3*Z4 Z0*X1*X3*Z4 Z0*Z1*Z2*Z3*Z4 CX rec[-1] 0 rec[-1] 1 rec[-1] 2 rec[-1] 3 rec[-1] 4 CZ rec[-5] 0 rec[-5] 2 rec[-4] 0 rec[-4] 1 rec[-4] 2 rec[-4] 3 rec[-3] 0 rec[-3] 1 rec[-3] 3 rec[-3] 4 rec[-2] 1 rec[-2] 4 )CIRCUIT")); }) ================================================ FILE: src/stim/util_top/count_determined_measurements.h ================================================ /* * 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. */ #ifndef _STIM_UTIL_TOP_COUNT_DETERMINED_MEASUREMENTS_H #define _STIM_UTIL_TOP_COUNT_DETERMINED_MEASUREMENTS_H #include "stim/circuit/circuit.h" namespace stim { template uint64_t count_determined_measurements(const Circuit &circuit, bool unknown_input = false); } // namespace stim #include "stim/util_top/count_determined_measurements.inl" #endif ================================================ FILE: src/stim/util_top/count_determined_measurements.inl ================================================ #include "stim/simulators/tableau_simulator.h" #include "stim/util_top/count_determined_measurements.h" namespace stim { template uint64_t count_determined_measurements(const Circuit &circuit, bool unknown_input) { auto n = circuit.count_qubits(); TableauSimulator sim(std::mt19937_64{0}, n); if (unknown_input) { sim.ensure_large_enough_for_qubits(2*n); for (uint32_t k = 0; k < n; k++) { std::array targets{GateTarget::qubit(k), GateTarget::qubit(k + (uint32_t)n)}; sim.do_XCX(CircuitInstruction{GateType::XCX, {}, targets, ""}); } n *= 2; } PauliString obs_buffer(n); uint64_t result = 0; circuit.for_each_operation([&](const CircuitInstruction &inst) { if (!(GATE_DATA[inst.gate_type].flags & GATE_PRODUCES_RESULTS)) { sim.do_gate(inst); return; } switch (inst.gate_type) { case GateType::M: [[fallthrough]]; case GateType::MR: { for (const auto &t : inst.targets) { assert(t.is_qubit_target()); result += sim.peek_z(t.qubit_value()) != 0; sim.do_gate(CircuitInstruction{inst.gate_type, {}, {&t}, ""}); } break; } case GateType::MX: [[fallthrough]]; case GateType::MRX: { for (const auto &t : inst.targets) { assert(t.is_qubit_target()); result += sim.peek_x(t.qubit_value()) != 0; sim.do_gate(CircuitInstruction{inst.gate_type, {}, {&t}, ""}); } break; } case GateType::MY: [[fallthrough]]; case GateType::MRY: { for (const auto &t : inst.targets) { assert(t.is_qubit_target()); result += sim.peek_y(t.qubit_value()) != 0; sim.do_gate(CircuitInstruction{inst.gate_type, {}, {&t}, ""}); } break; } case GateType::MXX: [[fallthrough]]; case GateType::MYY: [[fallthrough]]; case GateType::MZZ: { bool x = inst.gate_type != GateType::MZZ; bool z = inst.gate_type != GateType::MXX; for (size_t k = 0; k < inst.targets.size(); k += 2) { auto q0 = inst.targets[k].qubit_value(); auto q1 = inst.targets[k + 1].qubit_value(); obs_buffer.xs[q0] = x; obs_buffer.xs[q1] = x; obs_buffer.zs[q0] = z; obs_buffer.zs[q1] = z; result += sim.peek_observable_expectation(obs_buffer) != 0; obs_buffer.xs[q0] = 0; obs_buffer.xs[q1] = 0; obs_buffer.zs[q0] = 0; obs_buffer.zs[q1] = 0; sim.do_gate(CircuitInstruction{inst.gate_type, {}, inst.targets.sub(k, k + 2), ""}); } break; } case GateType::MPP: { for (size_t start = 0; start < inst.targets.size();) { size_t end = start + 1; while (end < inst.targets.size() && inst.targets[end].is_combiner()) { end += 2; } for (size_t k = start; k < end; k += 2) { auto t = inst.targets[k]; auto q = t.qubit_value(); obs_buffer.xs[q] = (bool)(t.data & TARGET_PAULI_X_BIT); obs_buffer.zs[q] = (bool)(t.data & TARGET_PAULI_Z_BIT); } result += sim.peek_observable_expectation(obs_buffer) != 0; obs_buffer.xs.clear(); obs_buffer.zs.clear(); sim.do_gate({inst.gate_type, {}, inst.targets.sub(start, end), ""}); start = end; } break; } default: throw std::invalid_argument("count_determined_measurements unhandled measurement type " + inst.str()); } }); return result; } } // namespace stim ================================================ FILE: src/stim/util_top/count_determined_measurements.test.cc ================================================ // 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. #include "stim/util_top/count_determined_measurements.h" #include "gtest/gtest.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_surface_code.h" #include "stim/mem/simd_word.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(count_determined_measurements, unknown_input, { ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MZZ 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements( Circuit(R"CIRCUIT( MZZ 0 1 )CIRCUIT"), true), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MPP Z0*Z1 X2*X3 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements( Circuit(R"CIRCUIT( MPP Z0*Z1 X2*X3 )CIRCUIT"), true), 0); ASSERT_EQ( count_determined_measurements( Circuit(R"CIRCUIT( MPP Z0*Z1 X2*X3 TICK MPP Z0*Z1 X2*X3 )CIRCUIT"), true), 2); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MPP Z0*Z1 X2*X3 TICK MPP Z0*Z1 X2*X3 )CIRCUIT")), 3); }) TEST_EACH_WORD_SIZE_W(count_determined_measurements, single_qubit_measurements_baseline, { ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MX 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MRX 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RZ 0 MX 0 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RZ 0 MRX 0 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RY 0 MY 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RY 0 MRY 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MY 0 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MRY 0 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RZ 0 MZ 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RZ 0 MRZ 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MZ 0 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MRZ 0 )CIRCUIT")), 0); }) TEST_EACH_WORD_SIZE_W(count_determined_measurements, pair_measurements_baseline, { ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 1 MXX 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RY 0 1 MXX 0 1 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RY 0 1 MYY 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 1 MYY 0 1 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RZ 0 1 MZZ 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RY 0 1 MZZ 0 1 )CIRCUIT")), 0); }) TEST_EACH_WORD_SIZE_W(count_determined_measurements, mpp_baseline, { ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MPP X0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RY 0 MPP X0 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RY 0 MPP Y0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MPP Y0 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RZ 0 MPP Z0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MPP Z0 )CIRCUIT")), 0); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 RY 1 RZ 2 MPP X0*Y1*Z2 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 RX 1 RZ 2 MPP X0*Y1*Z2 )CIRCUIT")), 0); }) TEST_EACH_WORD_SIZE_W(count_determined_measurements, converge, { ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MX 0 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MY 0 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MZ 0 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MRX 0 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MRY 0 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 MRZ 0 0 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MXX 0 1 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MYY 0 1 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 1 MZZ 0 1 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MXX 0 1 MYY 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MPP X0*X1 Y0*Y1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MPP X0*X1 X1*X2 !X0*X2 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( REPEAT 3 { MPP X0*X1 } )CIRCUIT")), 2); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MXX 0 1 MX 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( MYY 0 1 MY 0 1 )CIRCUIT")), 1); ASSERT_EQ( count_determined_measurements(Circuit(R"CIRCUIT( RX 0 1 MZZ 0 1 MZ 0 1 )CIRCUIT")), 1); }) TEST_EACH_WORD_SIZE_W(count_determined_measurements, surface_code, { CircuitGenParameters params(7, 5, "rotated_memory_x"); params.after_clifford_depolarization = 0.01; params.before_measure_flip_probability = 0; params.after_reset_flip_probability = 0; params.before_round_data_depolarization = 0; auto circuit = generate_surface_code_circuit(params).circuit; auto actual = count_determined_measurements(circuit); ASSERT_EQ(actual, circuit.count_detectors() + circuit.count_observables()); }) ================================================ FILE: src/stim/util_top/export_crumble_url.cc ================================================ #include "stim/util_top/export_crumble_url.h" #include "stim/simulators/matched_error.h" using namespace stim; void write_crumble_name_with_args(const CircuitInstruction &instruction, std::ostream &out) { if (instruction.gate_type == GateType::DETECTOR) { out << "DT"; } else if (instruction.gate_type == GateType::QUBIT_COORDS) { out << "Q"; } else if (instruction.gate_type == GateType::OBSERVABLE_INCLUDE) { out << "OI"; } else { out << GATE_DATA[instruction.gate_type].name; } if (!instruction.args.empty()) { out << '('; bool first = true; for (auto e : instruction.args) { if (first) { first = false; } else { out << ","; } if (e > (double)INT64_MIN && e < (double)INT64_MAX && (int64_t)e == e) { out << (int64_t)e; } else { out << e; } } out << ')'; } } void write_crumble_url( const Circuit &circuit, bool skip_detectors, const std::vector> &marks, std::ostream &out) { ExplainedError err; std::vector> active_marks; for (size_t k = 0; k < circuit.operations.size(); k++) { active_marks.clear(); for (const auto &mark : marks) { if (!mark.second.stack_frames.empty() && mark.second.stack_frames.back().instruction_offset == k) { active_marks.push_back(mark); active_marks.back().second.stack_frames.pop_back(); } } bool adding_markers = false; for (const auto &mark : active_marks) { adding_markers |= mark.second.stack_frames.empty(); } if (adding_markers) { out << ";TICK"; } for (const auto &mark : active_marks) { if (mark.second.stack_frames.empty()) { const auto &v1 = mark.second.flipped_pauli_product; const auto &v2 = mark.second.flipped_measurement.measured_observable; for (const auto &e : v1) { out << ";MARK" << e.gate_target.pauli_type() << "(" << mark.first << ")" << e.gate_target.qubit_value(); } if (!v2.empty()) { auto t = v2[0].gate_target; char c = "XZ"[t.pauli_type() == 'X']; out << ";MARK" << c << "(" << mark.first << ")" << v2[0].gate_target.qubit_value(); } } } if (adding_markers) { out << ";TICK"; } const auto &instruction = circuit.operations[k]; if (instruction.gate_type == GateType::DETECTOR && skip_detectors) { continue; } if (k > 0 || adding_markers) { out << ';'; } if (instruction.gate_type == GateType::REPEAT) { if (active_marks.empty()) { out << "REPEAT_" << instruction.repeat_block_rep_count() << "_{;"; write_crumble_url(instruction.repeat_block_body(circuit), skip_detectors, active_marks, out); out << ";}"; } else { std::vector> iter_marks; for (size_t k2 = 0; k2 < instruction.repeat_block_rep_count(); k2++) { iter_marks.clear(); for (const auto &mark : active_marks) { if (!mark.second.stack_frames.empty() && mark.second.stack_frames.back().iteration_index == k2) { iter_marks.push_back(mark); } } write_crumble_url(instruction.repeat_block_body(circuit), skip_detectors, iter_marks, out); } } continue; } write_crumble_name_with_args(instruction, out); for (size_t k2 = 0; k2 < instruction.targets.size(); k2++) { auto target = instruction.targets[k2]; if (target.is_combiner()) { out << '*'; k2 += 1; target = instruction.targets[k2]; } else if (k2 > 0 || instruction.args.empty()) { out << '_'; } target.write_succinct(out); } } } std::string stim::export_crumble_url( const Circuit &circuit, bool skip_detectors, const std::map> &mark) { std::vector> marks; for (const auto &[k, vs] : mark) { for (const auto &v : vs) { if (!v.circuit_error_locations.empty()) { marks.push_back({k, v.circuit_error_locations[0]}); } } } std::stringstream s; s << "https://algassert.com/crumble#circuit="; write_crumble_url(circuit, skip_detectors, marks, s); s << "_"; // This is a workaround for colab clipping a single character off the end of the URL in some contexts return s.str(); } ================================================ FILE: src/stim/util_top/export_crumble_url.h ================================================ #ifndef _STIM_UTIL_TOP_EXPORT_CRUMBLE_URL_H #define _STIM_UTIL_TOP_EXPORT_CRUMBLE_URL_H #include "stim/circuit/circuit.h" #include "stim/simulators/matched_error.h" namespace stim { std::string export_crumble_url( const Circuit &circuit, bool skip_detectors = false, const std::map> &mark = {}); } // namespace stim #endif ================================================ FILE: src/stim/util_top/export_crumble_url.test.cc ================================================ #include "stim/util_top/export_crumble_url.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/search/graphlike/algo.h" #include "stim/simulators/error_analyzer.h" #include "stim/simulators/error_matcher.h" using namespace stim; TEST(export_crumble, all_operations) { auto actual = export_crumble_url(generate_test_circuit_with_all_operations()); auto expected = "https://algassert.com/crumble#circuit=" "Q(1,2,3)0;" "I_0;" "X_1;" "Y_2;" "Z_3;" "TICK;" "C_XYZ_0;" "C_NXYZ_1;" "C_XNYZ_2;" "C_XYNZ_3;" "C_ZYX_4;" "C_NZYX_5;" "C_ZNYX_6;" "C_ZYNX_7;" "H_XY_0;" "H_1;" "H_YZ_2;" "H_NXY_3;" "H_NXZ_4;" "H_NYZ_5;" "SQRT_X_0;" "SQRT_X_DAG_1;" "SQRT_Y_2;" "SQRT_Y_DAG_3;" "S_4;" "S_DAG_5;" "TICK;" "CXSWAP_0_1;" "ISWAP_2_3;" "ISWAP_DAG_4_5;" "SWAP_6_7;" "SWAPCX_8_9;" "CZSWAP_10_11;" "SQRT_XX_0_1;" "SQRT_XX_DAG_2_3;" "SQRT_YY_4_5;" "SQRT_YY_DAG_6_7;" "SQRT_ZZ_8_9;" "SQRT_ZZ_DAG_10_11;" "II_12_13;" "XCX_0_1;" "XCY_2_3;" "XCZ_4_5;" "YCX_6_7;" "YCY_8_9;" "YCZ_10_11;" "CX_12_13;" "CY_14_15;" "CZ_16_17;" "TICK;" "E(0.01)X1_Y2_Z3;" "ELSE_CORRELATED_ERROR(0.02)X4_Y7_Z6;" "DEPOLARIZE1(0.02)0;" "DEPOLARIZE2(0.03)1_2;" "PAULI_CHANNEL_1(0.01,0.02,0.03)3;" "PAULI_CHANNEL_2(0.001,0.002,0.003,0.004,0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015)4_5;" "X_ERROR(0.01)0;" "Y_ERROR(0.02)1;" "Z_ERROR(0.03)2;" "HERALDED_ERASE(0.04)3;" "HERALDED_PAULI_CHANNEL_1(0.01,0.02,0.03,0.04)6;" "I_ERROR(0.06)7;" "II_ERROR(0.07)8_9;" "TICK;" "MPP_X0*Y1*Z2_Z0*Z1;" "SPP_X0*Y1*Z2_X3;" "SPP_DAG_X0*Y1*Z2_X2;" "TICK;" "MRX_0;" "MRY_1;" "MR_2;" "MX_3;" "MY_4;" "M_5_6;" "RX_7;" "RY_8;" "R_9;" "TICK;" "MXX_0_1_2_3;" "MYY_4_5;" "MZZ_6_7;" "TICK;" "REPEAT_3_{;" "H_0;" "CX_0_1;" "S_1;" "TICK;" "};" "TICK;" "MR_0;" "X_ERROR(0.1)0;" "MR(0.01)0;" "SHIFT_COORDS(1,2,3);" "DT(1,2,3)rec[-1];" "OI(0)rec[-1];" "MPAD_0_1_0;" "OI(1)Z2_Z3;" "TICK;" "MRX_!0;" "MY_!1;" "MZZ_!2_3;" "OI(1)rec[-1];" "MYY_!4_!5;" "MPP_X6*!Y7*Z8;" "TICK;" "CX_rec[-1]_0;" "CY_sweep[0]_1;" "CZ_2_rec[-1]" "_"; ASSERT_EQ(actual, expected); } TEST(export_crumble, graphlike_error) { Circuit circuit(R"CIRCUIT( R 0 1 2 3 X_ERROR(0.125) 0 1 M 0 1 M(0.125) 2 3 DETECTOR rec[-1] rec[-2] DETECTOR rec[-2] rec[-3] DETECTOR rec[-3] rec[-4] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); DetectorErrorModel dem = ErrorAnalyzer::circuit_to_detector_error_model(circuit, false, true, false, 1, false, false); DetectorErrorModel filter = shortest_graphlike_undetectable_logical_error(dem, false); auto error = ErrorMatcher::explain_errors_from_circuit(circuit, &filter, false); auto actual = export_crumble_url(circuit, true, {{0, error}}); auto expected = "https://algassert.com/crumble#circuit=" "R_0_1_2_3;" "TICK;" "MARKX(0)1;" "MARKX(0)0;" "TICK;" "X_ERROR(0.125)0_1;" "M_0_1;" "TICK;" "MARKX(0)2;" "MARKX(0)3;" "TICK;" "M(0.125)2_3;" "OI(0)rec[-1]" "_"; ASSERT_EQ(actual, expected); } ================================================ FILE: src/stim/util_top/export_crumble_url_pybind_test.py ================================================ import stim def test_to_crumble_url_simple(): c = stim.Circuit(""" QUBIT_COORDS(2, 1) 0 QUBIT_COORDS(2, 2) 1 H 0 TICK CX 0 1 TICK C_XYZ 0 S 1 TICK MZZ 1 0 """) assert c.to_crumble_url() == "https://algassert.com/crumble#circuit=Q(2,1)0;Q(2,2)1;H_0;TICK;CX_0_1;TICK;C_XYZ_0;S_1;TICK;MZZ_1_0_" def test_to_crumble_url_complex(): c = stim.Circuit.generated('surface_code:rotated_memory_x', distance=3, rounds=2, after_clifford_depolarization=0.001) assert 'DEPOLARIZE1' in c.to_crumble_url() def test_to_crumble_url_mark_error(): c = stim.Circuit.generated('surface_code:rotated_memory_x', distance=3, rounds=2, after_clifford_depolarization=0.001, before_round_data_depolarization=0.001) err = c.shortest_graphlike_error(canonicalize_circuit_errors=True) url = c.to_crumble_url(skip_detectors=True, mark={1: err}) assert 'MARKZ' in url assert 'DT' not in url ================================================ FILE: src/stim/util_top/export_qasm.cc ================================================ // 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. #include "stim/util_top/export_qasm.h" #include #include "stim/simulators/tableau_simulator.h" using namespace stim; struct QasmExporter { std::ostream &out; CircuitStats stats; int open_qasm_version; bool skip_dets_and_obs; simd_bits<64> reference_sample; uint64_t measurement_offset; uint64_t detector_offset; std::array qasm_names{}; std::bitset used_gates{}; std::stringstream buf_q1; std::stringstream buf_q2; std::stringstream buf_m; QasmExporter() = delete; QasmExporter(const QasmExporter &) = delete; QasmExporter(QasmExporter &&) = delete; QasmExporter(std::ostream &out, const Circuit &circuit, int open_qasm_version, bool skip_dets_and_obs) : out(out), stats(circuit.compute_stats()), open_qasm_version(open_qasm_version), skip_dets_and_obs(skip_dets_and_obs), reference_sample(stats.num_measurements), measurement_offset(0), detector_offset(0) { // Init used_gates. collect_used_gates(circuit); // Init reference_sample. if (stats.num_detectors > 0 || stats.num_observables > 0) { reference_sample = TableauSimulator<64>::reference_sample_circuit(circuit); } } void output_measurement(bool invert_measurement_result, const char *q_name, const char *m_name) { if (invert_measurement_result) { if (open_qasm_version == 3) { out << "measure " << q_name << " -> " << m_name << ";"; out << m_name << " = " << m_name << " ^ 1;"; } else { out << "x " << q_name << ";"; out << "measure " << q_name << " -> " << m_name << ";"; out << "x " << q_name << ";"; } } else { out << "measure " << q_name << " -> " << m_name << ";"; } } void output_decomposed_operation( bool invert_measurement_result, GateType g, const char *q0_name, const char *q1_name, const char *m_name) { auto q2n = [&](GateTarget t) { return t.qubit_value() == 0 ? q0_name : q1_name; }; bool first = true; for (const auto &inst : Circuit(GATE_DATA[g].h_s_cx_m_r_decomposition).operations) { switch (inst.gate_type) { case GateType::S: for (const auto &t : inst.targets) { if (!first) { out << " "; } first = false; out << "s " << q2n(t) << ";"; } break; case GateType::H: for (const auto &t : inst.targets) { if (!first) { out << " "; } first = false; out << "h " << q2n(t) << ";"; } break; case GateType::R: for (const auto &t : inst.targets) { if (!first) { out << " "; } first = false; out << "reset " << q2n(t) << ";"; } break; case GateType::CX: for (size_t k = 0; k < inst.targets.size(); k += 2) { if (!first) { out << " "; } first = false; auto t1 = inst.targets[k]; auto t2 = inst.targets[k + 1]; out << "cx " << q2n(t1) << ", " << q2n(t2) << ";"; } break; case GateType::M: for (const auto &t : inst.targets) { if (!first) { out << " "; } first = false; output_measurement(invert_measurement_result, q2n(t), m_name); } break; default: throw std::invalid_argument("Unhandled: " + inst.str()); } } } void output_decomposed_mpp_operation(const CircuitInstruction &inst) { out << "// --- begin decomposed " << inst << "\n"; decompose_mpp_operation(inst, stats.num_qubits, [&](const CircuitInstruction &inst) { output_instruction(inst); }); out << "// --- end decomposed MPP\n"; } void output_decomposed_spp_or_spp_dag_operation(const CircuitInstruction &inst) { out << "// --- begin decomposed " << inst << "\n"; decompose_spp_or_spp_dag_operation(inst, stats.num_qubits, false, [&](const CircuitInstruction &inst) { output_instruction(inst); }); out << "// --- end decomposed SPP\n"; } void output_decomposable_instruction(const CircuitInstruction &instruction, bool decompose_inline) { auto f = GATE_DATA[instruction.gate_type].flags; auto step = (f & GATE_TARGETS_PAIRS) ? 2 : 1; for (size_t k = 0; k < instruction.targets.size(); k += step) { auto t0 = instruction.targets[k]; auto t1 = instruction.targets[k + step - 1]; bool invert_measurement_result = t0.is_inverted_result_target(); if (step == 2) { invert_measurement_result ^= t1.is_inverted_result_target(); } if (decompose_inline) { buf_q1.str(""); buf_q2.str(""); buf_q1 << "q[" << t0.qubit_value() << "]"; buf_q2 << "q[" << t1.qubit_value() << "]"; if (f & GATE_PRODUCES_RESULTS) { buf_m.str(""); buf_m << "rec[" << measurement_offset << "]"; measurement_offset++; } output_decomposed_operation( invert_measurement_result, instruction.gate_type, buf_q1.str().c_str(), buf_q2.str().c_str(), buf_m.str().c_str()); out << " // decomposed " << GATE_DATA[instruction.gate_type].name << "\n"; } else { if (f & GATE_PRODUCES_RESULTS) { out << "rec[" << measurement_offset << "] = "; measurement_offset++; } out << qasm_names[(int)instruction.gate_type] << "("; out << "q[" << t0.qubit_value() << "]"; if (step == 2) { out << ", q[" << t1.qubit_value() << "]"; } out << ")"; if ((f & GATE_PRODUCES_RESULTS) && invert_measurement_result) { out << " ^ 1"; } out << ";\n"; } } } void output_two_qubit_unitary_instruction_with_possible_feedback(const CircuitInstruction &instruction) { for (size_t k = 0; k < instruction.targets.size(); k += 2) { auto t1 = instruction.targets[k]; auto t2 = instruction.targets[k + 1]; if (t1.is_qubit_target() && t2.is_qubit_target()) { out << qasm_names[(int)instruction.gate_type] << " q[" << t1.qubit_value() << "], q[" << t2.qubit_value() << "];\n"; } else if (t1.is_qubit_target() || t2.is_qubit_target()) { GateTarget control; GateTarget target; char basis; switch (instruction.gate_type) { case GateType::CX: basis = 'X'; control = t1; target = t2; break; case GateType::CY: basis = 'Y'; control = t1; target = t2; break; case GateType::CZ: basis = 'Z'; control = t1; target = t2; if (control.is_qubit_target()) { std::swap(control, target); } break; case GateType::XCZ: basis = 'X'; control = t2; target = t1; break; case GateType::YCZ: basis = 'Y'; control = t2; target = t1; break; default: throw std::invalid_argument( "Not implemented in output_two_qubit_unitary_instruction_with_possible_feedback: " + instruction.str()); } out << "if ("; if (control.is_measurement_record_target()) { if (open_qasm_version == 2) { throw std::invalid_argument( "The circuit contains feedback, but OPENQASM 2 doesn't support feedback.\n" "You can use `stim.Circuit.with_inlined_feedback` to drop feedback operations.\n" "Alternatively, pass the argument `open_qasm_version=3`."); } out << "ms[" << (measurement_offset + control.rec_offset()) << "]"; } else if (control.is_sweep_bit_target()) { if (open_qasm_version == 2) { throw std::invalid_argument( "The circuit contains sweep operation, but OPENQASM 2 doesn't support feedback.\n" "Remove these operations, or pass the argument `open_qasm_version=3`."); } out << "sweep[" << control.value() << "]"; } else { throw std::invalid_argument( "Not implemented in output_two_qubit_unitary_instruction_with_possible_feedback: " + instruction.str()); } out << ") {\n"; out << " " << basis << " q[" << target.qubit_value() << "];\n"; out << "}\n"; } } } void define_custom_single_qubit_gate(GateType g, const char *name) { const auto &gate = GATE_DATA[g]; qasm_names[(int)g] = name; if (!used_gates[(int)g]) { return; } out << "gate " << name << " q0 { U("; auto xyz = gate.to_euler_angles(); std::array angles{"0", "pi/2", "pi", "-pi/2"}; out << angles[(int)round(xyz[0] / 3.14159265359f) & 3]; out << ", " << angles[(int)round(xyz[1] / 3.14159265359f) & 3]; out << ", " << angles[(int)round(xyz[2] / 3.14159265359f) & 3]; out << ") q0; }\n"; } void define_custom_decomposed_gate(GateType g, const char *name) { const auto &gate = GATE_DATA[g]; qasm_names[(int)g] = name; if (!used_gates[(int)g]) { return; } Circuit c(gate.h_s_cx_m_r_decomposition); bool is_unitary = true; for (const auto &inst : c.operations) { is_unitary &= (GATE_DATA[inst.gate_type].flags & GATE_IS_UNITARY) != 0; } auto num_measurements = c.count_measurements(); if (is_unitary) { out << "gate " << name; out << " q0"; if (gate.flags & GateFlags::GATE_TARGETS_PAIRS) { out << ", q1"; } out << " { "; } else { if (open_qasm_version == 2) { // Have to decompose inline in the circuit. return; } out << "def " << name << "(qubit q0"; if (gate.flags & GateFlags::GATE_TARGETS_PAIRS) { out << ", qubit q1"; } out << ")"; if (num_measurements > 1) { throw std::invalid_argument("Multiple measurement gates not supported."); } else if (num_measurements == 1) { out << " -> bit { bit b; "; } else { out << " { "; } } output_decomposed_operation(false, g, "q0", "q1", "b"); if (num_measurements > 0) { out << " return b;"; } out << " }\n"; } void collect_used_gates(const Circuit &c) { for (const auto &inst : c.operations) { used_gates[(int)inst.gate_type] = true; if (inst.gate_type == GateType::REPEAT) { collect_used_gates(inst.repeat_block_body(c)); } } } void output_header() { if (open_qasm_version == 2) { out << "OPENQASM 2.0;\n"; } else { out << "OPENQASM 3.0;\n"; } } void output_storage_declarations() { if (stats.num_qubits > 0) { out << "qreg q[" << stats.num_qubits << "];\n"; } if (stats.num_measurements > 0) { out << "creg rec[" << stats.num_measurements << "];\n"; } if (stats.num_detectors > 0 && !skip_dets_and_obs) { out << "creg dets[" << stats.num_detectors << "];\n"; } if (stats.num_observables > 0 && !skip_dets_and_obs) { out << "creg obs[" << stats.num_observables << "];\n"; } if (stats.num_sweep_bits > 0) { out << "creg sweep[" << stats.num_sweep_bits << "];\n"; } out << "\n"; } void define_all_gates_and_output_gate_declarations() { if (open_qasm_version == 2) { out << "include \"qelib1.inc\";\n"; } else if (open_qasm_version == 3) { out << "include \"stdgates.inc\";\n"; } else { throw std::invalid_argument("Unrecognized open_qasm_version."); } qasm_names[(int)GateType::I] = "id"; qasm_names[(int)GateType::X] = "x"; qasm_names[(int)GateType::Y] = "y"; qasm_names[(int)GateType::Z] = "z"; qasm_names[(int)GateType::SQRT_X] = "sx"; qasm_names[(int)GateType::SQRT_X_DAG] = "sxdg"; qasm_names[(int)GateType::S] = "s"; qasm_names[(int)GateType::S_DAG] = "sdg"; qasm_names[(int)GateType::CX] = "cx"; qasm_names[(int)GateType::CY] = "cy"; qasm_names[(int)GateType::CZ] = "cz"; qasm_names[(int)GateType::SWAP] = "swap"; qasm_names[(int)GateType::H] = "h"; define_custom_single_qubit_gate(GateType::C_XYZ, "cxyz"); define_custom_single_qubit_gate(GateType::C_ZYX, "czyx"); define_custom_single_qubit_gate(GateType::C_NXYZ, "cnxyz"); define_custom_single_qubit_gate(GateType::C_XNYZ, "cxnyz"); define_custom_single_qubit_gate(GateType::C_XYNZ, "cxynz"); define_custom_single_qubit_gate(GateType::C_NZYX, "cnzyx"); define_custom_single_qubit_gate(GateType::C_ZNYX, "cznyx"); define_custom_single_qubit_gate(GateType::C_ZYNX, "czynx"); define_custom_single_qubit_gate(GateType::H_XY, "hxy"); define_custom_single_qubit_gate(GateType::H_YZ, "hyz"); define_custom_single_qubit_gate(GateType::H_NXY, "hnxy"); define_custom_single_qubit_gate(GateType::H_NXZ, "hnxz"); define_custom_single_qubit_gate(GateType::H_NYZ, "hnyz"); define_custom_single_qubit_gate(GateType::SQRT_Y, "sy"); define_custom_single_qubit_gate(GateType::SQRT_Y_DAG, "sydg"); define_custom_decomposed_gate(GateType::CXSWAP, "cxswap"); define_custom_decomposed_gate(GateType::CZSWAP, "czswap"); define_custom_decomposed_gate(GateType::ISWAP, "iswap"); define_custom_decomposed_gate(GateType::ISWAP_DAG, "iswapdg"); define_custom_decomposed_gate(GateType::SQRT_XX, "sxx"); define_custom_decomposed_gate(GateType::SQRT_XX_DAG, "sxxdg"); define_custom_decomposed_gate(GateType::SQRT_YY, "syy"); define_custom_decomposed_gate(GateType::SQRT_YY_DAG, "syydg"); define_custom_decomposed_gate(GateType::SQRT_ZZ, "szz"); define_custom_decomposed_gate(GateType::SQRT_ZZ_DAG, "szzdg"); define_custom_decomposed_gate(GateType::SWAPCX, "swapcx"); define_custom_decomposed_gate(GateType::XCX, "xcx"); define_custom_decomposed_gate(GateType::XCY, "xcy"); define_custom_decomposed_gate(GateType::XCZ, "xcz"); define_custom_decomposed_gate(GateType::YCX, "ycx"); define_custom_decomposed_gate(GateType::YCY, "ycy"); define_custom_decomposed_gate(GateType::YCZ, "ycz"); define_custom_decomposed_gate(GateType::MR, "mr"); define_custom_decomposed_gate(GateType::MRX, "mrx"); define_custom_decomposed_gate(GateType::MRY, "mry"); define_custom_decomposed_gate(GateType::MX, "mx"); define_custom_decomposed_gate(GateType::MXX, "mxx"); define_custom_decomposed_gate(GateType::MY, "my"); define_custom_decomposed_gate(GateType::MYY, "myy"); define_custom_decomposed_gate(GateType::MZZ, "mzz"); define_custom_decomposed_gate(GateType::RX, "rx"); define_custom_decomposed_gate(GateType::RY, "ry"); out << "\n"; } void output_instruction(const CircuitInstruction &instruction) { GateFlags f = GATE_DATA[instruction.gate_type].flags; switch (instruction.gate_type) { case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: // Skipped. return; case GateType::MPAD: for (const auto &t : instruction.targets) { if (open_qasm_version == 3) { out << "rec[" << measurement_offset << "] = " << t.qubit_value() << ";\n"; } else { if (t.qubit_value()) { throw std::invalid_argument( "The circuit contains a vacuous measurement with a non-zero result " "(like MPAD 1 or MPP !X1*X1) but OPENQASM 2 doesn't support classical assignment.\n" "Pass the argument `open_qasm_version=3` to fix this."); } } measurement_offset++; } return; case GateType::TICK: out << "barrier q;\n\n"; return; case GateType::M: for (const auto &t : instruction.targets) { if (t.is_inverted_result_target()) { if (open_qasm_version == 3) { out << "measure q[" << t.qubit_value() << "] -> rec[" << measurement_offset << "];"; out << "rec[" << measurement_offset << "] = rec[" << measurement_offset << "] ^ 1;"; } else { out << "x q[" << t.qubit_value() << "];"; out << "measure q[" << t.qubit_value() << "] -> rec[" << measurement_offset << "];"; out << "x q[" << t.qubit_value() << "];"; } } else { out << "measure q[" << t.qubit_value() << "] -> rec[" << measurement_offset << "];"; } out << "\n"; measurement_offset++; } return; case GateType::R: for (const auto &t : instruction.targets) { out << "reset q[" << t.qubit_value() << "];\n"; } return; case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: { if (skip_dets_and_obs) { return; } if (open_qasm_version == 2) { throw std::invalid_argument( "The circuit contains detectors or observables, but OPENQASM 2 doesn't support the operations " "needed for accumulating detector and observable values.\n" "To simply ignore detectors and observables, pass the argument `skip_dets_and_obs=True`.\n" "Alternatively, pass the argument `open_qasm_version=3`."); } if (instruction.gate_type == GateType::DETECTOR) { out << "dets[" << detector_offset << "] = "; detector_offset++; } else { out << "obs[" << (int)instruction.args[0] << "] = obs[" << (int)instruction.args[0] << "] ^ "; } int ref_value = 0; bool had_paulis = false; for (auto t : instruction.targets) { if (t.is_measurement_record_target()) { auto i = measurement_offset + t.rec_offset(); ref_value ^= reference_sample[i]; out << "rec[" << (measurement_offset + t.rec_offset()) << "] ^ "; } else if (t.is_pauli_target()) { had_paulis = true; } else { throw std::invalid_argument("Unexpected target for OBSERVABLE_INCLUDE: " + t.str()); } } out << ref_value << ";\n"; if (had_paulis) { out << "// Warning: ignored pauli terms in " << instruction << "\n"; } return; } case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::PAULI_CHANNEL_1: case GateType::PAULI_CHANNEL_2: case GateType::E: case GateType::ELSE_CORRELATED_ERROR: case GateType::HERALDED_ERASE: case GateType::HERALDED_PAULI_CHANNEL_1: throw std::invalid_argument( "The circuit contains noise, but OPENQASM 2 doesn't support noise operations.\n" "Use `stim.Circuit.without_noise` to get a version of the circuit without noise."); case GateType::MPP: output_decomposed_mpp_operation(instruction); return; case GateType::SPP: case GateType::SPP_DAG: output_decomposed_spp_or_spp_dag_operation(instruction); return; default: break; } if (f & (stim::GATE_IS_RESET | stim::GATE_PRODUCES_RESULTS)) { output_decomposable_instruction(instruction, open_qasm_version == 2); return; } if (f & stim::GATE_IS_UNITARY) { if (f & stim::GATE_IS_SINGLE_QUBIT_GATE) { for (const auto &t : instruction.targets) { assert(t.is_qubit_target()); out << qasm_names[(int)instruction.gate_type] << " q[" << t.qubit_value() << "];\n"; } return; } if (f & stim::GATE_TARGETS_PAIRS) { output_two_qubit_unitary_instruction_with_possible_feedback(instruction); return; } } throw std::invalid_argument("Not implemented in QasmExporter::output_instruction: " + instruction.str()); } }; void stim::export_open_qasm(const Circuit &circuit, std::ostream &out, int open_qasm_version, bool skip_dets_and_obs) { if (open_qasm_version != 2 && open_qasm_version != 3) { throw std::invalid_argument("Only open_qasm_version=2 and open_qasm_version=3 are supported."); } QasmExporter exporter(out, circuit, open_qasm_version, skip_dets_and_obs); exporter.output_header(); exporter.define_all_gates_and_output_gate_declarations(); exporter.output_storage_declarations(); circuit.for_each_operation([&](const CircuitInstruction &instruction) { exporter.output_instruction(instruction); }); } ================================================ FILE: src/stim/util_top/export_qasm.h ================================================ /* * 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. */ #ifndef _STIM_CIRCUIT_EXPORT_CIRCUIT_H #define _STIM_CIRCUIT_EXPORT_CIRCUIT_H #include "stim/circuit/circuit.h" namespace stim { void export_open_qasm(const Circuit &circuit, std::ostream &out, int open_qasm_version, bool skip_dets_and_obs); } // namespace stim #endif ================================================ FILE: src/stim/util_top/export_qasm.test.cc ================================================ #include "stim/util_top/export_qasm.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" #include "stim/util_top/transform_without_feedback.h" using namespace stim; TEST(export_qasm, export_open_qasm_feedback) { Circuit c(R"CIRCUIT( H 0 CX 0 1 C_XYZ 1 M 0 CX rec[-1] 1 CX sweep[5] 1 TICK H 0 )CIRCUIT"); std::stringstream out; export_open_qasm(c, out, 3, false); ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0; include "stdgates.inc"; gate cxyz q0 { U(pi/2, 0, pi/2) q0; } qreg q[2]; creg rec[1]; creg sweep[6]; h q[0]; cx q[0], q[1]; cxyz q[1]; measure q[0] -> rec[0]; if (ms[0]) { X q[1]; } if (sweep[5]) { X q[1]; } barrier q; h q[0]; )QASM"); } TEST(export_qasm, export_open_qasm_inverted_measurements) { Circuit c(R"CIRCUIT( M !0 MX !0 MXX !0 1 MPP !X0*Z1 )CIRCUIT"); std::stringstream out; export_open_qasm(c, out, 3, false); ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0; include "stdgates.inc"; def mx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; h q0; return b; } def mxx(qubit q0, qubit q1) -> bit { bit b; cx q0, q1; h q0; measure q0 -> b; h q0; cx q0, q1; return b; } qreg q[2]; creg rec[4]; measure q[0] -> rec[0];rec[0] = rec[0] ^ 1; rec[1] = mx(q[0]) ^ 1; rec[2] = mxx(q[0], q[1]) ^ 1; // --- begin decomposed MPP !X0*Z1 h q[0]; cx q[1], q[0]; measure q[0] -> rec[3];rec[3] = rec[3] ^ 1; cx q[1], q[0]; h q[0]; // --- end decomposed MPP )QASM"); } TEST(export_qasm, export_open_qasm_qec) { Circuit c(R"CIRCUIT( R 0 1 2 TICK X 0 TICK CX 0 1 TICK CX 2 1 TICK M 1 DETECTOR rec[-1] TICK R 1 TICK CX 0 1 TICK CX 2 1 TICK M 0 1 2 DETECTOR rec[-2] rec[-4] DETECTOR rec[-1] rec[-2] rec[-3] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); std::stringstream out; export_open_qasm(c, out, 3, false); ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0; include "stdgates.inc"; qreg q[3]; creg rec[4]; creg dets[3]; creg obs[1]; reset q[0]; reset q[1]; reset q[2]; barrier q; x q[0]; barrier q; cx q[0], q[1]; barrier q; cx q[2], q[1]; barrier q; measure q[1] -> rec[0]; dets[0] = rec[0] ^ 1; barrier q; reset q[1]; barrier q; cx q[0], q[1]; barrier q; cx q[2], q[1]; barrier q; measure q[0] -> rec[1]; measure q[1] -> rec[2]; measure q[2] -> rec[3]; dets[1] = rec[2] ^ rec[0] ^ 0; dets[2] = rec[3] ^ rec[2] ^ rec[1] ^ 0; obs[0] = obs[0] ^ rec[3] ^ 0; )QASM"); out.str(""); export_open_qasm(c, out, 2, true); ASSERT_EQ(out.str(), R"QASM(OPENQASM 2.0; include "qelib1.inc"; qreg q[3]; creg rec[4]; reset q[0]; reset q[1]; reset q[2]; barrier q; x q[0]; barrier q; cx q[0], q[1]; barrier q; cx q[2], q[1]; barrier q; measure q[1] -> rec[0]; barrier q; reset q[1]; barrier q; cx q[0], q[1]; barrier q; cx q[2], q[1]; barrier q; measure q[0] -> rec[1]; measure q[1] -> rec[2]; measure q[2] -> rec[3]; )QASM"); } TEST(export_qasm, export_open_qasm_mpad) { Circuit c(R"CIRCUIT( H 0 MPAD 0 1 0 M 0 )CIRCUIT"); std::stringstream out; export_open_qasm(c, out, 3, false); ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0; include "stdgates.inc"; qreg q[1]; creg rec[4]; h q[0]; rec[0] = 0; rec[1] = 1; rec[2] = 0; measure q[0] -> rec[3]; )QASM"); out.str(""); ASSERT_THROW({ export_open_qasm(c, out, 2, true); }, std::invalid_argument); c = Circuit(R"CIRCUIT( H 0 MPAD 0 0 0 M 0 )CIRCUIT"); out.str(""); export_open_qasm(c, out, 2, true); ASSERT_EQ(out.str(), R"QASM(OPENQASM 2.0; include "qelib1.inc"; qreg q[1]; creg rec[4]; h q[0]; measure q[0] -> rec[3]; )QASM"); } TEST(export_qasm, export_qasm_decomposed_operations) { Circuit c(R"CIRCUIT( R 3 RX 0 1 MX 2 TICK MXX 0 1 DETECTOR rec[-1] TICK M 2 MR 3 MRX 4 )CIRCUIT"); std::stringstream out; export_open_qasm(c, out, 3, false); ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0; include "stdgates.inc"; def mr(qubit q0) -> bit { bit b; measure q0 -> b; reset q0; return b; } def mrx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; reset q0; h q0; return b; } def mx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; h q0; return b; } def mxx(qubit q0, qubit q1) -> bit { bit b; cx q0, q1; h q0; measure q0 -> b; h q0; cx q0, q1; return b; } def rx(qubit q0) { reset q0; h q0; } qreg q[5]; creg rec[5]; creg dets[1]; reset q[3]; rx(q[0]); rx(q[1]); rec[0] = mx(q[2]); barrier q; rec[1] = mxx(q[0], q[1]); dets[0] = rec[1] ^ 0; barrier q; measure q[2] -> rec[2]; rec[3] = mr(q[3]); rec[4] = mrx(q[4]); )QASM"); out.str(""); export_open_qasm(c, out, 2, true); ASSERT_EQ(out.str(), R"QASM(OPENQASM 2.0; include "qelib1.inc"; qreg q[5]; creg rec[5]; reset q[3]; reset q[0]; h q[0]; // decomposed RX reset q[1]; h q[1]; // decomposed RX h q[2]; measure q[2] -> rec[0]; h q[2]; // decomposed MX barrier q; cx q[0], q[1]; h q[0]; measure q[0] -> rec[1]; h q[0]; cx q[0], q[1]; // decomposed MXX barrier q; measure q[2] -> rec[2]; measure q[3] -> rec[3]; reset q[3]; // decomposed MR h q[4]; measure q[4] -> rec[4]; reset q[4]; h q[4]; // decomposed MRX )QASM"); } TEST(export_qasm, export_qasm_all_operations_v3) { Circuit c = generate_test_circuit_with_all_operations(); c = c.without_noise(); std::stringstream out; export_open_qasm(c, out, 3, false); ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0; include "stdgates.inc"; gate cxyz q0 { U(pi/2, 0, pi/2) q0; } gate czyx q0 { U(pi/2, pi/2, pi/2) q0; } gate cnxyz q0 { U(pi/2, pi/2, pi/2) q0; } gate cxnyz q0 { U(pi/2, 0, -pi/2) q0; } gate cxynz q0 { U(pi/2, pi/2, -pi/2) q0; } gate cnzyx q0 { U(pi/2, -pi/2, 0) q0; } gate cznyx q0 { U(pi/2, -pi/2, pi/2) q0; } gate czynx q0 { U(pi/2, pi/2, 0) q0; } gate hxy q0 { U(pi/2, 0, pi/2) q0; } gate hyz q0 { U(pi/2, pi/2, pi/2) q0; } gate hnxy q0 { U(pi/2, 0, -pi/2) q0; } gate hnxz q0 { U(pi/2, pi/2, 0) q0; } gate hnyz q0 { U(pi/2, -pi/2, -pi/2) q0; } gate sy q0 { U(pi/2, 0, 0) q0; } gate sydg q0 { U(pi/2, pi/2, pi/2) q0; } gate cxswap q0, q1 { cx q1, q0; cx q0, q1; } gate czswap q0, q1 { h q0; cx q0, q1; cx q1, q0; h q1; } gate iswap q0, q1 { h q0; cx q0, q1; cx q1, q0; h q1; s q1; s q0; } gate iswapdg q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q1; cx q1, q0; cx q0, q1; h q0; } gate sxx q0, q1 { h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; } gate sxxdg q0, q1 { h q0; cx q0, q1; h q1; s q0; s q0; s q0; s q1; s q1; s q1; h q0; h q1; } gate syy q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; s q0; s q1; } gate syydg q0, q1 { s q0; s q0; s q0; s q1; h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; s q0; s q1; s q1; s q1; } gate szz q0, q1 { h q1; cx q0, q1; h q1; s q0; s q1; } gate szzdg q0, q1 { h q1; cx q0, q1; h q1; s q0; s q0; s q0; s q1; s q1; s q1; } gate swapcx q0, q1 { cx q0, q1; cx q1, q0; } gate xcx q0, q1 { h q0; cx q0, q1; h q0; } gate xcy q0, q1 { h q0; s q1; s q1; s q1; cx q0, q1; h q0; s q1; } gate xcz q0, q1 { cx q1, q0; } gate ycx q0, q1 { s q0; s q0; s q0; h q1; cx q1, q0; s q0; h q1; } gate ycy q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q0; cx q0, q1; h q0; s q0; s q1; } gate ycz q0, q1 { s q0; s q0; s q0; cx q1, q0; s q0; } def mr(qubit q0) -> bit { bit b; measure q0 -> b; reset q0; return b; } def mrx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; reset q0; h q0; return b; } def mry(qubit q0) -> bit { bit b; s q0; s q0; s q0; h q0; measure q0 -> b; reset q0; h q0; s q0; return b; } def mx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; h q0; return b; } def mxx(qubit q0, qubit q1) -> bit { bit b; cx q0, q1; h q0; measure q0 -> b; h q0; cx q0, q1; return b; } def my(qubit q0) -> bit { bit b; s q0; s q0; s q0; h q0; measure q0 -> b; h q0; s q0; return b; } def myy(qubit q0, qubit q1) -> bit { bit b; s q0; s q1; cx q0, q1; h q0; measure q0 -> b; s q1; s q1; h q0; cx q0, q1; s q0; s q1; return b; } def mzz(qubit q0, qubit q1) -> bit { bit b; cx q0, q1; measure q1 -> b; cx q0, q1; return b; } def rx(qubit q0) { reset q0; h q0; } def ry(qubit q0) { reset q0; h q0; s q0; } qreg q[18]; creg rec[25]; creg dets[1]; creg obs[2]; creg sweep[1]; id q[0]; x q[1]; y q[2]; z q[3]; barrier q; cxyz q[0]; cnxyz q[1]; cxnyz q[2]; cxynz q[3]; czyx q[4]; cnzyx q[5]; cznyx q[6]; czynx q[7]; hxy q[0]; h q[1]; hyz q[2]; hnxy q[3]; hnxz q[4]; hnyz q[5]; sx q[0]; sxdg q[1]; sy q[2]; sydg q[3]; s q[4]; sdg q[5]; barrier q; cxswap q[0], q[1]; iswap q[2], q[3]; iswapdg q[4], q[5]; swap q[6], q[7]; swapcx q[8], q[9]; czswap q[10], q[11]; sxx q[0], q[1]; sxxdg q[2], q[3]; syy q[4], q[5]; syydg q[6], q[7]; szz q[8], q[9]; szzdg q[10], q[11]; xcx q[0], q[1]; xcy q[2], q[3]; xcz q[4], q[5]; ycx q[6], q[7]; ycy q[8], q[9]; ycz q[10], q[11]; cx q[12], q[13]; cy q[14], q[15]; cz q[16], q[17]; barrier q; rec[0] = 0; rec[1] = 0; barrier q; // --- begin decomposed MPP X0*Y1*Z2 Z0*Z1 h q[0]; hyz q[1]; cx q[1], q[0]; cx q[2], q[0]; measure q[0] -> rec[2]; cx q[1], q[0]; cx q[2], q[0]; hyz q[1]; h q[0]; cx q[1], q[0]; measure q[0] -> rec[3]; cx q[1], q[0]; // --- end decomposed MPP // --- begin decomposed SPP X0*Y1*Z2 X3 h q[0]; hyz q[1]; cx q[1], q[0]; cx q[2], q[0]; s q[0]; cx q[1], q[0]; cx q[2], q[0]; hyz q[1]; h q[0]; h q[3]; s q[3]; h q[3]; // --- end decomposed SPP // --- begin decomposed SPP_DAG X0*Y1*Z2 X2 h q[0]; hyz q[1]; cx q[1], q[0]; cx q[2], q[0]; sdg q[0]; cx q[1], q[0]; cx q[2], q[0]; hyz q[1]; h q[0]; h q[2]; sdg q[2]; h q[2]; // --- end decomposed SPP barrier q; rec[4] = mrx(q[0]); rec[5] = mry(q[1]); rec[6] = mr(q[2]); rec[7] = mx(q[3]); rec[8] = my(q[4]); measure q[5] -> rec[9]; measure q[6] -> rec[10]; rx(q[7]); ry(q[8]); reset q[9]; barrier q; rec[11] = mxx(q[0], q[1]); rec[12] = mxx(q[2], q[3]); rec[13] = myy(q[4], q[5]); rec[14] = mzz(q[6], q[7]); barrier q; h q[0]; cx q[0], q[1]; s q[1]; barrier q; h q[0]; cx q[0], q[1]; s q[1]; barrier q; h q[0]; cx q[0], q[1]; s q[1]; barrier q; barrier q; rec[15] = mr(q[0]); rec[16] = mr(q[0]); dets[0] = rec[16] ^ 0; obs[0] = obs[0] ^ rec[16] ^ 0; rec[17] = 0; rec[18] = 1; rec[19] = 0; obs[1] = obs[1] ^ 0; // Warning: ignored pauli terms in OBSERVABLE_INCLUDE(1) Z2 Z3 barrier q; rec[20] = mrx(q[0]) ^ 1; rec[21] = my(q[1]) ^ 1; rec[22] = mzz(q[2], q[3]) ^ 1; obs[1] = obs[1] ^ rec[22] ^ 1; rec[23] = myy(q[4], q[5]); // --- begin decomposed MPP X6*!Y7*Z8 h q[6]; hyz q[7]; cx q[7], q[6]; cx q[8], q[6]; measure q[6] -> rec[24];rec[24] = rec[24] ^ 1; cx q[7], q[6]; cx q[8], q[6]; hyz q[7]; h q[6]; // --- end decomposed MPP barrier q; if (ms[24]) { X q[0]; } if (sweep[0]) { Y q[1]; } if (ms[24]) { Z q[2]; } )QASM"); } TEST(export_qasm, export_qasm_all_operations_v2) { Circuit c = generate_test_circuit_with_all_operations(); c = c.without_noise(); std::stringstream out; c = circuit_with_inlined_feedback(c); for (size_t k = 0; k < c.operations.size(); k++) { bool drop = false; for (auto t : c.operations[k].targets) { drop |= t.is_sweep_bit_target(); drop |= c.operations[k].gate_type == GateType::MPAD && t.qubit_value() > 0; } if (drop) { c.operations.erase(c.operations.begin() + k); k--; } } export_open_qasm(c, out, 2, true); ASSERT_EQ(out.str(), R"QASM(OPENQASM 2.0; include "qelib1.inc"; gate cxyz q0 { U(pi/2, 0, pi/2) q0; } gate czyx q0 { U(pi/2, pi/2, pi/2) q0; } gate cnxyz q0 { U(pi/2, pi/2, pi/2) q0; } gate cxnyz q0 { U(pi/2, 0, -pi/2) q0; } gate cxynz q0 { U(pi/2, pi/2, -pi/2) q0; } gate cnzyx q0 { U(pi/2, -pi/2, 0) q0; } gate cznyx q0 { U(pi/2, -pi/2, pi/2) q0; } gate czynx q0 { U(pi/2, pi/2, 0) q0; } gate hxy q0 { U(pi/2, 0, pi/2) q0; } gate hyz q0 { U(pi/2, pi/2, pi/2) q0; } gate hnxy q0 { U(pi/2, 0, -pi/2) q0; } gate hnxz q0 { U(pi/2, pi/2, 0) q0; } gate hnyz q0 { U(pi/2, -pi/2, -pi/2) q0; } gate sy q0 { U(pi/2, 0, 0) q0; } gate sydg q0 { U(pi/2, pi/2, pi/2) q0; } gate cxswap q0, q1 { cx q1, q0; cx q0, q1; } gate czswap q0, q1 { h q0; cx q0, q1; cx q1, q0; h q1; } gate iswap q0, q1 { h q0; cx q0, q1; cx q1, q0; h q1; s q1; s q0; } gate iswapdg q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q1; cx q1, q0; cx q0, q1; h q0; } gate sxx q0, q1 { h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; } gate sxxdg q0, q1 { h q0; cx q0, q1; h q1; s q0; s q0; s q0; s q1; s q1; s q1; h q0; h q1; } gate syy q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; s q0; s q1; } gate syydg q0, q1 { s q0; s q0; s q0; s q1; h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; s q0; s q1; s q1; s q1; } gate szz q0, q1 { h q1; cx q0, q1; h q1; s q0; s q1; } gate szzdg q0, q1 { h q1; cx q0, q1; h q1; s q0; s q0; s q0; s q1; s q1; s q1; } gate swapcx q0, q1 { cx q0, q1; cx q1, q0; } gate xcx q0, q1 { h q0; cx q0, q1; h q0; } gate xcy q0, q1 { h q0; s q1; s q1; s q1; cx q0, q1; h q0; s q1; } gate xcz q0, q1 { cx q1, q0; } gate ycx q0, q1 { s q0; s q0; s q0; h q1; cx q1, q0; s q0; h q1; } gate ycy q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q0; cx q0, q1; h q0; s q0; s q1; } gate ycz q0, q1 { s q0; s q0; s q0; cx q1, q0; s q0; } qreg q[18]; creg rec[22]; id q[0]; x q[1]; y q[2]; z q[3]; barrier q; cxyz q[0]; cnxyz q[1]; cxnyz q[2]; cxynz q[3]; czyx q[4]; cnzyx q[5]; cznyx q[6]; czynx q[7]; hxy q[0]; h q[1]; hyz q[2]; hnxy q[3]; hnxz q[4]; hnyz q[5]; sx q[0]; sxdg q[1]; sy q[2]; sydg q[3]; s q[4]; sdg q[5]; barrier q; cxswap q[0], q[1]; iswap q[2], q[3]; iswapdg q[4], q[5]; swap q[6], q[7]; swapcx q[8], q[9]; czswap q[10], q[11]; sxx q[0], q[1]; sxxdg q[2], q[3]; syy q[4], q[5]; syydg q[6], q[7]; szz q[8], q[9]; szzdg q[10], q[11]; xcx q[0], q[1]; xcy q[2], q[3]; xcz q[4], q[5]; ycx q[6], q[7]; ycy q[8], q[9]; ycz q[10], q[11]; cx q[12], q[13]; cy q[14], q[15]; cz q[16], q[17]; barrier q; barrier q; // --- begin decomposed MPP X0*Y1*Z2 Z0*Z1 h q[0]; hyz q[1]; cx q[1], q[0]; cx q[2], q[0]; measure q[0] -> rec[2]; cx q[1], q[0]; cx q[2], q[0]; hyz q[1]; h q[0]; cx q[1], q[0]; measure q[0] -> rec[3]; cx q[1], q[0]; // --- end decomposed MPP // --- begin decomposed SPP X0*Y1*Z2 X3 h q[0]; hyz q[1]; cx q[1], q[0]; cx q[2], q[0]; s q[0]; cx q[1], q[0]; cx q[2], q[0]; hyz q[1]; h q[0]; h q[3]; s q[3]; h q[3]; // --- end decomposed SPP // --- begin decomposed SPP_DAG X0*Y1*Z2 X2 h q[0]; hyz q[1]; cx q[1], q[0]; cx q[2], q[0]; sdg q[0]; cx q[1], q[0]; cx q[2], q[0]; hyz q[1]; h q[0]; h q[2]; sdg q[2]; h q[2]; // --- end decomposed SPP barrier q; h q[0]; measure q[0] -> rec[4]; reset q[0]; h q[0]; // decomposed MRX s q[1]; s q[1]; s q[1]; h q[1]; measure q[1] -> rec[5]; reset q[1]; h q[1]; s q[1]; // decomposed MRY measure q[2] -> rec[6]; reset q[2]; // decomposed MR h q[3]; measure q[3] -> rec[7]; h q[3]; // decomposed MX s q[4]; s q[4]; s q[4]; h q[4]; measure q[4] -> rec[8]; h q[4]; s q[4]; // decomposed MY measure q[5] -> rec[9]; measure q[6] -> rec[10]; reset q[7]; h q[7]; // decomposed RX reset q[8]; h q[8]; s q[8]; // decomposed RY reset q[9]; barrier q; cx q[0], q[1]; h q[0]; measure q[0] -> rec[11]; h q[0]; cx q[0], q[1]; // decomposed MXX cx q[2], q[3]; h q[2]; measure q[2] -> rec[12]; h q[2]; cx q[2], q[3]; // decomposed MXX s q[4]; s q[5]; cx q[4], q[5]; h q[4]; measure q[4] -> rec[13]; s q[5]; s q[5]; h q[4]; cx q[4], q[5]; s q[4]; s q[5]; // decomposed MYY cx q[6], q[7]; measure q[7] -> rec[14]; cx q[6], q[7]; // decomposed MZZ barrier q; h q[0]; cx q[0], q[1]; s q[1]; barrier q; h q[0]; cx q[0], q[1]; s q[1]; barrier q; h q[0]; cx q[0], q[1]; s q[1]; barrier q; barrier q; measure q[0] -> rec[15]; reset q[0]; // decomposed MR measure q[0] -> rec[16]; reset q[0]; // decomposed MR barrier q; h q[0]; x q[0];measure q[0] -> rec[17];x q[0]; reset q[0]; h q[0]; // decomposed MRX s q[1]; s q[1]; s q[1]; h q[1]; x q[1];measure q[1] -> rec[18];x q[1]; h q[1]; s q[1]; // decomposed MY cx q[2], q[3]; x q[3];measure q[3] -> rec[19];x q[3]; cx q[2], q[3]; // decomposed MZZ s q[4]; s q[5]; cx q[4], q[5]; h q[4]; measure q[4] -> rec[20]; s q[5]; s q[5]; h q[4]; cx q[4], q[5]; s q[4]; s q[5]; // decomposed MYY // --- begin decomposed MPP X6*!Y7*Z8 h q[6]; hyz q[7]; cx q[7], q[6]; cx q[8], q[6]; x q[6];measure q[6] -> rec[21];x q[6]; cx q[7], q[6]; cx q[8], q[6]; hyz q[7]; h q[6]; // --- end decomposed MPP barrier q; )QASM"); } ================================================ FILE: src/stim/util_top/export_qasm_pybind_test.py ================================================ import pytest import stim def test_to_qasm_exact_strings(): c = stim.Circuit(""" RX 0 1 TICK H 1 CX 0 1 TICK M 0 1 DETECTOR rec[-1] rec[-2] C_XYZ 0 """) assert c.to_qasm(open_qasm_version=3).strip() == """ OPENQASM 3.0; include "stdgates.inc"; gate cxyz q0 { U(pi/2, 0, pi/2) q0; } def rx(qubit q0) { reset q0; h q0; } qreg q[2]; creg rec[2]; creg dets[1]; rx(q[0]); rx(q[1]); barrier q; h q[1]; cx q[0], q[1]; barrier q; measure q[0] -> rec[0]; measure q[1] -> rec[1]; dets[0] = rec[1] ^ rec[0] ^ 0; cxyz q[0]; """.strip() assert c.to_qasm(open_qasm_version=2, skip_dets_and_obs=True).strip() == """ OPENQASM 2.0; include "qelib1.inc"; gate cxyz q0 { U(pi/2, 0, pi/2) q0; } qreg q[2]; creg rec[2]; reset q[0]; h q[0]; // decomposed RX reset q[1]; h q[1]; // decomposed RX barrier q; h q[1]; cx q[0], q[1]; barrier q; measure q[0] -> rec[0]; measure q[1] -> rec[1]; cxyz q[0]; """.strip() def test_to_qasm2_runs_in_qiskit(): pytest.importorskip("qiskit") pytest.importorskip("qiskit_aer") import qiskit import qiskit_aer stim_circuit = stim.Circuit(""" R 0 1 MZZ !0 1 MPAD 0 0 """) qasm = stim_circuit.to_qasm(open_qasm_version=2) qiskit_circuit = qiskit.QuantumCircuit.from_qasm_str(qasm) counts = qiskit_aer.AerSimulator().run(qiskit_circuit, shots=8).result().get_counts(qiskit_circuit) assert counts['001'] == 8 def test_to_qasm3_parses_in_qiskit(): pytest.importorskip("qiskit") pytest.importorskip("qiskit_qasm3_import") import qiskit.qasm3 # Note: can't really exercise this because currently `qiskit.qasm3.loads` # fails on subroutines, classical assignments, and all the other non-qasm-2 # stuff that's actually worth testing. stim_circuit = stim.Circuit(""" R 0 1 SQRT_XX 0 1 M 0 1 """) qiskit_circuit = qiskit.qasm3.loads(stim_circuit.to_qasm(open_qasm_version=3)) assert qiskit_circuit is not None ================================================ FILE: src/stim/util_top/export_quirk_url.cc ================================================ #include "stim/util_top/export_quirk_url.h" #include "stim/circuit/gate_decomposition.h" using namespace stim; static void for_each_target_group( CircuitInstruction instruction, const std::function &callback) { Gate g = GATE_DATA[instruction.gate_type]; if (g.flags & GATE_TARGETS_COMBINERS) { return for_each_combined_targets_group(instruction, callback); } else if (g.flags & GATE_TARGETS_PAIRS) { for (size_t k = 0; k < instruction.targets.size(); k += 2) { callback({instruction.gate_type, instruction.args, instruction.targets.sub(k, k + 2), instruction.tag}); } } else if (g.flags & GATE_IS_SINGLE_QUBIT_GATE) { for (GateTarget t : instruction.targets) { callback({instruction.gate_type, instruction.args, &t, instruction.tag}); } } else { callback(instruction); } } struct QuirkExporter { size_t num_qubits; size_t col_offset = 0; std::array used{}; std::array stim_name_to_quirk_name{}; std::array, NUM_DEFINED_GATES> control_target_type{}; std::array phase_type{}; std::array custom_gate_definition{}; std::map> cols; QuirkExporter(size_t num_qubits) : num_qubits(num_qubits) { custom_gate_definition[(int)GateType::H_XY] = R"URL({"id":"~Hxy","name":"Hxy","matrix":"{{0,-√½-√½i},{√½-√½i,0}}"})URL"; custom_gate_definition[(int)GateType::H_YZ] = R"URL({"id":"~Hyz","name":"Hyz","matrix":"{{-√½i,-√½},{√½,√½i}}"})URL"; custom_gate_definition[(int)GateType::H_NXY] = R"URL({"id":"~Hnxy","name":"Hnxy","matrix":"{{0,√½+√½i},{√½-√½i,0}}"})URL"; custom_gate_definition[(int)GateType::H_NXZ] = R"URL({"id":"~Hnxz","name":"Hnxz","matrix":"{{-√½,√½},{√½,√½}}"})URL"; custom_gate_definition[(int)GateType::H_NYZ] = R"URL({"id":"~Hnyz","name":"Hnyz","matrix":"{{-√½,-√½i},{√½i,√½}}"})URL"; custom_gate_definition[(int)GateType::C_XYZ] = R"URL({"id":"~Cxyz","name":"Cxyz","matrix":"{{½-½i,-½-½i},{½-½i,½+½i}}"})URL"; custom_gate_definition[(int)GateType::C_NXYZ] = R"URL({"id":"~Cnxyz","name":"Cnxyz","matrix":"{{½+½i,½-½i},{-½-½i,½-½i}}"})URL"; custom_gate_definition[(int)GateType::C_XNYZ] = R"URL({"id":"~Cxnyz","name":"Cxnyz","matrix":"{{½+½i,-½+½i},{½+½i,½-½i}}"})URL"; custom_gate_definition[(int)GateType::C_XYNZ] = R"URL({"id":"~Cxynz","name":"Cxynz","matrix":"{{½-½i,½+½i},{-½+½i,½+½i}}"})URL"; custom_gate_definition[(int)GateType::C_ZYX] = R"URL({"id":"~Czyx","name":"Czyx","matrix":"{{½+½i,½+½i},{-½+½i,½-½i}}"})URL"; custom_gate_definition[(int)GateType::C_ZYNX] = R"URL({"id":"~Czynx","name":"Czynx","matrix":"{{½-½i,-½+½i},{½+½i,½+½i}}"})URL"; custom_gate_definition[(int)GateType::C_ZNYX] = R"URL({"id":"~Cznyx","name":"Cznyx","matrix":"{{½-½i,½-½i},{-½-½i,½+½i}}"})URL"; custom_gate_definition[(int)GateType::C_NZYX] = R"URL({"id":"~Cnzyx","name":"Cnzyx","matrix":"{{½+½i,-½-½i},{½-½i,½-½i}}"})URL"; stim_name_to_quirk_name[(int)GateType::H] = "H"; stim_name_to_quirk_name[(int)GateType::H_XY] = "~Hxy"; stim_name_to_quirk_name[(int)GateType::H_YZ] = "~Hyz"; stim_name_to_quirk_name[(int)GateType::H_NXY] = "~Hnxy"; stim_name_to_quirk_name[(int)GateType::H_NXZ] = "~Hnxz"; stim_name_to_quirk_name[(int)GateType::H_NYZ] = "~Hnyz"; stim_name_to_quirk_name[(int)GateType::I] = "…"; stim_name_to_quirk_name[(int)GateType::X] = "X"; stim_name_to_quirk_name[(int)GateType::Y] = "Y"; stim_name_to_quirk_name[(int)GateType::Z] = "Z"; stim_name_to_quirk_name[(int)GateType::C_XYZ] = "~Cxyz"; stim_name_to_quirk_name[(int)GateType::C_NXYZ] = "~Cnxyz"; stim_name_to_quirk_name[(int)GateType::C_XNYZ] = "~Cxnyz"; stim_name_to_quirk_name[(int)GateType::C_XYNZ] = "~Cxynz"; stim_name_to_quirk_name[(int)GateType::C_ZYX] = "~Czyx"; stim_name_to_quirk_name[(int)GateType::C_NZYX] = "~Cnzyx"; stim_name_to_quirk_name[(int)GateType::C_ZNYX] = "~Cznyx"; stim_name_to_quirk_name[(int)GateType::C_ZYNX] = "~Czynx"; stim_name_to_quirk_name[(int)GateType::SQRT_X] = "X^½"; stim_name_to_quirk_name[(int)GateType::SQRT_X_DAG] = "X^-½"; stim_name_to_quirk_name[(int)GateType::SQRT_Y] = "Y^½"; stim_name_to_quirk_name[(int)GateType::SQRT_Y_DAG] = "Y^-½"; stim_name_to_quirk_name[(int)GateType::S] = "Z^½"; stim_name_to_quirk_name[(int)GateType::S_DAG] = "Z^-½"; stim_name_to_quirk_name[(int)GateType::MX] = "XDetector"; stim_name_to_quirk_name[(int)GateType::MY] = "YDetector"; stim_name_to_quirk_name[(int)GateType::M] = "ZDetector"; stim_name_to_quirk_name[(int)GateType::MRX] = "XDetectControlReset"; stim_name_to_quirk_name[(int)GateType::MRY] = "YDetectControlReset"; stim_name_to_quirk_name[(int)GateType::MR] = "ZDetectControlReset"; stim_name_to_quirk_name[(int)GateType::RX] = "XDetectControlReset"; stim_name_to_quirk_name[(int)GateType::RY] = "YDetectControlReset"; stim_name_to_quirk_name[(int)GateType::R] = "ZDetectControlReset"; std::string_view x_control = "⊖"; std::string_view y_control = "(/)"; std::string_view z_control = "•"; control_target_type[(int)GateType::XCX] = {x_control, "X"}; control_target_type[(int)GateType::XCY] = {x_control, "Y"}; control_target_type[(int)GateType::XCZ] = {x_control, "Z"}; control_target_type[(int)GateType::YCX] = {y_control, "X"}; control_target_type[(int)GateType::YCY] = {y_control, "Y"}; control_target_type[(int)GateType::YCZ] = {y_control, "Z"}; control_target_type[(int)GateType::CX] = {z_control, "X"}; control_target_type[(int)GateType::CY] = {z_control, "Y"}; control_target_type[(int)GateType::CZ] = {z_control, "Z"}; control_target_type[(int)GateType::SWAPCX] = {z_control, "X"}; control_target_type[(int)GateType::CXSWAP] = {x_control, "Z"}; control_target_type[(int)GateType::CZSWAP] = {z_control, "Z"}; control_target_type[(int)GateType::ISWAP] = {"zpar", "zpar"}; control_target_type[(int)GateType::ISWAP_DAG] = {"zpar", "zpar"}; control_target_type[(int)GateType::SQRT_XX] = {"xpar", "xpar"}; control_target_type[(int)GateType::SQRT_YY] = {"ypar", "ypar"}; control_target_type[(int)GateType::SQRT_ZZ] = {"zpar", "zpar"}; control_target_type[(int)GateType::SQRT_XX_DAG] = {"xpar", "xpar"}; control_target_type[(int)GateType::SQRT_YY_DAG] = {"ypar", "ypar"}; control_target_type[(int)GateType::SQRT_ZZ_DAG] = {"zpar", "zpar"}; control_target_type[(int)GateType::MXX] = {"xpar", "xpar"}; control_target_type[(int)GateType::MYY] = {"xpar", "xpar"}; control_target_type[(int)GateType::MZZ] = {"xpar", "xpar"}; phase_type[(int)GateType::SQRT_XX] = "i"; phase_type[(int)GateType::SQRT_YY] = "i"; phase_type[(int)GateType::SQRT_ZZ] = "i"; phase_type[(int)GateType::SQRT_XX_DAG] = "-i"; phase_type[(int)GateType::SQRT_YY_DAG] = "-i"; phase_type[(int)GateType::SQRT_ZZ_DAG] = "-i"; phase_type[(int)GateType::SPP] = "i"; phase_type[(int)GateType::SPP_DAG] = "-i"; phase_type[(int)GateType::ISWAP] = "i"; phase_type[(int)GateType::ISWAP_DAG] = "-i"; } void write_pauli_par_controls(GateType g, size_t col, std::span targets) { for (auto t : targets) { if (t.has_qubit_value()) { bool x = t.data & TARGET_PAULI_X_BIT; bool z = t.data & TARGET_PAULI_Z_BIT; uint8_t p = x + z * 2; if (!p) { cols[col][t.value()] = control_target_type[(int)g].first; } else { cols[col][t.value()] = std::array{"", "xpar", "zpar", "ypar"}[p]; } } } } size_t pick_free_qubit(std::span targets) { if (num_qubits <= 16) { return num_qubits; } std::set qs; for (auto t : targets) { if (t.has_qubit_value()) { qs.insert(t.value()); } } size_t q = 0; while (qs.contains(q)) { q += 1; } return q; } size_t pick_merge_qubit(std::span targets) { if (num_qubits <= 16) { return num_qubits; } for (auto t : targets) { uint8_t p = t.pauli_type(); if (p && t.has_qubit_value() && t.qubit_value() <= 16) { return t.qubit_value(); } } return num_qubits; } void do_single_qubit_gate(GateType g, GateTarget t) { if (t.has_qubit_value()) { if (cols[col_offset].contains(t.value()) || cols[col_offset + 1].contains(t.value()) || cols[col_offset + 2].contains(t.value())) { col_offset += 3; } auto n = stim_name_to_quirk_name[(int)g]; if (n == "XDetectControlReset") { cols[col_offset][t.value()] = n; cols[col_offset + 1][t.value()] = "H"; } else if (n == "YDetectControlReset") { cols[col_offset][t.value()] = n; cols[col_offset + 1][t.value()] = "~Hyz"; used[(int)GateType::H_YZ] = true; } else if (n == "ZDetectControlReset") { cols[col_offset][t.value()] = n; } else { cols[col_offset + 1][t.value()] = n; } } } void do_multi_phase_gate(GateType g, std::span group) { col_offset += 3; size_t q_free = pick_free_qubit(group); write_pauli_par_controls(g, col_offset, group); cols[col_offset][q_free] = phase_type[(int)g]; col_offset += 3; } void do_multi_measure_gate(GateType g, std::span group) { col_offset += 3; size_t q_free = pick_merge_qubit(group); write_pauli_par_controls(g, col_offset, group); if (q_free == num_qubits) { cols[col_offset][q_free] = "X"; cols[col_offset + 1][q_free] = "ZDetectControlReset"; } else { write_pauli_par_controls(g, col_offset + 2, group); auto r = cols[col_offset][q_free]; if (r == "xpar") { cols[col_offset][q_free] = "Z"; cols[col_offset + 1][q_free] = "XDetector"; cols[col_offset + 2][q_free] = "Z"; } else if (r == "ypar") { cols[col_offset][q_free] = "X"; cols[col_offset + 1][q_free] = "YDetector"; cols[col_offset + 2][q_free] = "X"; } else { cols[col_offset][q_free] = "X"; cols[col_offset + 1][q_free] = "ZDetector"; cols[col_offset + 2][q_free] = "X"; } } col_offset += 3; } void do_controlled_gate(GateType g, GateTarget t1, GateTarget t2) { if (t1.has_qubit_value() && t2.has_qubit_value()) { col_offset += 3; auto [c, t] = control_target_type[(int)g]; cols[col_offset][t1.qubit_value()] = c; cols[col_offset][t2.qubit_value()] = t; col_offset += 3; } } void do_swap_plus_gate(GateType g, GateTarget t1, GateTarget t2) { if (t1.has_qubit_value() && t2.has_qubit_value()) { col_offset += 3; cols[col_offset][t1.qubit_value()] = "Swap"; cols[col_offset][t2.qubit_value()] = "Swap"; if (g == GateType::ISWAP || g == GateType::ISWAP_DAG) { std::array group{t1, t2}; do_multi_phase_gate(g, group); } else { do_controlled_gate(g, t1, t2); } col_offset += 3; } } void do_circuit(const Circuit &circuit) { circuit.for_each_operation([&](CircuitInstruction full_instruction) { used[(int)full_instruction.gate_type] = true; for_each_target_group(full_instruction, [&](CircuitInstruction inst) { switch (inst.gate_type) { case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::MPAD: case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::PAULI_CHANNEL_1: case GateType::PAULI_CHANNEL_2: case GateType::E: case GateType::ELSE_CORRELATED_ERROR: case GateType::HERALDED_ERASE: case GateType::HERALDED_PAULI_CHANNEL_1: case GateType::II: case GateType::I_ERROR: case GateType::II_ERROR: // Ignored. break; case GateType::TICK: col_offset += 3; break; case GateType::MX: case GateType::MY: case GateType::M: case GateType::MRX: case GateType::MRY: case GateType::MR: case GateType::RX: case GateType::RY: case GateType::R: case GateType::H: case GateType::H_XY: case GateType::H_YZ: case GateType::H_NXY: case GateType::H_NXZ: case GateType::H_NYZ: case GateType::I: case GateType::X: case GateType::Y: case GateType::Z: case GateType::C_XYZ: case GateType::C_NXYZ: case GateType::C_XNYZ: case GateType::C_XYNZ: case GateType::C_ZYX: case GateType::C_NZYX: case GateType::C_ZNYX: case GateType::C_ZYNX: case GateType::SQRT_X: case GateType::SQRT_X_DAG: case GateType::SQRT_Y: case GateType::SQRT_Y_DAG: case GateType::S: case GateType::S_DAG: do_single_qubit_gate(inst.gate_type, inst.targets[0]); break; case GateType::SQRT_XX: case GateType::SQRT_YY: case GateType::SQRT_ZZ: case GateType::SQRT_XX_DAG: case GateType::SQRT_YY_DAG: case GateType::SQRT_ZZ_DAG: case GateType::SPP: case GateType::SPP_DAG: do_multi_phase_gate(inst.gate_type, inst.targets); break; case GateType::XCX: case GateType::XCY: case GateType::XCZ: case GateType::YCX: case GateType::YCY: case GateType::YCZ: case GateType::CX: case GateType::CY: case GateType::CZ: do_controlled_gate(inst.gate_type, inst.targets[0], inst.targets[1]); break; case GateType::SWAP: case GateType::ISWAP: case GateType::CXSWAP: case GateType::SWAPCX: case GateType::CZSWAP: case GateType::ISWAP_DAG: do_swap_plus_gate(inst.gate_type, inst.targets[0], inst.targets[1]); break; case GateType::MXX: case GateType::MYY: case GateType::MZZ: case GateType::MPP: do_multi_measure_gate(inst.gate_type, inst.targets); break; default: throw std::invalid_argument("Not supported in export_quirk_url: " + full_instruction.str()); } }); }); } }; std::string stim::export_quirk_url(const Circuit &circuit) { QuirkExporter exporter(circuit.count_qubits()); exporter.do_circuit(circuit); std::stringstream out; exporter.col_offset += 3; out << R"URL(https://algassert.com/quirk#circuit={"cols":[)URL"; bool has_col = false; for (size_t k = 0; k < exporter.col_offset; k++) { if (!exporter.cols.contains(k)) { continue; } const auto &col = exporter.cols.at(k); if (col.empty()) { continue; } std::vector entries; for (auto kv : col) { while (entries.size() <= kv.first) { entries.push_back(""); } entries[kv.first] = kv.second; } if (has_col) { out << ","; } has_col = true; out << "["; for (size_t q = 0; q < entries.size(); q++) { if (q) { out << ","; } if (entries[q].empty()) { out << "1"; } else { out << '"'; out << entries[q]; out << '"'; } } out << "]"; } out << R"URL(])URL"; bool has_custom_gates = false; for (size_t k = 0; k < NUM_DEFINED_GATES; k++) { if (!exporter.custom_gate_definition[k].empty() && exporter.used[k]) { if (!has_custom_gates) { out << R"URL(,"gates":[)URL"; has_custom_gates = true; } else { out << ','; } out << exporter.custom_gate_definition[k]; } } if (has_custom_gates) { out << "]"; } out << "}"; return out.str(); } ================================================ FILE: src/stim/util_top/export_quirk_url.h ================================================ #ifndef _STIM_UTIL_TOP_EXPORT_QUIRK_URL_H #define _STIM_UTIL_TOP_EXPORT_QUIRK_URL_H #include "stim/circuit/circuit.h" namespace stim { std::string export_quirk_url(const Circuit &circuit); } // namespace stim #endif ================================================ FILE: src/stim/util_top/export_quirk_url.test.cc ================================================ #include "stim/util_top/export_quirk_url.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.test.h" using namespace stim; TEST(export_quirk, simple) { auto actual = export_quirk_url(Circuit(R"CIRCUIT( R 0 H 0 1 S 2 H 2 CX 0 1 M 1 SQRT_ZZ 2 3 MXX 0 1 )CIRCUIT")); auto expected = R"URL(https://algassert.com/quirk#circuit={"cols":[)URL" R"URL(["ZDetectControlReset"],)URL" R"URL(["H","H","Z^½"],)URL" R"URL([1,1,"H"],)URL" R"URL(["•","X"],)URL" R"URL([1,"ZDetector"],)URL" R"URL([1,1,"zpar","zpar","i"],)URL" R"URL(["xpar","xpar",1,1,"X"],)URL" R"URL([1,1,1,1,"ZDetectControlReset"])URL" R"URL(]})URL"; ASSERT_EQ(actual, expected); actual = export_quirk_url(Circuit(R"CIRCUIT( MRY 0 )CIRCUIT")); expected = R"URL(https://algassert.com/quirk#circuit={"cols":[)URL" R"URL(["YDetectControlReset"],)URL" R"URL(["~Hyz"])URL" R"URL(],"gates":[)URL" R"URL({"id":"~Hyz","name":"Hyz","matrix":"{{-√½i,-√½},{√½,√½i}}"}]})URL"; ASSERT_EQ(actual, expected); actual = export_quirk_url(Circuit(R"CIRCUIT( R 0 H_XY 0 1 S 2 H 2 CX 0 1 M 1 SQRT_ZZ 2 3 MXX 0 1 )CIRCUIT")); expected = R"URL(https://algassert.com/quirk#circuit={"cols":[)URL" R"URL(["ZDetectControlReset"],)URL" R"URL(["~Hxy","~Hxy","Z^½"],)URL" R"URL([1,1,"H"],)URL" R"URL(["•","X"],)URL" R"URL([1,"ZDetector"],)URL" R"URL([1,1,"zpar","zpar","i"],)URL" R"URL(["xpar","xpar",1,1,"X"],)URL" R"URL([1,1,1,1,"ZDetectControlReset"])URL" R"URL(],"gates":[)URL" R"URL({"id":"~Hxy","name":"Hxy","matrix":"{{0,-√½-√½i},{√½-√½i,0}}"}]})URL"; ASSERT_EQ(actual, expected); actual = export_quirk_url(Circuit(R"CIRCUIT( R 0 H_XY 0 H_YZ 1 S 2 H 2 CX 0 1 M 1 SQRT_ZZ 2 3 MXX 0 1 )CIRCUIT")); expected = R"URL(https://algassert.com/quirk#circuit={"cols":[)URL" R"URL(["ZDetectControlReset"],)URL" R"URL(["~Hxy","~Hyz","Z^½"],)URL" R"URL([1,1,"H"],)URL" R"URL(["•","X"],)URL" R"URL([1,"ZDetector"],)URL" R"URL([1,1,"zpar","zpar","i"],)URL" R"URL(["xpar","xpar",1,1,"X"],)URL" R"URL([1,1,1,1,"ZDetectControlReset"])URL" R"URL(],"gates":[)URL" R"URL({"id":"~Hxy","name":"Hxy","matrix":"{{0,-√½-√½i},{√½-√½i,0}}"},)URL" R"URL({"id":"~Hyz","name":"Hyz","matrix":"{{-√½i,-√½},{√½,√½i}}"}]})URL"; ASSERT_EQ(actual, expected); } TEST(export_quirk, all_operations) { auto actual = export_quirk_url(generate_test_circuit_with_all_operations()); auto expected = R"URL(https://algassert.com/quirk#circuit={"cols":[)URL" R"URL(["…","X","Y","Z"],)URL" R"URL(["~Cxyz","~Cnxyz","~Cxnyz","~Cxynz","~Czyx","~Cnzyx","~Cznyx","~Czynx"],)URL" R"URL(["~Hxy","H","~Hyz","~Hnxy","~Hnxz","~Hnyz"],)URL" R"URL(["X^½","X^-½","Y^½","Y^-½","Z^½","Z^-½"],)URL" R"URL(["Swap","Swap"],)URL" R"URL(["⊖","Z"],)URL" R"URL([1,1,"Swap","Swap"],)URL" R"URL(["i",1,"zpar","zpar"],)URL" R"URL([1,1,1,1,"Swap","Swap"],)URL" R"URL(["-i",1,1,1,"zpar","zpar"],)URL" R"URL([1,1,1,1,1,1,"Swap","Swap"],)URL" R"URL([1,1,1,1,1,1,1,1],)URL" R"URL([1,1,1,1,1,1,1,1,"Swap","Swap"],)URL" R"URL([1,1,1,1,1,1,1,1,"•","X"],)URL" R"URL([1,1,1,1,1,1,1,1,1,1,"Swap","Swap"],)URL" R"URL([1,1,1,1,1,1,1,1,1,1,"•","Z"],)URL" R"URL(["xpar","xpar","i"],)URL" R"URL(["-i",1,"xpar","xpar"],)URL" R"URL(["i",1,1,1,"ypar","ypar"],)URL" R"URL(["-i",1,1,1,1,1,"ypar","ypar"],)URL" R"URL(["i",1,1,1,1,1,1,1,"zpar","zpar"],)URL" R"URL(["-i",1,1,1,1,1,1,1,1,1,"zpar","zpar"],)URL" R"URL(["⊖","X"],)URL" R"URL([1,1,"⊖","Y"],)URL" R"URL([1,1,1,1,"⊖","Z"],)URL" R"URL([1,1,1,1,1,1,"(/)","X"],)URL" R"URL([1,1,1,1,1,1,1,1,"(/)","Y"],)URL" R"URL([1,1,1,1,1,1,1,1,1,1,"(/)","Z"],)URL" R"URL([1,1,1,1,1,1,1,1,1,1,1,1,"•","X"],)URL" R"URL([1,1,1,1,1,1,1,1,1,1,1,1,1,1,"•","Y"],)URL" R"URL([1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,"•","Z"],)URL" R"URL(["Z","ypar","zpar"],)URL" R"URL(["XDetector"],)URL" R"URL(["Z","ypar","zpar"],)URL" R"URL(["X","zpar"],)URL" R"URL(["ZDetector"],)URL" R"URL(["X","zpar"],)URL" R"URL(["xpar","ypar","zpar","i"],)URL" R"URL(["i",1,1,"xpar"],)URL" R"URL(["xpar","ypar","zpar","-i"],)URL" R"URL(["-i",1,"xpar"],)URL" R"URL(["XDetectControlReset","YDetectControlReset","ZDetectControlReset",1,1,1,1,"XDetectControlReset","YDetectControlReset","ZDetectControlReset"],)URL" R"URL(["H","~Hyz",1,"XDetector","YDetector","ZDetector","ZDetector","H","~Hyz"],)URL" R"URL(["Z","xpar"],)URL" R"URL(["XDetector"],)URL" R"URL(["Z","xpar"],)URL" R"URL([1,1,"Z","xpar"],)URL" R"URL([1,1,"XDetector"],)URL" R"URL([1,1,"Z","xpar"],)URL" R"URL([1,1,1,1,"Z","xpar"],)URL" R"URL([1,1,1,1,"XDetector"],)URL" R"URL([1,1,1,1,"Z","xpar"],)URL" R"URL([1,1,1,1,1,1,"Z","xpar"],)URL" R"URL([1,1,1,1,1,1,"XDetector"],)URL" R"URL([1,1,1,1,1,1,"Z","xpar"],)URL" R"URL(["H"],)URL" R"URL(["•","X"],)URL" R"URL([1,"Z^½"],)URL" R"URL(["H"],)URL" R"URL(["•","X"],)URL" R"URL([1,"Z^½"],)URL" R"URL(["H"],)URL" R"URL(["•","X"],)URL" R"URL([1,"Z^½"],)URL" R"URL(["ZDetectControlReset"],)URL" R"URL(["ZDetectControlReset"],)URL" R"URL(["XDetectControlReset"],)URL" R"URL(["H","YDetector"],)URL" R"URL([1,1,"Z","xpar"],)URL" R"URL([1,1,"XDetector"],)URL" R"URL([1,1,"Z","xpar"],)URL" R"URL([1,1,1,1,"Z","xpar"],)URL" R"URL([1,1,1,1,"XDetector"],)URL" R"URL([1,1,1,1,"Z","xpar"],)URL" R"URL([1,1,1,1,1,1,"Z","ypar","zpar"],)URL" R"URL([1,1,1,1,1,1,"XDetector"],)URL" R"URL([1,1,1,1,1,1,"Z","ypar","zpar"]],"gates":)URL" R"URL([{"id":"~Hxy","name":"Hxy","matrix":"{{0,-√½-√½i},{√½-√½i,0}}"},)URL" R"URL({"id":"~Hyz","name":"Hyz","matrix":"{{-√½i,-√½},{√½,√½i}}"},)URL" R"URL({"id":"~Hnxy","name":"Hnxy","matrix":"{{0,√½+√½i},{√½-√½i,0}}"},)URL" R"URL({"id":"~Hnxz","name":"Hnxz","matrix":"{{-√½,√½},{√½,√½}}"},)URL" R"URL({"id":"~Hnyz","name":"Hnyz","matrix":"{{-√½,-√½i},{√½i,√½}}"},)URL" R"URL({"id":"~Cxyz","name":"Cxyz","matrix":"{{½-½i,-½-½i},{½-½i,½+½i}}"},)URL" R"URL({"id":"~Czyx","name":"Czyx","matrix":"{{½+½i,½+½i},{-½+½i,½-½i}}"},)URL" R"URL({"id":"~Cnxyz","name":"Cnxyz","matrix":"{{½+½i,½-½i},{-½-½i,½-½i}}"},)URL" R"URL({"id":"~Cxnyz","name":"Cxnyz","matrix":"{{½+½i,-½+½i},{½+½i,½-½i}}"},)URL" R"URL({"id":"~Cxynz","name":"Cxynz","matrix":"{{½-½i,½+½i},{-½+½i,½+½i}}"},)URL" R"URL({"id":"~Cnzyx","name":"Cnzyx","matrix":"{{½+½i,-½-½i},{½-½i,½-½i}}"},)URL" R"URL({"id":"~Cznyx","name":"Cznyx","matrix":"{{½-½i,½-½i},{-½-½i,½+½i}}"},)URL" R"URL({"id":"~Czynx","name":"Czynx","matrix":"{{½-½i,-½+½i},{½+½i,½+½i}}"}]})URL"; ASSERT_EQ(actual, expected); } ================================================ FILE: src/stim/util_top/export_quirk_url_pybind_test.py ================================================ import stim def test_to_quirk_url_simple(): c = stim.Circuit(""" QUBIT_COORDS(2, 1) 0 QUBIT_COORDS(2, 2) 1 H 0 TICK CX 0 1 TICK C_XYZ 0 S 1 TICK MZZ 1 0 """) assert c.to_quirk_url() == 'https://algassert.com/quirk#circuit={"cols":[["H"],["•","X"],["~Cxyz","Z^½"],["xpar","xpar","X"],[1,1,"ZDetectControlReset"]],"gates":[{"id":"~Cxyz","name":"Cxyz","matrix":"{{½-½i,-½-½i},{½-½i,½+½i}}"}]}' def test_to_quirk_url_complex(): c = stim.Circuit.generated('surface_code:rotated_memory_x', distance=2, rounds=2, after_clifford_depolarization=0.001) assert '"H"' in c.to_quirk_url() ================================================ FILE: src/stim/util_top/has_flow.cc ================================================ #include "stim/util_top/has_flow.h" using namespace stim; Circuit stim::flow_test_block_for_circuit( const Circuit &circuit, GateTarget ancilla_qubit, const std::set &obs_indices) { Circuit result; for (CircuitInstruction inst : circuit.operations) { if (inst.gate_type == GateType::REPEAT) { const Circuit &body = inst.repeat_block_body(circuit); Circuit new_body = flow_test_block_for_circuit(body, ancilla_qubit, obs_indices); result.append_repeat_block(inst.repeat_block_rep_count(), std::move(new_body), inst.tag); } else if (inst.gate_type == GateType::OBSERVABLE_INCLUDE && obs_indices.contains((uint32_t)inst.args[0])) { for (GateTarget t : inst.targets) { if (t.is_inverted_result_target()) { result.safe_append(CircuitInstruction{GateType::X, {}, &ancilla_qubit, inst.tag}); } if (t.is_measurement_record_target()) { std::array targets{t, ancilla_qubit}; result.safe_append( CircuitInstruction{ GateType::CX, {}, targets, inst.tag, }); } else if (t.is_x_target()) { std::array targets{GateTarget::qubit(t.qubit_value()), ancilla_qubit}; result.safe_append( CircuitInstruction{ GateType::XCX, {}, targets, inst.tag, }); } else if (t.is_y_target()) { std::array targets{GateTarget::qubit(t.qubit_value()), ancilla_qubit}; result.safe_append( CircuitInstruction{ GateType::YCX, {}, targets, inst.tag, }); } else if (t.is_z_target()) { std::array targets{GateTarget::qubit(t.qubit_value()), ancilla_qubit}; result.safe_append( CircuitInstruction{ GateType::CX, {}, targets, inst.tag, }); } else { throw std::invalid_argument("Not handled: " + inst.str()); } } } else { result.safe_append(inst); } } return result; } ================================================ FILE: src/stim/util_top/has_flow.h ================================================ /* * 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. */ #ifndef _STIM_UTIL_TOP_HAS_FLOW_H #define _STIM_UTIL_TOP_HAS_FLOW_H #include #include #include "stim/circuit/circuit.h" #include "stim/stabilizers/flow.h" namespace stim { /// Probabilistically verifies that the given circuit has the specified flows. /// /// Args: /// num_samples: How many times to sample the circuit. Each sample has a 50/50 chance /// of catching a bad stabilizer flow. /// rng: Random number generator for the sampling process. /// circuit: The circuit that should have the given flows. /// flows: The flows that the circuit should have, /// /// Returns: /// A vector containing one boolean for each flow. The k'th boolean is true if the /// k'th flow passed all checks. template std::vector sample_if_circuit_has_stabilizer_flows( size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, std::span> flows); template std::vector check_if_circuit_has_unsigned_stabilizer_flows( const Circuit &circuit, std::span> flows); template std::ostream &operator<<(std::ostream &out, const Flow &flow); /// Internal helper method. Circuit flow_test_block_for_circuit( const Circuit &circuit, GateTarget ancilla_qubit, const std::set &obs_indices); } // namespace stim #include "stim/util_top/has_flow.inl" #endif ================================================ FILE: src/stim/util_top/has_flow.inl ================================================ #include "stim/util_top/has_flow.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/simulators/tableau_simulator.h" #include "stim/simulators/sparse_rev_frame_tracker.h" namespace stim { template void _pauli_string_controlled_not(PauliStringRef control, uint32_t target, Circuit &out) { control.for_each_active_pauli([&](size_t q) { uint32_t q32 = (uint32_t)q; auto p = control.xs[q] + 2 * control.zs[q]; if (p == 1) { out.safe_append_u("XCX", {q32, target}); } else if (p == 2) { out.safe_append_u("ZCX", {q32, target}); } else if (p == 3) { out.safe_append_u("YCX", {q32, target}); } }); if (control.sign) { out.safe_append_u("X", {target}); } } template static GateTarget measurement_index_to_target(int32_t m, uint64_t num_measurements, const Flow &flow) { if ((m >= 0 && (uint64_t)m >= num_measurements) || (m < 0 && (uint64_t)-(int64_t)m > num_measurements)) { std::stringstream ss; ss << "The flow '" << flow; ss << "' is malformed for the given circuit. "; ss << "The flow mentions a measurement index '" << m; ss << "', but this index out of range because the circuit only has "; ss << num_measurements << " measurements."; throw std::invalid_argument(ss.str()); } if (m >= 0) { m -= num_measurements; } return GateTarget::rec(m); } template bool _sample_if_noiseless_circuit_has_stabilizer_flow( size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, const Flow &flow) { uint32_t num_qubits = (uint32_t)circuit.count_qubits(); uint64_t num_measurements = circuit.count_measurements(); num_qubits = std::max(num_qubits, (uint32_t)flow.input.num_qubits); num_qubits = std::max(num_qubits, (uint32_t)flow.output.num_qubits); std::set obs_indices; for (uint32_t obs_index : flow.observables) { obs_indices.insert(obs_index); } // Max-mix all the qubits. Circuit augmented_circuit; GateTarget ancilla = GateTarget::qubit(num_qubits); for (uint32_t k = 0; k < num_qubits; k++) { augmented_circuit.safe_append_u("X_ERROR", {k}, {0.5}); } for (uint32_t k = 0; k < num_qubits; k++) { augmented_circuit.safe_append_u("Z_ERROR", {k}, {0.5}); } // Xor input onto ancilla. _pauli_string_controlled_not(flow.input, num_qubits, augmented_circuit); // Perform circuit (accounting for desired observables impacting ancilla) augmented_circuit += flow_test_block_for_circuit(circuit, ancilla, obs_indices); for (int32_t m : flow.measurements) { std::array targets{measurement_index_to_target(m, num_measurements, flow), ancilla}; augmented_circuit.safe_append(CircuitInstruction(GateType::CX, {}, targets, "")); } // Xor output onto ancilla. _pauli_string_controlled_not(flow.output, num_qubits, augmented_circuit); // The ancilla should end up deterministically in |0> if the flow is valid. augmented_circuit.safe_append_u("M", {num_qubits}, {}); simd_bits reference_sample = TableauSimulator::reference_sample_circuit(augmented_circuit); num_samples = (num_samples + W - 1) / W * W; auto result = sample_batch_measurements( augmented_circuit, reference_sample, num_samples, rng, false); return !result[num_measurements].not_zero(); } template std::vector sample_if_circuit_has_stabilizer_flows( size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, std::span> flows) { const auto &noiseless = circuit.aliased_noiseless_circuit(); std::vector result; for (const auto &flow : flows) { result.push_back(_sample_if_noiseless_circuit_has_stabilizer_flow(num_samples, rng, noiseless, flow)); } return result; } template std::vector check_if_circuit_has_unsigned_stabilizer_flows( const Circuit &circuit, std::span> flows) { auto stats = circuit.compute_stats(); size_t num_qubits = stats.num_qubits; for (const auto &flow : flows) { num_qubits = std::max(num_qubits, flow.input.num_qubits); num_qubits = std::max(num_qubits, flow.output.num_qubits); } SparseUnsignedRevFrameTracker rev(num_qubits, stats.num_measurements, flows.size(), false); // Add end of flows into frames. for (size_t f = 0; f < flows.size(); f++) { const auto &flow = flows[f]; for (size_t q = 0; q < flow.output.num_qubits; q++) { if (flow.output.xs[q]) { rev.xs[q].xor_item(DemTarget::relative_detector_id(f)); } if (flow.output.zs[q]) { rev.zs[q].xor_item(DemTarget::relative_detector_id(f)); } } } // Add observable-to-flow effects. std::map> obs_effects; for (size_t f = 0; f < flows.size(); f++) { const auto &flow = flows[f]; for (const auto &obs : flow.observables) { obs_effects[obs].sorted_items.push_back(DemTarget::relative_detector_id(f)); } } // Mark measurements for inclusion. for (size_t f = flows.size(); f--;) { const auto &flow = flows[f]; std::vector targets; for (int32_t m : flow.measurements) { targets.push_back(measurement_index_to_target(m, stats.num_measurements, flow)); } rev.undo_DETECTOR(CircuitInstruction{GateType::DETECTOR, {}, targets, ""}); } // Undo the circuit. circuit.for_each_operation_reverse([&](const CircuitInstruction &inst) { if (inst.gate_type == GateType::DETECTOR) { // Ignore detectors; we're using them for tracking flows. } else if (inst.gate_type == GateType::OBSERVABLE_INCLUDE) { // Map observable effects onto the flows that depended on that observable. uint64_t obs_id = (uint32_t)inst.args[0]; auto effects = obs_effects.find(obs_id); if (effects == obs_effects.end()) { return; } for (auto t : inst.targets) { if (t.is_measurement_record_target()) { int64_t index = t.rec_offset() + (int64_t)rev.num_measurements_in_past; if (index < 0) { throw std::invalid_argument("Referred to a measurement result before the beginning of time."); } rev.rec_bits[index] ^= effects->second; } else if (t.is_pauli_target()) { if (t.data & TARGET_PAULI_X_BIT) { rev.xs[t.qubit_value()] ^= effects->second; } if (t.data & TARGET_PAULI_Z_BIT) { rev.zs[t.qubit_value()] ^= effects->second; } } else { throw std::invalid_argument("Unexpected target for OBSERVABLE_INCLUDE: " + t.str()); } } } else { rev.undo_gate(inst); } }); // Remove start of flows from frames. for (size_t f = 0; f < flows.size(); f++) { const auto &flow = flows[f]; for (size_t q = 0; q < flow.input.num_qubits; q++) { if (flow.input.xs[q]) { rev.xs[q].xor_item(DemTarget::relative_detector_id(f)); } if (flow.input.zs[q]) { rev.zs[q].xor_item(DemTarget::relative_detector_id(f)); } } } // Determine which flows survived. std::vector result(flows.size(), true); for (const auto &xs : rev.xs) { for (const auto &t : xs) { result[t.val()] = false; } } for (const auto &zs : rev.zs) { for (const auto &t : zs) { result[t.val()] = false; } } for (const auto &[dem_target, gate_target] : rev.anticommutations) { result[dem_target.val()] = false; } return result; } } // namespace stim ================================================ FILE: src/stim/util_top/has_flow.test.cc ================================================ // 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. #include "stim/util_top/has_flow.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows, { auto rng = INDEPENDENT_TEST_RNG(); auto results = sample_if_circuit_has_stabilizer_flows( 256, rng, Circuit(R"CIRCUIT( R 4 CX 0 4 1 4 2 4 3 4 M 4 )CIRCUIT"), std::vector>{ Flow::from_str("Z___ -> Z____"), Flow::from_str("_Z__ -> _Z__"), Flow::from_str("__Z_ -> __Z_"), Flow::from_str("___Z -> ___Z"), Flow::from_str("XX__ -> XX__"), Flow::from_str("XXXX -> XXXX"), Flow::from_str("XYZ_ -> XYZ_"), Flow::from_str("XXX_ -> XXX_"), Flow::from_str("ZZZZ -> ____ xor rec[-1]"), Flow::from_str("+___Z -> -___Z"), Flow::from_str("-___Z -> -___Z"), Flow::from_str("-___Z -> +___Z"), }); ASSERT_EQ(results, (std::vector{1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows_signed_checked, { auto rng = INDEPENDENT_TEST_RNG(); auto results = sample_if_circuit_has_stabilizer_flows( 256, rng, Circuit(R"CIRCUIT( R 2 3 X 1 3 )CIRCUIT"), std::vector>{ Flow::from_str("Z0 -> Z0"), Flow::from_str("Z1 -> -Z1"), Flow::from_str("1 -> Z2"), Flow::from_str("1 -> -Z3"), Flow::from_str("Z0 -> -Z0"), Flow::from_str("Z1 -> Z1"), Flow::from_str("1 -> -Z2"), Flow::from_str("1 -> Z3"), }); ASSERT_EQ(results, (std::vector{1, 1, 1, 1, 0, 0, 0, 0})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows_measurements_signed_checked, { auto rng = INDEPENDENT_TEST_RNG(); auto results = sample_if_circuit_has_stabilizer_flows( 256, rng, Circuit(R"CIRCUIT( X 1 M 0 1 2 X 2 )CIRCUIT"), std::vector>{ Flow::from_str("Z0 -> Z0"), Flow::from_str("Z1 -> -Z1"), Flow::from_str("Z2 -> -Z2"), Flow::from_str("Z0 -> rec[-3]"), Flow::from_str("-Z1 -> rec[-2]"), Flow::from_str("Z2 -> rec[-1]"), Flow::from_str("1 -> Z0 xor rec[-3]"), Flow::from_str("1 -> Z1 xor rec[-2]"), Flow::from_str("1 -> -Z2 xor rec[-1]"), Flow::from_str("Z0 -> -Z0"), Flow::from_str("Z1 -> Z1"), Flow::from_str("Z2 -> Z2"), Flow::from_str("-Z0 -> rec[-3]"), Flow::from_str("Z1 -> rec[-2]"), Flow::from_str("-Z2 -> rec[-1]"), Flow::from_str("1 -> -Z0 xor rec[-3]"), Flow::from_str("1 -> -Z1 xor rec[-2]"), Flow::from_str("1 -> Z2 xor rec[-1]"), }); ASSERT_EQ(results, (std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows_signed_obs, { auto rng = INDEPENDENT_TEST_RNG(); auto results = sample_if_circuit_has_stabilizer_flows( 256, rng, Circuit(R"CIRCUIT( X 1 M 0 1 2 X 2 OBSERVABLE_INCLUDE(0) rec[-3] OBSERVABLE_INCLUDE(1) rec[-2] OBSERVABLE_INCLUDE(2) rec[-1] )CIRCUIT"), std::vector>{ Flow::from_str("Z0 -> Z0"), Flow::from_str("Z1 -> -Z1"), Flow::from_str("Z2 -> -Z2"), Flow::from_str("Z0 -> obs[0]"), Flow::from_str("-Z1 -> obs[1]"), Flow::from_str("Z2 -> obs[2]"), Flow::from_str("1 -> Z0 xor obs[0]"), Flow::from_str("1 -> Z1 xor obs[1]"), Flow::from_str("1 -> -Z2 xor obs[2]"), Flow::from_str("Z0 -> -Z0"), Flow::from_str("Z1 -> Z1"), Flow::from_str("Z2 -> Z2"), Flow::from_str("-Z0 -> obs[0]"), Flow::from_str("Z1 -> obs[1]"), Flow::from_str("-Z2 -> obs[2]"), Flow::from_str("1 -> -Z0 xor obs[0]"), Flow::from_str("1 -> -Z1 xor obs[1]"), Flow::from_str("1 -> Z2 xor obs[2]"), }); ASSERT_EQ(results, (std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows_obs, { auto rng = INDEPENDENT_TEST_RNG(); auto results = sample_if_circuit_has_stabilizer_flows( 256, rng, Circuit(R"CIRCUIT( OBSERVABLE_INCLUDE(3) X0 OBSERVABLE_INCLUDE(2) Y0 )CIRCUIT"), std::vector>{ Flow::from_str("X0 -> obs[3]"), Flow::from_str("Y0 -> obs[2]"), Flow::from_str("-X0 -> obs[3]"), Flow::from_str("X0 -> obs[2]"), Flow::from_str("Y0 -> obs[3]"), }); ASSERT_EQ(results, (std::vector{1, 1, 0, 0, 0})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows_inverted_obs_pauli, { auto rng = INDEPENDENT_TEST_RNG(); auto results = sample_if_circuit_has_stabilizer_flows( 256, rng, Circuit(R"CIRCUIT( OBSERVABLE_INCLUDE(3) X0 OBSERVABLE_INCLUDE(2) !X0 )CIRCUIT"), std::vector>{ Flow::from_str("X0 -> obs[3]"), Flow::from_str("-X0 -> obs[2]"), Flow::from_str("-X0 -> obs[3]"), Flow::from_str("X0 -> obs[2]"), }); ASSERT_EQ(results, (std::vector{1, 1, 0, 0})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows_inverted_obs_rec, { auto rng = INDEPENDENT_TEST_RNG(); auto results = sample_if_circuit_has_stabilizer_flows( 256, rng, Circuit(R"CIRCUIT( M !0 OBSERVABLE_INCLUDE(3) rec[-1] )CIRCUIT"), std::vector>{ Flow::from_str("-Z0 -> obs[3]"), Flow::from_str("Z0 -> obs[3]"), }); ASSERT_EQ(results, (std::vector{1, 0})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, check_if_circuit_has_unsigned_stabilizer_flows, { auto results = check_if_circuit_has_unsigned_stabilizer_flows( Circuit(R"CIRCUIT( R 4 CX 0 4 1 4 2 4 3 4 M 4 )CIRCUIT"), std::vector>{ Flow::from_str("Z___ -> Z____"), Flow::from_str("_Z__ -> _Z__"), Flow::from_str("__Z_ -> __Z_"), Flow::from_str("___Z -> ___Z"), Flow::from_str("XX__ -> XX__"), Flow::from_str("XXXX -> XXXX"), Flow::from_str("XYZ_ -> XYZ_"), Flow::from_str("XXX_ -> XXX_"), Flow::from_str("ZZZZ -> ____ xor rec[-1]"), Flow::from_str("+___Z -> -___Z"), Flow::from_str("-___Z -> -___Z"), Flow::from_str("-___Z -> +___Z"), }); ASSERT_EQ(results, (std::vector{1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1})); }); TEST_EACH_WORD_SIZE_W(stabilizer_flow, check_if_circuit_has_unsigned_stabilizer_flows_historical_failure, { auto results = check_if_circuit_has_unsigned_stabilizer_flows( Circuit(R"CIRCUIT( CX 0 1 S 0 )CIRCUIT"), std::vector>{ Flow::from_str("X_ -> YX"), Flow::from_str("Y_ -> XX"), Flow::from_str("X_ -> XX"), }); ASSERT_EQ(results, (std::vector{1, 1, 0})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, check_if_circuit_has_unsigned_stabilizer_flows_mzz, { auto results = check_if_circuit_has_unsigned_stabilizer_flows( Circuit(R"CIRCUIT( MZZ 0 1 )CIRCUIT"), std::vector>{ Flow::from_str("X0*X1 -> Y0*Y1 xor rec[-1]"), Flow::from_str("X0*X1 -> Z0*Z1 xor rec[-1]"), Flow::from_str("X0*X1 -> X0*X1"), Flow::from_str("Z0 -> Z1 xor rec[-1]"), Flow::from_str("Z0 -> Z0"), }); ASSERT_EQ(results, (std::vector{1, 0, 1, 1, 1})); }) TEST_EACH_WORD_SIZE_W(stabilizer_flow, has_flow_observable, { auto results = check_if_circuit_has_unsigned_stabilizer_flows( Circuit(R"CIRCUIT( MZZ 0 1 OBSERVABLE_INCLUDE(2) rec[-1] )CIRCUIT"), std::vector>{ Flow::from_str("Z0*Z1 -> obs[2]"), Flow::from_str("1 -> Z0*Z1 xor obs[2]"), Flow::from_str("X0*X1 -> X0*X1 xor obs[0]"), Flow::from_str("X0*X1 -> Y0*Y1 xor obs[2]"), Flow::from_str("X0*X1 -> Y0*Y1 xor obs[1]"), Flow::from_str("X0*X1 -> Y0*Y1 xor rec[-1]"), }); ASSERT_EQ(results, (std::vector{1, 1, 1, 1, 0, 1})); results = check_if_circuit_has_unsigned_stabilizer_flows( Circuit(R"CIRCUIT( OBSERVABLE_INCLUDE(3) X0 Y1 Z2 OBSERVABLE_INCLUDE(2) Y0 )CIRCUIT"), std::vector>{ Flow::from_str("X0*Y1*Z2 -> obs[3]"), Flow::from_str("-Y0 -> obs[2]"), Flow::from_str("Y0 -> obs[3]"), Flow::from_str("1 -> X0*Y1*Z2 xor obs[3]"), }); ASSERT_EQ(results, (std::vector{1, 1, 0, 1})); }) ================================================ FILE: src/stim/util_top/mbqc_decomposition.cc ================================================ #include "stim/util_top/mbqc_decomposition.h" using namespace stim; const char *stim::mbqc_decomposition(GateType gate) { switch (gate) { case GateType::NOT_A_GATE: case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: case GateType::REPEAT: case GateType::MPAD: case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::I_ERROR: case GateType::II_ERROR: case GateType::PAULI_CHANNEL_1: case GateType::PAULI_CHANNEL_2: case GateType::E: case GateType::ELSE_CORRELATED_ERROR: case GateType::HERALDED_ERASE: case GateType::HERALDED_PAULI_CHANNEL_1: return nullptr; case GateType::MX: return R"CIRCUIT( MX 0 )CIRCUIT"; case GateType::MY: return R"CIRCUIT( MY 0 )CIRCUIT"; case GateType::M: return R"CIRCUIT( MZ 0 )CIRCUIT"; case GateType::MRX: return R"CIRCUIT( MX 0 CZ rec[-1] 0 )CIRCUIT"; case GateType::MRY: return R"CIRCUIT( MY 0 CX rec[-1] 0 )CIRCUIT"; case GateType::MR: return R"CIRCUIT( MZ 0 CX rec[-1] 0 )CIRCUIT"; case GateType::RX: return R"CIRCUIT( MX 0 CZ rec[-1] 0 )CIRCUIT"; case GateType::RY: return R"CIRCUIT( MY 0 CX rec[-1] 0 )CIRCUIT"; case GateType::R: return R"CIRCUIT( MZ 0 CX rec[-1] 0 )CIRCUIT"; case GateType::XCX: return R"CIRCUIT( MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 CX rec[-11] 0 rec[-10] 1 rec[-8] 0 rec[-6] 0 rec[-3] 0 rec[-13] 1 rec[-12] 1 rec[-7] 1 CY rec[-10] 0 rec[-9] 0 rec[-5] 0 rec[-4] 0 CZ rec[-13] 0 rec[-12] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::XCY: return R"CIRCUIT( MY 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZ 2 MXX 1 2 MY 2 X 0 CX rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 CY rec[-6] 1 rec[-4] 1 )CIRCUIT"; case GateType::XCZ: return R"CIRCUIT( MZ 2 MXX 0 2 MZZ 1 2 MX 2 CX rec[-4] 0 rec[-2] 0 CZ rec[-3] 1 rec[-1] 1 )CIRCUIT"; case GateType::YCX: return R"CIRCUIT( MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 X 1 CX rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-7] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-5] 1 CY rec[-6] 0 rec[-4] 0 )CIRCUIT"; case GateType::YCY: return R"CIRCUIT( MX 2 MZZ 1 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 MZZ 1 2 MX 2 Y 1 CX rec[-10] 0 rec[-9] 0 rec[-5] 0 rec[-4] 0 rec[-3] 0 rec[-11] 1 CY rec[-13] 0 rec[-12] 0 rec[-10] 1 rec[-8] 0 rec[-6] 0 rec[-7] 1 CZ rec[-13] 1 rec[-12] 1 rec[-11] 0 rec[-3] 1 rec[-2] 1 rec[-1] 1 )CIRCUIT"; case GateType::YCZ: return R"CIRCUIT( MX 2 MZZ 0 2 MY 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 0 2 MY 2 Z 0 CY rec[-6] 0 rec[-4] 0 CZ rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-7] 0 rec[-7] 1 rec[-3] 0 rec[-3] 1 rec[-2] 0 rec[-1] 0 rec[-5] 1 )CIRCUIT"; case GateType::CX: return R"CIRCUIT( MX 2 MZZ 0 2 MXX 1 2 MZ 2 CX rec[-3] 1 rec[-1] 1 CZ rec[-4] 0 rec[-2] 0 )CIRCUIT"; case GateType::CY: return R"CIRCUIT( MY 2 MZZ 1 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MX 2 MZZ 1 2 MY 2 Z 0 CY rec[-6] 1 rec[-4] 1 CZ rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 )CIRCUIT"; case GateType::CZ: return R"CIRCUIT( MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 1 2 MXX 0 2 MZ 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 CX rec[-13] 0 rec[-12] 0 rec[-2] 0 rec[-1] 0 CY rec[-10] 0 rec[-9] 0 rec[-5] 0 rec[-4] 0 CZ rec[-11] 0 rec[-10] 1 rec[-8] 0 rec[-6] 0 rec[-3] 0 rec[-13] 1 rec[-12] 1 rec[-7] 1 )CIRCUIT"; case GateType::H: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 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 )CIRCUIT"; case GateType::H_XY: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 X 0 CZ rec[-3] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::H_YZ: return R"CIRCUIT( MZ 1 MXX 0 1 MY 1 Z 0 CX rec[-3] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::H_NXY: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 Y 0 CZ rec[-3] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::H_NXZ: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 MX 1 MZZ 0 1 MY 1 Y 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 )CIRCUIT"; case GateType::H_NYZ: return R"CIRCUIT( MZ 1 MXX 0 1 MY 1 Y 0 CX rec[-3] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::I: return R"CIRCUIT( )CIRCUIT"; case GateType::X: return R"CIRCUIT( X 0 )CIRCUIT"; case GateType::Y: return R"CIRCUIT( Y 0 )CIRCUIT"; case GateType::Z: return R"CIRCUIT( Z 0 )CIRCUIT"; case GateType::C_XYZ: return R"CIRCUIT( MZ 1 MXX 0 1 MY 1 MZZ 0 1 MX 1 CX rec[-3] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::C_ZYX: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 CX rec[-2] 0 rec[-1] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-3] 0 )CIRCUIT"; case GateType::C_NXYZ: return R"CIRCUIT( MZ 1 MXX 0 1 MY 1 MZZ 0 1 MX 1 Z 0 CX rec[-3] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::C_XNYZ: return R"CIRCUIT( MZ 1 MXX 0 1 MY 1 MZZ 0 1 MX 1 X 0 CX rec[-3] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::C_XYNZ: return R"CIRCUIT( MZ 1 MXX 0 1 MY 1 MZZ 0 1 MX 1 Y 0 CX rec[-3] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::C_NZYX: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 X 0 CX rec[-2] 0 rec[-1] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-3] 0 )CIRCUIT"; case GateType::C_ZNYX: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 Z 0 CX rec[-2] 0 rec[-1] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-3] 0 )CIRCUIT"; case GateType::C_ZYNX: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 Y 0 CX rec[-2] 0 rec[-1] 0 CY rec[-5] 0 rec[-4] 0 CZ rec[-3] 0 )CIRCUIT"; case GateType::SQRT_X: return R"CIRCUIT( MZ 1 MXX 0 1 MY 1 CX rec[-3] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::SQRT_X_DAG: return R"CIRCUIT( MY 1 MXX 0 1 MZ 1 CX rec[-3] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::SQRT_Y: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 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 )CIRCUIT"; case GateType::SQRT_Y_DAG: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 MXX 0 1 MZ 1 MX 1 MZZ 0 1 MY 1 Z 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 )CIRCUIT"; case GateType::S: return R"CIRCUIT( MY 1 MZZ 0 1 MX 1 CZ rec[-3] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::S_DAG: return R"CIRCUIT( MX 1 MZZ 0 1 MY 1 CZ rec[-3] 0 rec[-2] 0 rec[-1] 0 )CIRCUIT"; case GateType::II: return ""; case GateType::SQRT_XX: return R"CIRCUIT( MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MY 2 MXX 1 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 X 1 CX rec[-18] 1 rec[-17] 1 rec[-16] 0 rec[-13] 0 rec[-11] 0 rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-15] 1 rec[-12] 1 rec[-10] 1 rec[-9] 1 rec[-8] 1 CY rec[-18] 0 rec[-17] 0 rec[-5] 0 rec[-4] 0 CZ rec[-15] 0 rec[-14] 0 rec[-8] 0 rec[-7] 0 )CIRCUIT"; case GateType::SQRT_XX_DAG: return R"CIRCUIT( MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MY 2 MXX 1 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZ 2 MXX 0 2 MY 2 X 0 CX rec[-18] 1 rec[-17] 1 rec[-16] 0 rec[-13] 0 rec[-11] 0 rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-15] 1 rec[-12] 1 rec[-10] 1 rec[-9] 1 rec[-8] 1 CY rec[-18] 0 rec[-17] 0 rec[-5] 0 rec[-4] 0 CZ rec[-15] 0 rec[-14] 0 rec[-8] 0 rec[-7] 0 )CIRCUIT"; case GateType::SQRT_YY: return R"CIRCUIT( MX 2 MZZ 1 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MY 2 MZZ 1 2 MX 2 X 0 Y 1 CX rec[-16] 1 rec[-15] 1 rec[-14] 0 rec[-6] 0 rec[-5] 0 rec[-3] 1 CY rec[-16] 0 rec[-15] 0 rec[-11] 0 rec[-8] 0 rec[-5] 1 rec[-13] 1 rec[-10] 1 rec[-4] 1 CZ rec[-13] 0 rec[-12] 0 rec[-7] 0 rec[-14] 1 rec[-2] 1 rec[-1] 1 )CIRCUIT"; case GateType::SQRT_YY_DAG: return R"CIRCUIT( MX 2 MZZ 1 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MZZ 0 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MXX 1 2 MY 2 MZZ 1 2 MX 2 Z 0 CX rec[-16] 1 rec[-15] 1 rec[-14] 0 rec[-6] 0 rec[-5] 0 rec[-3] 1 CY rec[-16] 0 rec[-15] 0 rec[-11] 0 rec[-8] 0 rec[-5] 1 rec[-13] 1 rec[-10] 1 rec[-4] 1 CZ rec[-13] 0 rec[-12] 0 rec[-7] 0 rec[-14] 1 rec[-2] 1 rec[-1] 1 )CIRCUIT"; case GateType::SQRT_ZZ: return R"CIRCUIT( MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 1 2 MXX 0 2 MZ 2 MY 2 MZZ 1 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MX 2 MZZ 0 2 MY 2 Z 0 CX rec[-15] 0 rec[-14] 0 rec[-8] 0 rec[-7] 0 CY rec[-18] 0 rec[-17] 0 rec[-5] 0 rec[-4] 0 CZ rec[-18] 1 rec[-17] 1 rec[-16] 0 rec[-13] 0 rec[-11] 0 rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-15] 1 rec[-12] 1 rec[-10] 1 rec[-9] 1 rec[-8] 1 )CIRCUIT"; case GateType::SQRT_ZZ_DAG: return R"CIRCUIT( MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 1 2 MXX 0 2 MZ 2 MY 2 MZZ 1 2 MX 2 MZZ 0 2 MY 2 MXX 0 2 MZ 2 MX 2 MZZ 0 2 MY 2 Z 1 CX rec[-15] 0 rec[-14] 0 rec[-8] 0 rec[-7] 0 CY rec[-18] 0 rec[-17] 0 rec[-5] 0 rec[-4] 0 CZ rec[-18] 1 rec[-17] 1 rec[-16] 0 rec[-13] 0 rec[-11] 0 rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 rec[-15] 1 rec[-12] 1 rec[-10] 1 rec[-9] 1 rec[-8] 1 )CIRCUIT"; case GateType::SWAP: return R"CIRCUIT( MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 CX rec[-6] 0 rec[-6] 1 rec[-2] 0 rec[-10] 1 rec[-8] 1 rec[-4] 1 CZ rec[-9] 0 rec[-5] 0 rec[-5] 1 rec[-7] 1 rec[-3] 1 rec[-1] 1 )CIRCUIT"; case GateType::ISWAP: return R"CIRCUIT( MX 2 MZZ 0 2 MY 2 MZZ 1 2 MX 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 1 2 MY 2 MXX 1 2 MZ 2 Z 1 CX rec[-10] 0 rec[-6] 0 rec[-15] 1 rec[-14] 1 rec[-2] 1 rec[-1] 1 CY rec[-12] 1 rec[-9] 1 rec[-7] 1 rec[-4] 1 CZ rec[-18] 0 rec[-18] 1 rec[-17] 0 rec[-16] 0 rec[-15] 0 rec[-14] 0 rec[-12] 0 rec[-9] 0 rec[-20] 1 rec[-19] 1 rec[-13] 1 rec[-10] 1 rec[-8] 1 rec[-3] 1 )CIRCUIT"; case GateType::CXSWAP: return R"CIRCUIT( MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 CX rec[-7] 0 rec[-7] 1 rec[-5] 0 rec[-5] 1 rec[-3] 1 rec[-1] 1 CZ rec[-6] 0 rec[-6] 1 rec[-2] 0 rec[-4] 1 )CIRCUIT"; case GateType::SWAPCX: return R"CIRCUIT( MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 CX rec[-6] 0 rec[-6] 1 rec[-2] 0 rec[-4] 1 CZ rec[-7] 0 rec[-7] 1 rec[-5] 0 rec[-5] 1 rec[-3] 1 rec[-1] 1 )CIRCUIT"; case GateType::CZSWAP: return R"CIRCUIT( MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 1 2 MY 2 MXX 1 2 MZ 2 CX rec[-10] 0 rec[-6] 0 rec[-15] 1 rec[-14] 1 rec[-2] 1 rec[-1] 1 CY rec[-12] 1 rec[-9] 1 rec[-7] 1 rec[-4] 1 CZ rec[-15] 0 rec[-14] 0 rec[-12] 0 rec[-9] 0 rec[-13] 1 rec[-10] 1 rec[-8] 1 rec[-3] 1 )CIRCUIT"; case GateType::ISWAP_DAG: return R"CIRCUIT( MX 2 MZZ 0 2 MY 2 MZZ 1 2 MX 2 MZ 2 MXX 0 2 MY 2 MZZ 0 2 MX 2 MZZ 0 2 MXX 1 2 MZ 2 MXX 0 2 MZZ 1 2 MX 2 MZZ 1 2 MY 2 MXX 1 2 MZ 2 Z 0 CX rec[-10] 0 rec[-6] 0 rec[-15] 1 rec[-14] 1 rec[-2] 1 rec[-1] 1 CY rec[-12] 1 rec[-9] 1 rec[-7] 1 rec[-4] 1 CZ rec[-18] 0 rec[-18] 1 rec[-17] 0 rec[-16] 0 rec[-15] 0 rec[-14] 0 rec[-12] 0 rec[-9] 0 rec[-20] 1 rec[-19] 1 rec[-13] 1 rec[-10] 1 rec[-8] 1 rec[-3] 1 )CIRCUIT"; case GateType::MXX: return R"CIRCUIT( MXX 0 1 )CIRCUIT"; case GateType::MYY: return R"CIRCUIT( MX 2 MZZ 0 2 MY 2 MX 2 MZZ 1 2 MY 2 MXX 0 1 MX 2 MZZ 0 2 MY 2 MX 2 MZZ 1 2 MY 2 Z 0 1 CZ rec[-13] 0 rec[-12] 0 rec[-11] 0 rec[-6] 0 rec[-5] 0 rec[-4] 0 rec[-10] 1 rec[-9] 1 rec[-8] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 )CIRCUIT"; case GateType::MZZ: return R"CIRCUIT( MZZ 0 1 )CIRCUIT"; case GateType::SPP: return R"CIRCUIT( MY 3 MZZ 1 3 MX 3 MZZ 0 3 MXX 1 3 MZ 3 MX 3 MZZ 1 3 MY 3 MZ 3 MXX 0 3 MY 3 MZZ 0 3 MX 3 MZZ 2 3 MXX 0 3 MZ 3 MX 3 MZZ 0 3 MY 3 MXX 0 3 MZ 3 MXX 0 3 MY 3 MZ 3 MXX 0 3 MY 3 MZZ 0 3 MX 3 MZZ 2 3 MXX 0 3 MZ 3 MX 3 MZZ 0 3 MY 3 MXX 0 3 MZ 3 MY 3 MZZ 1 3 MX 3 MZZ 0 3 MXX 1 3 MZ 3 MX 3 MZZ 1 3 MY 3 X 0 Y 1 Z 2 CX rec[-46] 0 rec[-46] 1 rec[-45] 0 rec[-45] 1 rec[-37] 0 rec[-36] 0 rec[-26] 0 rec[-24] 0 rec[-23] 0 rec[-22] 0 rec[-21] 0 rec[-11] 0 rec[-10] 0 CY rec[-42] 0 rec[-42] 1 rec[-37] 1 rec[-36] 1 rec[-35] 0 rec[-35] 1 rec[-32] 0 rec[-32] 1 rec[-30] 0 rec[-30] 1 rec[-27] 0 rec[-27] 1 rec[-26] 1 rec[-24] 1 rec[-23] 1 rec[-22] 1 rec[-21] 1 rec[-19] 0 rec[-19] 1 rec[-18] 0 rec[-18] 1 rec[-14] 0 rec[-14] 1 rec[-13] 0 rec[-13] 1 rec[-11] 1 rec[-10] 1 rec[-43] 1 rec[-41] 1 rec[-6] 1 rec[-4] 1 CZ rec[-44] 0 rec[-44] 1 rec[-42] 2 rec[-40] 0 rec[-40] 1 rec[-39] 0 rec[-39] 1 rec[-38] 0 rec[-38] 1 rec[-35] 2 rec[-34] 0 rec[-34] 2 rec[-33] 0 rec[-32] 2 rec[-30] 2 rec[-29] 0 rec[-28] 0 rec[-27] 2 rec[-20] 0 rec[-19] 2 rec[-17] 0 rec[-15] 0 rec[-12] 0 rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-26] 2 rec[-24] 2 rec[-23] 2 rec[-22] 2 rec[-21] 2 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 rec[-46] 2 rec[-45] 2 rec[-31] 2 rec[-16] 2 )CIRCUIT"; case GateType::SPP_DAG: return R"CIRCUIT( MY 3 MZZ 1 3 MX 3 MZZ 0 3 MXX 1 3 MZ 3 MX 3 MZZ 1 3 MY 3 MZ 3 MXX 0 3 MY 3 MZZ 0 3 MX 3 MZZ 2 3 MXX 0 3 MZ 3 MX 3 MZZ 0 3 MY 3 MXX 0 3 MZ 3 MXX 0 3 MY 3 MZ 3 MXX 0 3 MY 3 MZZ 0 3 MX 3 MZZ 2 3 MXX 0 3 MZ 3 MX 3 MZZ 0 3 MY 3 MXX 0 3 MZ 3 MY 3 MZZ 1 3 MX 3 MZZ 0 3 MXX 1 3 MZ 3 MX 3 MZZ 1 3 MY 3 CX rec[-46] 0 rec[-46] 1 rec[-45] 0 rec[-45] 1 rec[-37] 0 rec[-36] 0 rec[-26] 0 rec[-24] 0 rec[-23] 0 rec[-22] 0 rec[-21] 0 rec[-11] 0 rec[-10] 0 CY rec[-42] 0 rec[-42] 1 rec[-37] 1 rec[-36] 1 rec[-35] 0 rec[-35] 1 rec[-32] 0 rec[-32] 1 rec[-30] 0 rec[-30] 1 rec[-27] 0 rec[-27] 1 rec[-26] 1 rec[-24] 1 rec[-23] 1 rec[-22] 1 rec[-21] 1 rec[-19] 0 rec[-19] 1 rec[-18] 0 rec[-18] 1 rec[-14] 0 rec[-14] 1 rec[-13] 0 rec[-13] 1 rec[-11] 1 rec[-10] 1 rec[-43] 1 rec[-41] 1 rec[-6] 1 rec[-4] 1 CZ rec[-44] 0 rec[-44] 1 rec[-42] 2 rec[-40] 0 rec[-40] 1 rec[-39] 0 rec[-39] 1 rec[-38] 0 rec[-38] 1 rec[-35] 2 rec[-34] 0 rec[-34] 2 rec[-33] 0 rec[-32] 2 rec[-30] 2 rec[-29] 0 rec[-28] 0 rec[-27] 2 rec[-20] 0 rec[-19] 2 rec[-17] 0 rec[-15] 0 rec[-12] 0 rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-26] 2 rec[-24] 2 rec[-23] 2 rec[-22] 2 rec[-21] 2 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 rec[-46] 2 rec[-45] 2 rec[-31] 2 rec[-16] 2 )CIRCUIT"; case GateType::MPP: return R"CIRCUIT( MY 5 MZZ 1 5 MX 5 MZZ 0 5 MXX 1 5 MZ 5 MX 5 MZZ 1 5 MY 5 MZ 5 MXX 2 5 MY 5 MZZ 2 5 MX 5 MXX 0 2 MXX 3 4 MX 5 MZZ 2 5 MY 5 MXX 2 5 MZ 5 MY 5 MZZ 1 5 MX 5 MZZ 0 5 MXX 1 5 MZ 5 MX 5 MZZ 1 5 MY 5 CX rec[-21] 2 rec[-20] 2 rec[-11] 2 rec[-10] 2 CY rec[-27] 1 rec[-25] 1 rec[-6] 1 rec[-4] 1 rec[-18] 2 rec[-13] 2 CZ rec[-28] 0 rec[-28] 1 rec[-26] 0 rec[-24] 0 rec[-24] 1 rec[-23] 0 rec[-23] 1 rec[-22] 0 rec[-22] 1 rec[-9] 0 rec[-9] 1 rec[-8] 0 rec[-8] 1 rec[-5] 0 rec[-30] 1 rec[-29] 1 rec[-7] 1 rec[-3] 1 rec[-2] 1 rec[-1] 1 rec[-19] 2 rec[-12] 2 )CIRCUIT"; default: throw std::invalid_argument("Unhandled gate type " + std::string(GATE_DATA[gate].name)); } } ================================================ FILE: src/stim/util_top/mbqc_decomposition.h ================================================ #ifndef _STIM_UTIL_TOP_MBQC_DECOMPOSITION_H #define _STIM_UTIL_TOP_MBQC_DECOMPOSITION_H #include "stim/gates/gates.h" namespace stim { const char *mbqc_decomposition(GateType gate); } // namespace stim #endif ================================================ FILE: src/stim/util_top/mbqc_decomposition.test.cc ================================================ #include "stim/util_top/mbqc_decomposition.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.h" #include "stim/util_bot/test_util.test.h" #include "stim/util_top/has_flow.h" using namespace stim; TEST(mbqc_decomposition, all_gates) { auto rng = INDEPENDENT_TEST_RNG(); for (const auto &g : GATE_DATA.items) { const char *decomp = mbqc_decomposition(g.id); if (decomp == nullptr) { EXPECT_TRUE(g.flow_data.empty()) << g.name; continue; } else { EXPECT_FALSE(g.flow_data.empty()) << g.name; } Circuit circuit(decomp); std::vector> verified_flows; for (auto &flow : g.flows<64>()) { if (flow.input.ref().weight() && flow.output.ref().weight()) { verified_flows.push_back(std::move(flow)); } } std::vector result = sample_if_circuit_has_stabilizer_flows<64>(256, rng, circuit, verified_flows); bool correct = true; for (auto b : result) { correct &= b; } EXPECT_TRUE(correct) << g.name; } } ================================================ FILE: src/stim/util_top/missing_detectors.cc ================================================ #include "stim/util_top/missing_detectors.h" #include "stim/util_top/circuit_flow_generators.h" using namespace stim; static Circuit missing_detectors_impl(const Circuit &circuit) { size_t num_measurements = circuit.count_measurements(); std::vector, bool>> rows; std::vector> logical_operators; std::set ignored_logical_operators; // Turn existing detectors and observables into rows in the table. uint64_t measurement_offset = 0; circuit.for_each_operation([&](CircuitInstruction inst) { measurement_offset += inst.count_measurement_results(); if (inst.gate_type == GateType::DETECTOR || inst.gate_type == GateType::OBSERVABLE_INCLUDE) { if (inst.gate_type == GateType::DETECTOR) { rows.push_back({simd_bits<64>(num_measurements), false}); } else { while (logical_operators.size() <= (size_t)inst.args[0]) { logical_operators.push_back(simd_bits<64>(num_measurements)); } } simd_bits_range_ref<64> row = inst.gate_type == GateType::DETECTOR ? rows.back().first : logical_operators[(size_t)inst.args[0]]; for (auto e : inst.targets) { if (e.is_measurement_record_target()) { row[e.rec_offset() + measurement_offset] ^= true; } else if (e.is_pauli_target() && inst.gate_type == GateType::OBSERVABLE_INCLUDE) { ignored_logical_operators.insert((size_t)inst.args[0]); } } } }); for (size_t k = 0; k < logical_operators.size(); k++) { if (!ignored_logical_operators.contains(k)) { rows.push_back({std::move(logical_operators[k]), false}); } } std::vector> original_detector_rows_for_cleanup; for (const auto &row : rows) { original_detector_rows_for_cleanup.push_back(row.first); } // Turn measurement invariants into rows in the table. for (const auto &generator : circuit_flow_generators<64>(circuit)) { if (generator.input.ref().has_no_pauli_terms() && generator.output.ref().has_no_pauli_terms() && generator.observables.empty()) { rows.push_back({simd_bits<64>(num_measurements), true}); for (int32_t e : generator.measurements) { if (e < 0) { rows.back().first[e + num_measurements] ^= true; } else { rows.back().first[e] ^= true; } } } } // Perform Gaussian elimination on the table. size_t num_solved = 0; for (size_t k = 0; k < num_measurements; k++) { size_t pivot = SIZE_MAX; // Try to find a DETECTOR pivot. for (size_t r = num_solved; r < rows.size() && pivot == SIZE_MAX; r++) { if (rows[r].first[k] && !rows[r].second) { pivot = r; } } // Fall back to a flow invariant pivot. for (size_t r = num_solved; r < rows.size() && pivot == SIZE_MAX; r++) { if (rows[r].first[k]) { pivot = r; } } if (pivot == SIZE_MAX) { continue; } for (size_t r = 0; r < rows.size(); r++) { if (rows[r].first[k] && r != pivot) { rows[r].first ^= rows[pivot].first; } } if (pivot != num_solved) { std::swap(rows[pivot], rows[num_solved]); } num_solved++; } // Any rows from invariants that the detector rows failed to clear are assumed to be the missing detectors. Circuit result; for (auto &r : rows) { if (r.second && r.first.not_zero()) { // Attempt to reduce the weight of the results by at least not overlapping with existing detectors. for (const auto &det : original_detector_rows_for_cleanup) { if (r.first.is_subset_of_or_equal_to(det)) { r.first ^= det; } } // Convert set bits into a DETECTOR instruction. r.first.for_each_set_bit([&](size_t bit_position) { result.target_buf.append_tail(GateTarget::rec((int32_t)bit_position - (int32_t)num_measurements)); }); result.operations.push_back( CircuitInstruction{ GateType::DETECTOR, {}, result.target_buf.commit_tail(), "", }); } } return result; } Circuit stim::missing_detectors(const Circuit &circuit, bool unknown_input) { if (unknown_input) { return missing_detectors_impl(circuit); } else { Circuit with_resets; uint32_t num_qubits = (uint32_t)circuit.count_qubits(); for (uint32_t k = 0; k < num_qubits; k++) { with_resets.target_buf.append_tail(GateTarget::qubit(k)); } with_resets.operations.push_back( CircuitInstruction{ GateType::R, {}, with_resets.target_buf.commit_tail(), "", }); with_resets += circuit; return missing_detectors_impl(with_resets); } } ================================================ FILE: src/stim/util_top/missing_detectors.h ================================================ /* * 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. */ #ifndef _STIM_UTIL_TOP_MISSING_DETECTORS_H #define _STIM_UTIL_TOP_MISSING_DETECTORS_H #include "stim/circuit/circuit.h" namespace stim { Circuit missing_detectors(const Circuit &circuit, bool unknown_input); } // namespace stim #endif ================================================ FILE: src/stim/util_top/missing_detectors.test.cc ================================================ #include "stim/util_top/missing_detectors.h" #include "gtest/gtest.h" #include "stim/circuit/circuit.h" #include "stim/simulators/error_analyzer.h" #include "stim/util_bot/test_util.test.h" #include "stim/util_top/has_flow.h" using namespace stim; TEST(missing_detectors, circuit) { ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( )CIRCUIT"), false), Circuit(R"CIRCUIT( )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( R 0 M 0 DETECTOR rec[-1] )CIRCUIT"), false), Circuit(R"CIRCUIT( )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( R 0 M 0 DETECTOR rec[-1] DETECTOR rec[-1] )CIRCUIT"), false), Circuit(R"CIRCUIT( )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( R 0 M 0 )CIRCUIT"), false), Circuit(R"CIRCUIT( DETECTOR rec[-1] )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( M 0 )CIRCUIT"), false), Circuit(R"CIRCUIT( DETECTOR rec[-1] )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( M 0 )CIRCUIT"), true), Circuit(R"CIRCUIT( )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( R 0 1 M 0 1 DETECTOR rec[-1] )CIRCUIT"), false), Circuit(R"CIRCUIT( DETECTOR rec[-2] )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( MPP Z0*Z1 X0*X1 TICK MPP Z0*Z1 X0*X1 DETECTOR rec[-1] rec[-3] DETECTOR rec[-2] rec[-4] )CIRCUIT"), false), Circuit(R"CIRCUIT( DETECTOR rec[-4] )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( MPP Z0*Z1 X0*X1 TICK MPP Z0*Z1 X0*X1 DETECTOR rec[-1] rec[-3] DETECTOR rec[-2] rec[-4] DETECTOR rec[-1] rec[-3] rec[-2] rec[-4] )CIRCUIT"), false), Circuit(R"CIRCUIT( DETECTOR rec[-3] rec[-2] rec[-1] )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( MPP Z0*Z1 X0*X1 TICK MPP Z0*Z1 X0*X1 DETECTOR rec[-1] rec[-3] DETECTOR rec[-2] rec[-4] )CIRCUIT"), true), Circuit(R"CIRCUIT( )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( MPP Z0*Z1 X0*X1 TICK MPP Z0*Z1 X0*X1 OBSERVABLE_INCLUDE(0) rec[-1] DETECTOR rec[-2] rec[-4] OBSERVABLE_INCLUDE(0) rec[-3] )CIRCUIT"), true), Circuit(R"CIRCUIT( )CIRCUIT")); ASSERT_EQ( missing_detectors( Circuit(R"CIRCUIT( OBSERVABLE_INCLUDE(0) Z0 Z1 MPP Z0*Z1 X0*X1 TICK MPP Z0*Z1 X0*X1 OBSERVABLE_INCLUDE(0) Z0 Z1 OBSERVABLE_INCLUDE(0) rec[-1] DETECTOR rec[-2] rec[-4] OBSERVABLE_INCLUDE(0) rec[-3] )CIRCUIT"), true), Circuit(R"CIRCUIT( DETECTOR rec[-3] rec[-1] )CIRCUIT")); } TEST(missing_detectors, big_case_honeycomb_code) { Circuit c = Circuit(R"CIRCUIT( R 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 TICK MYY 8 9 17 18 26 27 35 36 44 45 51 52 5 6 14 15 23 24 32 33 41 42 0 1 7 16 25 34 43 50 11 12 20 21 29 30 38 39 47 48 3 4 13 22 31 40 2 10 19 28 37 46 49 53 TICK MX 0 1 2 3 4 8 51 50 52 41 47 53 MXX 48 49 9 10 18 19 27 28 36 37 45 46 11 20 29 38 6 7 15 16 24 25 33 34 42 43 12 13 21 22 30 31 39 40 5 14 23 32 17 26 35 44 DETECTOR rec[-32] rec[-33] rec[-49] DETECTOR rec[-29] rec[-30] rec[-40] DETECTOR rec[-25] rec[-27] rec[-55] DETECTOR rec[-21] rec[-22] rec[-23] rec[-34] rec[-41] DETECTOR rec[-20] rec[-28] rec[-31] rec[-37] rec[-60] DETECTOR rec[-9] rec[-24] rec[-26] rec[-46] rec[-50] DETECTOR rec[-7] rec[-8] rec[-15] rec[-39] rec[-44] rec[-45] DETECTOR rec[-5] rec[-6] rec[-14] rec[-38] rec[-42] rec[-43] DETECTOR rec[-4] rec[-12] rec[-13] rec[-48] rec[-53] rec[-54] DETECTOR rec[-3] rec[-10] rec[-11] rec[-47] rec[-51] rec[-52] DETECTOR rec[-2] rec[-18] rec[-19] rec[-36] rec[-58] rec[-59] DETECTOR rec[-1] rec[-16] rec[-17] rec[-35] rec[-56] rec[-57] TICK M 0 5 14 23 32 41 13 22 31 40 49 53 MZZ 19 20 28 29 37 38 46 47 10 11 21 30 4 12 2 3 39 48 16 17 25 26 34 35 43 44 50 51 7 8 15 24 1 6 33 42 9 18 27 36 45 52 TICK MYY 8 9 17 18 26 27 35 36 44 45 51 52 5 6 14 15 23 24 32 33 41 42 0 1 7 16 25 34 43 50 11 12 20 21 29 30 38 39 47 48 3 4 13 22 31 40 2 10 19 28 37 46 49 53 DETECTOR rec[-15] rec[-26] rec[-27] rec[-30] rec[-34] rec[-39] rec[-108] rec[-119] rec[-120] DETECTOR rec[-14] rec[-24] rec[-25] rec[-29] rec[-37] rec[-38] rec[-107] rec[-117] rec[-118] DETECTOR rec[-13] rec[-22] rec[-23] rec[-28] rec[-35] rec[-36] rec[-106] rec[-115] rec[-116] DETECTOR rec[-4] rec[-7] rec[-12] rec[-41] rec[-42] rec[-44] rec[-97] rec[-100] rec[-105] DETECTOR rec[-3] rec[-10] rec[-11] rec[-43] rec[-47] rec[-48] rec[-96] rec[-103] rec[-104] DETECTOR rec[-2] rec[-8] rec[-9] rec[-40] rec[-45] rec[-46] rec[-95] rec[-101] rec[-102] TICK M 0 5 14 23 32 41 13 22 31 40 49 53 MZZ 19 20 28 29 37 38 46 47 10 11 21 30 4 12 2 3 39 48 16 17 25 26 34 35 43 44 50 51 7 8 15 24 1 6 33 42 9 18 27 36 45 52 DETECTOR rec[-26] rec[-27] rec[-86] rec[-87] DETECTOR rec[-24] rec[-25] rec[-84] rec[-85] DETECTOR rec[-22] rec[-23] rec[-82] rec[-83] DETECTOR rec[-16] rec[-20] rec[-21] rec[-76] rec[-80] rec[-81] DETECTOR rec[-14] rec[-15] rec[-17] rec[-74] rec[-75] rec[-77] DETECTOR rec[-13] rec[-18] rec[-19] rec[-73] rec[-78] rec[-79] DETECTOR rec[-6] rec[-30] rec[-31] rec[-66] rec[-90] rec[-91] DETECTOR rec[-5] rec[-32] rec[-33] rec[-65] rec[-92] rec[-93] DETECTOR rec[-4] rec[-28] rec[-29] rec[-64] rec[-88] rec[-89] DETECTOR rec[-3] rec[-7] rec[-12] rec[-63] rec[-67] rec[-72] DETECTOR rec[-2] rec[-10] rec[-11] rec[-62] rec[-70] rec[-71] DETECTOR rec[-1] rec[-8] rec[-9] rec[-61] rec[-68] rec[-69] TICK MX 0 1 2 3 4 8 51 50 52 41 47 53 MXX 48 49 9 10 18 19 27 28 36 37 45 46 11 20 29 38 6 7 15 16 24 25 33 34 42 43 12 13 21 22 30 31 39 40 5 14 23 32 17 26 35 44 DETECTOR rec[-30] rec[-31] rec[-47] rec[-107] rec[-156] rec[-157] DETECTOR rec[-26] rec[-27] rec[-41] rec[-101] rec[-152] rec[-153] DETECTOR rec[-16] rec[-23] rec[-25] rec[-34] rec[-51] rec[-94] rec[-111] rec[-142] rec[-149] rec[-151] DETECTOR rec[-15] rec[-19] rec[-20] rec[-36] rec[-50] rec[-54] rec[-96] rec[-110] rec[-114] rec[-141] rec[-145] rec[-146] DETECTOR rec[-14] rec[-17] rec[-18] rec[-35] rec[-52] rec[-53] rec[-95] rec[-112] rec[-113] rec[-140] rec[-143] rec[-144] DETECTOR rec[-13] rec[-28] rec[-32] rec[-38] rec[-40] rec[-98] rec[-100] rec[-139] rec[-154] rec[-158] DETECTOR rec[-2] rec[-11] rec[-12] rec[-39] rec[-44] rec[-45] rec[-99] rec[-104] rec[-105] rec[-128] rec[-137] rec[-138] DETECTOR rec[-1] rec[-9] rec[-10] rec[-37] rec[-42] rec[-43] rec[-97] rec[-102] rec[-103] rec[-127] rec[-135] rec[-136] TICK MYY 8 9 17 18 26 27 35 36 44 45 51 52 5 6 14 15 23 24 32 33 41 42 0 1 7 16 25 34 43 50 11 12 20 21 29 30 38 39 47 48 3 4 13 22 31 40 2 10 19 28 37 46 49 53 DETECTOR rec[-15] rec[-20] rec[-21] rec[-31] rec[-39] rec[-40] rec[-157] rec[-165] rec[-166] rec[-201] rec[-206] rec[-207] DETECTOR rec[-14] rec[-18] rec[-19] rec[-30] rec[-37] rec[-38] rec[-156] rec[-163] rec[-164] rec[-200] rec[-204] rec[-205] DETECTOR rec[-6] rec[-11] rec[-12] rec[-34] rec[-35] rec[-42] rec[-160] rec[-161] rec[-168] rec[-192] rec[-197] rec[-198] DETECTOR rec[-5] rec[-9] rec[-10] rec[-32] rec[-33] rec[-41] rec[-158] rec[-159] rec[-167] rec[-191] rec[-195] rec[-196] DETECTOR rec[-3] rec[-25] rec[-26] rec[-29] rec[-45] rec[-46] rec[-155] rec[-171] rec[-172] rec[-189] rec[-211] rec[-212] DETECTOR rec[-2] rec[-23] rec[-24] rec[-28] rec[-43] rec[-44] rec[-154] rec[-169] rec[-170] rec[-188] rec[-209] rec[-210] TICK MX 0 1 2 3 4 8 51 50 52 41 47 53 MXX 48 49 9 10 18 19 27 28 36 37 45 46 11 20 29 38 6 7 15 16 24 25 33 34 42 43 12 13 21 22 30 31 39 40 5 14 23 32 17 26 35 44 DETECTOR rec[-32] rec[-33] rec[-92] rec[-93] DETECTOR rec[-29] rec[-30] rec[-89] rec[-90] DETECTOR rec[-25] rec[-27] rec[-85] rec[-87] DETECTOR rec[-21] rec[-22] rec[-23] rec[-81] rec[-82] rec[-83] DETECTOR rec[-20] rec[-28] rec[-31] rec[-80] rec[-88] rec[-91] DETECTOR rec[-9] rec[-24] rec[-26] rec[-69] rec[-84] rec[-86] DETECTOR rec[-7] rec[-8] rec[-15] rec[-67] rec[-68] rec[-75] DETECTOR rec[-5] rec[-6] rec[-14] rec[-65] rec[-66] rec[-74] DETECTOR rec[-4] rec[-12] rec[-13] rec[-64] rec[-72] rec[-73] DETECTOR rec[-3] rec[-10] rec[-11] rec[-63] rec[-70] rec[-71] DETECTOR rec[-2] rec[-18] rec[-19] rec[-62] rec[-78] rec[-79] DETECTOR rec[-1] rec[-16] rec[-17] rec[-61] rec[-76] rec[-77] TICK M 0 5 14 23 32 41 13 22 31 40 49 53 MZZ 19 20 28 29 37 38 46 47 10 11 21 30 4 12 2 3 39 48 16 17 25 26 34 35 43 44 50 51 7 8 15 24 1 6 33 42 9 18 27 36 45 52 DETECTOR rec[-31] rec[-32] rec[-37] rec[-97] rec[-157] rec[-158] DETECTOR rec[-29] rec[-30] rec[-36] rec[-96] rec[-155] rec[-156] DETECTOR rec[-16] rec[-25] rec[-26] rec[-39] rec[-40] rec[-99] rec[-100] rec[-142] rec[-151] rec[-152] DETECTOR rec[-13] rec[-23] rec[-24] rec[-38] rec[-54] rec[-98] rec[-114] rec[-139] rec[-149] rec[-150] DETECTOR rec[-6] rec[-11] rec[-12] rec[-35] rec[-44] rec[-45] rec[-95] rec[-104] rec[-105] rec[-132] rec[-137] rec[-138] DETECTOR rec[-4] rec[-9] rec[-10] rec[-34] rec[-42] rec[-43] rec[-94] rec[-102] rec[-103] rec[-130] rec[-135] rec[-136] DETECTOR rec[-3] rec[-17] rec[-21] rec[-48] rec[-52] rec[-53] rec[-108] rec[-112] rec[-113] rec[-129] rec[-143] rec[-147] DETECTOR rec[-2] rec[-19] rec[-20] rec[-47] rec[-50] rec[-51] rec[-107] rec[-110] rec[-111] rec[-128] rec[-145] rec[-146] TICK MYY 8 9 17 18 26 27 35 36 44 45 51 52 5 6 14 15 23 24 32 33 41 42 0 1 7 16 25 34 43 50 11 12 20 21 29 30 38 39 47 48 3 4 13 22 31 40 2 10 19 28 37 46 49 53 DETECTOR rec[-15] rec[-26] rec[-27] rec[-30] rec[-34] rec[-39] rec[-156] rec[-160] rec[-165] rec[-201] rec[-212] rec[-213] DETECTOR rec[-14] rec[-24] rec[-25] rec[-29] rec[-37] rec[-38] rec[-155] rec[-163] rec[-164] rec[-200] rec[-210] rec[-211] DETECTOR rec[-13] rec[-22] rec[-23] rec[-28] rec[-35] rec[-36] rec[-154] rec[-161] rec[-162] rec[-199] rec[-208] rec[-209] DETECTOR rec[-4] rec[-7] rec[-12] rec[-41] rec[-42] rec[-44] rec[-167] rec[-168] rec[-170] rec[-190] rec[-193] rec[-198] DETECTOR rec[-3] rec[-10] rec[-11] rec[-43] rec[-47] rec[-48] rec[-169] rec[-173] rec[-174] rec[-189] rec[-196] rec[-197] DETECTOR rec[-2] rec[-8] rec[-9] rec[-40] rec[-45] rec[-46] rec[-166] rec[-171] rec[-172] rec[-188] rec[-194] rec[-195] TICK M 0 5 14 23 32 41 13 22 31 40 49 53 MZZ 19 20 28 29 37 38 46 47 10 11 21 30 4 12 2 3 39 48 16 17 25 26 34 35 43 44 50 51 7 8 15 24 1 6 33 42 9 18 27 36 45 52 DETECTOR rec[-26] rec[-27] rec[-86] rec[-87] DETECTOR rec[-24] rec[-25] rec[-84] rec[-85] DETECTOR rec[-22] rec[-23] rec[-82] rec[-83] DETECTOR rec[-16] rec[-20] rec[-21] rec[-76] rec[-80] rec[-81] # OOPS DETECTOR rec[-14] rec[-15] rec[-17] rec[-74] rec[-75] rec[-77] DETECTOR rec[-13] rec[-18] rec[-19] rec[-73] rec[-78] rec[-79] DETECTOR rec[-6] rec[-30] rec[-31] rec[-66] rec[-90] rec[-91] DETECTOR rec[-5] rec[-32] rec[-33] rec[-65] rec[-92] rec[-93] DETECTOR rec[-4] rec[-28] rec[-29] rec[-64] rec[-88] rec[-89] DETECTOR rec[-3] rec[-7] rec[-12] rec[-63] rec[-67] rec[-72] DETECTOR rec[-2] rec[-10] rec[-11] rec[-62] rec[-70] rec[-71] DETECTOR rec[-1] rec[-8] rec[-9] rec[-61] rec[-68] rec[-69] TICK MX 0 1 2 3 4 8 51 50 52 41 47 53 MXX 48 49 9 10 18 19 27 28 36 37 45 46 11 20 29 38 6 7 15 16 24 25 33 34 42 43 12 13 21 22 30 31 39 40 5 14 23 32 17 26 35 44 DETECTOR rec[-30] rec[-31] rec[-47] rec[-107] rec[-156] rec[-157] DETECTOR rec[-26] rec[-27] rec[-41] rec[-101] rec[-152] rec[-153] DETECTOR rec[-16] rec[-23] rec[-25] rec[-34] rec[-51] rec[-94] rec[-111] rec[-142] rec[-149] rec[-151] DETECTOR rec[-15] rec[-19] rec[-20] rec[-36] rec[-50] rec[-54] rec[-96] rec[-110] rec[-114] rec[-141] rec[-145] rec[-146] DETECTOR rec[-14] rec[-17] rec[-18] rec[-35] rec[-52] rec[-53] rec[-95] rec[-112] rec[-113] rec[-140] rec[-143] rec[-144] DETECTOR rec[-13] rec[-28] rec[-32] rec[-38] rec[-40] rec[-98] rec[-100] rec[-139] rec[-154] rec[-158] DETECTOR rec[-2] rec[-11] rec[-12] rec[-39] rec[-44] rec[-45] rec[-99] rec[-104] rec[-105] rec[-128] rec[-137] rec[-138] DETECTOR rec[-1] rec[-9] rec[-10] rec[-37] rec[-42] rec[-43] rec[-97] rec[-102] rec[-103] rec[-127] rec[-135] rec[-136] TICK MYY 8 9 17 18 26 27 35 36 44 45 51 52 5 6 14 15 23 24 32 33 41 42 0 1 7 16 25 34 43 50 11 12 20 21 29 30 38 39 47 48 3 4 13 22 31 40 2 10 19 28 37 46 49 53 DETECTOR rec[-15] rec[-20] rec[-21] rec[-31] rec[-39] rec[-40] rec[-157] rec[-165] rec[-166] rec[-201] rec[-206] rec[-207] DETECTOR rec[-14] rec[-18] rec[-19] rec[-30] rec[-37] rec[-38] rec[-156] rec[-163] rec[-164] rec[-200] rec[-204] rec[-205] DETECTOR rec[-6] rec[-11] rec[-12] rec[-34] rec[-35] rec[-42] rec[-160] rec[-161] rec[-168] rec[-192] rec[-197] rec[-198] DETECTOR rec[-5] rec[-9] rec[-10] rec[-32] rec[-33] rec[-41] rec[-158] rec[-159] rec[-167] rec[-191] rec[-195] rec[-196] DETECTOR rec[-3] rec[-25] rec[-26] rec[-29] rec[-45] rec[-46] rec[-155] rec[-171] rec[-172] rec[-189] rec[-211] rec[-212] DETECTOR rec[-2] rec[-23] rec[-24] rec[-28] rec[-43] rec[-44] rec[-154] rec[-169] rec[-170] rec[-188] rec[-209] rec[-210] TICK MX 0 1 2 3 4 8 51 50 52 41 47 53 MXX 48 49 9 10 18 19 27 28 36 37 45 46 11 20 29 38 6 7 15 16 24 25 33 34 42 43 12 13 21 22 30 31 39 40 5 14 23 32 17 26 35 44 DETECTOR rec[-32] rec[-33] rec[-92] rec[-93] DETECTOR rec[-29] rec[-30] rec[-89] rec[-90] DETECTOR rec[-25] rec[-27] rec[-85] rec[-87] DETECTOR rec[-21] rec[-22] rec[-23] rec[-81] rec[-82] rec[-83] DETECTOR rec[-20] rec[-28] rec[-31] rec[-80] rec[-88] rec[-91] DETECTOR rec[-9] rec[-24] rec[-26] rec[-69] rec[-84] rec[-86] DETECTOR rec[-7] rec[-8] rec[-15] rec[-67] rec[-68] rec[-75] DETECTOR rec[-5] rec[-6] rec[-14] rec[-65] rec[-66] rec[-74] DETECTOR rec[-4] rec[-12] rec[-13] rec[-64] rec[-72] rec[-73] DETECTOR rec[-3] rec[-10] rec[-11] rec[-63] rec[-70] rec[-71] DETECTOR rec[-2] rec[-18] rec[-19] rec[-62] rec[-78] rec[-79] DETECTOR rec[-1] rec[-16] rec[-17] rec[-61] rec[-76] rec[-77] TICK M 0 5 14 23 32 41 13 22 31 40 49 53 MZZ 19 20 28 29 37 38 46 47 10 11 21 30 4 12 2 3 39 48 16 17 25 26 34 35 43 44 50 51 7 8 15 24 1 6 33 42 9 18 27 36 45 52 DETECTOR rec[-31] rec[-32] rec[-37] rec[-97] rec[-157] rec[-158] DETECTOR rec[-29] rec[-30] rec[-36] rec[-96] rec[-155] rec[-156] DETECTOR rec[-16] rec[-25] rec[-26] rec[-39] rec[-40] rec[-99] rec[-100] rec[-142] rec[-151] rec[-152] DETECTOR rec[-13] rec[-23] rec[-24] rec[-38] rec[-54] rec[-98] rec[-114] rec[-139] rec[-149] rec[-150] DETECTOR rec[-6] rec[-11] rec[-12] rec[-35] rec[-44] rec[-45] rec[-95] rec[-104] rec[-105] rec[-132] rec[-137] rec[-138] DETECTOR rec[-4] rec[-9] rec[-10] rec[-34] rec[-42] rec[-43] rec[-94] rec[-102] rec[-103] rec[-130] rec[-135] rec[-136] DETECTOR rec[-3] rec[-17] rec[-21] rec[-48] rec[-52] rec[-53] rec[-108] rec[-112] rec[-113] rec[-129] rec[-143] rec[-147] DETECTOR rec[-2] rec[-19] rec[-20] rec[-47] rec[-50] rec[-51] rec[-107] rec[-110] rec[-111] rec[-128] rec[-145] rec[-146] TICK MYY 8 9 17 18 26 27 35 36 44 45 51 52 5 6 14 15 23 24 32 33 41 42 0 1 7 16 25 34 43 50 11 12 20 21 29 30 38 39 47 48 3 4 13 22 31 40 2 10 19 28 37 46 49 53 DETECTOR rec[-15] rec[-26] rec[-27] rec[-30] rec[-34] rec[-39] rec[-156] rec[-160] rec[-165] rec[-201] rec[-212] rec[-213] DETECTOR rec[-14] rec[-24] rec[-25] rec[-29] rec[-37] rec[-38] rec[-155] rec[-163] rec[-164] rec[-200] rec[-210] rec[-211] DETECTOR rec[-13] rec[-22] rec[-23] rec[-28] rec[-35] rec[-36] rec[-154] rec[-161] rec[-162] rec[-199] rec[-208] rec[-209] DETECTOR rec[-4] rec[-7] rec[-12] rec[-41] rec[-42] rec[-44] rec[-167] rec[-168] rec[-170] rec[-190] rec[-193] rec[-198] DETECTOR rec[-3] rec[-10] rec[-11] rec[-43] rec[-47] rec[-48] rec[-169] rec[-173] rec[-174] rec[-189] rec[-196] rec[-197] DETECTOR rec[-2] rec[-8] rec[-9] rec[-40] rec[-45] rec[-46] rec[-166] rec[-171] rec[-172] rec[-188] rec[-194] rec[-195] TICK M 0 5 14 23 32 41 13 22 31 40 49 53 MZZ 19 20 28 29 37 38 46 47 10 11 21 30 4 12 2 3 39 48 16 17 25 26 34 35 43 44 50 51 7 8 15 24 1 6 33 42 9 18 27 36 45 52 DETECTOR rec[-26] rec[-27] rec[-86] rec[-87] DETECTOR rec[-24] rec[-25] rec[-84] rec[-85] DETECTOR rec[-22] rec[-23] rec[-82] rec[-83] DETECTOR rec[-16] rec[-20] rec[-21] rec[-76] rec[-80] rec[-81] DETECTOR rec[-14] rec[-15] rec[-17] rec[-74] rec[-75] rec[-77] DETECTOR rec[-13] rec[-18] rec[-19] rec[-73] rec[-78] rec[-79] DETECTOR rec[-6] rec[-30] rec[-31] rec[-66] rec[-90] rec[-91] DETECTOR rec[-5] rec[-32] rec[-33] rec[-65] rec[-92] rec[-93] DETECTOR rec[-4] rec[-28] rec[-29] rec[-64] rec[-88] rec[-89] DETECTOR rec[-3] rec[-7] rec[-12] rec[-63] rec[-67] rec[-72] DETECTOR rec[-2] rec[-10] rec[-11] rec[-62] rec[-70] rec[-71] DETECTOR rec[-1] rec[-8] rec[-9] rec[-61] rec[-68] rec[-69] TICK MX 0 1 2 3 4 8 51 50 52 41 47 53 MXX 9 10 18 19 27 28 36 37 45 46 11 20 29 38 6 7 15 16 24 25 33 34 42 43 12 13 21 22 30 31 39 40 5 14 23 32 17 26 35 44 48 49 DETECTOR rec[-30] rec[-31] rec[-47] rec[-107] rec[-156] rec[-157] DETECTOR rec[-26] rec[-27] rec[-41] rec[-101] rec[-152] rec[-153] DETECTOR rec[-17] rec[-23] rec[-25] rec[-34] rec[-51] rec[-94] rec[-111] rec[-142] rec[-149] rec[-151] DETECTOR rec[-16] rec[-20] rec[-21] rec[-36] rec[-50] rec[-54] rec[-96] rec[-110] rec[-114] rec[-141] rec[-145] rec[-146] DETECTOR rec[-15] rec[-18] rec[-19] rec[-35] rec[-52] rec[-53] rec[-95] rec[-112] rec[-113] rec[-140] rec[-143] rec[-144] DETECTOR rec[-14] rec[-28] rec[-32] rec[-38] rec[-40] rec[-98] rec[-100] rec[-139] rec[-154] rec[-158] DETECTOR rec[-3] rec[-12] rec[-13] rec[-39] rec[-44] rec[-45] rec[-99] rec[-104] rec[-105] rec[-128] rec[-137] rec[-138] DETECTOR rec[-2] rec[-10] rec[-11] rec[-37] rec[-42] rec[-43] rec[-97] rec[-102] rec[-103] rec[-127] rec[-135] rec[-136] TICK MYY 8 9 17 18 26 27 35 36 44 45 51 52 5 6 14 15 23 24 32 33 41 42 0 1 7 16 25 34 43 50 11 12 20 21 29 30 38 39 47 48 3 4 13 22 31 40 2 10 19 28 37 46 49 53 DETECTOR rec[-15] rec[-20] rec[-21] rec[-32] rec[-40] rec[-41] rec[-157] rec[-165] rec[-166] rec[-201] rec[-206] rec[-207] DETECTOR rec[-14] rec[-18] rec[-19] rec[-31] rec[-38] rec[-39] rec[-156] rec[-163] rec[-164] rec[-200] rec[-204] rec[-205] DETECTOR rec[-6] rec[-11] rec[-12] rec[-35] rec[-36] rec[-43] rec[-160] rec[-161] rec[-168] rec[-192] rec[-197] rec[-198] DETECTOR rec[-5] rec[-9] rec[-10] rec[-33] rec[-34] rec[-42] rec[-158] rec[-159] rec[-167] rec[-191] rec[-195] rec[-196] DETECTOR rec[-3] rec[-25] rec[-26] rec[-30] rec[-46] rec[-47] rec[-155] rec[-171] rec[-172] rec[-189] rec[-211] rec[-212] DETECTOR rec[-2] rec[-23] rec[-24] rec[-29] rec[-44] rec[-45] rec[-154] rec[-169] rec[-170] rec[-188] rec[-209] rec[-210] TICK M 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 DETECTOR rec[-53] rec[-54] rec[-70] rec[-113] rec[-114] DETECTOR rec[-50] rec[-51] rec[-61] rec[-110] rec[-111] DETECTOR rec[-44] rec[-45] rec[-46] rec[-52] rec[-58] rec[-81] rec[-102] rec[-109] rec[-112] DETECTOR rec[-42] rec[-43] rec[-44] rec[-50] rec[-51] rec[-52] rec[-58] rec[-61] rec[-66] rec[-128] rec[-129] rec[-131] rec[-151] rec[-154] rec[-159] DETECTOR rec[-38] rec[-39] rec[-40] rec[-47] rec[-48] rec[-49] rec[-69] rec[-74] rec[-75] rec[-86] rec[-94] rec[-95] DETECTOR rec[-36] rec[-37] rec[-38] rec[-45] rec[-46] rec[-47] rec[-69] rec[-80] rec[-81] rec[-117] rec[-121] rec[-126] rec[-162] rec[-173] rec[-174] DETECTOR rec[-32] rec[-33] rec[-34] rec[-41] rec[-42] rec[-43] rec[-60] rec[-65] rec[-66] rec[-89] rec[-90] rec[-97] DETECTOR rec[-26] rec[-27] rec[-28] rec[-35] rec[-36] rec[-37] rec[-57] rec[-79] rec[-80] rec[-84] rec[-100] rec[-101] DETECTOR rec[-24] rec[-25] rec[-26] rec[-33] rec[-34] rec[-35] rec[-57] rec[-64] rec[-65] rec[-130] rec[-134] rec[-135] rec[-150] rec[-157] rec[-158] DETECTOR rec[-20] rec[-21] rec[-22] rec[-29] rec[-30] rec[-31] rec[-68] rec[-72] rec[-73] rec[-85] rec[-92] rec[-93] DETECTOR rec[-18] rec[-19] rec[-20] rec[-27] rec[-28] rec[-29] rec[-68] rec[-78] rec[-79] rec[-116] rec[-124] rec[-125] rec[-161] rec[-171] rec[-172] DETECTOR rec[-14] rec[-15] rec[-16] rec[-23] rec[-24] rec[-25] rec[-59] rec[-63] rec[-64] rec[-87] rec[-88] rec[-96] DETECTOR rec[-8] rec[-9] rec[-10] rec[-17] rec[-18] rec[-19] rec[-56] rec[-77] rec[-78] rec[-83] rec[-98] rec[-99] DETECTOR rec[-6] rec[-7] rec[-8] rec[-15] rec[-16] rec[-17] rec[-56] rec[-62] rec[-63] rec[-127] rec[-132] rec[-133] rec[-149] rec[-155] rec[-156] DETECTOR rec[-4] rec[-11] rec[-12] rec[-13] rec[-67] rec[-71] rec[-91] rec[-105] rec[-107] DETECTOR rec[-2] rec[-3] rec[-76] rec[-106] rec[-108] DETECTOR rec[-2] rec[-3] rec[-4] rec[-9] rec[-10] rec[-11] rec[-67] rec[-76] rec[-77] rec[-115] rec[-122] rec[-123] rec[-160] rec[-169] rec[-170] DETECTOR rec[-1] rec[-5] rec[-6] rec[-7] rec[-55] rec[-62] rec[-82] rec[-103] rec[-104] OBSERVABLE_INCLUDE(0) rec[-2] rec[-3] rec[-9] rec[-10] rec[-18] rec[-19] rec[-27] rec[-28] rec[-36] rec[-37] rec[-45] rec[-46] rec[-76] rec[-77] rec[-78] rec[-79] rec[-80] rec[-81] rec[-83] rec[-84] rec[-108] rec[-109] rec[-115] rec[-116] rec[-117] rec[-175] rec[-176] rec[-177] rec[-208] rec[-209] rec[-234] rec[-235] rec[-268] rec[-269] rec[-294] rec[-295] rec[-301] rec[-302] rec[-303] rec[-361] rec[-362] rec[-363] rec[-394] rec[-395] rec[-420] rec[-421] rec[-454] rec[-455] rec[-480] rec[-481] rec[-487] rec[-488] rec[-489] rec[-547] rec[-548] rec[-549] rec[-580] rec[-581] rec[-606] rec[-607] rec[-634] rec[-635] rec[-636] rec[-637] rec[-638] rec[-639] )CIRCUIT"); Circuit suffix = missing_detectors(c, true); ASSERT_EQ(suffix, Circuit(R"CIRCUIT( DETECTOR rec[-377] rec[-375] rec[-374] rec[-317] rec[-315] rec[-314] )CIRCUIT")); } TEST(missing_detectors, toric_code_global_stabilizer_product) { Circuit c = Circuit(R"CIRCUIT( R 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 TICK MPP X0*X4*X5*X1 X2*X6*X7*X3 X10*X14*X15*X11 X8*X12*X13*X9 TICK MPP X5*X9*X10*X6 X1*X13*X14*X2 X0*X12*X15*X3 X4*X8*X11*X7 TICK MPP Z4*Z8*Z9*Z5 Z6*Z10*Z11*Z7 Z2*Z14*Z15*Z3 Z0*Z12*Z13*Z1 TICK MPP Z1*Z5*Z6*Z2 Z9*Z13*Z14*Z10 Z8*Z12*Z15*Z11 Z0*Z4*Z7*Z3 DETECTOR rec[-1] DETECTOR rec[-2] DETECTOR rec[-3] DETECTOR rec[-4] DETECTOR rec[-5] DETECTOR rec[-6] DETECTOR rec[-7] DETECTOR rec[-8] )CIRCUIT"); Circuit suffix = missing_detectors(c, true); ASSERT_EQ(suffix, Circuit(R"CIRCUIT( DETECTOR rec[-16] rec[-15] rec[-14] rec[-13] rec[-12] rec[-11] rec[-10] rec[-9] )CIRCUIT")); } ================================================ FILE: src/stim/util_top/reference_sample_tree.cc ================================================ #include "stim/util_top/reference_sample_tree.h" #if defined(_WIN32) #include #pragma intrinsic(_umul128) #endif using namespace stim; bool ReferenceSampleTree::empty() const { if (repetitions == 0) { return true; } if (!prefix_bits.empty()) { return false; } for (const auto &child : suffix_children) { if (!child.empty()) { return false; } } return true; } void ReferenceSampleTree::flatten_and_simplify_into(std::vector &out) const { if (repetitions == 0) { return; } // Flatten children. std::vector flattened; if (!prefix_bits.empty()) { flattened.push_back( ReferenceSampleTree{ .prefix_bits = prefix_bits, .suffix_children = {}, .repetitions = 1, }); } for (const auto &child : suffix_children) { child.flatten_and_simplify_into(flattened); } // Fuse children. std::vector fused; if (!flattened.empty()) { fused.push_back(std::move(flattened[0])); } for (size_t k = 1; k < flattened.size(); k++) { auto &dst = fused.back(); auto &src = flattened[k]; // Combine children with identical contents by adding their rep counts. if (dst.prefix_bits == src.prefix_bits && dst.suffix_children == src.suffix_children) { dst.repetitions += src.repetitions; // Fuse children with unrepeated contents if they can be fused. } else if (src.repetitions == 1 && dst.repetitions == 1 && dst.suffix_children.empty()) { dst.suffix_children = std::move(src.suffix_children); dst.prefix_bits.insert(dst.prefix_bits.end(), src.prefix_bits.begin(), src.prefix_bits.end()); } else { fused.push_back(std::move(src)); } } if (repetitions == 1) { // Un-nest all the children. for (auto &e : fused) { out.push_back(e); } } else if (fused.size() == 1) { // Merge with single child. fused[0].repetitions *= repetitions; out.push_back(std::move(fused[0])); } else if (fused.empty()) { // Nothing to report. } else if (fused[0].suffix_children.empty() && fused[0].repetitions == 1) { // Take payload from first child. ReferenceSampleTree result = std::move(fused[0]); fused.erase(fused.begin()); result.repetitions = repetitions; result.suffix_children = std::move(fused); out.push_back(std::move(result)); } else { out.push_back( ReferenceSampleTree{ .prefix_bits = {}, .suffix_children = std::move(fused), .repetitions = repetitions, }); } } /// Finds how far back feedback operations ever look, within the loop. uint64_t stim::max_feedback_lookback_in_loop(const Circuit &loop) { uint64_t furthest_lookback = 0; for (const auto &inst : loop.operations) { if (inst.gate_type == GateType::REPEAT) { furthest_lookback = std::max(furthest_lookback, max_feedback_lookback_in_loop(inst.repeat_block_body(loop))); } else { auto f = GATE_DATA[inst.gate_type].flags; if ((f & GateFlags::GATE_CAN_TARGET_BITS) && (f & GateFlags::GATE_TARGETS_PAIRS)) { // Feedback-capable operation. Check for any measurement record targets. for (auto t : inst.targets) { if (t.is_measurement_record_target()) { furthest_lookback = std::max(furthest_lookback, (uint64_t)-t.rec_offset()); } } } } } return furthest_lookback; } void ReferenceSampleTree::try_factorize(size_t period_factor) { if (prefix_bits.size() != 0 || suffix_children.size() % period_factor != 0) { return; } // Check if contents are periodic with the factor. size_t h = suffix_children.size() / period_factor; for (size_t k = h; k < suffix_children.size(); k++) { if (suffix_children[k - h] != suffix_children[k]) { return; } } // Factorize. suffix_children.resize(h); repetitions *= period_factor; } ReferenceSampleTree ReferenceSampleTree::simplified() const { std::vector flat; flatten_and_simplify_into(flat); if (flat.empty()) { return ReferenceSampleTree(); } else if (flat.size() == 1) { return std::move(flat[0]); } ReferenceSampleTree result; result.repetitions = 1; // Take payload from first child. if (flat[0].repetitions == 1 && flat[0].suffix_children.empty()) { result = std::move(flat[0]); flat.erase(flat.begin()); } result.suffix_children = std::move(flat); return result; } uint64_t ReferenceSampleTree::size() const { uint64_t result = prefix_bits.size(); for (const auto &child : suffix_children) { result += child.size(); } #if defined(__GNUC__) || defined(__clang__) bool overflow = __builtin_mul_overflow(result, repetitions, &result); assert(!overflow); #elif defined(_WIN64) uint64_t overflow; result = _umul128(result, repetitions, &overflow); assert(overflow == 0); #else assert((repetitions == 0) || (result <= (UINT64_MAX / repetitions))); result *= repetitions; #endif return result; } void ReferenceSampleTree::decompress_into(std::vector &output) const { for (uint64_t k = 0; k < repetitions; k++) { output.insert(output.end(), prefix_bits.begin(), prefix_bits.end()); for (const auto &child : suffix_children) { child.decompress_into(output); } } } ReferenceSampleTree ReferenceSampleTree::from_circuit_reference_sample(const Circuit &circuit) { auto stats = circuit.compute_stats(); std::mt19937_64 irrelevant_rng{0}; CompressedReferenceSampleHelper helper( TableauSimulator( std::move(irrelevant_rng), stats.num_qubits, +1, MeasureRecord(stats.max_lookback))); return helper.do_loop_with_tortoise_hare_folding(circuit, 1).simplified(); } std::string ReferenceSampleTree::str() const { std::stringstream ss; ss << *this; return ss.str(); } bool ReferenceSampleTree::operator==(const ReferenceSampleTree &other) const { return repetitions == other.repetitions && prefix_bits == other.prefix_bits && suffix_children == other.suffix_children; } bool ReferenceSampleTree::operator!=(const ReferenceSampleTree &other) const { return !(*this == other); } bool ReferenceSampleTree::operator[](uint64_t index) const { uint64_t current_absolute_index = 0; bool result; bool value_found = try_get_bit_value(index, current_absolute_index, result); assert(value_found); return result; } bool ReferenceSampleTree::try_get_bit_value( uint64_t desired_absolute_index, uint64_t ¤t_absolute_index, bool &bit_value) const { // Run through once to allow shallow accesses (without unnecessary full iteration of the tree). const uint64_t current_relative_starting_index = current_absolute_index; { const uint64_t relative_index = desired_absolute_index - current_absolute_index; if (relative_index < prefix_bits.size()) { bit_value = prefix_bits[relative_index]; return true; } current_absolute_index += prefix_bits.size(); for (const ReferenceSampleTree &child : suffix_children) { if (child.try_get_bit_value(desired_absolute_index, current_absolute_index, bit_value)) { return true; } } } // After the first full iteration, extrapolate the repetition size and skip to the proper iteration. const uint64_t single_iteration_size = current_absolute_index - current_relative_starting_index; const uint64_t skip_to_iteration_count = (desired_absolute_index - current_relative_starting_index) / single_iteration_size; if (skip_to_iteration_count < repetitions) { // If the desired index is in this part of the tree, skip forward to the appropriate iteration. current_absolute_index += single_iteration_size * (skip_to_iteration_count - 1); // Do the final iteration to find the value. const uint64_t relative_index = desired_absolute_index - current_absolute_index; if (relative_index < prefix_bits.size()) { bit_value = prefix_bits[relative_index]; return true; } current_absolute_index += prefix_bits.size(); for (const ReferenceSampleTree &child : suffix_children) { if (child.try_get_bit_value(desired_absolute_index, current_absolute_index, bit_value)) { return true; } } } else { // Advance past this node for parent to continue. current_absolute_index += single_iteration_size * (repetitions - 1); } return false; } std::ostream &stim::operator<<(std::ostream &out, const ReferenceSampleTree &v) { out << v.repetitions << "*"; out << "("; out << "'"; for (auto b : v.prefix_bits) { out << "01"[b]; } out << "'"; for (const auto &child : v.suffix_children) { out << "+"; out << child; } out << ")"; return out; } ================================================ FILE: src/stim/util_top/reference_sample_tree.h ================================================ #ifndef _STIM_UTIL_TOP_REFERENCE_SAMPLE_TREE_H #define _STIM_UTIL_TOP_REFERENCE_SAMPLE_TREE_H #include "stim/simulators/tableau_simulator.h" namespace stim { /// A compressed tree representation of a reference sample. struct ReferenceSampleTree { /// Raw bits to output before bits from the children. std::vector prefix_bits; /// Compressed representations of additional bits to output after the prefix. std::vector suffix_children; /// The number of times to repeatedly output the prefix+suffix bits. size_t repetitions = 0; /// Initializes a reference sample tree containing a reference sample for the given circuit. static ReferenceSampleTree from_circuit_reference_sample(const Circuit &circuit); /// Returns a tree with the same compressed contents, but a simpler tree structure. ReferenceSampleTree simplified() const; /// Checks if two trees are exactly the same, including structure (not just uncompressed contents). bool operator==(const ReferenceSampleTree &other) const; /// Checks if two trees are not exactly the same, including structure (not just uncompressed contents). bool operator!=(const ReferenceSampleTree &other) const; /// Returns the bit value for a given absolute index. bool operator[](uint64_t index) const; /// Returns a simple description of the tree's structure, like "5*('101'+6*('11'))". std::string str() const; /// Determines whether the tree contains any bits at all. bool empty() const; /// Computes the total size of the uncompressed bits represented by the tree. uint64_t size() const; /// Writes the contents of the tree into the given output vector. void decompress_into(std::vector &output) const; /// Writes the contents of the tree into the given output simd_bits. template void decompress_into(simd_bits &output) const; /// Folds redundant children into the repetition count, if they repeat this many times. /// /// For example, if the tree's children are [A, B, C, A, B, C] and the tree has no /// prefix, then `try_factorize(2)` will reduce the children to [A, B, C] and double /// the repetition count. void try_factorize(size_t period_factor); private: /// Helper method for `simplified`. void flatten_and_simplify_into(std::vector &out) const; /// Helper method for `operator[]`. bool try_get_bit_value(uint64_t desired_absolute_index, uint64_t ¤t_absolute_index, bool &bit_value) const; }; std::ostream &operator<<(std::ostream &out, const ReferenceSampleTree &v); /// Helper class for computing compressed reference samples. template struct CompressedReferenceSampleHelper { TableauSimulator sim; CompressedReferenceSampleHelper(TableauSimulator sim) : sim(sim) { } /// Processes a loop with no top-level folding. /// /// Loops containing within the body of this loop (or circuit body) may /// still be compressed. Only the top-level loop is not folded. ReferenceSampleTree do_loop_with_no_folding(const Circuit &loop, uint64_t reps); /// Runs tortoise-and-hare analysis of the loop while simulating its /// reference sample, in order to attempt to return a compressed /// representation. ReferenceSampleTree do_loop_with_tortoise_hare_folding(const Circuit &loop, uint64_t reps); bool in_same_recent_state_as( const CompressedReferenceSampleHelper &other, uint64_t max_record_lookback, bool allow_false_negative) const; }; uint64_t max_feedback_lookback_in_loop(const Circuit &loop); } // namespace stim #include "stim/util_top/reference_sample_tree.inl" #endif ================================================ FILE: src/stim/util_top/reference_sample_tree.inl ================================================ #include "stim/util_top/reference_sample_tree.h" namespace stim { template void ReferenceSampleTree::decompress_into(simd_bits &output) const { std::vector v; this->decompress_into(v); simd_bits result(v.size()); for (size_t k = 0; k < v.size(); k++) { result[k] ^= v[k]; } output = std::move(result); } template ReferenceSampleTree CompressedReferenceSampleHelper::do_loop_with_no_folding(const Circuit &loop, uint64_t reps) { ReferenceSampleTree result; result.repetitions = 1; size_t start_size = sim.measurement_record.storage.size(); auto flush_recorded_into_result = [&]() { size_t end_size = sim.measurement_record.storage.size(); if (end_size > start_size) { result.suffix_children.push_back({}); auto &child = result.suffix_children.back(); child.repetitions = 1; child.prefix_bits.insert( child.prefix_bits.end(), sim.measurement_record.storage.begin() + start_size, sim.measurement_record.storage.begin() + end_size); } start_size = end_size; }; for (size_t k = 0; k < reps; k++) { for (const auto &inst : loop.operations) { if (inst.gate_type == GateType::REPEAT) { uint64_t repeats = inst.repeat_block_rep_count(); const auto &block = inst.repeat_block_body(loop); flush_recorded_into_result(); result.suffix_children.push_back(do_loop_with_tortoise_hare_folding(block, repeats)); start_size = sim.measurement_record.storage.size(); } else { sim.do_gate(inst); } } } flush_recorded_into_result(); return result; } template ReferenceSampleTree CompressedReferenceSampleHelper::do_loop_with_tortoise_hare_folding( const Circuit &loop, uint64_t reps) { if (reps < 10) { // Probably not worth the overhead of tortoise-and-hare. Just run it raw. return do_loop_with_no_folding(loop, reps); } ReferenceSampleTree result; result.repetitions = 1; CompressedReferenceSampleHelper tortoise(sim); CompressedReferenceSampleHelper hare(std::move(sim)); uint64_t max_feedback_lookback = max_feedback_lookback_in_loop(loop); uint64_t tortoise_steps = 0; uint64_t hare_steps = 0; while (hare_steps < reps) { hare_steps++; result.suffix_children.push_back(hare.do_loop_with_no_folding(loop, 1)); assert(result.suffix_children.size() == hare_steps); if (tortoise.in_same_recent_state_as(hare, max_feedback_lookback, hare_steps < 10)) { break; } // Tortoise advances half as quickly. if (hare_steps & 1) { tortoise_steps++; tortoise.do_loop_with_no_folding(loop, 1); } } if (hare_steps == reps) { // No periodic state found before reaching the end of the loop. sim = std::move(hare.sim); return result; } // Run more loop iterations until the remaining iterations are a multiple of the found period. assert(result.suffix_children.size() == hare_steps); uint64_t period = hare_steps - tortoise_steps; size_t period_steps_left = (reps - hare_steps) / period; while ((reps - hare_steps) % period) { result.suffix_children.push_back(hare.do_loop_with_no_folding(loop, 1)); hare_steps += 1; } assert(hare_steps + period_steps_left * period == reps); assert(hare_steps >= period); sim = std::move(hare.sim); // Move the periodic measurements out of the hare's tail, into a loop node. ReferenceSampleTree loop_contents; for (size_t k = hare_steps - period; k < hare_steps; k++) { loop_contents.suffix_children.push_back(std::move(result.suffix_children[k])); } result.suffix_children.resize(hare_steps - period); // Add skipped iterations' measurement data into the sim's measurement record. loop_contents.repetitions = 1; sim.measurement_record.discard_results_past_max_lookback(); for (size_t k = 0; k < period_steps_left && sim.measurement_record.storage.size() < sim.measurement_record.max_lookback * 2; k++) { loop_contents.decompress_into(sim.measurement_record.storage); } sim.measurement_record.discard_results_past_max_lookback(); // Add the loop node to the output data. loop_contents.repetitions = period_steps_left + 1; loop_contents.try_factorize(2); loop_contents.try_factorize(3); loop_contents.try_factorize(5); result.suffix_children.push_back(std::move(loop_contents)); return result; } template bool CompressedReferenceSampleHelper::in_same_recent_state_as( const CompressedReferenceSampleHelper &other, uint64_t max_record_lookback, bool allow_false_negative) const { const auto &s1 = sim.measurement_record.storage; const auto &s2 = other.sim.measurement_record.storage; // Check that recent measurements gave identical results. if (s1.size() < max_record_lookback || s2.size() < max_record_lookback) { return false; } for (size_t k = 0; k < max_record_lookback; k++) { if (s1[s1.size() - k - 1] != s2[s2.size() - k - 1]) { return false; } } // Check that quantum states are identical. if (allow_false_negative) { return sim.inv_state == other.sim.inv_state; } return sim.canonical_stabilizers() == other.sim.canonical_stabilizers(); } } // namespace stim ================================================ FILE: src/stim/util_top/reference_sample_tree.perf.cc ================================================ #include "stim/util_top/reference_sample_tree.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_surface_code.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(reference_sample_tree_surface_code_d31_r1000000000) { CircuitGenParameters params(1000000000, 31, "rotated_memory_x"); auto circuit = generate_surface_code_circuit(params).circuit; simd_bits ref(0); auto total = 0; benchmark_go([&]() { auto result = ReferenceSampleTree::from_circuit_reference_sample(circuit); total += result.empty(); }).goal_millis(25); if (total) { std::cerr << "data dependence"; } } BENCHMARK(reference_sample_tree_nested_circuit) { Circuit circuit(R"CIRCUIT( M 0 REPEAT 100000 { REPEAT 100000 { REPEAT 100000 { X 0 M 0 } X 0 M 0 } X 0 M 0 } X 0 M 0 )CIRCUIT"); simd_bits ref(0); auto total = 0; benchmark_go([&]() { auto result = ReferenceSampleTree::from_circuit_reference_sample(circuit); total += result.empty(); }).goal_micros(230); if (total) { std::cerr << "data dependence"; } } ================================================ FILE: src/stim/util_top/reference_sample_tree.test.cc ================================================ #include "stim/util_top/reference_sample_tree.h" #include "gtest/gtest.h" #include "stim/gen/gen_surface_code.h" using namespace stim; void expect_tree_matches_normal_reference_sample_of(const ReferenceSampleTree &tree, const Circuit &circuit) { std::vector decompressed; tree.decompress_into(decompressed); simd_bits actual(decompressed.size()); for (size_t k = 0; k < decompressed.size(); k++) { actual[k] = decompressed[k]; } auto expected = TableauSimulator::reference_sample_circuit(circuit); EXPECT_EQ(actual, expected); for (size_t index = 0; index < decompressed.size(); ++index) { ASSERT_EQ(tree[index], decompressed[index]) << "index: " << index; } } TEST(ReferenceSampleTree, equality) { ReferenceSampleTree empty1{ .prefix_bits = {}, .suffix_children = {}, .repetitions = 0, }; ReferenceSampleTree empty2; ASSERT_EQ(empty1, empty2); ASSERT_FALSE(empty1 != empty2); ASSERT_NE(empty1, (ReferenceSampleTree{.prefix_bits = {}, .suffix_children{}, .repetitions = 1})); ASSERT_NE(empty1, (ReferenceSampleTree{.prefix_bits = {0}, .suffix_children{}, .repetitions = 0})); ASSERT_NE(empty1, (ReferenceSampleTree{.prefix_bits = {}, .suffix_children{{}}, .repetitions = 0})); } TEST(ReferenceSampleTree, str) { ASSERT_EQ( (ReferenceSampleTree{ .prefix_bits = {}, .suffix_children = {}, .repetitions = 0, } .str()), "0*('')"); ASSERT_EQ( (ReferenceSampleTree{ .prefix_bits = {1, 1, 0, 1}, .suffix_children = {}, .repetitions = 0, } .str()), "0*('1101')"); ASSERT_EQ( (ReferenceSampleTree{ .prefix_bits = {1, 1, 0, 1}, .suffix_children = {}, .repetitions = 2, } .str()), "2*('1101')"); ASSERT_EQ( (ReferenceSampleTree{ .prefix_bits = {1, 1, 0, 1}, .suffix_children = {ReferenceSampleTree{ .prefix_bits = {1}, .suffix_children = {}, .repetitions = 5, }}, .repetitions = 2, } .str()), "2*('1101'+5*('1'))"); } TEST(ReferenceSampleTree, simplified) { ReferenceSampleTree raw{ .prefix_bits = {}, .suffix_children = { ReferenceSampleTree{ .prefix_bits = {}, .suffix_children = {}, .repetitions = 1, }, ReferenceSampleTree{ .prefix_bits = {1, 0, 1}, .suffix_children = {{}}, .repetitions = 0, }, ReferenceSampleTree{ .prefix_bits = {1, 1, 1}, .suffix_children = {}, .repetitions = 2, }, }, .repetitions = 3, }; ASSERT_EQ(raw.simplified().str(), "6*('111')"); } TEST(ReferenceSampleTree, decompress_into) { std::vector result; ReferenceSampleTree tree_under_test{ .prefix_bits = {1, 1, 0, 1}, .suffix_children = {ReferenceSampleTree{ .prefix_bits = {1}, .suffix_children = {}, .repetitions = 5, }}, .repetitions = 2, }; tree_under_test.decompress_into(result); std::vector expected = std::vector{1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1}; ASSERT_EQ(result, expected); for (size_t index = 0; index < expected.size(); ++index) { ASSERT_EQ(tree_under_test[index], expected[index]) << "index: " << index; } result.clear(); tree_under_test = ReferenceSampleTree{ .prefix_bits = {1, 1, 0, 1}, .suffix_children = { ReferenceSampleTree{ .prefix_bits = {1, 0, 1}, .suffix_children = {}, .repetitions = 8, }, ReferenceSampleTree{ .prefix_bits = {0, 0}, .suffix_children = {}, .repetitions = 1, }, }, .repetitions = 1, }; tree_under_test.decompress_into(result); expected = std::vector{1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0}; ASSERT_EQ(result, expected); for (size_t index = 0; index < expected.size(); ++index) { ASSERT_EQ(tree_under_test[index], expected[index]) << "index: " << index; } } TEST(ReferenceSampleTree, simple_circuit) { Circuit circuit(R"CIRCUIT( M 0 X 0 M 0 )CIRCUIT"); auto ref = ReferenceSampleTree::from_circuit_reference_sample(circuit); expect_tree_matches_normal_reference_sample_of(ref, circuit); ASSERT_EQ(ref.str(), "1*('01')"); } TEST(ReferenceSampleTree, simple_loop) { Circuit circuit(R"CIRCUIT( REPEAT 50 { M 0 X 0 M 0 } )CIRCUIT"); auto ref = ReferenceSampleTree::from_circuit_reference_sample(circuit); expect_tree_matches_normal_reference_sample_of(ref, circuit); ASSERT_EQ(ref.str(), "25*('0110')"); } TEST(ReferenceSampleTree, period4_loop) { Circuit circuit(R"CIRCUIT( M 0 X 0 M 0 REPEAT 50 { CX 0 1 1 2 2 3 M 0 1 2 3 } X 0 M 0 X 2 M 2 2 2 2 2 MPAD 1 0 1 0 1 1 )CIRCUIT"); auto ref = ReferenceSampleTree::from_circuit_reference_sample(circuit); ASSERT_EQ(ref.size(), circuit.count_measurements()); expect_tree_matches_normal_reference_sample_of(ref, circuit); ASSERT_EQ(ref.str(), "1*('01111110101100100011111010'+11*('1100100011111010')+1*('000000101011'))"); } TEST(ReferenceSampleTree, feedback) { Circuit circuit(R"CIRCUIT( MPAD 0 0 1 0 REPEAT 200 { CX rec[-4] 1 M 1 } )CIRCUIT"); auto ref = ReferenceSampleTree::from_circuit_reference_sample(circuit); ASSERT_EQ(ref.size(), circuit.count_measurements()); expect_tree_matches_normal_reference_sample_of(ref, circuit); ASSERT_EQ(ref.str(), "1*('0010'+2*('0')+4*('1')+1*('01011001000111')+12*('101011001000111'))"); } TEST(max_feedback_lookback_in_loop, simple) { ASSERT_EQ(max_feedback_lookback_in_loop(Circuit()), 0); ASSERT_EQ( max_feedback_lookback_in_loop(Circuit(R"CIRCUIT( REPEAT 100 { REPEAT 100 { M 0 X 0 M 0 } REPEAT 200 { M 0 DETECTOR rec[-1] } X 1 CX 1 0 } )CIRCUIT")), 0); ASSERT_EQ( max_feedback_lookback_in_loop(Circuit(R"CIRCUIT( CX rec[-1] 0 )CIRCUIT")), 1); ASSERT_EQ( max_feedback_lookback_in_loop(Circuit(R"CIRCUIT( CZ 0 rec[-2] )CIRCUIT")), 2); ASSERT_EQ( max_feedback_lookback_in_loop(Circuit(R"CIRCUIT( CZ 0 rec[-2] CY 0 rec[-3] )CIRCUIT")), 3); ASSERT_EQ( max_feedback_lookback_in_loop(Circuit(R"CIRCUIT( CZ 0 rec[-2] REPEAT 100 { CX rec[-5] 0 } )CIRCUIT")), 5); } TEST(ReferenceSampleTree, nested_loops) { Circuit circuit(R"CIRCUIT( REPEAT 100 { REPEAT 100 { M 0 X 0 M 0 } REPEAT 200 { M 0 } X 1 CX 1 0 } )CIRCUIT"); auto ref = ReferenceSampleTree::from_circuit_reference_sample(circuit); expect_tree_matches_normal_reference_sample_of(ref, circuit); ASSERT_EQ( ref.str(), "1*(''+50*('0110')+200*('0')+50*('1001')+200*('1')+50*('1001')+200*('1')+50*('0110')+200*('0')+24*(''+50*('" "0110')+200*('0')+50*('1001')+200*('1')+50*('1001')+200*('1')+50*('0110')+200*('0')))"); } TEST(ReferenceSampleTree, surface_code) { CircuitGenParameters params(10000, 5, "rotated_memory_x"); auto circuit = generate_surface_code_circuit(params).circuit; auto ref = ReferenceSampleTree::from_circuit_reference_sample(circuit); ASSERT_EQ(ref.str(), "1*(''+10000*('000000000000000000000000')+1*('0000000000000000000000000'))"); } TEST(ReferenceSampleTree, surface_code_with_pauli) { CircuitGenParameters params(10000, 3, "rotated_memory_x"); auto circuit = generate_surface_code_circuit(params).circuit; circuit.blocks[0].append_from_text("X 10 11 12 13"); auto ref = ReferenceSampleTree::from_circuit_reference_sample(circuit); ASSERT_EQ(ref.str(), "1*(''+2*('00000000')+4999*('0110000000110000')+1*('000000000'))"); } TEST(ReferenceSampleTree, surface_code_with_pauli_vs_normal_reference_sample) { CircuitGenParameters params(20, 3, "rotated_memory_x"); auto circuit = generate_surface_code_circuit(params).circuit; circuit.blocks[0].append_from_text("X 10 11 12 13"); auto ref = ReferenceSampleTree::from_circuit_reference_sample(circuit); ASSERT_EQ(ref.size(), circuit.count_measurements()); expect_tree_matches_normal_reference_sample_of(ref, circuit); } TEST(ReferenceSampleTree, random_access_large_tree) { ReferenceSampleTree tree_under_test{ .prefix_bits = {1, 1, 0, 1}, .suffix_children = {ReferenceSampleTree{ .prefix_bits = {1, 0, 1}, .suffix_children = {}, .repetitions = 60'000'000, }, ReferenceSampleTree{ .prefix_bits = {0, 0, 0, 0, 0, 0, 1}, .suffix_children = {ReferenceSampleTree{ .prefix_bits = {1, 1, 1, 0, 0, 1}, .suffix_children = {}, .repetitions = 42, }}, .repetitions = 2'000'000'000, }, ReferenceSampleTree{ .prefix_bits = {0, 0, 0, 0, 1}, .suffix_children = {}, .repetitions = 999'000'000, }}, .repetitions = 1'234'000, }; std::vector expected_beginning = std::vector{1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1}; for (size_t index = 0; index < expected_beginning.size(); ++index) { ASSERT_EQ(tree_under_test[index], expected_beginning[index]) << "index: " << index; } uint64_t whole_tree_size = tree_under_test.size(); ASSERT_EQ( whole_tree_size, 1'234'000ULL * (4 + (60'000'000ULL * 3) + (2'000'000'000ULL * (7 + (42 * 6))) + (999'000'000ULL * 5))); std::vector expected_ending = std::vector{0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; for (uint64_t index = 0; index < expected_ending.size(); ++index) { ASSERT_EQ(tree_under_test[whole_tree_size - expected_ending.size() + index], expected_ending[index]) << "index: " << index; } // Same thing on previous outer iteration. uint64_t one_outer_iter_before_ending = whole_tree_size - (whole_tree_size / 1'234'000ULL); for (uint64_t index = 0; index < expected_ending.size(); ++index) { ASSERT_EQ( tree_under_test[one_outer_iter_before_ending - expected_ending.size() + index], expected_ending[index]) << "index: " << index; } } ================================================ FILE: src/stim/util_top/simplified_circuit.cc ================================================ #include "stim/util_top/simplified_circuit.h" #include #include "stim/circuit/gate_decomposition.h" #include "stim/mem/simd_bits.h" using namespace stim; struct Simplifier { size_t num_qubits; std::function yield; simd_bits<64> used; std::vector qs1_buf; std::vector qs2_buf; std::vector qs_buf; Simplifier(size_t num_qubits, std::function init_yield) : num_qubits(num_qubits), yield(init_yield), used(num_qubits) { } void do_xcz(SpanRef targets, std::string_view tag) { if (targets.empty()) { return; } qs_buf.clear(); for (size_t k = 0; k < targets.size(); k += 2) { qs_buf.push_back(targets[k + 1]); qs_buf.push_back(targets[k]); } yield(CircuitInstruction{GateType::CX, {}, qs_buf, tag}); } void simplify_potentially_overlapping_1q_instruction(const CircuitInstruction &inst) { used.clear(); size_t start = 0; for (size_t k = 0; k < inst.targets.size(); k++) { auto t = inst.targets[k]; if (t.has_qubit_value() && used[t.qubit_value()]) { CircuitInstruction disjoint = CircuitInstruction{inst.gate_type, inst.args, inst.targets.sub(start, k), inst.tag}; simplify_disjoint_1q_instruction(disjoint); used.clear(); start = k; } if (t.has_qubit_value()) { used[t.qubit_value()] = true; } } simplify_disjoint_1q_instruction( CircuitInstruction{inst.gate_type, inst.args, inst.targets.sub(start, inst.targets.size()), inst.tag}); } void simplify_potentially_overlapping_2q_instruction(const CircuitInstruction &inst) { used.clear(); size_t start = 0; for (size_t k = 0; k < inst.targets.size(); k += 2) { auto a = inst.targets[k]; auto b = inst.targets[k + 1]; if ((a.has_qubit_value() && used[a.qubit_value()]) || (b.has_qubit_value() && used[b.qubit_value()])) { CircuitInstruction disjoint = CircuitInstruction{inst.gate_type, inst.args, inst.targets.sub(start, k), inst.tag}; simplify_disjoint_2q_instruction(disjoint); used.clear(); start = k; } if (a.has_qubit_value()) { used[a.qubit_value()] = true; } if (b.has_qubit_value()) { used[b.qubit_value()] = true; } } simplify_disjoint_2q_instruction( CircuitInstruction{inst.gate_type, inst.args, inst.targets.sub(start, inst.targets.size()), inst.tag}); } void simplify_disjoint_1q_instruction(const CircuitInstruction &inst) { const auto &ts = inst.targets; switch (inst.gate_type) { case GateType::I: // Do nothing. break; case GateType::X: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::Y: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::Z: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::C_XYZ: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::C_NXYZ: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::C_XNYZ: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::C_XYNZ: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::C_ZYX: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::C_ZYNX: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::C_ZNYX: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::C_NZYX: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::H: yield({GateType::H, {}, ts, inst.tag}); break; case GateType::H_XY: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::H_YZ: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::H_NXY: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::H_NXZ: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::H_NYZ: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::S: yield({GateType::S, {}, ts, inst.tag}); break; case GateType::SQRT_X: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::SQRT_X_DAG: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::SQRT_Y: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::SQRT_Y_DAG: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::S_DAG: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::MX: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::M, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::MY: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::M, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::M: yield({GateType::M, {}, ts, inst.tag}); break; case GateType::MRX: yield({GateType::H, {}, ts, inst.tag}); yield({GateType::M, {}, ts, inst.tag}); yield({GateType::R, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::MRY: yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::M, {}, ts, inst.tag}); yield({GateType::R, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::MR: yield({GateType::M, {}, ts, inst.tag}); yield({GateType::R, {}, ts, inst.tag}); break; case GateType::RX: yield({GateType::R, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); break; case GateType::RY: yield({GateType::R, {}, ts, inst.tag}); yield({GateType::H, {}, ts, inst.tag}); yield({GateType::S, {}, ts, inst.tag}); break; case GateType::R: yield({GateType::R, {}, ts, inst.tag}); break; default: throw std::invalid_argument("Unhandled in Simplifier::simplify_disjoint_1q_instruction: " + inst.str()); } } void simplify_disjoint_2q_instruction(const CircuitInstruction &inst) { const auto &ts = inst.targets; qs_buf.clear(); qs1_buf.clear(); qs2_buf.clear(); for (size_t k = 0; k < inst.targets.size(); k += 2) { auto a = inst.targets[k]; auto b = inst.targets[k + 1]; if (a.has_qubit_value()) { auto t = GateTarget::qubit(a.qubit_value()); qs1_buf.push_back(t); qs_buf.push_back(t); } if (b.has_qubit_value()) { auto t = GateTarget::qubit(b.qubit_value()); qs2_buf.push_back(t); qs_buf.push_back(t); } } switch (inst.gate_type) { case GateType::CX: yield({GateType::CX, {}, ts, inst.tag}); break; case GateType::XCZ: do_xcz(ts, inst.tag); break; case GateType::XCX: yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); break; case GateType::XCY: yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); break; case GateType::YCX: yield({GateType::S, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs1_buf, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs1_buf, inst.tag}); break; case GateType::YCY: yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); break; case GateType::YCZ: yield({GateType::S, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs1_buf, inst.tag}); do_xcz(ts, inst.tag); yield({GateType::S, {}, qs1_buf, inst.tag}); break; case GateType::CY: yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); break; case GateType::CZ: yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs2_buf, inst.tag}); break; case GateType::SQRT_XX: yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::H, {}, qs_buf, inst.tag}); break; case GateType::SQRT_XX_DAG: yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::H, {}, qs_buf, inst.tag}); break; case GateType::SQRT_YY: yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::H, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); break; case GateType::SQRT_YY_DAG: yield({GateType::S, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs1_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::H, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); break; case GateType::SQRT_ZZ: yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); break; case GateType::SQRT_ZZ_DAG: yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); break; case GateType::SWAP: yield({GateType::CX, {}, ts, inst.tag}); do_xcz(ts, inst.tag); yield({GateType::CX, {}, ts, inst.tag}); break; case GateType::ISWAP: yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); do_xcz(ts, inst.tag); yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); break; case GateType::ISWAP_DAG: yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); do_xcz(ts, inst.tag); yield({GateType::H, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); break; case GateType::CXSWAP: do_xcz(ts, inst.tag); yield({GateType::CX, {}, ts, inst.tag}); break; case GateType::SWAPCX: yield({GateType::CX, {}, ts, inst.tag}); do_xcz(ts, inst.tag); break; case GateType::CZSWAP: yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); do_xcz(ts, inst.tag); yield({GateType::H, {}, qs2_buf, inst.tag}); break; case GateType::MXX: yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::M, {}, qs1_buf, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); break; case GateType::MYY: yield({GateType::S, {}, qs_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::S, {}, qs2_buf, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::M, {}, qs1_buf, inst.tag}); yield({GateType::H, {}, qs1_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::S, {}, qs_buf, inst.tag}); break; case GateType::MZZ: yield({GateType::CX, {}, ts, inst.tag}); yield({GateType::M, {}, qs2_buf, inst.tag}); yield({GateType::CX, {}, ts, inst.tag}); break; default: throw std::invalid_argument("Unhandled in Simplifier::simplify_instruction: " + inst.str()); } } void simplify_instruction(const CircuitInstruction &inst) { const Gate &g = GATE_DATA[inst.gate_type]; switch (inst.gate_type) { case GateType::I: case GateType::II: // Dropped. break; case GateType::MPP: decompose_mpp_operation(inst, num_qubits, [&](const CircuitInstruction sub) { simplify_instruction(sub); }); break; case GateType::SPP: case GateType::SPP_DAG: decompose_spp_or_spp_dag_operation(inst, num_qubits, false, [&](const CircuitInstruction sub) { simplify_instruction(sub); }); break; case GateType::MPAD: // Can't be easily simplified into M. yield(inst); break; case GateType::DETECTOR: case GateType::OBSERVABLE_INCLUDE: case GateType::TICK: case GateType::QUBIT_COORDS: case GateType::SHIFT_COORDS: // Annotations can't be simplified. yield(inst); break; case GateType::DEPOLARIZE1: case GateType::DEPOLARIZE2: case GateType::X_ERROR: case GateType::Y_ERROR: case GateType::Z_ERROR: case GateType::I_ERROR: case GateType::II_ERROR: case GateType::PAULI_CHANNEL_1: case GateType::PAULI_CHANNEL_2: case GateType::E: case GateType::ELSE_CORRELATED_ERROR: case GateType::HERALDED_ERASE: case GateType::HERALDED_PAULI_CHANNEL_1: // Noise isn't simplified. yield(inst); break; default: { if (g.flags & GATE_IS_SINGLE_QUBIT_GATE) { simplify_potentially_overlapping_1q_instruction(inst); } else if (g.flags & GATE_TARGETS_PAIRS) { simplify_potentially_overlapping_2q_instruction(inst); } else { throw std::invalid_argument( "Unhandled in simplify_potentially_overlapping_instruction: " + inst.str()); } } } } }; Circuit stim::simplified_circuit(const Circuit &circuit) { Circuit output; Simplifier simplifier(circuit.count_qubits(), [&](const CircuitInstruction &inst) { output.safe_append(inst); }); for (auto inst : circuit.operations) { if (inst.gate_type == GateType::REPEAT) { output.append_repeat_block( inst.repeat_block_rep_count(), simplified_circuit(inst.repeat_block_body(circuit)), inst.tag); } else { simplifier.simplify_instruction(inst); } } return output; } ================================================ FILE: src/stim/util_top/simplified_circuit.h ================================================ #ifndef _STIM_UTIL_TOP_SIMPLIFIED_CIRCUIT_H #define _STIM_UTIL_TOP_SIMPLIFIED_CIRCUIT_H #include "stim/circuit/circuit.h" namespace stim { Circuit simplified_circuit(const Circuit &circuit); } // namespace stim #endif ================================================ FILE: src/stim/util_top/simplified_circuit.test.cc ================================================ #include "stim/util_top/simplified_circuit.h" #include "gtest/gtest.h" #include "stim/cmd/command_help.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/test_util.test.h" using namespace stim; static std::pair>, std::vector>> circuit_output_eq_val( const Circuit &circuit) { // CAUTION: this is not 100% reliable when measurement count is larger than 1. TableauSimulator<64> sim1(INDEPENDENT_TEST_RNG(), circuit.count_qubits(), -1); TableauSimulator<64> sim2(INDEPENDENT_TEST_RNG(), circuit.count_qubits(), +1); sim1.safe_do_circuit(circuit); sim2.safe_do_circuit(circuit); return {sim1.canonical_stabilizers(), sim2.canonical_stabilizers()}; } bool is_simplification_correct(const Gate &gate) { std::vector args; while (args.size() < gate.arg_count && gate.arg_count != ARG_COUNT_SYGIL_ANY && gate.arg_count != ARG_COUNT_SYGIL_ZERO_OR_ONE) { args.push_back(args.empty() ? 1 : 0); } Circuit original; original.safe_append(CircuitInstruction(gate.id, args, gate_decomposition_help_targets_for_gate_type(gate.id), "")); Circuit simplified = simplified_circuit(original); if (gate.h_s_cx_m_r_decomposition == nullptr) { return simplified == original; } uint32_t n = original.count_qubits(); Circuit epr; for (uint32_t q = 0; q < n; q++) { epr.safe_append_u("H", {q}); } for (uint32_t q = 0; q < n; q++) { epr.safe_append_u("CNOT", {q, q + n}); } Circuit circuit1 = epr + original; Circuit circuit2 = epr + simplified; // Reset gates make the ancillary qubits irrelevant because the final value is unrelated to the initial value. // So, for reset gates, discard the ancillary qubits. // CAUTION: this could give false positives if "partial reset" gates are added in the future. // (E.g. a two qubit gate that resets only one of the qubits.) if ((gate.flags & GATE_IS_RESET) && !(gate.flags & GATE_PRODUCES_RESULTS)) { for (uint32_t q = 0; q < n; q++) { circuit1.safe_append_u("R", {q + n}); circuit2.safe_append_u("R", {q + n}); } } // Verify decomposed all the way to base gate set, if the gate has a decomposition. for (const auto &op : circuit2.operations) { if (op.gate_type != GateType::CX && op.gate_type != GateType::H && op.gate_type != GateType::S && op.gate_type != GateType::M && op.gate_type != GateType::R) { return false; } } auto v1 = circuit_output_eq_val(circuit1); auto v2 = circuit_output_eq_val(circuit2); return v1 == v2; } TEST(gate_decomposition, simplifications_are_correct) { for (const auto &g : GATE_DATA.items) { if (g.id != GateType::NOT_A_GATE && g.id != GateType::REPEAT) { EXPECT_TRUE(is_simplification_correct(g)) << g.name; } } } ================================================ FILE: src/stim/util_top/stabilizers_to_tableau.h ================================================ #ifndef _STIM_STABILIZERS_CONVERSIONS_H #define _STIM_STABILIZERS_CONVERSIONS_H #include "stim/circuit/circuit.h" #include "stim/dem/dem_instruction.h" #include "stim/stabilizers/flex_pauli_string.h" #include "stim/stabilizers/tableau.h" namespace stim { /// Computes destabilizers for the given stabilizers, and packages into a tableau. /// /// Args: /// stabilizers: The desired stabilizers for the tableau. Every stabilizer must have the same number of qubits. /// allow_redundant: If false, including a redundant stabilizer will result in an error. /// If true, redundant stabilizers are quietly dropped. /// allow_underconstrained: If false, the number of independent stabilizers must equal the number of qubits in each /// stabilizer. If true, the returned result will arbitrarily fill in missing stabilizers. /// invert: Return the inverse tableau instead of the tableau with the stabilizers as its Z outputs. /// /// Returns: /// A tableau containing the given stabilizers, but extended to also include matching stabilizers. /// The Z outputs of the tableau will be the given stabilizers (skipping any redundant ones). template Tableau stabilizers_to_tableau( const std::vector> &stabilizers, bool allow_redundant, bool allow_underconstrained, bool invert); } // namespace stim #include "stim/util_top/stabilizers_to_tableau.inl" #endif ================================================ FILE: src/stim/util_top/stabilizers_to_tableau.inl ================================================ #include "stim/util_top/circuit_vs_tableau.h" #include "stim/util_top/stabilizers_to_tableau.h" namespace stim { template Tableau stabilizers_to_tableau( const std::vector> &stabilizers, bool allow_redundant, bool allow_underconstrained, bool invert) { size_t num_qubits = 0; for (const auto &e : stabilizers) { num_qubits = std::max(num_qubits, e.num_qubits); } simd_bit_table buf_xs(stabilizers.size(), num_qubits); simd_bit_table buf_zs(stabilizers.size(), num_qubits); simd_bits buf_signs(stabilizers.size()); for (size_t k = 0; k < stabilizers.size(); k++) { memcpy(buf_xs[k].u8, stabilizers[k].xs.u8, stabilizers[k].xs.num_u8_padded()); memcpy(buf_zs[k].u8, stabilizers[k].zs.u8, stabilizers[k].zs.num_u8_padded()); buf_signs[k] = stabilizers[k].sign; } buf_xs = buf_xs.transposed(); buf_zs = buf_zs.transposed(); Circuit elimination_instructions; auto fail_due_to_anticommutation = [&]() { for (size_t k1 = 0; k1 < stabilizers.size(); k1++) { for (size_t k2 = k1 + 1; k2 < stabilizers.size(); k2++) { if (!stabilizers[k1].ref().commutes(stabilizers[k2])) { std::stringstream ss; ss << "Some of the given stabilizers anticommute.\n"; ss << "For example:"; ss << "\n stabilizers[" << k1 << "] = " << stabilizers[k1]; ss << "\nanticommutes with"; ss << "\n stabilizers[" << k2 << "] = " << stabilizers[k2]; throw std::invalid_argument(ss.str()); } } } throw std::invalid_argument( "The given stabilizers commute but the solver failed in a way that suggests they anticommute. Please " "report this as a bug."); }; auto print_redundant_z_product_parts = [&](size_t stabilizer_index, std::ostream &out) { PauliString target = stabilizers[stabilizer_index]; target.ensure_num_qubits(num_qubits, 1.0); target = target.ref().after(elimination_instructions); if (num_qubits > 0) { GateTarget t = GateTarget::qubit(num_qubits - 1); elimination_instructions.safe_append(CircuitInstruction{GateType::X, {}, &t, ""}); elimination_instructions.safe_append(CircuitInstruction{GateType::X, {}, &t, ""}); } Tableau inverse = circuit_to_tableau(elimination_instructions, false, false, false, true); target.ref().for_each_active_pauli([&](size_t q) { out << "\n "; for (size_t k = 0; k < stabilizers.size(); k++) { PauliString s = stabilizers[k]; s.ensure_num_qubits(num_qubits, 1.0); if (s == inverse.zs[q]) { out << "stabilizers[" << k << "] = " << stabilizers[k]; return; } } out << inverse.zs[q]; }); }; size_t used = 0; for (size_t k = 0; k < stabilizers.size(); k++) { // Find a non-identity term in the Pauli string past the region used by other stabilizers. size_t pivot; for (size_t q = 0; q < used; q++) { if (buf_xs[q][k]) { fail_due_to_anticommutation(); } } for (pivot = used; pivot < num_qubits; pivot++) { if (buf_xs[pivot][k] || buf_zs[pivot][k]) { break; } } // Check for incompatible / redundant stabilizers. if (pivot == num_qubits) { if (buf_signs[k]) { std::stringstream ss; ss << "Some of the given stabilizers contradict each other.\n"; ss << "For example:"; ss << "\n stabilizers[" << k << "] = " << stabilizers[k]; ss << "\nis the negation of the product of the following stabilizers: {"; print_redundant_z_product_parts(k, ss); ss << "\n}"; throw std::invalid_argument(ss.str()); } if (!allow_redundant) { std::stringstream ss; ss << "Some of the given stabilizers are redundant."; ss << "\nTo allow redundant stabilizers, pass the argument allow_redundant=True."; ss << "\n"; ss << "\nFor example:"; ss << "\n stabilizers[" << k << "] = " << stabilizers[k]; ss << "\nis the product of the following stabilizers: {"; print_redundant_z_product_parts(k, ss); ss << "\n}"; throw std::invalid_argument(ss.str()); } continue; } // Change pivot basis to the Z axis. if (buf_xs[pivot][k]) { GateType g = buf_zs[pivot][k] ? GateType::H_YZ : GateType::H; GateTarget t = GateTarget::qubit(pivot); CircuitInstruction instruction{g, {}, &t, ""}; elimination_instructions.safe_append(instruction); size_t q = pivot; simd_bits_range_ref xs1 = buf_xs[q]; simd_bits_range_ref zs1 = buf_zs[q]; simd_bits_range_ref ss = buf_signs; switch (g) { case GateType::H_YZ: ss.for_each_word(xs1, zs1, [](auto &s, auto &x, auto &z) { x ^= z; s ^= z.andnot(x); }); break; case GateType::H: ss.for_each_word(xs1, zs1, [](auto &s, auto &x, auto &z) { std::swap(x, z); s ^= x & z; }); break; default: throw std::invalid_argument("Unrecognized gate type."); } } // Cancel other terms in Pauli string. for (size_t q = 0; q < num_qubits; q++) { int p = buf_xs[q][k] + buf_zs[q][k] * 2; if (p && q != pivot) { std::array targets{GateTarget::qubit(pivot), GateTarget::qubit(q)}; GateType g = p == 1 ? GateType::XCX : p == 2 ? GateType::XCZ : GateType::XCY; CircuitInstruction instruction{g, {}, targets, ""}; elimination_instructions.safe_append(instruction); size_t q1 = targets[0].qubit_value(); size_t q2 = targets[1].qubit_value(); simd_bits_range_ref ss = buf_signs; simd_bits_range_ref xs1 = buf_xs[q1]; simd_bits_range_ref zs1 = buf_zs[q1]; simd_bits_range_ref xs2 = buf_xs[q2]; simd_bits_range_ref zs2 = buf_zs[q2]; switch (g) { case GateType::XCX: ss.for_each_word(xs1, zs1, xs2, zs2, [](auto &s, auto &x1, auto &z1, auto &x2, auto &z2) { s ^= (x1 ^ x2) & z1 & z2; x1 ^= z2; x2 ^= z1; }); break; case GateType::XCY: ss.for_each_word(xs1, zs1, xs2, zs2, [](auto &s, auto &x1, auto &z1, auto &x2, auto &z2) { x1 ^= x2 ^ z2; x2 ^= z1; z2 ^= z1; s ^= x1.andnot(z1) & x2.andnot(z2); s ^= x1 & z1 & z2.andnot(x2); }); break; case GateType::XCZ: ss.for_each_word(xs1, zs1, xs2, zs2, [](auto &s, auto &x1, auto &z1, auto &x2, auto &z2) { z2 ^= z1; x1 ^= x2; s ^= (z2 ^ x1).andnot(z1 & x2); }); break; default: throw std::invalid_argument("Unrecognized gate type."); } } } // Move pivot to diagonal. if (pivot != used) { std::array targets{GateTarget::qubit(pivot), GateTarget::qubit(used)}; CircuitInstruction instruction{GateType::SWAP, {}, targets, ""}; elimination_instructions.safe_append(instruction); buf_xs[pivot].swap_with(buf_xs[used]); buf_zs[pivot].swap_with(buf_zs[used]); } // Fix sign. if (buf_signs[k]) { GateTarget t = GateTarget::qubit(used); CircuitInstruction instruction{GateType::X, {}, &t, ""}; elimination_instructions.safe_append(instruction); buf_signs ^= buf_zs[used]; } used++; } if (used < num_qubits) { if (!allow_underconstrained) { throw std::invalid_argument( "There weren't enough stabilizers to uniquely specify the state. " "To allow underspecifying the state, pass the argument allow_underconstrained=True."); } } if (num_qubits > 0) { // Force size of resulting tableau to be correct. GateTarget t = GateTarget::qubit(num_qubits - 1); elimination_instructions.safe_append(CircuitInstruction{GateType::X, {}, &t, ""}); elimination_instructions.safe_append(CircuitInstruction{GateType::X, {}, &t, ""}); } if (invert) { return circuit_to_tableau(elimination_instructions.inverse(), false, false, false, true); } return circuit_to_tableau(elimination_instructions, false, false, false, true); } } // namespace stim ================================================ FILE: src/stim/util_top/stabilizers_to_tableau.perf.cc ================================================ #include "stim/util_top/stabilizers_to_tableau.h" #include "stim/perf.perf.h" using namespace stim; BENCHMARK(stabilizers_to_tableau_144) { std::vector> offsets{ {1, 0}, {-1, 0}, {0, 1}, {0, -1}, {3, 6}, {-6, 3}, }; size_t w = 24; size_t h = 12; auto normalize = [&](std::complex c) -> std::complex { return {fmodf(c.real() + w * 10, w), fmodf(c.imag() + h * 10, h)}; }; auto q2i = [&](std::complex c) -> size_t { c = normalize(c); return (int)c.real() / 2 + c.imag() * (w / 2); }; std::vector> stabilizers; for (size_t x = 0; x < w; x++) { for (size_t y = x % 2; y < h; y += 2) { std::complex s{x % 2 ? -1.0f : +1.0f, 0.0f}; std::complex c{(float)x, (float)y}; stim::PauliString<64> ps(w * h / 2); for (const auto &offset : offsets) { size_t i = q2i(c + offset * s); if (x % 2 == 0) { ps.xs[i] = 1; } else { ps.zs[i] = 1; } } stabilizers.push_back(ps); } } size_t dep = 0; benchmark_go([&]() { Tableau<64> t = stabilizers_to_tableau(stabilizers, true, true, false); dep += t.xs[0].zs[0]; }).goal_micros(500); if (dep == 99999999) { std::cout << "data dependence"; } } BENCHMARK(stabilizers_to_tableau_576) { std::vector> offsets{ {1, 0}, {-1, 0}, {0, 1}, {0, -1}, {3, 6}, {-6, 3}, }; size_t w = 24 * 4; size_t h = 12 * 4; auto normalize = [&](std::complex c) -> std::complex { return {fmodf(c.real() + w * 10, w), fmodf(c.imag() + h * 10, h)}; }; auto q2i = [&](std::complex c) -> size_t { c = normalize(c); return (int)c.real() / 2 + c.imag() * (w / 2); }; std::vector> stabilizers; for (size_t x = 0; x < w; x++) { for (size_t y = x % 2; y < h; y += 2) { std::complex s{x % 2 ? -1.0f : +1.0f, 0.0f}; std::complex c{(float)x, (float)y}; stim::PauliString<64> ps(w * h / 2); for (const auto &offset : offsets) { size_t i = q2i(c + offset * s); if (x % 2 == 0) { ps.xs[i] = 1; } else { ps.zs[i] = 1; } } stabilizers.push_back(ps); } } size_t dep = 0; benchmark_go([&]() { Tableau<64> t = stabilizers_to_tableau(stabilizers, true, true, false); dep += t.xs[0].zs[0]; }).goal_millis(200); if (dep == 99999999) { std::cout << "data dependence"; } } ================================================ FILE: src/stim/util_top/stabilizers_to_tableau.test.cc ================================================ #include "stim/util_top/stabilizers_to_tableau.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(conversions, stabilizers_to_tableau_fuzz, { auto rng = INDEPENDENT_TEST_RNG(); for (size_t n = 0; n < 10; n++) { auto t = Tableau::random(n, rng); std::vector> expected_stabilizers; for (size_t k = 0; k < n; k++) { expected_stabilizers.push_back(t.zs[k]); } auto actual = stabilizers_to_tableau(expected_stabilizers, false, false, false); for (size_t k = 0; k < n; k++) { ASSERT_EQ(actual.zs[k], expected_stabilizers[k]); } ASSERT_TRUE(actual.satisfies_invariants()); } }) TEST_EACH_WORD_SIZE_W(conversions, stabilizers_to_tableau_partial_fuzz, { auto rng = INDEPENDENT_TEST_RNG(); for (size_t n = 0; n < 10; n++) { for (size_t skipped = 1; skipped < n && skipped < 4; skipped++) { auto t = Tableau::random(n, rng); std::vector> expected_stabilizers; for (size_t k = 0; k < n - skipped; k++) { expected_stabilizers.push_back(t.zs[k]); } ASSERT_THROW( { stabilizers_to_tableau(expected_stabilizers, false, false, false); }, std::invalid_argument); auto actual = stabilizers_to_tableau(expected_stabilizers, false, true, false); for (size_t k = 0; k < n - skipped; k++) { ASSERT_EQ(actual.zs[k], expected_stabilizers[k]); } ASSERT_TRUE(actual.satisfies_invariants()); auto inverted = stabilizers_to_tableau(expected_stabilizers, false, true, true); ASSERT_EQ(actual.inverse(), inverted); } } }) TEST_EACH_WORD_SIZE_W(conversions, stabilizers_to_tableau_overconstrained, { auto rng = INDEPENDENT_TEST_RNG(); for (size_t n = 4; n < 10; n++) { auto t = Tableau::random(n, rng); std::vector> expected_stabilizers; expected_stabilizers.push_back(PauliString(n)); expected_stabilizers.push_back(PauliString(n)); uint8_t s = 0; s += expected_stabilizers.back().ref().inplace_right_mul_returning_log_i_scalar(t.zs[1]); s += expected_stabilizers.back().ref().inplace_right_mul_returning_log_i_scalar(t.zs[3]); if (s & 2) { expected_stabilizers.back().sign ^= true; } for (size_t k = 0; k < n; k++) { expected_stabilizers.push_back(t.zs[k]); } ASSERT_THROW({ stabilizers_to_tableau(expected_stabilizers, false, false, false); }, std::invalid_argument); auto actual = stabilizers_to_tableau(expected_stabilizers, true, false, false); for (size_t k = 0; k < n; k++) { ASSERT_EQ(actual.zs[k], expected_stabilizers[k + 1 + (k > 3)]); } ASSERT_TRUE(actual.satisfies_invariants()); } }) TEST_EACH_WORD_SIZE_W(conversions, stabilizers_to_tableau_bell_pair, { std::vector> input_stabilizers; input_stabilizers.push_back(PauliString::from_str("XX")); input_stabilizers.push_back(PauliString::from_str("ZZ")); auto actual = stabilizers_to_tableau(input_stabilizers, false, false, false); Tableau expected(2); expected.zs[0] = PauliString::from_str("XX"); expected.zs[1] = PauliString::from_str("ZZ"); expected.xs[0] = PauliString::from_str("Z_"); expected.xs[1] = PauliString::from_str("_X"); ASSERT_EQ(actual, expected); input_stabilizers.push_back(PauliString::from_str("-YY")); ASSERT_THROW({ stabilizers_to_tableau(input_stabilizers, false, false, false); }, std::invalid_argument); actual = stabilizers_to_tableau(input_stabilizers, true, false, false); ASSERT_EQ(actual, expected); input_stabilizers[2] = PauliString::from_str("+YY"); // Sign is wrong! ASSERT_THROW({ stabilizers_to_tableau(input_stabilizers, true, true, false); }, std::invalid_argument); input_stabilizers[2] = PauliString::from_str("+Z_"); // Anticommutes! ASSERT_THROW({ stabilizers_to_tableau(input_stabilizers, true, true, false); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(conversions, stabilizer_to_tableau_detect_anticommutation, { std::vector> input_stabilizers; input_stabilizers.push_back(PauliString::from_str("YY")); input_stabilizers.push_back(PauliString::from_str("YX")); ASSERT_THROW({ stabilizers_to_tableau(input_stabilizers, false, false, false); }, std::invalid_argument); }) TEST_EACH_WORD_SIZE_W(conversions, stabilizer_to_tableau_size_affecting_redundancy, { std::vector> input_stabilizers; input_stabilizers.push_back(PauliString::from_str("X_")); input_stabilizers.push_back(PauliString::from_str("_X")); for (size_t k = 0; k < 150; k++) { input_stabilizers.push_back(PauliString::from_str("__")); } auto t = stabilizers_to_tableau(input_stabilizers, true, true, false); ASSERT_EQ(t.num_qubits, 2); ASSERT_EQ(t.zs[0], PauliString::from_str("X_")); ASSERT_EQ(t.zs[1], PauliString::from_str("_X")); }) ================================================ FILE: src/stim/util_top/stabilizers_vs_amplitudes.h ================================================ #ifndef _STIM_UTIL_TOP_STABILIZERS_VS_AMPLITUDES_H #define _STIM_UTIL_TOP_STABILIZERS_VS_AMPLITUDES_H #include "stim/circuit/circuit.h" #include "stim/stabilizers/tableau.h" namespace stim { /// Converts a tableau into a unitary matrix. template std::vector>> tableau_to_unitary(const Tableau &tableau, bool little_endian); /// Converts a unitary matrix into a stabilizer tableau. /// /// Args: /// matrix: The unitary matrix to convert. Must correspond to a Clifford. // little_endian: Whether the amplitude ordering is little endian or big endian. /// /// Returns: /// A tableau implementing the same operation as the unitary matrix (up to global phase). /// /// Throws: /// std::invalid_argument: The given unitary matrix isn't a Clifford operation. template Tableau unitary_to_tableau(const std::vector>> &matrix, bool little_endian); } // namespace stim #include "stim/util_top/stabilizers_vs_amplitudes.inl" #endif ================================================ FILE: src/stim/util_top/stabilizers_vs_amplitudes.inl ================================================ #include "stim/util_bot/twiddle.h" #include "stim/util_top/circuit_inverse_unitary.h" #include "stim/util_top/circuit_vs_amplitudes.h" #include "stim/util_top/circuit_vs_tableau.h" namespace stim { template std::vector>> tableau_to_unitary(const Tableau &tableau, bool little_endian) { auto flat = tableau.to_flat_unitary_matrix(little_endian); std::vector>> result; size_t n = 1 << tableau.num_qubits; for (size_t row = 0; row < n; row++) { result.push_back({}); auto &back = result.back(); std::complex *start = &flat[row * n]; back.insert(back.end(), start, start + n); } return result; } template Tableau unitary_to_tableau(const std::vector>> &matrix, bool little_endian) { // Verify matrix is square. size_t num_amplitudes = matrix.size(); if (!is_power_of_2(num_amplitudes)) { throw std::invalid_argument( "Matrix width and height must be a power of 2. Height was " + std::to_string(num_amplitudes)); } for (size_t r = 0; r < num_amplitudes; r++) { if (matrix[r].size() != num_amplitudes) { std::stringstream ss; ss << "Matrix must be square, but row " << r; ss << " had width " << matrix[r].size(); ss << " while matrix had height " << num_amplitudes; throw std::invalid_argument(ss.str()); } } // Use first column to solve how to get out of superposition and to a phased permutation. std::vector> first_col; for (const auto &row : matrix) { first_col.push_back(row[0]); } Circuit recorded_circuit = stabilizer_state_vector_to_circuit(first_col, true); recorded_circuit = circuit_inverse_unitary(recorded_circuit); // Use the state channel duality to get the operation into the vector simulator. VectorSimulator sim(0); float m2v = sqrtf(num_amplitudes); sim.state.clear(); sim.state.reserve(num_amplitudes * num_amplitudes); for (size_t r = 0; r < num_amplitudes; r++) { for (size_t c = 0; c < num_amplitudes; c++) { sim.state.push_back(matrix[c][r] / m2v); } } // Convert to a phased permutation (assuming the matrix was Clifford). sim.do_unitary_circuit(recorded_circuit); sim.smooth_stabilizer_state(sim.state[0]); auto apply = [&](GateType gate_type, uint32_t target) { sim.apply(gate_type, target); recorded_circuit.safe_append( CircuitInstruction(gate_type, {}, std::vector{GateTarget::qubit(target)}, "")); }; auto apply2 = [&](GateType gate_type, uint32_t target, uint32_t target2) { sim.apply(gate_type, target, target2); recorded_circuit.safe_append(CircuitInstruction( gate_type, {}, std::vector{GateTarget::qubit(target), GateTarget::qubit(target2)}, "")); }; // Undo the permutation and also single-qubit phases. size_t num_qubits = floor_lg2(num_amplitudes); for (size_t q = 0; q < num_qubits; q++) { size_t c = 1 << q; // Find the single entry in the column and move it to the diagonal. for (size_t r = 0; r < num_amplitudes; r++) { auto ratio = sim.state[c * num_amplitudes + r]; if (ratio != std::complex{0, 0}) { // Move to diagonal. if (r != c) { size_t pivot = first_set_bit(r, q); for (size_t b = 0; b < num_qubits; b++) { if (((r >> b) & 1) != 0 && b != pivot) { apply2(GateType::CX, pivot, b); } } if (pivot != q) { apply2(GateType::SWAP, q, pivot); } } // Undo phasing on this qubit. if (ratio.real() == -1) { apply(GateType::Z, q); } else if (ratio.imag() == -1) { apply(GateType::S, q); } else if (ratio.imag() == +1) { apply(GateType::S_DAG, q); } break; } } } // Undo double qubit phases. for (size_t q1 = 0; q1 < num_qubits; q1++) { for (size_t q2 = q1 + 1; q2 < num_qubits; q2++) { size_t v = (1 << q1) | (1 << q2); size_t d = v * num_amplitudes + v; if (sim.state[d].real() == -1) { apply2(GateType::CZ, q1, q2); } } } // Verify that we actually reduced the matrix to the identity. // If we failed, it wasn't actually a Clifford. for (size_t r = 0; r < num_amplitudes; r++) { for (size_t c = 0; c < num_amplitudes; c++) { if (sim.state[r * num_amplitudes + c] != std::complex{r == c ? 1.0f : 0.0f}) { throw std::invalid_argument("The given unitary matrix wasn't a Clifford operation."); } } } // Conjugate by swaps to handle endianness. if (!little_endian) { for (size_t q = 0; 2 * q + 1 < num_qubits; q++) { recorded_circuit.safe_append_u("SWAP", {(uint32_t)q, (uint32_t)(num_qubits - q - 1)}); } } recorded_circuit = circuit_inverse_unitary(recorded_circuit); if (!little_endian) { for (size_t q = 0; 2 * q + 1 < num_qubits; q++) { recorded_circuit.safe_append_u("SWAP", {(uint32_t)q, (uint32_t)(num_qubits - q - 1)}); } } return circuit_to_tableau(recorded_circuit, false, false, false); } } // namespace stim ================================================ FILE: src/stim/util_top/stabilizers_vs_amplitudes.test.cc ================================================ #include "stim/util_top/stabilizers_vs_amplitudes.h" #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/tableau_simulator.h" #include "stim/util_bot/test_util.test.h" using namespace stim; TEST_EACH_WORD_SIZE_W(conversions, unitary_to_tableau_vs_gate_data, { for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { EXPECT_EQ(unitary_to_tableau(gate.unitary(), true), gate.tableau()) << gate.name; } } }) TEST_EACH_WORD_SIZE_W(conversions, tableau_to_unitary_vs_gate_data, { VectorSimulator v1(2); VectorSimulator v2(2); for (const auto &gate : GATE_DATA.items) { if (gate.has_known_unitary_matrix()) { auto actual = tableau_to_unitary(gate.tableau(), true); auto expected = gate.unitary(); v1.state.clear(); for (const auto &row : actual) { v1.state.insert(v1.state.end(), row.begin(), row.end()); } v2.state.clear(); for (const auto &row : expected) { v2.state.insert(v2.state.end(), row.begin(), row.end()); } for (auto &v : v1.state) { v /= sqrtf(actual.size()); } for (auto &v : v2.state) { v /= sqrtf(actual.size()); } ASSERT_TRUE(v1.approximate_equals(v2, true)) << gate.name; } } }) TEST_EACH_WORD_SIZE_W(conversions, unitary_vs_tableau_basic, { ASSERT_EQ(unitary_to_tableau(GATE_DATA.at("XCZ").unitary(), false), GATE_DATA.at("ZCX").tableau()); ASSERT_EQ(unitary_to_tableau(GATE_DATA.at("XCZ").unitary(), true), GATE_DATA.at("XCZ").tableau()); ASSERT_EQ(unitary_to_tableau(GATE_DATA.at("ZCX").unitary(), false), GATE_DATA.at("XCZ").tableau()); ASSERT_EQ(unitary_to_tableau(GATE_DATA.at("ZCX").unitary(), true), GATE_DATA.at("ZCX").tableau()); ASSERT_EQ(unitary_to_tableau(GATE_DATA.at("XCY").unitary(), false), GATE_DATA.at("YCX").tableau()); ASSERT_EQ(unitary_to_tableau(GATE_DATA.at("XCY").unitary(), true), GATE_DATA.at("XCY").tableau()); ASSERT_EQ(unitary_to_tableau(GATE_DATA.at("YCX").unitary(), false), GATE_DATA.at("XCY").tableau()); ASSERT_EQ(unitary_to_tableau(GATE_DATA.at("YCX").unitary(), true), GATE_DATA.at("YCX").tableau()); }) TEST_EACH_WORD_SIZE_W(conversions, unitary_to_tableau_fuzz_vs_tableau_to_unitary, { auto rng = INDEPENDENT_TEST_RNG(); for (bool little_endian : std::vector{false, true}) { for (size_t n = 0; n < 6; n++) { auto desired = Tableau::random(n, rng); auto unitary = tableau_to_unitary(desired, little_endian); auto actual = unitary_to_tableau(unitary, little_endian); ASSERT_EQ(actual, desired) << "little_endian=" << little_endian << ", n=" << n; } } }) TEST_EACH_WORD_SIZE_W(conversions, unitary_to_tableau_fail, { ASSERT_THROW( { unitary_to_tableau({{{1}, {0}}, {{0}, {sqrtf(0.5), sqrtf(0.5)}}}, false); }, std::invalid_argument); ASSERT_THROW( { unitary_to_tableau( { {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, {0, 1}}, }, false); }, std::invalid_argument); ASSERT_THROW( { unitary_to_tableau( { {1, 0, 0, 0, 0, 0, 0, 0}, {0, 1, 0, 0, 0, 0, 0, 0}, {0, 0, 1, 0, 0, 0, 0, 0}, {0, 0, 0, 1, 0, 0, 0, 0}, {0, 0, 0, 0, 1, 0, 0, 0}, {0, 0, 0, 0, 0, 1, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 1, 0}, }, false); }, std::invalid_argument); }) ================================================ FILE: src/stim/util_top/transform_without_feedback.cc ================================================ // 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. #include "stim/util_top/transform_without_feedback.h" #include #include #include #include "stim/circuit/gate_decomposition.h" #include "stim/simulators/sparse_rev_frame_tracker.h" using namespace stim; struct WithoutFeedbackHelper { Circuit reversed_semi_flattened_output; SparseUnsignedRevFrameTracker tracker; SparseXorVec tmp_sensitivity_buf; std::map> obs_changes; std::map> det_changes; WithoutFeedbackHelper(const Circuit &circuit) : tracker(circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors()) { } const SparseXorVec &anticommuting_sensitivity_at(uint32_t qubit, bool x, bool z) { if (x > z) { return tracker.zs[qubit]; } else if (z > x) { return tracker.xs[qubit]; } else { tmp_sensitivity_buf.clear(); tmp_sensitivity_buf ^= tracker.xs[qubit]; tmp_sensitivity_buf ^= tracker.zs[qubit]; return tmp_sensitivity_buf; } } void do_single_feedback(GateTarget rec, uint32_t qubit, bool x, bool z) { const SparseXorVec &sensitivity = anticommuting_sensitivity_at(qubit, x, z); for (const auto &d : sensitivity) { if (d.is_observable_id()) { obs_changes[d.raw_id()].xor_item(rec); } else { det_changes[d.raw_id()].xor_item(tracker.num_measurements_in_past + rec.rec_offset()); } } } void undo_feedback_capable_pcp_operation(const CircuitInstruction &op) { for (size_t k = op.targets.size(); k > 0;) { k -= 2; CircuitInstruction op_piece = {op.gate_type, op.args, {&op.targets[k], &op.targets[k + 2]}, op.tag}; auto t1 = op.targets[k]; auto t2 = op.targets[k + 1]; auto b1 = t1.is_measurement_record_target(); auto b2 = t2.is_measurement_record_target(); if (b1 > b2) { if (op.gate_type == GateType::CX) { do_single_feedback(t1, t2.qubit_value(), true, false); } else if (op.gate_type == GateType::CY) { do_single_feedback(t1, t2.qubit_value(), true, true); } else if (op.gate_type == GateType::CZ) { do_single_feedback(t1, t2.qubit_value(), false, true); } else { throw std::invalid_argument("Unknown feedback gate."); } } else if (b2 > b1) { if (op.gate_type == GateType::CX) { do_single_feedback(t2, t1.qubit_value(), true, false); } else if (op.gate_type == GateType::CY) { do_single_feedback(t2, t1.qubit_value(), true, true); } else if (op.gate_type == GateType::CZ) { do_single_feedback(t2, t1.qubit_value(), false, true); } else { throw std::invalid_argument("Unknown feedback gate."); } } else if (!b1 && !b2) { reversed_semi_flattened_output.operations.push_back( CircuitInstruction{ op_piece.gate_type, reversed_semi_flattened_output.arg_buf.take_copy(op_piece.args), reversed_semi_flattened_output.target_buf.take_copy(op_piece.targets), op_piece.tag, }); } tracker.undo_gate(op_piece); } for (const auto &e : obs_changes) { if (!e.second.empty()) { reversed_semi_flattened_output.arg_buf.append_tail((double)e.first); reversed_semi_flattened_output.operations.push_back( CircuitInstruction{ GateType::OBSERVABLE_INCLUDE, reversed_semi_flattened_output.arg_buf.commit_tail(), reversed_semi_flattened_output.target_buf.take_copy(e.second.range()), op.tag, }); } } obs_changes.clear(); } void undo_repeat_block(const Circuit &circuit, const CircuitInstruction &op) { const Circuit &loop = op.repeat_block_body(circuit); uint64_t reps = op.repeat_block_rep_count(); Circuit tmp = std::move(reversed_semi_flattened_output); for (size_t rep = 0; rep < reps; rep++) { reversed_semi_flattened_output.clear(); undo_circuit(loop); tmp.append_repeat_block(1, std::move(reversed_semi_flattened_output), op.tag); } reversed_semi_flattened_output = std::move(tmp); } void undo_gate(const CircuitInstruction &op) { if (GATE_DATA[op.gate_type].flags & GATE_CAN_TARGET_BITS) { undo_feedback_capable_pcp_operation(op); } else { reversed_semi_flattened_output.safe_append(op, true); tracker.undo_gate(op); } } void undo_circuit(const Circuit &circuit) { for (size_t k = circuit.operations.size(); k--;) { const auto &op = circuit.operations[k]; if (op.gate_type == GateType::REPEAT) { undo_repeat_block(circuit, op); } else { undo_gate(op); } } } Circuit build_output(const Circuit &reversed) { Circuit result; for (size_t k = reversed.operations.size(); k--;) { const auto &op = reversed.operations[k]; tracker.num_measurements_in_past += op.count_measurement_results(); if (op.gate_type == GateType::REPEAT) { result.append_repeat_block( op.repeat_block_rep_count(), build_output(op.repeat_block_body(reversed)), op.tag); continue; } if (op.gate_type == GateType::DETECTOR) { auto p = det_changes.find(tracker.num_detectors_in_past); tracker.num_detectors_in_past++; if (p != det_changes.end()) { auto &changes = p->second; for (const auto &t : op.targets) { changes.xor_item(tracker.num_measurements_in_past + t.rec_offset()); } // Build new targets at tail of reversed_semi_flattened_output. for (const auto &m : changes) { reversed_semi_flattened_output.target_buf.append_tail( GateTarget::rec((int64_t)m - (int64_t)tracker.num_measurements_in_past)); } result.safe_append(CircuitInstruction( op.gate_type, op.args, reversed_semi_flattened_output.target_buf.tail, op.tag)); reversed_semi_flattened_output.target_buf.discard_tail(); continue; } } result.safe_append(op); } return result; } }; Circuit circuit_with_identical_adjacent_loops_fused(const Circuit &circuit) { Circuit result; Circuit growing_loop; uint64_t loop_reps = 0; std::string_view loop_tag; auto flush_loop = [&]() { if (loop_reps > 0) { growing_loop = circuit_with_identical_adjacent_loops_fused(growing_loop); if (loop_reps > 1) { result.append_repeat_block(loop_reps, std::move(growing_loop), loop_tag); } else if (loop_reps == 1) { result += growing_loop; } } loop_tag = ""; loop_reps = 0; }; for (const auto &op : circuit.operations) { bool is_loop = op.gate_type == GateType::REPEAT; // Grow the growing loop or flush it if needed. if (loop_reps > 0) { if (is_loop && growing_loop == op.repeat_block_body(circuit)) { loop_reps += op.repeat_block_rep_count(); continue; } flush_loop(); } // Start a new growing loop if needed. assert(loop_reps == 0); if (is_loop) { growing_loop = op.repeat_block_body(circuit); loop_reps = op.repeat_block_rep_count(); loop_tag = op.tag; continue; } assert(loop_reps == 0); result.safe_append(op); } flush_loop(); return result; } Circuit stim::circuit_with_inlined_feedback(const Circuit &circuit) { WithoutFeedbackHelper helper(circuit); helper.undo_circuit(circuit); assert(helper.tracker.num_measurements_in_past == 0); assert(helper.tracker.num_detectors_in_past == 0); Circuit output = helper.build_output(helper.reversed_semi_flattened_output); return circuit_with_identical_adjacent_loops_fused(output); } ================================================ FILE: src/stim/util_top/transform_without_feedback.h ================================================ /* * 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. */ #ifndef _STIM_UTIL_TOP_TRANSFORM_WITHOUT_FEEDBACK_H #define _STIM_UTIL_TOP_TRANSFORM_WITHOUT_FEEDBACK_H #include #include #include #include #include "stim/circuit/circuit.h" #include "stim/stabilizers/pauli_string.h" namespace stim { Circuit circuit_with_inlined_feedback(const Circuit &circuit); } // namespace stim #endif ================================================ FILE: src/stim/util_top/transform_without_feedback.test.cc ================================================ // 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. #include "stim/util_top/transform_without_feedback.h" #include "gtest/gtest.h" #include "stim/simulators/error_analyzer.h" using namespace stim; TEST(circuit_with_inlined_feedback, basic) { ASSERT_EQ( circuit_with_inlined_feedback(Circuit(R"CIRCUIT( MR 0 H 0 CX sweep[5] 0 CY rec[-1] 0 rec[-1] 0 2 3 rec[-1] 0 H 0 M 0 DETECTOR rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] )CIRCUIT")), Circuit(R"CIRCUIT( MR 0 H 0 CX sweep[5] 0 OBSERVABLE_INCLUDE(2) rec[-1] CY 2 3 H 0 M 0 DETECTOR rec[-2] rec[-1] OBSERVABLE_INCLUDE(2) rec[-1] )CIRCUIT")); } TEST(circuit_with_inlined_feedback, demolition_feedback) { Circuit inp = Circuit(R"CIRCUIT( CX 0 1 M 1 CX rec[-1] 1 CX 0 1 M 1 DETECTOR rec[-1] rec[-2] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT"); ASSERT_EQ(circuit_with_inlined_feedback(inp), Circuit(R"CIRCUIT( CX 0 1 M 1 OBSERVABLE_INCLUDE(0) rec[-1] CX 0 1 M 1 DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] )CIRCUIT")); } TEST(circuit_with_inlined_feedback, loop) { Circuit inp = Circuit(R"CIRCUIT( R 0 1 X_ERROR(0.125) 0 1 CX 0 1 M 1 CX rec[-1] 1 DETECTOR rec[-1] REPEAT 30 { X_ERROR(0.125) 0 1 CX 0 1 M 1 CX rec[-1] 1 DETECTOR rec[-1] rec[-2] } M 0 DETECTOR rec[-1] rec[-2] )CIRCUIT"); auto actual = circuit_with_inlined_feedback(inp); auto dem1 = ErrorAnalyzer::circuit_to_detector_error_model(inp, true, true, false, 0, false, true); auto dem2 = ErrorAnalyzer::circuit_to_detector_error_model(actual, true, true, false, 0, false, true); dem1 = dem1.flattened(); dem2 = dem2.flattened(); ASSERT_TRUE(dem1.approx_equals(dem2, 1e-5)); ASSERT_EQ(actual, Circuit(R"CIRCUIT( R 0 1 X_ERROR(0.125) 0 1 CX 0 1 M 1 DETECTOR rec[-1] X_ERROR(0.125) 0 1 CX 0 1 M 1 DETECTOR rec[-1] REPEAT 29 { X_ERROR(0.125) 0 1 CX 0 1 M 1 DETECTOR rec[-3] rec[-1] } M 0 DETECTOR rec[-3] rec[-2] rec[-1] )CIRCUIT")); } TEST(circuit_with_inlined_feedback, mpp) { Circuit inp = Circuit(R"CIRCUIT( RX 0 RY 1 RZ 2 MPP X0*Y1*Z2 Z5 CX rec[-2] 3 M 3 DETECTOR rec[-1] )CIRCUIT"); auto actual = circuit_with_inlined_feedback(inp); auto dem1 = ErrorAnalyzer::circuit_to_detector_error_model(inp, true, true, false, 0, false, true); auto dem2 = ErrorAnalyzer::circuit_to_detector_error_model(actual, true, true, false, 0, false, true); dem1 = dem1.flattened(); dem2 = dem2.flattened(); ASSERT_TRUE(dem1.approx_equals(dem2, 1e-5)); ASSERT_EQ(actual, Circuit(R"CIRCUIT( RX 0 RY 1 R 2 MPP X0*Y1*Z2 Z5 M 3 DETECTOR rec[-3] rec[-1] )CIRCUIT")); } TEST(circuit_with_inlined_feedback, interleaved_feedback_does_not_reorder_operations) { ASSERT_EQ( circuit_with_inlined_feedback(Circuit(R"CIRCUIT( H 0 CZ H 1 )CIRCUIT")), Circuit(R"CIRCUIT( H 0 1 )CIRCUIT")); ASSERT_EQ( circuit_with_inlined_feedback(Circuit(R"CIRCUIT( M 0 CX M 1 )CIRCUIT")), Circuit(R"CIRCUIT( M 0 1 )CIRCUIT")); ASSERT_EQ( circuit_with_inlined_feedback(Circuit(R"CIRCUIT( M 0 1 CX M 2 CX rec[-1] 3 M 3 DETECTOR rec[-1] )CIRCUIT")), Circuit(R"CIRCUIT( M 0 1 2 3 DETECTOR rec[-2] rec[-1] )CIRCUIT")); } ================================================ FILE: src/stim.cc ================================================ /* * 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. */ #include "stim.h" ================================================ FILE: src/stim.h ================================================ #ifndef _STIM_H #define _STIM_H /// WARNING: THE STIM C++ API MAKES NO COMPATIBILITY GUARANTEES. /// It may change arbitrarily and catastrophically from minor version to minor version. /// If you need a stable API, use stim's Python API. #include "stim/circuit/circuit.h" #include "stim/circuit/circuit_instruction.h" #include "stim/circuit/gate_decomposition.h" #include "stim/circuit/gate_target.h" #include "stim/cmd/command_analyze_errors.h" #include "stim/cmd/command_convert.h" #include "stim/cmd/command_detect.h" #include "stim/cmd/command_diagram.h" #include "stim/cmd/command_explain_errors.h" #include "stim/cmd/command_gen.h" #include "stim/cmd/command_help.h" #include "stim/cmd/command_m2d.h" #include "stim/cmd/command_repl.h" #include "stim/cmd/command_sample.h" #include "stim/cmd/command_sample_dem.h" #include "stim/dem/dem_instruction.h" #include "stim/dem/detector_error_model.h" #include "stim/diagram/ascii_diagram.h" #include "stim/diagram/base64.h" #include "stim/diagram/basic_3d_diagram.h" #include "stim/diagram/circuit_timeline_helper.h" #include "stim/diagram/coord.h" #include "stim/diagram/crumble.h" #include "stim/diagram/crumble_data.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/diagram_util.h" #include "stim/diagram/gate_data_3d.h" #include "stim/diagram/gate_data_3d_texture_data.h" #include "stim/diagram/gate_data_svg.h" #include "stim/diagram/gltf.h" #include "stim/diagram/graph/match_graph_3d_drawer.h" #include "stim/diagram/graph/match_graph_svg_drawer.h" #include "stim/diagram/json_obj.h" #include "stim/diagram/lattice_map.h" #include "stim/diagram/timeline/timeline_3d_drawer.h" #include "stim/diagram/timeline/timeline_ascii_drawer.h" #include "stim/diagram/timeline/timeline_svg_drawer.h" #include "stim/gates/gates.h" #include "stim/gen/circuit_gen_params.h" #include "stim/gen/gen_color_code.h" #include "stim/gen/gen_rep_code.h" #include "stim/gen/gen_surface_code.h" #include "stim/io/measure_record.h" #include "stim/io/measure_record_batch.h" #include "stim/io/measure_record_batch_writer.h" #include "stim/io/measure_record_reader.h" #include "stim/io/measure_record_writer.h" #include "stim/io/raii_file.h" #include "stim/io/sparse_shot.h" #include "stim/io/stim_data_formats.h" #include "stim/main_namespaced.h" #include "stim/mem/bit_ref.h" #include "stim/mem/bitword.h" #include "stim/mem/bitword_128_sse.h" #include "stim/mem/bitword_256_avx.h" #include "stim/mem/bitword_64.h" #include "stim/mem/fixed_cap_vector.h" #include "stim/mem/monotonic_buffer.h" #include "stim/mem/simd_bit_table.h" #include "stim/mem/simd_bits.h" #include "stim/mem/simd_bits_range_ref.h" #include "stim/mem/simd_util.h" #include "stim/mem/simd_word.h" #include "stim/mem/span_ref.h" #include "stim/mem/sparse_xor_vec.h" #include "stim/search/graphlike/algo.h" #include "stim/search/graphlike/edge.h" #include "stim/search/graphlike/graph.h" #include "stim/search/graphlike/node.h" #include "stim/search/graphlike/search_state.h" #include "stim/search/hyper/algo.h" #include "stim/search/hyper/edge.h" #include "stim/search/hyper/graph.h" #include "stim/search/hyper/node.h" #include "stim/search/hyper/search_state.h" #include "stim/search/sat/wcnf.h" #include "stim/search/search.h" #include "stim/simulators/dem_sampler.h" #include "stim/simulators/error_analyzer.h" #include "stim/simulators/error_matcher.h" #include "stim/simulators/force_streaming.h" #include "stim/simulators/frame_simulator.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/simulators/graph_simulator.h" #include "stim/simulators/matched_error.h" #include "stim/simulators/measurements_to_detection_events.h" #include "stim/simulators/sparse_rev_frame_tracker.h" #include "stim/simulators/tableau_simulator.h" #include "stim/simulators/vector_simulator.h" #include "stim/stabilizers/clifford_string.h" #include "stim/stabilizers/flex_pauli_string.h" #include "stim/stabilizers/flow.h" #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/pauli_string_iter.h" #include "stim/stabilizers/pauli_string_ref.h" #include "stim/stabilizers/tableau.h" #include "stim/stabilizers/tableau_iter.h" #include "stim/stabilizers/tableau_transposed_raii.h" #include "stim/util_bot/arg_parse.h" #include "stim/util_bot/error_decomp.h" #include "stim/util_bot/probability_util.h" #include "stim/util_bot/str_util.h" #include "stim/util_bot/twiddle.h" #include "stim/util_top/circuit_flow_generators.h" #include "stim/util_top/circuit_inverse_qec.h" #include "stim/util_top/circuit_inverse_unitary.h" #include "stim/util_top/circuit_to_dem.h" #include "stim/util_top/circuit_to_detecting_regions.h" #include "stim/util_top/circuit_vs_amplitudes.h" #include "stim/util_top/circuit_vs_tableau.h" #include "stim/util_top/count_determined_measurements.h" #include "stim/util_top/export_crumble_url.h" #include "stim/util_top/export_qasm.h" #include "stim/util_top/export_quirk_url.h" #include "stim/util_top/has_flow.h" #include "stim/util_top/mbqc_decomposition.h" #include "stim/util_top/missing_detectors.h" #include "stim/util_top/reference_sample_tree.h" #include "stim/util_top/simplified_circuit.h" #include "stim/util_top/stabilizers_to_tableau.h" #include "stim/util_top/stabilizers_vs_amplitudes.h" #include "stim/util_top/transform_without_feedback.h" #endif ================================================ FILE: src/stim.test.cc ================================================ /* * 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. */ #include "stim.h" #include "gtest/gtest.h" TEST(stim, include1) { stim::Circuit c("H 0"); ASSERT_EQ(c.count_qubits(), 1); ASSERT_EQ(stim::GATE_DATA.at("PAULI_CHANNEL_2").arg_count, 15); } TEST(stim, include3) { stim::ErrorAnalyzer::circuit_to_detector_error_model({}, false, false, false, 0.0, false, true); } ================================================ FILE: src/stim_included_twice.test.cc ================================================ /* * 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. */ #include "gtest/gtest.h" #include "stim.h" // The other include is in stim.test.cc; this is the second one. TEST(stim, include2) { stim::Circuit c("H 0"); ASSERT_EQ(c.count_qubits(), 1); ASSERT_EQ(stim::GATE_DATA.at("PAULI_CHANNEL_2").arg_count, 15); } ================================================ FILE: testdata/circuit_all_ops_3d.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.4375],"min":[0,0.375],"name":"tex_coords_gate_I","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.4375],"min":[0,0.375],"name":"tex_coords_gate_X","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.5],"min":[0,0.4375],"name":"tex_coords_gate_Y","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.5625],"min":[0,0.5],"name":"tex_coords_gate_Z","type":"VEC2"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.625],"min":[0.0625,0.5625],"name":"tex_coords_gate_C_XYZ","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.6875],"min":[0.375,0.625],"name":"tex_coords_gate_C_NXYZ","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.6875],"min":[0.4375,0.625],"name":"tex_coords_gate_C_XNYZ","type":"VEC2"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.6875],"min":[0.5,0.625],"name":"tex_coords_gate_C_XYNZ","type":"VEC2"},{"bufferView":9,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.625],"min":[0.125,0.5625],"name":"tex_coords_gate_C_ZYX","type":"VEC2"},{"bufferView":10,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.6875],"min":[0.5625,0.625],"name":"tex_coords_gate_C_NZYX","type":"VEC2"},{"bufferView":11,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.6875],"min":[0.625,0.625],"name":"tex_coords_gate_C_ZNYX","type":"VEC2"},{"bufferView":12,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.6875],"min":[0.6875,0.625],"name":"tex_coords_gate_C_ZYNX","type":"VEC2"},{"bufferView":13,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5625],"min":[0.0625,0.5],"name":"tex_coords_gate_H_XY","type":"VEC2"},{"bufferView":14,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5],"min":[0.0625,0.4375],"name":"tex_coords_gate_H","type":"VEC2"},{"bufferView":15,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.4375],"min":[0.0625,0.375],"name":"tex_coords_gate_H_YZ","type":"VEC2"},{"bufferView":16,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.6875],"min":[0.75,0.625],"name":"tex_coords_gate_H_NXY","type":"VEC2"},{"bufferView":17,"byteOffset":0,"componentType":5126,"count":12,"max":[0.875,0.6875],"min":[0.8125,0.625],"name":"tex_coords_gate_H_NXZ","type":"VEC2"},{"bufferView":18,"byteOffset":0,"componentType":5126,"count":12,"max":[0.9375,0.6875],"min":[0.875,0.625],"name":"tex_coords_gate_H_NYZ","type":"VEC2"},{"bufferView":19,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.4375],"min":[0.125,0.375],"name":"tex_coords_gate_SQRT_X","type":"VEC2"},{"bufferView":20,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.4375],"min":[0.1875,0.375],"name":"tex_coords_gate_SQRT_X_DAG","type":"VEC2"},{"bufferView":21,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.5],"min":[0.125,0.4375],"name":"tex_coords_gate_SQRT_Y","type":"VEC2"},{"bufferView":22,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.5],"min":[0.1875,0.4375],"name":"tex_coords_gate_SQRT_Y_DAG","type":"VEC2"},{"bufferView":23,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.5625],"min":[0.125,0.5],"name":"tex_coords_gate_S","type":"VEC2"},{"bufferView":24,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.5625],"min":[0.1875,0.5],"name":"tex_coords_gate_S_DAG","type":"VEC2"},{"bufferView":25,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":26,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.45254835486412,0.45254835486412],"min":[0,-0.45254835486412,-0.45254835486412],"name":"control_zswap_line_cross","type":"VEC3"},{"bufferView":27,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":28,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.45254835486412,0.45254835486412],"min":[0,-0.45254835486412,-0.45254835486412],"name":"control_xswap_line_cross","type":"VEC3"},{"bufferView":29,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.625],"min":[0.3125,0.5625],"name":"tex_coords_gate_ISWAP","type":"VEC2"},{"bufferView":30,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.625],"min":[0.375,0.5625],"name":"tex_coords_gate_ISWAP_DAG","type":"VEC2"},{"bufferView":31,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.625],"min":[0.4375,0.5625],"name":"tex_coords_gate_SWAP","type":"VEC2"},{"bufferView":32,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.4375],"min":[0.6875,0.375],"name":"tex_coords_gate_SQRT_XX","type":"VEC2"},{"bufferView":33,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.4375],"min":[0.75,0.375],"name":"tex_coords_gate_SQRT_XX_DAG","type":"VEC2"},{"bufferView":34,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.5],"min":[0.6875,0.4375],"name":"tex_coords_gate_SQRT_YY","type":"VEC2"},{"bufferView":35,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.5],"min":[0.75,0.4375],"name":"tex_coords_gate_SQRT_YY_DAG","type":"VEC2"},{"bufferView":36,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.5625],"min":[0.6875,0.5],"name":"tex_coords_gate_SQRT_ZZ","type":"VEC2"},{"bufferView":37,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.5625],"min":[0.75,0.5],"name":"tex_coords_gate_SQRT_ZZ_DAG","type":"VEC2"},{"bufferView":38,"byteOffset":0,"componentType":5126,"count":12,"max":[1,0.6875],"min":[0.9375,0.625],"name":"tex_coords_gate_II","type":"VEC2"},{"bufferView":39,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":40,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"control_x_line_cross","type":"VEC3"},{"bufferView":41,"byteOffset":0,"componentType":5126,"count":3,"max":[0,0.400000005960464,0.346410155296326],"min":[0,-0.200000032782555,-0.346410185098648],"name":"circle_loop","type":"VEC3"},{"bufferView":42,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":43,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.4375],"min":[0.5,0.375],"name":"tex_coords_gate_E:X","type":"VEC2"},{"bufferView":44,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.5],"min":[0.5,0.4375],"name":"tex_coords_gate_E:Y","type":"VEC2"},{"bufferView":45,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.5625],"min":[0.5,0.5],"name":"tex_coords_gate_E:Z","type":"VEC2"},{"bufferView":46,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.4375],"min":[0.5625,0.375],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","type":"VEC2"},{"bufferView":47,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.5],"min":[0.5625,0.4375],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","type":"VEC2"},{"bufferView":48,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.5625],"min":[0.5625,0.5],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","type":"VEC2"},{"bufferView":49,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.625],"min":[0.1875,0.5625],"name":"tex_coords_gate_DEPOLARIZE1","type":"VEC2"},{"bufferView":50,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.625],"min":[0.25,0.5625],"name":"tex_coords_gate_DEPOLARIZE2","type":"VEC2"},{"bufferView":51,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.625],"min":[0.5,0.5625],"name":"tex_coords_gate_PAULI_CHANNEL_1","type":"VEC2"},{"bufferView":52,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.625],"min":[0.5625,0.5625],"name":"tex_coords_gate_PAULI_CHANNEL_2","type":"VEC2"},{"bufferView":53,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.4375],"min":[0.4375,0.375],"name":"tex_coords_gate_X_ERROR","type":"VEC2"},{"bufferView":54,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.5],"min":[0.4375,0.4375],"name":"tex_coords_gate_Y_ERROR","type":"VEC2"},{"bufferView":55,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.5625],"min":[0.4375,0.5],"name":"tex_coords_gate_Z_ERROR","type":"VEC2"},{"bufferView":56,"byteOffset":0,"componentType":5126,"count":12,"max":[0.9375,0.625],"min":[0.875,0.5625],"name":"tex_coords_gate_HERALDED_ERASE","type":"VEC2"},{"bufferView":57,"byteOffset":0,"componentType":5126,"count":12,"max":[1,0.625],"min":[0.9375,0.5625],"name":"tex_coords_gate_HERALDED_PAULI_CHANNEL_1","type":"VEC2"},{"bufferView":58,"byteOffset":0,"componentType":5126,"count":12,"max":[1,0.5625],"min":[0.9375,0.5],"name":"tex_coords_gate_I_ERROR","type":"VEC2"},{"bufferView":59,"byteOffset":0,"componentType":5126,"count":12,"max":[1,0.75],"min":[0.9375,0.6875],"name":"tex_coords_gate_II_ERROR","type":"VEC2"},{"bufferView":60,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.4375],"min":[0.625,0.375],"name":"tex_coords_gate_MPP:X","type":"VEC2"},{"bufferView":61,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.5],"min":[0.625,0.4375],"name":"tex_coords_gate_MPP:Y","type":"VEC2"},{"bufferView":62,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.5625],"min":[0.625,0.5],"name":"tex_coords_gate_MPP:Z","type":"VEC2"},{"bufferView":63,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.6875],"min":[0,0.625],"name":"tex_coords_gate_SPP:X","type":"VEC2"},{"bufferView":64,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.6875],"min":[0.0625,0.625],"name":"tex_coords_gate_SPP:Y","type":"VEC2"},{"bufferView":65,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.6875],"min":[0.125,0.625],"name":"tex_coords_gate_SPP:Z","type":"VEC2"},{"bufferView":66,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.6875],"min":[0.1875,0.625],"name":"tex_coords_gate_SPP_DAG:X","type":"VEC2"},{"bufferView":67,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.6875],"min":[0.25,0.625],"name":"tex_coords_gate_SPP_DAG:Y","type":"VEC2"},{"bufferView":68,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.6875],"min":[0.3125,0.625],"name":"tex_coords_gate_SPP_DAG:Z","type":"VEC2"},{"bufferView":69,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.4375],"min":[0.375,0.375],"name":"tex_coords_gate_MRX","type":"VEC2"},{"bufferView":70,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.5],"min":[0.375,0.4375],"name":"tex_coords_gate_MRY","type":"VEC2"},{"bufferView":71,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.5625],"min":[0.375,0.5],"name":"tex_coords_gate_MR","type":"VEC2"},{"bufferView":72,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.4375],"min":[0.25,0.375],"name":"tex_coords_gate_MX","type":"VEC2"},{"bufferView":73,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5],"min":[0.25,0.4375],"name":"tex_coords_gate_MY","type":"VEC2"},{"bufferView":74,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5625],"min":[0.25,0.5],"name":"tex_coords_gate_M","type":"VEC2"},{"bufferView":75,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.4375],"min":[0.3125,0.375],"name":"tex_coords_gate_RX","type":"VEC2"},{"bufferView":76,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5],"min":[0.3125,0.4375],"name":"tex_coords_gate_RY","type":"VEC2"},{"bufferView":77,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5625],"min":[0.3125,0.5],"name":"tex_coords_gate_R","type":"VEC2"},{"bufferView":78,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.625],"min":[0.625,0.5625],"name":"tex_coords_gate_MXX","type":"VEC2"},{"bufferView":79,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.625],"min":[0.6875,0.5625],"name":"tex_coords_gate_MYY","type":"VEC2"},{"bufferView":80,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.625],"min":[0.75,0.5625],"name":"tex_coords_gate_MZZ","type":"VEC2"},{"bufferView":81,"byteOffset":0,"componentType":5126,"count":12,"max":[0.875,0.625],"min":[0.8125,0.5625],"name":"tex_coords_gate_MPAD","type":"VEC2"},{"bufferView":82,"byteOffset":0,"componentType":5126,"count":12,"max":[0.875,0.4375],"min":[0.8125,0.375],"name":"tex_coords_gate_X:REC","type":"VEC2"},{"bufferView":83,"byteOffset":0,"componentType":5126,"count":12,"max":[0.9375,0.5],"min":[0.875,0.4375],"name":"tex_coords_gate_Y:SWEEP","type":"VEC2"},{"bufferView":84,"byteOffset":0,"componentType":5126,"count":12,"max":[0.875,0.5625],"min":[0.8125,0.5],"name":"tex_coords_gate_Z:REC","type":"VEC2"},{"bufferView":85,"byteOffset":0,"componentType":5126,"count":146,"max":[1,-2,-0],"min":[-30,-34,-32],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":86,"byteOffset":0,"componentType":5126,"count":30,"max":[0,0.5,1],"min":[-21.25,-35,-33],"name":"buf_red_scattered_lines","type":"VEC3"},{"bufferView":87,"byteOffset":0,"componentType":5126,"count":96,"max":[-0.75,-1.20000004768372,0.5],"min":[-27.25,-1.60000002384186,-32.5],"name":"buf_blue_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_I","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Y","target":34962},{"buffer":4,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Z","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_XYZ","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_NXYZ","target":34962},{"buffer":7,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_XNYZ","target":34962},{"buffer":8,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_XYNZ","target":34962},{"buffer":9,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_ZYX","target":34962},{"buffer":10,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_NZYX","target":34962},{"buffer":11,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_ZNYX","target":34962},{"buffer":12,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_ZYNX","target":34962},{"buffer":13,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_XY","target":34962},{"buffer":14,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H","target":34962},{"buffer":15,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_YZ","target":34962},{"buffer":16,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_NXY","target":34962},{"buffer":17,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_NXZ","target":34962},{"buffer":18,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_NYZ","target":34962},{"buffer":19,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_X","target":34962},{"buffer":20,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_X_DAG","target":34962},{"buffer":21,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_Y","target":34962},{"buffer":22,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_Y_DAG","target":34962},{"buffer":23,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_S","target":34962},{"buffer":24,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_S_DAG","target":34962},{"buffer":25,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":26,"byteLength":48,"byteOffset":0,"name":"control_zswap_line_cross","target":34962},{"buffer":27,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":28,"byteLength":48,"byteOffset":0,"name":"control_xswap_line_cross","target":34962},{"buffer":29,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ISWAP","target":34962},{"buffer":30,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ISWAP_DAG","target":34962},{"buffer":31,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SWAP","target":34962},{"buffer":32,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_XX","target":34962},{"buffer":33,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_XX_DAG","target":34962},{"buffer":34,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_YY","target":34962},{"buffer":35,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_YY_DAG","target":34962},{"buffer":36,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_ZZ","target":34962},{"buffer":37,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_ZZ_DAG","target":34962},{"buffer":38,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_II","target":34962},{"buffer":39,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":40,"byteLength":48,"byteOffset":0,"name":"control_x_line_cross","target":34962},{"buffer":41,"byteLength":36,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":42,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":43,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:X","target":34962},{"buffer":44,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:Y","target":34962},{"buffer":45,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:Z","target":34962},{"buffer":46,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","target":34962},{"buffer":47,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","target":34962},{"buffer":48,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","target":34962},{"buffer":49,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_DEPOLARIZE1","target":34962},{"buffer":50,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_DEPOLARIZE2","target":34962},{"buffer":51,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_PAULI_CHANNEL_1","target":34962},{"buffer":52,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_PAULI_CHANNEL_2","target":34962},{"buffer":53,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X_ERROR","target":34962},{"buffer":54,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Y_ERROR","target":34962},{"buffer":55,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Z_ERROR","target":34962},{"buffer":56,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_HERALDED_ERASE","target":34962},{"buffer":57,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_HERALDED_PAULI_CHANNEL_1","target":34962},{"buffer":58,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_I_ERROR","target":34962},{"buffer":59,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_II_ERROR","target":34962},{"buffer":60,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:X","target":34962},{"buffer":61,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:Y","target":34962},{"buffer":62,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:Z","target":34962},{"buffer":63,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SPP:X","target":34962},{"buffer":64,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SPP:Y","target":34962},{"buffer":65,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SPP:Z","target":34962},{"buffer":66,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SPP_DAG:X","target":34962},{"buffer":67,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SPP_DAG:Y","target":34962},{"buffer":68,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SPP_DAG:Z","target":34962},{"buffer":69,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MRX","target":34962},{"buffer":70,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MRY","target":34962},{"buffer":71,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MR","target":34962},{"buffer":72,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MX","target":34962},{"buffer":73,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MY","target":34962},{"buffer":74,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_M","target":34962},{"buffer":75,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_RX","target":34962},{"buffer":76,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_RY","target":34962},{"buffer":77,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_R","target":34962},{"buffer":78,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MXX","target":34962},{"buffer":79,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MYY","target":34962},{"buffer":80,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MZZ","target":34962},{"buffer":81,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPAD","target":34962},{"buffer":82,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X:REC","target":34962},{"buffer":83,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Y:SWEEP","target":34962},{"buffer":84,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Z:REC","target":34962},{"buffer":85,"byteLength":1752,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":86,"byteLength":360,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962},{"buffer":87,"byteLength":1152,"byteOffset":0,"name":"buf_blue_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_I","uri":"data:application/octet-stream;base64,AACAPQAAwD4AAAAAAADAPgAAgD0AAOA+AAAAAAAAwD4AAAAAAADgPgAAgD0AAOA+AACAPQAA4D4AAIA9AADAPgAAAAAAAOA+AAAAAAAA4D4AAIA9AADAPgAAAAAAAMA+"},{"byteLength":96,"name":"tex_coords_gate_X","uri":"data:application/octet-stream;base64,AACAPQAAwD4AAAAAAADAPgAAgD0AAOA+AAAAAAAAwD4AAAAAAADgPgAAgD0AAOA+AACAPQAA4D4AAIA9AADAPgAAAAAAAOA+AAAAAAAA4D4AAIA9AADAPgAAAAAAAMA+"},{"byteLength":96,"name":"tex_coords_gate_Y","uri":"data:application/octet-stream;base64,AACAPQAA4D4AAAAAAADgPgAAgD0AAAA/AAAAAAAA4D4AAAAAAAAAPwAAgD0AAAA/AACAPQAAAD8AAIA9AADgPgAAAAAAAAA/AAAAAAAAAD8AAIA9AADgPgAAAAAAAOA+"},{"byteLength":96,"name":"tex_coords_gate_Z","uri":"data:application/octet-stream;base64,AACAPQAAAD8AAAAAAAAAPwAAgD0AABA/AAAAAAAAAD8AAAAAAAAQPwAAgD0AABA/AACAPQAAED8AAIA9AAAAPwAAAAAAABA/AAAAAAAAED8AAIA9AAAAPwAAAAAAAAA/"},{"byteLength":96,"name":"tex_coords_gate_C_XYZ","uri":"data:application/octet-stream;base64,AAAAPgAAED8AAIA9AAAQPwAAAD4AACA/AACAPQAAED8AAIA9AAAgPwAAAD4AACA/AAAAPgAAID8AAAA+AAAQPwAAgD0AACA/AACAPQAAID8AAAA+AAAQPwAAgD0AABA/"},{"byteLength":96,"name":"tex_coords_gate_C_NXYZ","uri":"data:application/octet-stream;base64,AADgPgAAID8AAMA+AAAgPwAA4D4AADA/AADAPgAAID8AAMA+AAAwPwAA4D4AADA/AADgPgAAMD8AAOA+AAAgPwAAwD4AADA/AADAPgAAMD8AAOA+AAAgPwAAwD4AACA/"},{"byteLength":96,"name":"tex_coords_gate_C_XNYZ","uri":"data:application/octet-stream;base64,AAAAPwAAID8AAOA+AAAgPwAAAD8AADA/AADgPgAAID8AAOA+AAAwPwAAAD8AADA/AAAAPwAAMD8AAAA/AAAgPwAA4D4AADA/AADgPgAAMD8AAAA/AAAgPwAA4D4AACA/"},{"byteLength":96,"name":"tex_coords_gate_C_XYNZ","uri":"data:application/octet-stream;base64,AAAQPwAAID8AAAA/AAAgPwAAED8AADA/AAAAPwAAID8AAAA/AAAwPwAAED8AADA/AAAQPwAAMD8AABA/AAAgPwAAAD8AADA/AAAAPwAAMD8AABA/AAAgPwAAAD8AACA/"},{"byteLength":96,"name":"tex_coords_gate_C_ZYX","uri":"data:application/octet-stream;base64,AABAPgAAED8AAAA+AAAQPwAAQD4AACA/AAAAPgAAED8AAAA+AAAgPwAAQD4AACA/AABAPgAAID8AAEA+AAAQPwAAAD4AACA/AAAAPgAAID8AAEA+AAAQPwAAAD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_C_NZYX","uri":"data:application/octet-stream;base64,AAAgPwAAID8AABA/AAAgPwAAID8AADA/AAAQPwAAID8AABA/AAAwPwAAID8AADA/AAAgPwAAMD8AACA/AAAgPwAAED8AADA/AAAQPwAAMD8AACA/AAAgPwAAED8AACA/"},{"byteLength":96,"name":"tex_coords_gate_C_ZNYX","uri":"data:application/octet-stream;base64,AAAwPwAAID8AACA/AAAgPwAAMD8AADA/AAAgPwAAID8AACA/AAAwPwAAMD8AADA/AAAwPwAAMD8AADA/AAAgPwAAID8AADA/AAAgPwAAMD8AADA/AAAgPwAAID8AACA/"},{"byteLength":96,"name":"tex_coords_gate_C_ZYNX","uri":"data:application/octet-stream;base64,AABAPwAAID8AADA/AAAgPwAAQD8AADA/AAAwPwAAID8AADA/AAAwPwAAQD8AADA/AABAPwAAMD8AAEA/AAAgPwAAMD8AADA/AAAwPwAAMD8AAEA/AAAgPwAAMD8AACA/"},{"byteLength":96,"name":"tex_coords_gate_H_XY","uri":"data:application/octet-stream;base64,AAAAPgAAAD8AAIA9AAAAPwAAAD4AABA/AACAPQAAAD8AAIA9AAAQPwAAAD4AABA/AAAAPgAAED8AAAA+AAAAPwAAgD0AABA/AACAPQAAED8AAAA+AAAAPwAAgD0AAAA/"},{"byteLength":96,"name":"tex_coords_gate_H","uri":"data:application/octet-stream;base64,AAAAPgAA4D4AAIA9AADgPgAAAD4AAAA/AACAPQAA4D4AAIA9AAAAPwAAAD4AAAA/AAAAPgAAAD8AAAA+AADgPgAAgD0AAAA/AACAPQAAAD8AAAA+AADgPgAAgD0AAOA+"},{"byteLength":96,"name":"tex_coords_gate_H_YZ","uri":"data:application/octet-stream;base64,AAAAPgAAwD4AAIA9AADAPgAAAD4AAOA+AACAPQAAwD4AAIA9AADgPgAAAD4AAOA+AAAAPgAA4D4AAAA+AADAPgAAgD0AAOA+AACAPQAA4D4AAAA+AADAPgAAgD0AAMA+"},{"byteLength":96,"name":"tex_coords_gate_H_NXY","uri":"data:application/octet-stream;base64,AABQPwAAID8AAEA/AAAgPwAAUD8AADA/AABAPwAAID8AAEA/AAAwPwAAUD8AADA/AABQPwAAMD8AAFA/AAAgPwAAQD8AADA/AABAPwAAMD8AAFA/AAAgPwAAQD8AACA/"},{"byteLength":96,"name":"tex_coords_gate_H_NXZ","uri":"data:application/octet-stream;base64,AABgPwAAID8AAFA/AAAgPwAAYD8AADA/AABQPwAAID8AAFA/AAAwPwAAYD8AADA/AABgPwAAMD8AAGA/AAAgPwAAUD8AADA/AABQPwAAMD8AAGA/AAAgPwAAUD8AACA/"},{"byteLength":96,"name":"tex_coords_gate_H_NYZ","uri":"data:application/octet-stream;base64,AABwPwAAID8AAGA/AAAgPwAAcD8AADA/AABgPwAAID8AAGA/AAAwPwAAcD8AADA/AABwPwAAMD8AAHA/AAAgPwAAYD8AADA/AABgPwAAMD8AAHA/AAAgPwAAYD8AACA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_X","uri":"data:application/octet-stream;base64,AABAPgAAwD4AAAA+AADAPgAAQD4AAOA+AAAAPgAAwD4AAAA+AADgPgAAQD4AAOA+AABAPgAA4D4AAEA+AADAPgAAAD4AAOA+AAAAPgAA4D4AAEA+AADAPgAAAD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_X_DAG","uri":"data:application/octet-stream;base64,AACAPgAAwD4AAEA+AADAPgAAgD4AAOA+AABAPgAAwD4AAEA+AADgPgAAgD4AAOA+AACAPgAA4D4AAIA+AADAPgAAQD4AAOA+AABAPgAA4D4AAIA+AADAPgAAQD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_Y","uri":"data:application/octet-stream;base64,AABAPgAA4D4AAAA+AADgPgAAQD4AAAA/AAAAPgAA4D4AAAA+AAAAPwAAQD4AAAA/AABAPgAAAD8AAEA+AADgPgAAAD4AAAA/AAAAPgAAAD8AAEA+AADgPgAAAD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_Y_DAG","uri":"data:application/octet-stream;base64,AACAPgAA4D4AAEA+AADgPgAAgD4AAAA/AABAPgAA4D4AAEA+AAAAPwAAgD4AAAA/AACAPgAAAD8AAIA+AADgPgAAQD4AAAA/AABAPgAAAD8AAIA+AADgPgAAQD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_S","uri":"data:application/octet-stream;base64,AABAPgAAAD8AAAA+AAAAPwAAQD4AABA/AAAAPgAAAD8AAAA+AAAQPwAAQD4AABA/AABAPgAAED8AAEA+AAAAPwAAAD4AABA/AAAAPgAAED8AAEA+AAAAPwAAAD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_S_DAG","uri":"data:application/octet-stream;base64,AACAPgAAAD8AAEA+AAAAPwAAgD4AABA/AABAPgAAAD8AAEA+AAAQPwAAgD4AABA/AACAPgAAED8AAIA+AAAAPwAAQD4AABA/AABAPgAAED8AAIA+AAAAPwAAQD4AAAA/"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_zswap_line_cross","uri":"data:application/octet-stream;base64,AAAAAGu0575rtOe+AAAAAGu05z5rtOc+AAAAAGu0575rtOc+AAAAAGu05z5rtOe+"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_xswap_line_cross","uri":"data:application/octet-stream;base64,AAAAAGu0575rtOe+AAAAAGu05z5rtOc+AAAAAGu0575rtOc+AAAAAGu05z5rtOe+"},{"byteLength":96,"name":"tex_coords_gate_ISWAP","uri":"data:application/octet-stream;base64,AADAPgAAED8AAKA+AAAQPwAAwD4AACA/AACgPgAAED8AAKA+AAAgPwAAwD4AACA/AADAPgAAID8AAMA+AAAQPwAAoD4AACA/AACgPgAAID8AAMA+AAAQPwAAoD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_ISWAP_DAG","uri":"data:application/octet-stream;base64,AADgPgAAED8AAMA+AAAQPwAA4D4AACA/AADAPgAAED8AAMA+AAAgPwAA4D4AACA/AADgPgAAID8AAOA+AAAQPwAAwD4AACA/AADAPgAAID8AAOA+AAAQPwAAwD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_SWAP","uri":"data:application/octet-stream;base64,AAAAPwAAED8AAOA+AAAQPwAAAD8AACA/AADgPgAAED8AAOA+AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAAQPwAA4D4AACA/AADgPgAAID8AAAA/AAAQPwAA4D4AABA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_XX","uri":"data:application/octet-stream;base64,AABAPwAAwD4AADA/AADAPgAAQD8AAOA+AAAwPwAAwD4AADA/AADgPgAAQD8AAOA+AABAPwAA4D4AAEA/AADAPgAAMD8AAOA+AAAwPwAA4D4AAEA/AADAPgAAMD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_XX_DAG","uri":"data:application/octet-stream;base64,AABQPwAAwD4AAEA/AADAPgAAUD8AAOA+AABAPwAAwD4AAEA/AADgPgAAUD8AAOA+AABQPwAA4D4AAFA/AADAPgAAQD8AAOA+AABAPwAA4D4AAFA/AADAPgAAQD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_YY","uri":"data:application/octet-stream;base64,AABAPwAA4D4AADA/AADgPgAAQD8AAAA/AAAwPwAA4D4AADA/AAAAPwAAQD8AAAA/AABAPwAAAD8AAEA/AADgPgAAMD8AAAA/AAAwPwAAAD8AAEA/AADgPgAAMD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_YY_DAG","uri":"data:application/octet-stream;base64,AABQPwAA4D4AAEA/AADgPgAAUD8AAAA/AABAPwAA4D4AAEA/AAAAPwAAUD8AAAA/AABQPwAAAD8AAFA/AADgPgAAQD8AAAA/AABAPwAAAD8AAFA/AADgPgAAQD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_ZZ","uri":"data:application/octet-stream;base64,AABAPwAAAD8AADA/AAAAPwAAQD8AABA/AAAwPwAAAD8AADA/AAAQPwAAQD8AABA/AABAPwAAED8AAEA/AAAAPwAAMD8AABA/AAAwPwAAED8AAEA/AAAAPwAAMD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_ZZ_DAG","uri":"data:application/octet-stream;base64,AABQPwAAAD8AAEA/AAAAPwAAUD8AABA/AABAPwAAAD8AAEA/AAAQPwAAUD8AABA/AABQPwAAED8AAFA/AAAAPwAAQD8AABA/AABAPwAAED8AAFA/AAAAPwAAQD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_II","uri":"data:application/octet-stream;base64,AACAPwAAID8AAHA/AAAgPwAAgD8AADA/AABwPwAAID8AAHA/AAAwPwAAgD8AADA/AACAPwAAMD8AAIA/AAAgPwAAcD8AADA/AABwPwAAMD8AAIA/AAAgPwAAcD8AACA/"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_x_line_cross","uri":"data:application/octet-stream;base64,AAAAAM3MzL4AAAAAAAAAAM3MzD4AAAAAAAAAAAAAAADNzMy+AAAAAAAAAADNzMw+"},{"byteLength":36,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAM/MTL6sXLE+AAAAAMvMTL6tXLG+"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":96,"name":"tex_coords_gate_E:X","uri":"data:application/octet-stream;base64,AAAQPwAAwD4AAAA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/AADgPgAAED8AAOA+AAAQPwAA4D4AABA/AADAPgAAAD8AAOA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_E:Y","uri":"data:application/octet-stream;base64,AAAQPwAA4D4AAAA/AADgPgAAED8AAAA/AAAAPwAA4D4AAAA/AAAAPwAAED8AAAA/AAAQPwAAAD8AABA/AADgPgAAAD8AAAA/AAAAPwAAAD8AABA/AADgPgAAAD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_E:Z","uri":"data:application/octet-stream;base64,AAAQPwAAAD8AAAA/AAAAPwAAED8AABA/AAAAPwAAAD8AAAA/AAAQPwAAED8AABA/AAAQPwAAED8AABA/AAAAPwAAAD8AABA/AAAAPwAAED8AABA/AAAAPwAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","uri":"data:application/octet-stream;base64,AAAgPwAAwD4AABA/AADAPgAAID8AAOA+AAAQPwAAwD4AABA/AADgPgAAID8AAOA+AAAgPwAA4D4AACA/AADAPgAAED8AAOA+AAAQPwAA4D4AACA/AADAPgAAED8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","uri":"data:application/octet-stream;base64,AAAgPwAA4D4AABA/AADgPgAAID8AAAA/AAAQPwAA4D4AABA/AAAAPwAAID8AAAA/AAAgPwAAAD8AACA/AADgPgAAED8AAAA/AAAQPwAAAD8AACA/AADgPgAAED8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","uri":"data:application/octet-stream;base64,AAAgPwAAAD8AABA/AAAAPwAAID8AABA/AAAQPwAAAD8AABA/AAAQPwAAID8AABA/AAAgPwAAED8AACA/AAAAPwAAED8AABA/AAAQPwAAED8AACA/AAAAPwAAED8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_DEPOLARIZE1","uri":"data:application/octet-stream;base64,AACAPgAAED8AAEA+AAAQPwAAgD4AACA/AABAPgAAED8AAEA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AAAQPwAAQD4AACA/AABAPgAAID8AAIA+AAAQPwAAQD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_DEPOLARIZE2","uri":"data:application/octet-stream;base64,AACgPgAAED8AAIA+AAAQPwAAoD4AACA/AACAPgAAED8AAIA+AAAgPwAAoD4AACA/AACgPgAAID8AAKA+AAAQPwAAgD4AACA/AACAPgAAID8AAKA+AAAQPwAAgD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_PAULI_CHANNEL_1","uri":"data:application/octet-stream;base64,AAAQPwAAED8AAAA/AAAQPwAAED8AACA/AAAAPwAAED8AAAA/AAAgPwAAED8AACA/AAAQPwAAID8AABA/AAAQPwAAAD8AACA/AAAAPwAAID8AABA/AAAQPwAAAD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_PAULI_CHANNEL_2","uri":"data:application/octet-stream;base64,AAAgPwAAED8AABA/AAAQPwAAID8AACA/AAAQPwAAED8AABA/AAAgPwAAID8AACA/AAAgPwAAID8AACA/AAAQPwAAED8AACA/AAAQPwAAID8AACA/AAAQPwAAED8AABA/"},{"byteLength":96,"name":"tex_coords_gate_X_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAAwD4AAOA+AADAPgAAAD8AAOA+AADgPgAAwD4AAOA+AADgPgAAAD8AAOA+AAAAPwAA4D4AAAA/AADAPgAA4D4AAOA+AADgPgAA4D4AAAA/AADAPgAA4D4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_Y_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAA4D4AAOA+AADgPgAAAD8AAAA/AADgPgAA4D4AAOA+AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AADgPgAA4D4AAAA/AADgPgAAAD8AAAA/AADgPgAA4D4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_Z_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAAAD8AAOA+AAAAPwAAAD8AABA/AADgPgAAAD8AAOA+AAAQPwAAAD8AABA/AAAAPwAAED8AAAA/AAAAPwAA4D4AABA/AADgPgAAED8AAAA/AAAAPwAA4D4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_HERALDED_ERASE","uri":"data:application/octet-stream;base64,AABwPwAAED8AAGA/AAAQPwAAcD8AACA/AABgPwAAED8AAGA/AAAgPwAAcD8AACA/AABwPwAAID8AAHA/AAAQPwAAYD8AACA/AABgPwAAID8AAHA/AAAQPwAAYD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_HERALDED_PAULI_CHANNEL_1","uri":"data:application/octet-stream;base64,AACAPwAAED8AAHA/AAAQPwAAgD8AACA/AABwPwAAED8AAHA/AAAgPwAAgD8AACA/AACAPwAAID8AAIA/AAAQPwAAcD8AACA/AABwPwAAID8AAIA/AAAQPwAAcD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_I_ERROR","uri":"data:application/octet-stream;base64,AACAPwAAAD8AAHA/AAAAPwAAgD8AABA/AABwPwAAAD8AAHA/AAAQPwAAgD8AABA/AACAPwAAED8AAIA/AAAAPwAAcD8AABA/AABwPwAAED8AAIA/AAAAPwAAcD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_II_ERROR","uri":"data:application/octet-stream;base64,AACAPwAAMD8AAHA/AAAwPwAAgD8AAEA/AABwPwAAMD8AAHA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AAAwPwAAcD8AAEA/AABwPwAAQD8AAIA/AAAwPwAAcD8AADA/"},{"byteLength":96,"name":"tex_coords_gate_MPP:X","uri":"data:application/octet-stream;base64,AAAwPwAAwD4AACA/AADAPgAAMD8AAOA+AAAgPwAAwD4AACA/AADgPgAAMD8AAOA+AAAwPwAA4D4AADA/AADAPgAAID8AAOA+AAAgPwAA4D4AADA/AADAPgAAID8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:Y","uri":"data:application/octet-stream;base64,AAAwPwAA4D4AACA/AADgPgAAMD8AAAA/AAAgPwAA4D4AACA/AAAAPwAAMD8AAAA/AAAwPwAAAD8AADA/AADgPgAAID8AAAA/AAAgPwAAAD8AADA/AADgPgAAID8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:Z","uri":"data:application/octet-stream;base64,AAAwPwAAAD8AACA/AAAAPwAAMD8AABA/AAAgPwAAAD8AACA/AAAQPwAAMD8AABA/AAAwPwAAED8AADA/AAAAPwAAID8AABA/AAAgPwAAED8AADA/AAAAPwAAID8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_SPP:X","uri":"data:application/octet-stream;base64,AACAPQAAID8AAAAAAAAgPwAAgD0AADA/AAAAAAAAID8AAAAAAAAwPwAAgD0AADA/AACAPQAAMD8AAIA9AAAgPwAAAAAAADA/AAAAAAAAMD8AAIA9AAAgPwAAAAAAACA/"},{"byteLength":96,"name":"tex_coords_gate_SPP:Y","uri":"data:application/octet-stream;base64,AAAAPgAAID8AAIA9AAAgPwAAAD4AADA/AACAPQAAID8AAIA9AAAwPwAAAD4AADA/AAAAPgAAMD8AAAA+AAAgPwAAgD0AADA/AACAPQAAMD8AAAA+AAAgPwAAgD0AACA/"},{"byteLength":96,"name":"tex_coords_gate_SPP:Z","uri":"data:application/octet-stream;base64,AABAPgAAID8AAAA+AAAgPwAAQD4AADA/AAAAPgAAID8AAAA+AAAwPwAAQD4AADA/AABAPgAAMD8AAEA+AAAgPwAAAD4AADA/AAAAPgAAMD8AAEA+AAAgPwAAAD4AACA/"},{"byteLength":96,"name":"tex_coords_gate_SPP_DAG:X","uri":"data:application/octet-stream;base64,AACAPgAAID8AAEA+AAAgPwAAgD4AADA/AABAPgAAID8AAEA+AAAwPwAAgD4AADA/AACAPgAAMD8AAIA+AAAgPwAAQD4AADA/AABAPgAAMD8AAIA+AAAgPwAAQD4AACA/"},{"byteLength":96,"name":"tex_coords_gate_SPP_DAG:Y","uri":"data:application/octet-stream;base64,AACgPgAAID8AAIA+AAAgPwAAoD4AADA/AACAPgAAID8AAIA+AAAwPwAAoD4AADA/AACgPgAAMD8AAKA+AAAgPwAAgD4AADA/AACAPgAAMD8AAKA+AAAgPwAAgD4AACA/"},{"byteLength":96,"name":"tex_coords_gate_SPP_DAG:Z","uri":"data:application/octet-stream;base64,AADAPgAAID8AAKA+AAAgPwAAwD4AADA/AACgPgAAID8AAKA+AAAwPwAAwD4AADA/AADAPgAAMD8AAMA+AAAgPwAAoD4AADA/AACgPgAAMD8AAMA+AAAgPwAAoD4AACA/"},{"byteLength":96,"name":"tex_coords_gate_MRX","uri":"data:application/octet-stream;base64,AADgPgAAwD4AAMA+AADAPgAA4D4AAOA+AADAPgAAwD4AAMA+AADgPgAA4D4AAOA+AADgPgAA4D4AAOA+AADAPgAAwD4AAOA+AADAPgAA4D4AAOA+AADAPgAAwD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MRY","uri":"data:application/octet-stream;base64,AADgPgAA4D4AAMA+AADgPgAA4D4AAAA/AADAPgAA4D4AAMA+AAAAPwAA4D4AAAA/AADgPgAAAD8AAOA+AADgPgAAwD4AAAA/AADAPgAAAD8AAOA+AADgPgAAwD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_MR","uri":"data:application/octet-stream;base64,AADgPgAAAD8AAMA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+AAAQPwAA4D4AABA/AADgPgAAED8AAOA+AAAAPwAAwD4AABA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MX","uri":"data:application/octet-stream;base64,AACgPgAAwD4AAIA+AADAPgAAoD4AAOA+AACAPgAAwD4AAIA+AADgPgAAoD4AAOA+AACgPgAA4D4AAKA+AADAPgAAgD4AAOA+AACAPgAA4D4AAKA+AADAPgAAgD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MY","uri":"data:application/octet-stream;base64,AACgPgAA4D4AAIA+AADgPgAAoD4AAAA/AACAPgAA4D4AAIA+AAAAPwAAoD4AAAA/AACgPgAAAD8AAKA+AADgPgAAgD4AAAA/AACAPgAAAD8AAKA+AADgPgAAgD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_M","uri":"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_RX","uri":"data:application/octet-stream;base64,AADAPgAAwD4AAKA+AADAPgAAwD4AAOA+AACgPgAAwD4AAKA+AADgPgAAwD4AAOA+AADAPgAA4D4AAMA+AADAPgAAoD4AAOA+AACgPgAA4D4AAMA+AADAPgAAoD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_RY","uri":"data:application/octet-stream;base64,AADAPgAA4D4AAKA+AADgPgAAwD4AAAA/AACgPgAA4D4AAKA+AAAAPwAAwD4AAAA/AADAPgAAAD8AAMA+AADgPgAAoD4AAAA/AACgPgAAAD8AAMA+AADgPgAAoD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_R","uri":"data:application/octet-stream;base64,AADAPgAAAD8AAKA+AAAAPwAAwD4AABA/AACgPgAAAD8AAKA+AAAQPwAAwD4AABA/AADAPgAAED8AAMA+AAAAPwAAoD4AABA/AACgPgAAED8AAMA+AAAAPwAAoD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MXX","uri":"data:application/octet-stream;base64,AAAwPwAAED8AACA/AAAQPwAAMD8AACA/AAAgPwAAED8AACA/AAAgPwAAMD8AACA/AAAwPwAAID8AADA/AAAQPwAAID8AACA/AAAgPwAAID8AADA/AAAQPwAAID8AABA/"},{"byteLength":96,"name":"tex_coords_gate_MYY","uri":"data:application/octet-stream;base64,AABAPwAAED8AADA/AAAQPwAAQD8AACA/AAAwPwAAED8AADA/AAAgPwAAQD8AACA/AABAPwAAID8AAEA/AAAQPwAAMD8AACA/AAAwPwAAID8AAEA/AAAQPwAAMD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_MZZ","uri":"data:application/octet-stream;base64,AABQPwAAED8AAEA/AAAQPwAAUD8AACA/AABAPwAAED8AAEA/AAAgPwAAUD8AACA/AABQPwAAID8AAFA/AAAQPwAAQD8AACA/AABAPwAAID8AAFA/AAAQPwAAQD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_MPAD","uri":"data:application/octet-stream;base64,AABgPwAAED8AAFA/AAAQPwAAYD8AACA/AABQPwAAED8AAFA/AAAgPwAAYD8AACA/AABgPwAAID8AAGA/AAAQPwAAUD8AACA/AABQPwAAID8AAGA/AAAQPwAAUD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_X:REC","uri":"data:application/octet-stream;base64,AABgPwAAwD4AAFA/AADAPgAAYD8AAOA+AABQPwAAwD4AAFA/AADgPgAAYD8AAOA+AABgPwAA4D4AAGA/AADAPgAAUD8AAOA+AABQPwAA4D4AAGA/AADAPgAAUD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_Y:SWEEP","uri":"data:application/octet-stream;base64,AABwPwAA4D4AAGA/AADgPgAAcD8AAAA/AABgPwAA4D4AAGA/AAAAPwAAcD8AAAA/AABwPwAAAD8AAHA/AADgPgAAYD8AAAA/AABgPwAAAD8AAHA/AADgPgAAYD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_Z:REC","uri":"data:application/octet-stream;base64,AABgPwAAAD8AAFA/AAAAPwAAYD8AABA/AABQPwAAAD8AAFA/AAAQPwAAYD8AABA/AABgPwAAED8AAGA/AAAAPwAAUD8AABA/AABQPwAAED8AAGA/AAAAPwAAUD8AAAA/"},{"byteLength":1752,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAwAAAAMIAAADCAACIwAAAiMEAAIDBAACIwAAAiMEAAIDBAACAwAAAAMAAAACAAACAwAAAgMAAAACAAACAwAAAwMAAAACAAACAwAAAAMEAAACAAACAwAAAIMEAAACAAACAwAAAQMEAAACAAACAwAAAYMEAAACAAACAwAAAgMEAAACAAACAwAAAkMEAAACAAACAwAAAoMEAAACAAACAwAAAsMEAAACAAACgwAAAAMIAAADCAACowAAAiMEAAIDBAACowAAAiMEAAIDBAACgwAAAAMAAAACAAACgwAAAgMAAAACAAACgwAAAwMAAAACAAACgwAAAAMEAAACAAACgwAAAIMEAAACAAACgwAAAQMEAAACAAACgwAAAYMEAAACAAACgwAAAgMEAAACAAACgwAAAkMEAAACAAACgwAAAoMEAAACAAACgwAAAsMEAAACAAACgwAAAwMEAAACAAACgwAAA0MEAAACAAADAwAAAAMIAAADCAADIwAAAiMEAAIDBAADIwAAAiMEAAIDBAADAwAAAAMAAAACAAADAwAAAgMAAAACAAADAwAAAwMAAAACAAADAwAAAAMEAAACAAADAwAAAIMEAAACAAADAwAAAQMEAAACAAADAwAAAYMEAAACAAADAwAAAgMEAAACAAADAwAAAkMEAAACAAADAwAAAoMEAAACAAADAwAAAsMEAAACAAADAwAAAwMEAAACAAADAwAAA0MEAAACAAADAwAAA4MEAAACAAADAwAAA8MEAAACAAADAwAAAAMIAAACAAADAwAAACMIAAACAAADgwAAAgMAAAACAAADgwAAAAMAAAACAAADgwAAAwMAAAACAAADgwAAAgMAAAACAAADgwAAAYMEAAACAAADowAAAMMEAAACAAADowAAAMMEAAACAAADgwAAAAMEAAACAAADgwAAAQMEAAACAAADgwAAAYMEAAACAAAAAwQAAAMAAAACAAAAAwQAAgMAAAACAAAAAwQAAAMEAAACAAAAAwQAAIMEAAACAAAAQwQAAgMEAAACAAAAQwQAAkMEAAACAAAAgwQAAAMAAAACAAAAkwQAAiMEAAIDBAAAkwQAAiMEAAIDBAAAgwQAAAMIAAADCAAAgwQAAgMAAAACAAAAgwQAAAMAAAACAAAAwwQAAAMAAAACAAAA0wQAAiMEAAIDBAAA0wQAAiMEAAIDBAAAwwQAAAMIAAADCAABAwQAAAMAAAACAAABEwQAAiMEAAIDBAABEwQAAiMEAAIDBAABAwQAAAMIAAADCAABAwQAAgMAAAACAAABAwQAAAMAAAACAAABQwQAAAMAAAACAAABUwQAAiMEAAIDBAABUwQAAiMEAAIDBAABQwQAAAMIAAADCAABQwQAAgMAAAACAAABQwQAAAMAAAACAAACAwQAAAMIAAADCAACCwQAAiMEAAIDBAACCwQAAiMEAAIDBAACAwQAAAMAAAACAAACAwQAAgMAAAACAAACAwQAAwMAAAACAAACAwQAAAMEAAACAAACAwQAAIMEAAACAAACAwQAAQMEAAACAAACAwQAAYMEAAACAAACYwQAAAMIAAADCAACawQAAiMEAAIDBAACawQAAiMEAAIDBAACYwQAAAMAAAACAAADgwQAAgMAAAACAAADgwQAAwMAAAACAAADgwQAAAMEAAACAAADgwQAAIMEAAACAAADgwQAAYMEAAACAAADgwQAAQMEAAACAAADgwQAAgMEAAACAAADgwQAAYMEAAACAAACAPwAAAMIAAADCAADwwQAAAMIAAADCAACAPwAAAMAAAACAAADwwQAAAMAAAACAAACAPwAAgMAAAACAAADwwQAAgMAAAACAAACAPwAAwMAAAACAAADwwQAAwMAAAACAAACAPwAAAMEAAACAAADwwQAAAMEAAACAAACAPwAAIMEAAACAAADwwQAAIMEAAACAAACAPwAAQMEAAACAAADwwQAAQMEAAACAAACAPwAAYMEAAACAAADwwQAAYMEAAACAAACAPwAAgMEAAACAAADwwQAAgMEAAACAAACAPwAAkMEAAACAAADwwQAAkMEAAACAAACAPwAAoMEAAACAAADwwQAAoMEAAACAAACAPwAAsMEAAACAAADwwQAAsMEAAACAAACAPwAAwMEAAACAAADwwQAAwMEAAACAAACAPwAA0MEAAACAAADwwQAA0MEAAACAAACAPwAA4MEAAACAAADwwQAA4MEAAACAAACAPwAA8MEAAACAAADwwQAA8MEAAACAAACAPwAAAMIAAACAAADwwQAAAMIAAACAAACAPwAACMIAAACAAADwwQAACMIAAACA"},{"byteLength":360,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAIAAAIDBAABAwAAAAIAAAIDBAAAgwAAAAL8AAIDBAABAwAAAAIAAAIDBAAAgwAAAAD8AAIDBAABAwAAAAIAAAIDBAACOwQAAgL8AAIA/AACOwQAAgL8AAATCAACOwQAAgL8AAIA/AACOwQAADMIAAIA/AACOwQAAgL8AAIA/AACqwQAAgL8AAIA/AACOwQAAgL8AAATCAACOwQAADMIAAATCAACOwQAAgL8AAATCAACqwQAAgL8AAATCAACOwQAADMIAAIA/AACOwQAADMIAAATCAACOwQAADMIAAIA/AACqwQAADMIAAIA/AACOwQAADMIAAATCAACqwQAADMIAAATCAACqwQAAgL8AAIA/AACqwQAAgL8AAATCAACqwQAAgL8AAIA/AACqwQAADMIAAIA/AACqwQAAgL8AAATCAACqwQAADMIAAATCAACqwQAADMIAAIA/AACqwQAADMIAAATC"},{"byteLength":1152,"name":"buf_blue_scattered_lines","uri":"data:application/octet-stream;base64,AABAv83MzL8AAAA/AABAv5qZmb8AAAA/AABAv83MzL8AAALCAABAv5qZmb8AAALCAABAv5qZmb8AAAA/AABAv5qZmb8AAALCAABAv5qZmb8AAAA/AABQwJqZmb8AAAA/AABAv5qZmb8AAALCAABQwJqZmb8AAALCAABQwM3MzL8AAAA/AABQwJqZmb8AAAA/AABQwM3MzL8AAALCAABQwJqZmb8AAALCAABQwJqZmb8AAAA/AABQwJqZmb8AAALCAABwwM3MzL8AAAA/AABwwJqZmb8AAAA/AABwwM3MzL8AAALCAABwwJqZmb8AAALCAABwwJqZmb8AAAA/AABwwJqZmb8AAALCAABwwJqZmb8AAAA/AADIwJqZmb8AAAA/AABwwJqZmb8AAALCAADIwJqZmb8AAALCAADIwM3MzL8AAAA/AADIwJqZmb8AAAA/AADIwM3MzL8AAALCAADIwJqZmb8AAALCAADIwJqZmb8AAAA/AADIwJqZmb8AAALCAADYwM3MzL8AAAA/AADYwJqZmb8AAAA/AADYwM3MzL8AAALCAADYwJqZmb8AAALCAADYwJqZmb8AAAA/AADYwJqZmb8AAALCAADYwJqZmb8AAAA/AAAUwZqZmb8AAAA/AADYwJqZmb8AAALCAAAUwZqZmb8AAALCAAAUwc3MzL8AAAA/AAAUwZqZmb8AAAA/AAAUwc3MzL8AAALCAAAUwZqZmb8AAALCAAAUwZqZmb8AAAA/AAAUwZqZmb8AAALCAAAcwc3MzL8AAAA/AAAcwZqZmb8AAAA/AAAcwc3MzL8AAALCAAAcwZqZmb8AAALCAAAcwZqZmb8AAAA/AAAcwZqZmb8AAALCAAAcwZqZmb8AAAA/AABkwZqZmb8AAAA/AAAcwZqZmb8AAALCAABkwZqZmb8AAALCAABkwc3MzL8AAAA/AABkwZqZmb8AAAA/AABkwc3MzL8AAALCAABkwZqZmb8AAALCAABkwZqZmb8AAAA/AABkwZqZmb8AAALCAACOwc3MzL8AAAA/AACOwZqZmb8AAAA/AACOwc3MzL8AAALCAACOwZqZmb8AAALCAACOwZqZmb8AAAA/AACOwZqZmb8AAALCAACOwZqZmb8AAAA/AACiwZqZmb8AAAA/AACOwZqZmb8AAALCAACiwZqZmb8AAALCAACiwc3MzL8AAAA/AACiwZqZmb8AAAA/AACiwc3MzL8AAALCAACiwZqZmb8AAALCAACiwZqZmb8AAAA/AACiwZqZmb8AAALCAAC2wc3MzL8AAAA/AAC2wZqZmb8AAAA/AAC2wc3MzL8AAALCAAC2wZqZmb8AAALCAAC2wZqZmb8AAAA/AAC2wZqZmb8AAALCAAC2wZqZmb8AAAA/AADawZqZmb8AAAA/AAC2wZqZmb8AAALCAADawZqZmb8AAALCAADawc3MzL8AAAA/AADawZqZmb8AAAA/AADawc3MzL8AAALCAADawZqZmb8AAALCAADawZqZmb8AAAA/AADawZqZmb8AAALC"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0.5,0.5,0.5,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,1,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":4},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":5},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":6},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":7},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":8},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":9},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":10},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":11},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":12},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":13},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":14},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":15},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":16},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":17},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":18},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":19},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":20},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":21},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":22},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":23},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":24},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":25},"material":1,"mode":6},{"attributes":{"POSITION":26},"material":2,"mode":1}]},{"primitives":[{"attributes":{"POSITION":27},"material":3,"mode":6},{"attributes":{"POSITION":27},"material":4,"mode":3},{"attributes":{"POSITION":28},"material":4,"mode":1}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":29},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":30},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":31},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":32},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":33},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":34},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":35},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":36},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":37},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":38},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":39},"material":5,"mode":6},{"attributes":{"POSITION":39},"material":6,"mode":3},{"attributes":{"POSITION":40},"material":6,"mode":1}]},{"primitives":[{"attributes":{"POSITION":41},"material":7,"mode":2},{"attributes":{"POSITION":41},"material":8,"mode":4}]},{"primitives":[{"attributes":{"POSITION":42},"material":9,"mode":6}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":43},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":44},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":45},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":46},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":47},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":48},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":49},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":50},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":51},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":52},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":53},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":54},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":55},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":56},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":57},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":58},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":59},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":60},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":61},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":62},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":63},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":64},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":65},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":66},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":67},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":68},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":69},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":70},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":71},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":72},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":73},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":74},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":75},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":76},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":77},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":78},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":79},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":80},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":81},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":82},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":83},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":84},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":85},"material":10,"mode":1}]},{"primitives":[{"attributes":{"POSITION":86},"material":11,"mode":1}]},{"primitives":[{"attributes":{"POSITION":87},"material":12,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-32,-32]},{"mesh":1,"translation":[-0,-2,-0]},{"mesh":2,"translation":[-0,-4,-0]},{"mesh":3,"translation":[-0,-6,-0]},{"mesh":4,"translation":[-1,-32,-32]},{"mesh":5,"translation":[-1,-2,-0]},{"mesh":6,"translation":[-1,-4,-0]},{"mesh":7,"translation":[-1,-6,-0]},{"mesh":8,"translation":[-1,-8,-0]},{"mesh":9,"translation":[-1,-10,-0]},{"mesh":10,"translation":[-1,-12,-0]},{"mesh":11,"translation":[-1,-14,-0]},{"mesh":12,"translation":[-2,-32,-32]},{"mesh":13,"translation":[-2,-2,-0]},{"mesh":14,"translation":[-2,-4,-0]},{"mesh":15,"translation":[-2,-6,-0]},{"mesh":16,"translation":[-2,-8,-0]},{"mesh":17,"translation":[-2,-10,-0]},{"mesh":18,"translation":[-3,-32,-32]},{"mesh":19,"translation":[-3,-2,-0]},{"mesh":20,"translation":[-3,-4,-0]},{"mesh":21,"translation":[-3,-6,-0]},{"mesh":22,"translation":[-3,-8,-0]},{"mesh":23,"translation":[-3,-10,-0]},{"mesh":24,"translation":[-4,-32,-32]},{"mesh":25,"translation":[-4,-2,-0]},{"mesh":26,"translation":[-4,-4,-0]},{"mesh":26,"translation":[-4,-6,-0]},{"mesh":27,"translation":[-4,-8,-0]},{"mesh":27,"translation":[-4,-10,-0]},{"mesh":28,"translation":[-4,-12,-0]},{"mesh":28,"translation":[-4,-14,-0]},{"mesh":25,"translation":[-4,-16,-0]},{"mesh":24,"translation":[-4,-18,-0]},{"mesh":24,"translation":[-4,-20,-0]},{"mesh":24,"translation":[-4,-22,-0]},{"mesh":29,"translation":[-5,-32,-32]},{"mesh":29,"translation":[-5,-2,-0]},{"mesh":30,"translation":[-5,-4,-0]},{"mesh":30,"translation":[-5,-6,-0]},{"mesh":31,"translation":[-5,-8,-0]},{"mesh":31,"translation":[-5,-10,-0]},{"mesh":32,"translation":[-5,-12,-0]},{"mesh":32,"translation":[-5,-14,-0]},{"mesh":33,"translation":[-5,-16,-0]},{"mesh":33,"translation":[-5,-18,-0]},{"mesh":34,"translation":[-5,-20,-0]},{"mesh":34,"translation":[-5,-22,-0]},{"mesh":35,"translation":[-5,-24,-0]},{"mesh":35,"translation":[-5,-26,-0]},{"mesh":36,"translation":[-6,-32,-32]},{"mesh":36,"translation":[-6,-2,-0]},{"mesh":36,"translation":[-6,-4,-0]},{"mesh":37,"translation":[-6,-6,-0]},{"mesh":36,"translation":[-6,-8,-0]},{"mesh":38,"translation":[-6,-10,-0]},{"mesh":37,"translation":[-6,-12,-0]},{"mesh":36,"translation":[-6,-14,-0]},{"mesh":37,"translation":[-6,-16,-0]},{"mesh":37,"translation":[-6,-18,-0]},{"mesh":37,"translation":[-6,-20,-0]},{"mesh":38,"translation":[-6,-22,-0]},{"mesh":38,"translation":[-6,-24,-0]},{"mesh":36,"translation":[-6,-26,-0]},{"mesh":38,"translation":[-6,-28,-0]},{"mesh":37,"translation":[-6,-30,-0]},{"mesh":38,"translation":[-6,-32,-0]},{"mesh":38,"translation":[-6,-34,-0]},{"mesh":39,"translation":[-7,-2,-0]},{"mesh":40,"translation":[-7,-4,-0]},{"mesh":41,"translation":[-7,-6,-0]},{"mesh":42,"translation":[-7,-8,-0]},{"mesh":43,"translation":[-7,-14,-0]},{"mesh":44,"translation":[-7,-12,-0]},{"mesh":45,"translation":[-7,-32,-32]},{"mesh":46,"translation":[-8,-2,-0]},{"mesh":46,"translation":[-8,-4,-0]},{"mesh":47,"translation":[-8,-6,-0]},{"mesh":48,"translation":[-8,-8,-0]},{"mesh":48,"translation":[-8,-10,-0]},{"mesh":49,"translation":[-8,-32,-32]},{"mesh":50,"translation":[-9,-2,-0]},{"mesh":51,"translation":[-9,-4,-0]},{"mesh":52,"translation":[-9,-6,-0]},{"mesh":53,"translation":[-9,-12,-0]},{"mesh":54,"translation":[-9,-14,-0]},{"mesh":55,"translation":[-9,-16,-0]},{"mesh":55,"translation":[-9,-18,-0]},{"mesh":56,"translation":[-10,-32,-32]},{"mesh":57,"translation":[-10,-2,-0]},{"mesh":58,"translation":[-10,-4,-0]},{"mesh":58,"translation":[-11,-32,-32]},{"mesh":58,"translation":[-11,-2,-0]},{"mesh":59,"translation":[-12,-32,-32]},{"mesh":60,"translation":[-12,-2,-0]},{"mesh":61,"translation":[-12,-4,-0]},{"mesh":59,"translation":[-12,-6,-0]},{"mesh":62,"translation":[-13,-32,-32]},{"mesh":63,"translation":[-13,-2,-0]},{"mesh":64,"translation":[-13,-4,-0]},{"mesh":62,"translation":[-14,-4,-0]},{"mesh":65,"translation":[-15,-32,-32]},{"mesh":66,"translation":[-15,-2,-0]},{"mesh":67,"translation":[-15,-4,-0]},{"mesh":68,"translation":[-15,-6,-0]},{"mesh":69,"translation":[-15,-8,-0]},{"mesh":70,"translation":[-15,-10,-0]},{"mesh":70,"translation":[-15,-12,-0]},{"mesh":71,"translation":[-15,-14,-0]},{"mesh":72,"translation":[-15,-16,-0]},{"mesh":73,"translation":[-15,-18,-0]},{"mesh":74,"translation":[-16,-32,-32]},{"mesh":74,"translation":[-16,-2,-0]},{"mesh":74,"translation":[-16,-4,-0]},{"mesh":74,"translation":[-16,-6,-0]},{"mesh":75,"translation":[-16,-8,-0]},{"mesh":75,"translation":[-16,-10,-0]},{"mesh":76,"translation":[-16,-12,-0]},{"mesh":76,"translation":[-16,-14,-0]},{"mesh":13,"translation":[-18,-32,-32]},{"mesh":38,"translation":[-19,-32,-32]},{"mesh":36,"translation":[-19,-2,-0]},{"mesh":22,"translation":[-20,-2,-0]},{"mesh":67,"translation":[-23,-32,-32]},{"mesh":49,"translation":[-24,-32,-32]},{"mesh":67,"translation":[-25,-32,-32]},{"mesh":77,"translation":[-26,-32,-32]},{"mesh":77,"translation":[-26,-2,-0]},{"mesh":77,"translation":[-27,-32,-32]},{"mesh":65,"translation":[-28,-32,-32]},{"mesh":69,"translation":[-28,-2,-0]},{"mesh":76,"translation":[-28,-4,-0]},{"mesh":76,"translation":[-28,-6,-0]},{"mesh":75,"translation":[-28,-8,-0]},{"mesh":75,"translation":[-28,-10,-0]},{"mesh":56,"translation":[-28,-12,-0]},{"mesh":57,"translation":[-28,-14,-0]},{"mesh":58,"translation":[-28,-16,-0]},{"mesh":78,"translation":[-29,-32,-32]},{"mesh":79,"translation":[-29,-2,-0]},{"mesh":80,"translation":[-29,-4,-0]},{"mesh":81,"translation":[0,0,0]},{"mesh":82,"translation":[0,0,0]},{"mesh":83,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/classical_feedback.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5625],"min":[0.25,0.5],"name":"tex_coords_gate_M","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.875,0.4375],"min":[0.8125,0.375],"name":"tex_coords_gate_X:REC","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.9375,0.5],"min":[0.875,0.4375],"name":"tex_coords_gate_Y:SWEEP","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":6,"max":[1,-0,-0],"min":[-1,-4,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":6,"max":[0,2.5,-0],"min":[-3,1.5,-0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_M","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X:REC","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Y:SWEEP","target":34962},{"buffer":4,"byteLength":72,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":5,"byteLength":72,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_M","uri":"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_X:REC","uri":"data:application/octet-stream;base64,AABgPwAAwD4AAFA/AADAPgAAYD8AAOA+AABQPwAAwD4AAFA/AADgPgAAYD8AAOA+AABgPwAA4D4AAGA/AADAPgAAUD8AAOA+AABQPwAA4D4AAGA/AADAPgAAUD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_Y:SWEEP","uri":"data:application/octet-stream;base64,AABwPwAA4D4AAGA/AADgPgAAcD8AAAA/AABgPwAA4D4AAGA/AAAAPwAAcD8AAAA/AABwPwAAAD8AAHA/AADgPgAAYD8AAAA/AABgPwAAAD8AAHA/AADgPgAAYD8AAOA+"},{"byteLength":72,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAPwAAAIAAAACAAACAvwAAAIAAAACAAACAPwAAAMAAAACAAACAvwAAAMAAAACAAACAPwAAgMAAAACAAACAvwAAgMAAAACA"},{"byteLength":72,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACA"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":5},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":1,"translation":[-0,-2,-0]},{"mesh":2,"translation":[-0,-4,-0]},{"mesh":3,"translation":[0,0,0]},{"mesh":4,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/collapsing.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5625],"min":[0.3125,0.5],"name":"tex_coords_gate_R","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.4375],"min":[0.3125,0.375],"name":"tex_coords_gate_RX","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5],"min":[0.3125,0.4375],"name":"tex_coords_gate_RY","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5625],"min":[0.25,0.5],"name":"tex_coords_gate_M","type":"VEC2"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.5625],"min":[0.375,0.5],"name":"tex_coords_gate_MR","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.4375],"min":[0.375,0.375],"name":"tex_coords_gate_MRX","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.5],"min":[0.375,0.4375],"name":"tex_coords_gate_MRY","type":"VEC2"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.4375],"min":[0.25,0.375],"name":"tex_coords_gate_MX","type":"VEC2"},{"bufferView":9,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5],"min":[0.25,0.4375],"name":"tex_coords_gate_MY","type":"VEC2"},{"bufferView":10,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.4375],"min":[0.625,0.375],"name":"tex_coords_gate_MPP:X","type":"VEC2"},{"bufferView":11,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.5],"min":[0.625,0.4375],"name":"tex_coords_gate_MPP:Y","type":"VEC2"},{"bufferView":12,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.5625],"min":[0.625,0.5],"name":"tex_coords_gate_MPP:Z","type":"VEC2"},{"bufferView":13,"byteOffset":0,"componentType":5126,"count":14,"max":[1,-0,-0],"min":[-8,-6,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":14,"byteOffset":0,"componentType":5126,"count":6,"max":[0,2.5,-0],"min":[-3,1.5,-0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_R","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_RX","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_RY","target":34962},{"buffer":4,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_M","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MR","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MRX","target":34962},{"buffer":7,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MRY","target":34962},{"buffer":8,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MX","target":34962},{"buffer":9,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MY","target":34962},{"buffer":10,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:X","target":34962},{"buffer":11,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:Y","target":34962},{"buffer":12,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:Z","target":34962},{"buffer":13,"byteLength":168,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":14,"byteLength":72,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_R","uri":"data:application/octet-stream;base64,AADAPgAAAD8AAKA+AAAAPwAAwD4AABA/AACgPgAAAD8AAKA+AAAQPwAAwD4AABA/AADAPgAAED8AAMA+AAAAPwAAoD4AABA/AACgPgAAED8AAMA+AAAAPwAAoD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_RX","uri":"data:application/octet-stream;base64,AADAPgAAwD4AAKA+AADAPgAAwD4AAOA+AACgPgAAwD4AAKA+AADgPgAAwD4AAOA+AADAPgAA4D4AAMA+AADAPgAAoD4AAOA+AACgPgAA4D4AAMA+AADAPgAAoD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_RY","uri":"data:application/octet-stream;base64,AADAPgAA4D4AAKA+AADgPgAAwD4AAAA/AACgPgAA4D4AAKA+AAAAPwAAwD4AAAA/AADAPgAAAD8AAMA+AADgPgAAoD4AAAA/AACgPgAAAD8AAMA+AADgPgAAoD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_M","uri":"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MR","uri":"data:application/octet-stream;base64,AADgPgAAAD8AAMA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+AAAQPwAA4D4AABA/AADgPgAAED8AAOA+AAAAPwAAwD4AABA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MRX","uri":"data:application/octet-stream;base64,AADgPgAAwD4AAMA+AADAPgAA4D4AAOA+AADAPgAAwD4AAMA+AADgPgAA4D4AAOA+AADgPgAA4D4AAOA+AADAPgAAwD4AAOA+AADAPgAA4D4AAOA+AADAPgAAwD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MRY","uri":"data:application/octet-stream;base64,AADgPgAA4D4AAMA+AADgPgAA4D4AAAA/AADAPgAA4D4AAMA+AAAAPwAA4D4AAAA/AADgPgAAAD8AAOA+AADgPgAAwD4AAAA/AADAPgAAAD8AAOA+AADgPgAAwD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_MX","uri":"data:application/octet-stream;base64,AACgPgAAwD4AAIA+AADAPgAAoD4AAOA+AACAPgAAwD4AAIA+AADgPgAAoD4AAOA+AACgPgAA4D4AAKA+AADAPgAAgD4AAOA+AACAPgAA4D4AAKA+AADAPgAAgD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MY","uri":"data:application/octet-stream;base64,AACgPgAA4D4AAIA+AADgPgAAoD4AAAA/AACAPgAA4D4AAIA+AAAAPwAAoD4AAAA/AACgPgAAAD8AAKA+AADgPgAAgD4AAAA/AACAPgAAAD8AAKA+AADgPgAAgD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:X","uri":"data:application/octet-stream;base64,AAAwPwAAwD4AACA/AADAPgAAMD8AAOA+AAAgPwAAwD4AACA/AADgPgAAMD8AAOA+AAAwPwAA4D4AADA/AADAPgAAID8AAOA+AAAgPwAA4D4AADA/AADAPgAAID8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:Y","uri":"data:application/octet-stream;base64,AAAwPwAA4D4AACA/AADgPgAAMD8AAAA/AAAgPwAA4D4AACA/AAAAPwAAMD8AAAA/AAAwPwAAAD8AADA/AADgPgAAID8AAAA/AAAgPwAAAD8AADA/AADgPgAAID8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:Z","uri":"data:application/octet-stream;base64,AAAwPwAAAD8AACA/AAAAPwAAMD8AABA/AAAgPwAAAD8AACA/AAAQPwAAMD8AABA/AAAwPwAAED8AADA/AAAAPwAAID8AABA/AAAgPwAAED8AADA/AAAAPwAAID8AAAA/"},{"byteLength":168,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AADAwAAAgMAAAACAAADIwAAAAMAAAACAAADIwAAAAMAAAACAAADAwAAAAIAAAACAAADgwAAAwMAAAACAAADgwAAAgMAAAACAAACAPwAAAIAAAACAAAAAwQAAAIAAAACAAACAPwAAAMAAAACAAAAAwQAAAMAAAACAAACAPwAAgMAAAACAAAAAwQAAgMAAAACAAACAPwAAwMAAAACAAAAAwQAAwMAAAACA"},{"byteLength":72,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACA"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":4},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":5},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":6},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":7},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":8},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":9},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":10},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":11},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":12},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":13},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":14},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":1,"translation":[-0,-2,-0]},{"mesh":2,"translation":[-0,-4,-0]},{"mesh":0,"translation":[-0,-6,-0]},{"mesh":3,"translation":[-1,-0,-0]},{"mesh":3,"translation":[-1,-2,-0]},{"mesh":4,"translation":[-2,-2,-0]},{"mesh":4,"translation":[-2,-0,-0]},{"mesh":5,"translation":[-3,-2,-0]},{"mesh":5,"translation":[-3,-4,-0]},{"mesh":6,"translation":[-3,-0,-0]},{"mesh":6,"translation":[-3,-6,-0]},{"mesh":6,"translation":[-4,-2,-0]},{"mesh":4,"translation":[-4,-0,-0]},{"mesh":7,"translation":[-5,-2,-0]},{"mesh":8,"translation":[-5,-4,-0]},{"mesh":3,"translation":[-5,-6,-0]},{"mesh":9,"translation":[-6,-0,-0]},{"mesh":10,"translation":[-6,-4,-0]},{"mesh":11,"translation":[-6,-6,-0]},{"mesh":9,"translation":[-6,-2,-0]},{"mesh":11,"translation":[-7,-4,-0]},{"mesh":10,"translation":[-7,-6,-0]},{"mesh":12,"translation":[0,0,0]},{"mesh":13,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/detector_pseudo_targets.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5625],"min":[0.25,0.5],"name":"tex_coords_gate_M","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[1,-0,-0],"min":[-3,-10,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":30,"max":[0,2.5,1],"min":[-3,-11,-1],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_M","target":34962},{"buffer":2,"byteLength":144,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":3,"byteLength":360,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_M","uri":"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/"},{"byteLength":144,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAPwAAAIAAAACAAABAwAAAAIAAAACAAACAPwAAAMAAAACAAABAwAAAAMAAAACAAACAPwAAgMAAAACAAABAwAAAgMAAAACAAACAPwAAwMAAAACAAABAwAAAwMAAAACAAACAPwAAAMEAAACAAABAwAAAAMEAAACAAACAPwAAIMEAAACAAABAwAAAIMEAAACA"},{"byteLength":360,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACAAABAvwAAgD8AAIA/AABAvwAAgD8AAIC/AABAvwAAgD8AAIA/AABAvwAAMMEAAIA/AABAvwAAgD8AAIA/AACgvwAAgD8AAIA/AABAvwAAgD8AAIC/AABAvwAAMMEAAIC/AABAvwAAgD8AAIC/AACgvwAAgD8AAIC/AABAvwAAMMEAAIA/AABAvwAAMMEAAIC/AABAvwAAMMEAAIA/AACgvwAAMMEAAIA/AABAvwAAMMEAAIC/AACgvwAAMMEAAIC/AACgvwAAgD8AAIA/AACgvwAAgD8AAIC/AACgvwAAgD8AAIA/AACgvwAAMMEAAIA/AACgvwAAgD8AAIC/AACgvwAAMMEAAIC/AACgvwAAMMEAAIA/AACgvwAAMMEAAIC/"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":2},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":3},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":0,"translation":[-0,-2,-0]},{"mesh":0,"translation":[-0,-4,-0]},{"mesh":0,"translation":[-0,-6,-0]},{"mesh":0,"translation":[-0,-8,-0]},{"mesh":0,"translation":[-0,-10,-0]},{"mesh":0,"translation":[-1,-2,-0]},{"mesh":0,"translation":[-1,-4,-0]},{"mesh":1,"translation":[0,0,0]},{"mesh":2,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/empty_match_graph.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":32,"max":[8,2,0],"min":[0,-1,0],"name":"buf_blue_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":384,"byteOffset":0,"name":"buf_blue_scattered_lines","target":34962}],"buffers":[{"byteLength":384,"name":"buf_blue_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAACAPwAAAEAAAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAQAAAgD8AAAAAAABAQAAAgD8AAAAAAAAAQAAAAAAAAAAAAAAAQAAAgD8AAAAAAAAgQAAAAAAAAAAAAAAgQAAAgD8AAAAAAABAQAAAAAAAAAAAAABAQAAAgD8AAAAAAACAQAAAgD8AAAAAAACAQAAAgL8AAAAAAACAQAAAgD8AAAAAAACgQAAAgD8AAAAAAACgQAAAgD8AAAAAAACgQAAAAAAAAAAAAACAQAAAAAAAAAAAAACgQAAAAAAAAAAAAADAQAAAAAAAAAAAAADAQAAAAEAAAAAAAACwQAAAwD8AAAAAAADQQAAAwD8AAAAAAADgQAAAgL8AAAAAAAAAQQAAgD8AAAAAAADgQAAAgD8AAAAAAADwQAAAAAAAAAAA"}],"materials":[{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0},"material":0,"mode":1}]}],"nodes":[{"mesh":0,"translation":[0,0,0]}],"scene":0,"scenes":[{"nodes":[0]}]} ================================================ FILE: testdata/lattice_surgery_cnot.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5625],"min":[0.3125,0.5],"name":"tex_coords_gate_R","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.4375],"min":[0.625,0.375],"name":"tex_coords_gate_MPP:X","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.5625],"min":[0.625,0.5],"name":"tex_coords_gate_MPP:Z","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.4375],"min":[0.25,0.375],"name":"tex_coords_gate_MX","type":"VEC2"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.875,0.5625],"min":[0.8125,0.5],"name":"tex_coords_gate_Z:REC","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.875,0.4375],"min":[0.8125,0.375],"name":"tex_coords_gate_X:REC","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":12,"max":[1,-0,-0],"min":[-5,-4,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":6,"max":[0,2.5,-0],"min":[-3,1.5,-0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_R","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:X","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:Z","target":34962},{"buffer":4,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MX","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Z:REC","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X:REC","target":34962},{"buffer":7,"byteLength":144,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":8,"byteLength":72,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_R","uri":"data:application/octet-stream;base64,AADAPgAAAD8AAKA+AAAAPwAAwD4AABA/AACgPgAAAD8AAKA+AAAQPwAAwD4AABA/AADAPgAAED8AAMA+AAAAPwAAoD4AABA/AACgPgAAED8AAMA+AAAAPwAAoD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MPP:X","uri":"data:application/octet-stream;base64,AAAwPwAAwD4AACA/AADAPgAAMD8AAOA+AAAgPwAAwD4AACA/AADgPgAAMD8AAOA+AAAwPwAA4D4AADA/AADAPgAAID8AAOA+AAAgPwAA4D4AADA/AADAPgAAID8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:Z","uri":"data:application/octet-stream;base64,AAAwPwAAAD8AACA/AAAAPwAAMD8AABA/AAAgPwAAAD8AACA/AAAQPwAAMD8AABA/AAAwPwAAED8AADA/AAAAPwAAID8AABA/AAAgPwAAED8AADA/AAAAPwAAID8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MX","uri":"data:application/octet-stream;base64,AACgPgAAwD4AAIA+AADAPgAAoD4AAOA+AACAPgAAwD4AAIA+AADgPgAAoD4AAOA+AACgPgAA4D4AAKA+AADAPgAAgD4AAOA+AACAPgAA4D4AAKA+AADAPgAAgD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_Z:REC","uri":"data:application/octet-stream;base64,AABgPwAAAD8AAFA/AAAAPwAAYD8AABA/AABQPwAAAD8AAFA/AAAQPwAAYD8AABA/AABgPwAAED8AAGA/AAAAPwAAUD8AABA/AABQPwAAED8AAGA/AAAAPwAAUD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_X:REC","uri":"data:application/octet-stream;base64,AABgPwAAwD4AAFA/AADAPgAAYD8AAOA+AABQPwAAwD4AAFA/AADgPgAAYD8AAOA+AABgPwAA4D4AAGA/AADAPgAAUD8AAOA+AABQPwAA4D4AAGA/AADAPgAAUD8AAMA+"},{"byteLength":144,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAvwAAgMAAAACAAACAvwAAAMAAAACAAAAAwAAAgMAAAACAAAAQwAAAAMAAAACAAAAQwAAAAMAAAACAAAAAwAAAAIAAAACAAACAPwAAAIAAAACAAACgwAAAAIAAAACAAACAPwAAAMAAAACAAACgwAAAAMAAAACAAACAPwAAgMAAAACAAACgwAAAgMAAAACA"},{"byteLength":72,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACA"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":4},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":5},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":6},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":7},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":8},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-4,-0]},{"mesh":1,"translation":[-1,-2,-0]},{"mesh":1,"translation":[-1,-4,-0]},{"mesh":2,"translation":[-2,-0,-0]},{"mesh":2,"translation":[-2,-4,-0]},{"mesh":3,"translation":[-3,-4,-0]},{"mesh":4,"translation":[-3,-0,-0]},{"mesh":5,"translation":[-3,-2,-0]},{"mesh":4,"translation":[-4,-0,-0]},{"mesh":6,"translation":[0,0,0]},{"mesh":7,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/match_graph_no_coords.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":9,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0,0.400000005960464],"min":[-0.400000005960464,0,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0.400000005960464,0],"min":[-0.400000005960464,-0.400000005960464,0],"name":"circle_loop","type":"VEC3"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":9,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0,0.400000005960464],"min":[-0.400000005960464,0,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0.400000005960464,0],"min":[-0.400000005960464,-0.400000005960464,0],"name":"circle_loop","type":"VEC3"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":18,"max":[9,14.9442720413208,-1],"min":[-1.4721360206604,0,-1],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":2,"max":[0,0,-1],"min":[-8.32050228118896,-5.54700183868408,-1],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":1,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":2,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":3,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":4,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":5,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":6,"byteLength":216,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":7,"byteLength":24,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAMPQkD7D0JA+AAAAAPIwlrLNzMw+AAAAAMPQkL7D0JA+AAAAAM3MzL7yMBazAAAAAMHQkL7E0JC+AAAAAPLkozHNzMy+AAAAAMbQkD6/0JC+AAAAAM3MzD4AAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAAAAAADNzMw+w9CQPgAAAADD0JA+zczMPgAAAADyMJayw9CQPgAAAADD0JC+8jAWswAAAADNzMy+xNCQvgAAAADB0JC+zczMvgAAAADy5KMxv9CQvgAAAADG0JA+AAAAAAAAAADNzMw+"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,zczMPgAAAAAAAAAAw9CQPsPQkD4AAAAA8jCWss3MzD4AAAAAw9CQvsPQkD4AAAAAzczMvvIwFrMAAAAAwdCQvsTQkL4AAAAA8uSjMc3MzL4AAAAAxtCQPr/QkL4AAAAAzczMPgAAAAAAAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAMPQkD7D0JA+AAAAAPIwlrLNzMw+AAAAAMPQkL7D0JA+AAAAAM3MzL7yMBazAAAAAMHQkL7E0JC+AAAAAPLkozHNzMy+AAAAAMbQkD6/0JC+AAAAAM3MzD4AAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAAAAAADNzMw+w9CQPgAAAADD0JA+zczMPgAAAADyMJayw9CQPgAAAADD0JC+8jAWswAAAADNzMy+xNCQvgAAAADB0JC+zczMvgAAAADy5KMxv9CQvgAAAADG0JA+AAAAAAAAAADNzMw+"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,zczMPgAAAAAAAAAAw9CQPsPQkD4AAAAA8jCWss3MzD4AAAAAw9CQvsPQkD4AAAAAzczMvvIwFrMAAAAAwdCQvsTQkL4AAAAA8uSjMc3MzL4AAAAAxtCQPr/QkL4AAAAAzczMPgAAAAAAAAAA"},{"byteLength":216,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAAAAAIC/AABAQAAAAAAAAIC/AABAQAAAAAAAAIC/AAAAAAAAQEAAAIC/AAAAAAAAQEAAAIC/AAAAAAAAwEAAAIC/AAAAAAAAwEAAAIC/AABAQAAAQEAAAIC/AABAQAAAQEAAAIC/AADAQAAAAAAAAIC/AADAQAAAAAAAAIC/AAAQQQAAAAAAAIC/AAAQQQAAAAAAAIC/AADAQAAAQEAAAIC/AADAQAAAQEAAAIC/AABAQAAAwEAAAIC/AABAQAAAwEAAAIC/9G68v70bb0EAAIC/"},{"byteLength":24,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAAAAAIC/xyAFwQqBscAAAIC/"}],"materials":[{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0.5,0.5,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0},"material":0,"mode":6},{"attributes":{"POSITION":1},"material":0,"mode":6},{"attributes":{"POSITION":2},"material":0,"mode":6}]},{"primitives":[{"attributes":{"POSITION":3},"material":1,"mode":6},{"attributes":{"POSITION":4},"material":1,"mode":6},{"attributes":{"POSITION":5},"material":1,"mode":6}]},{"primitives":[{"attributes":{"POSITION":6},"material":2,"mode":1}]},{"primitives":[{"attributes":{"POSITION":7},"material":3,"mode":1}]}],"nodes":[{"mesh":0,"translation":[0,0,-1]},{"mesh":1,"translation":[3,0,-1]},{"mesh":1,"translation":[0,3,-1]},{"mesh":1,"translation":[0,6,-1]},{"mesh":1,"translation":[3,3,-1]},{"mesh":1,"translation":[6,0,-1]},{"mesh":1,"translation":[9,0,-1]},{"mesh":1,"translation":[6,3,-1]},{"mesh":1,"translation":[3,6,-1]},{"mesh":2,"translation":[0,0,0]},{"mesh":3,"translation":[0,0,0]}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10]}]} ================================================ FILE: testdata/match_graph_repetition_code.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":9,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0,0.400000005960464],"min":[-0.400000005960464,0,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0.400000005960464,0],"min":[-0.400000005960464,-0.400000005960464,0],"name":"circle_loop","type":"VEC3"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":9,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0,0.400000005960464],"min":[-0.400000005960464,0,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0.400000005960464,0],"min":[-0.400000005960464,-0.400000005960464,0],"name":"circle_loop","type":"VEC3"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":500,"max":[33,37.0710678100586,0],"min":[-7,-7.07106781005859,0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":80,"max":[43,37.0710678100586,0],"min":[33,-7.07106781005859,0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":1,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":2,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":3,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":4,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":5,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":6,"byteLength":6000,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":7,"byteLength":960,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAMPQkD7D0JA+AAAAAPIwlrLNzMw+AAAAAMPQkL7D0JA+AAAAAM3MzL7yMBazAAAAAMHQkL7E0JC+AAAAAPLkozHNzMy+AAAAAMbQkD6/0JC+AAAAAM3MzD4AAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAAAAAADNzMw+w9CQPgAAAADD0JA+zczMPgAAAADyMJayw9CQPgAAAADD0JC+8jAWswAAAADNzMy+xNCQvgAAAADB0JC+zczMvgAAAADy5KMxv9CQvgAAAADG0JA+AAAAAAAAAADNzMw+"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,zczMPgAAAAAAAAAAw9CQPsPQkD4AAAAA8jCWss3MzD4AAAAAw9CQvsPQkD4AAAAAzczMvvIwFrMAAAAAwdCQvsTQkL4AAAAA8uSjMc3MzL4AAAAAxtCQPr/QkL4AAAAAzczMPgAAAAAAAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAMPQkD7D0JA+AAAAAPIwlrLNzMw+AAAAAMPQkL7D0JA+AAAAAM3MzL7yMBazAAAAAMHQkL7E0JC+AAAAAPLkozHNzMy+AAAAAMbQkD6/0JC+AAAAAM3MzD4AAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAAAAAADNzMw+w9CQPgAAAADD0JA+zczMPgAAAADyMJayw9CQPgAAAADD0JC+8jAWswAAAADNzMy+xNCQvgAAAADB0JC+zczMvgAAAADy5KMxv9CQvgAAAADG0JA+AAAAAAAAAADNzMw+"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,zczMPgAAAAAAAAAAw9CQPsPQkD4AAAAA8jCWss3MzD4AAAAAw9CQvsPQkD4AAAAAzczMvvIwFrMAAAAAwdCQvsTQkL4AAAAA8uSjMc3MzL4AAAAAxtCQPr/QkL4AAAAAzczMPgAAAAAAAAAA"},{"byteLength":6000,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AABAQAAAAAAAAAAAMEaCwDBG4sAAAAAAAABAQAAAAAAAAAAAAAAQQQAAAAAAAAAAAABAQAAAAAAAAAAAAABAQAAAQEAAAAAAAABAQAAAAAAAAAAAAAAQQQAAQEAAAAAAAAAQQQAAAAAAAAAAAABwQQAAAAAAAAAAAAAQQQAAAAAAAAAAAAAQQQAAQEAAAAAAAAAQQQAAAAAAAAAAAABwQQAAQEAAAAAAAABwQQAAAAAAAAAAAACoQQAAAAAAAAAAAABwQQAAAAAAAAAAAABwQQAAQEAAAAAAAABwQQAAAAAAAAAAAACoQQAAQEAAAAAAAACoQQAAAAAAAAAAAADYQQAAAAAAAAAAAACoQQAAAAAAAAAAAACoQQAAQEAAAAAAAACoQQAAAAAAAAAAAADYQQAAQEAAAAAAAADYQQAAAAAAAAAAAAAEQgAAAAAAAAAAAADYQQAAAAAAAAAAAADYQQAAQEAAAAAAAADYQQAAAAAAAAAAAAAEQgAAQEAAAAAAAAAEQgAAAAAAAAAAAAAEQgAAQEAAAAAAAABAQAAAQEAAAAAAxeCZwAjOT8AAAAAAAABAQAAAQEAAAAAAAAAQQQAAQEAAAAAAAABAQAAAQEAAAAAAxeCZwAjOT8AAAAAAAABAQAAAAAAAAAAAMEaCwDBG4sAAAAAAAAAQQQAAQEAAAAAAAABwQQAAQEAAAAAAAABwQQAAQEAAAAAAAACoQQAAQEAAAAAAAACoQQAAQEAAAAAAAADYQQAAQEAAAAAAAADYQQAAQEAAAAAAAAAEQgAAQEAAAAAAAABAQAAAQEAAAAAAxeCZwAjOT8AAAAAAAABAQAAAQEAAAAAAAAAQQQAAQEAAAAAAAABAQAAAQEAAAAAAAABAQAAAwEAAAAAAAABAQAAAQEAAAAAAAAAQQQAAwEAAAAAAAAAQQQAAQEAAAAAAAABwQQAAQEAAAAAAAAAQQQAAQEAAAAAAAAAQQQAAwEAAAAAAAAAQQQAAQEAAAAAAAABwQQAAwEAAAAAAAABwQQAAQEAAAAAAAACoQQAAQEAAAAAAAABwQQAAQEAAAAAAAABwQQAAwEAAAAAAAABwQQAAQEAAAAAAAACoQQAAwEAAAAAAAACoQQAAQEAAAAAAAADYQQAAQEAAAAAAAACoQQAAQEAAAAAAAACoQQAAwEAAAAAAAACoQQAAQEAAAAAAAADYQQAAwEAAAAAAAADYQQAAQEAAAAAAAAAEQgAAQEAAAAAAAADYQQAAQEAAAAAAAADYQQAAwEAAAAAAAADYQQAAQEAAAAAAAAAEQgAAwEAAAAAAAAAEQgAAQEAAAAAAAAAEQgAAwEAAAAAAAABAQAAAwEAAAAAA1GWywAjkWj8AAAAAAABAQAAAwEAAAAAAAAAQQQAAwEAAAAAAAABAQAAAwEAAAAAA1GWywAjkWj8AAAAAAABAQAAAQEAAAAAAxeCZwAjOT8AAAAAAAAAQQQAAwEAAAAAAAABwQQAAwEAAAAAAAABwQQAAwEAAAAAAAACoQQAAwEAAAAAAAACoQQAAwEAAAAAAAADYQQAAwEAAAAAAAADYQQAAwEAAAAAAAAAEQgAAwEAAAAAAAABAQAAAwEAAAAAA1GWywAjkWj8AAAAAAABAQAAAwEAAAAAAAAAQQQAAwEAAAAAAAABAQAAAwEAAAAAAAABAQAAAEEEAAAAAAABAQAAAwEAAAAAAAAAQQQAAEEEAAAAAAAAQQQAAwEAAAAAAAABwQQAAwEAAAAAAAAAQQQAAwEAAAAAAAAAQQQAAEEEAAAAAAAAQQQAAwEAAAAAAAABwQQAAEEEAAAAAAABwQQAAwEAAAAAAAACoQQAAwEAAAAAAAABwQQAAwEAAAAAAAABwQQAAEEEAAAAAAABwQQAAwEAAAAAAAACoQQAAEEEAAAAAAACoQQAAwEAAAAAAAADYQQAAwEAAAAAAAACoQQAAwEAAAAAAAACoQQAAEEEAAAAAAACoQQAAwEAAAAAAAADYQQAAEEEAAAAAAADYQQAAwEAAAAAAAAAEQgAAwEAAAAAAAADYQQAAwEAAAAAAAADYQQAAEEEAAAAAAADYQQAAwEAAAAAAAAAEQgAAEEEAAAAAAAAEQgAAwEAAAAAAAAAEQgAAEEEAAAAAAABAQAAAEEEAAAAA0BzJwK0nqUAAAAAAAABAQAAAEEEAAAAAAAAQQQAAEEEAAAAAAABAQAAAEEEAAAAA0BzJwK0nqUAAAAAAAABAQAAAwEAAAAAA1GWywAjkWj8AAAAAAAAQQQAAEEEAAAAAAABwQQAAEEEAAAAAAABwQQAAEEEAAAAAAACoQQAAEEEAAAAAAACoQQAAEEEAAAAAAADYQQAAEEEAAAAAAADYQQAAEEEAAAAAAAAEQgAAEEEAAAAAAABAQAAAEEEAAAAA0BzJwK0nqUAAAAAAAABAQAAAEEEAAAAAAAAQQQAAEEEAAAAAAABAQAAAEEEAAAAAAABAQAAAQEEAAAAAAABAQAAAEEEAAAAAAAAQQQAAQEEAAAAAAAAQQQAAEEEAAAAAAABwQQAAEEEAAAAAAAAQQQAAEEEAAAAAAAAQQQAAQEEAAAAAAAAQQQAAEEEAAAAAAABwQQAAQEEAAAAAAABwQQAAEEEAAAAAAACoQQAAEEEAAAAAAABwQQAAEEEAAAAAAABwQQAAQEEAAAAAAABwQQAAEEEAAAAAAACoQQAAQEEAAAAAAACoQQAAEEEAAAAAAADYQQAAEEEAAAAAAACoQQAAEEEAAAAAAACoQQAAQEEAAAAAAACoQQAAEEEAAAAAAADYQQAAQEEAAAAAAADYQQAAEEEAAAAAAAAEQgAAEEEAAAAAAADYQQAAEEEAAAAAAADYQQAAQEEAAAAAAADYQQAAEEEAAAAAAAAEQgAAQEEAAAAAAAAEQgAAEEEAAAAAAAAEQgAAQEEAAAAAAABAQAAAQEEAAAAALMnZwBWfIEEAAAAAAABAQAAAQEEAAAAAAAAQQQAAQEEAAAAAAABAQAAAQEEAAAAALMnZwBWfIEEAAAAAAABAQAAAEEEAAAAA0BzJwK0nqUAAAAAAAAAQQQAAQEEAAAAAAABwQQAAQEEAAAAAAABwQQAAQEEAAAAAAACoQQAAQEEAAAAAAACoQQAAQEEAAAAAAADYQQAAQEEAAAAAAADYQQAAQEEAAAAAAAAEQgAAQEEAAAAAAABAQAAAQEEAAAAALMnZwBWfIEEAAAAAAABAQAAAQEEAAAAAAAAQQQAAQEEAAAAAAABAQAAAQEEAAAAAAABAQAAAcEEAAAAAAABAQAAAQEEAAAAAAAAQQQAAcEEAAAAAAAAQQQAAQEEAAAAAAABwQQAAQEEAAAAAAAAQQQAAQEEAAAAAAAAQQQAAcEEAAAAAAAAQQQAAQEEAAAAAAABwQQAAcEEAAAAAAABwQQAAQEEAAAAAAACoQQAAQEEAAAAAAABwQQAAQEEAAAAAAABwQQAAcEEAAAAAAABwQQAAQEEAAAAAAACoQQAAcEEAAAAAAACoQQAAQEEAAAAAAADYQQAAQEEAAAAAAACoQQAAQEEAAAAAAACoQQAAcEEAAAAAAACoQQAAQEEAAAAAAADYQQAAcEEAAAAAAADYQQAAQEEAAAAAAAAEQgAAQEEAAAAAAADYQQAAQEEAAAAAAADYQQAAcEEAAAAAAADYQQAAQEEAAAAAAAAEQgAAcEEAAAAAAAAEQgAAQEEAAAAAAAAEQgAAcEEAAAAAAABAQAAAcEEAAAAAAADgwAAAcEEAAAAAAABAQAAAcEEAAAAAAAAQQQAAcEEAAAAAAABAQAAAcEEAAAAAAADgwAAAcEEAAAAAAABAQAAAQEEAAAAALMnZwBWfIEEAAAAAAAAQQQAAcEEAAAAAAABwQQAAcEEAAAAAAABwQQAAcEEAAAAAAACoQQAAcEEAAAAAAACoQQAAcEEAAAAAAADYQQAAcEEAAAAAAADYQQAAcEEAAAAAAAAEQgAAcEEAAAAAAABAQAAAcEEAAAAAAADgwAAAcEEAAAAAAABAQAAAcEEAAAAAAAAQQQAAcEEAAAAAAABAQAAAcEEAAAAAAABAQAAAkEEAAAAAAABAQAAAcEEAAAAAAAAQQQAAkEEAAAAAAAAQQQAAcEEAAAAAAABwQQAAcEEAAAAAAAAQQQAAcEEAAAAAAAAQQQAAkEEAAAAAAAAQQQAAcEEAAAAAAABwQQAAkEEAAAAAAABwQQAAcEEAAAAAAACoQQAAcEEAAAAAAABwQQAAcEEAAAAAAABwQQAAkEEAAAAAAABwQQAAcEEAAAAAAACoQQAAkEEAAAAAAACoQQAAcEEAAAAAAADYQQAAcEEAAAAAAACoQQAAcEEAAAAAAACoQQAAkEEAAAAAAACoQQAAcEEAAAAAAADYQQAAkEEAAAAAAADYQQAAcEEAAAAAAAAEQgAAcEEAAAAAAADYQQAAcEEAAAAAAADYQQAAkEEAAAAAAADYQQAAcEEAAAAAAAAEQgAAkEEAAAAAAAAEQgAAcEEAAAAAAAAEQgAAkEEAAAAAAABAQAAAkEEAAAAALMnZwHWwn0EAAAAAAABAQAAAkEEAAAAAAAAQQQAAkEEAAAAAAABAQAAAkEEAAAAALMnZwHWwn0EAAAAAAABAQAAAcEEAAAAAAADgwAAAcEEAAAAAAAAQQQAAkEEAAAAAAABwQQAAkEEAAAAAAABwQQAAkEEAAAAAAACoQQAAkEEAAAAAAACoQQAAkEEAAAAAAADYQQAAkEEAAAAAAADYQQAAkEEAAAAAAAAEQgAAkEEAAAAAAABAQAAAkEEAAAAALMnZwHWwn0EAAAAAAABAQAAAkEEAAAAAAAAQQQAAkEEAAAAAAABAQAAAkEEAAAAAAABAQAAAqEEAAAAAAABAQAAAkEEAAAAAAAAQQQAAqEEAAAAAAAAQQQAAkEEAAAAAAABwQQAAkEEAAAAAAAAQQQAAkEEAAAAAAAAQQQAAqEEAAAAAAAAQQQAAkEEAAAAAAABwQQAAqEEAAAAAAABwQQAAkEEAAAAAAACoQQAAkEEAAAAAAABwQQAAkEEAAAAAAABwQQAAqEEAAAAAAABwQQAAkEEAAAAAAACoQQAAqEEAAAAAAACoQQAAkEEAAAAAAADYQQAAkEEAAAAAAACoQQAAkEEAAAAAAACoQQAAqEEAAAAAAACoQQAAkEEAAAAAAADYQQAAqEEAAAAAAADYQQAAkEEAAAAAAAAEQgAAkEEAAAAAAADYQQAAkEEAAAAAAADYQQAAqEEAAAAAAADYQQAAkEEAAAAAAAAEQgAAqEEAAAAAAAAEQgAAkEEAAAAAAAAEQgAAqEEAAAAAAABAQAAAqEEAAAAA0BzJwBW2xUEAAAAAAABAQAAAqEEAAAAAAAAQQQAAqEEAAAAAAABAQAAAqEEAAAAA0BzJwBW2xUEAAAAAAABAQAAAkEEAAAAALMnZwHWwn0EAAAAAAAAQQQAAqEEAAAAAAABwQQAAqEEAAAAAAABwQQAAqEEAAAAAAACoQQAAqEEAAAAAAACoQQAAqEEAAAAAAADYQQAAqEEAAAAAAADYQQAAqEEAAAAAAAAEQgAAqEEAAAAAAABAQAAAqEEAAAAA0BzJwBW2xUEAAAAAAABAQAAAqEEAAAAAAAAQQQAAqEEAAAAAAABAQAAAqEEAAAAAAABAQAAAwEEAAAAAAABAQAAAqEEAAAAAAAAQQQAAwEEAAAAAAAAQQQAAqEEAAAAAAABwQQAAqEEAAAAAAAAQQQAAqEEAAAAAAAAQQQAAwEEAAAAAAAAQQQAAqEEAAAAAAABwQQAAwEEAAAAAAABwQQAAqEEAAAAAAACoQQAAqEEAAAAAAABwQQAAqEEAAAAAAABwQQAAwEEAAAAAAABwQQAAqEEAAAAAAACoQQAAwEEAAAAAAACoQQAAqEEAAAAAAADYQQAAqEEAAAAAAACoQQAAqEEAAAAAAACoQQAAwEEAAAAAAACoQQAAqEEAAAAAAADYQQAAwEEAAAAAAADYQQAAqEEAAAAAAAAEQgAAqEEAAAAAAADYQQAAqEEAAAAAAADYQQAAwEEAAAAAAADYQQAAqEEAAAAAAAAEQgAAwEEAAAAAAAAEQgAAqEEAAAAAAAAEQgAAwEEAAAAAAABAQAAAwEEAAAAA1GWywOAo6UEAAAAAAABAQAAAwEEAAAAAAAAQQQAAwEEAAAAAAABAQAAAwEEAAAAA1GWywOAo6UEAAAAAAABAQAAAqEEAAAAA0BzJwBW2xUEAAAAAAAAQQQAAwEEAAAAAAABwQQAAwEEAAAAAAABwQQAAwEEAAAAAAACoQQAAwEEAAAAAAACoQQAAwEEAAAAAAADYQQAAwEEAAAAAAADYQQAAwEEAAAAAAAAEQgAAwEEAAAAAAABAQAAAwEEAAAAA1GWywOAo6UEAAAAAAABAQAAAwEEAAAAAAAAQQQAAwEEAAAAAAABAQAAAwEEAAAAAAABAQAAA2EEAAAAAAABAQAAAwEEAAAAAAAAQQQAA2EEAAAAAAAAQQQAAwEEAAAAAAABwQQAAwEEAAAAAAAAQQQAAwEEAAAAAAAAQQQAA2EEAAAAAAAAQQQAAwEEAAAAAAABwQQAA2EEAAAAAAABwQQAAwEEAAAAAAACoQQAAwEEAAAAAAABwQQAAwEEAAAAAAABwQQAA2EEAAAAAAABwQQAAwEEAAAAAAACoQQAA2EEAAAAAAACoQQAAwEEAAAAAAADYQQAAwEEAAAAAAACoQQAAwEEAAAAAAACoQQAA2EEAAAAAAACoQQAAwEEAAAAAAADYQQAA2EEAAAAAAADYQQAAwEEAAAAAAAAEQgAAwEEAAAAAAADYQQAAwEEAAAAAAADYQQAA2EEAAAAAAADYQQAAwEEAAAAAAAAEQgAA2EEAAAAAAAAEQgAAwEEAAAAAAAAEQgAA2EEAAAAAAABAQAAA2EEAAAAAxeCZwOD8BEIAAAAAAABAQAAA2EEAAAAAAAAQQQAA2EEAAAAAAABAQAAA2EEAAAAAxeCZwOD8BEIAAAAAAABAQAAAwEEAAAAA1GWywOAo6UEAAAAAAAAQQQAA2EEAAAAAAABwQQAA2EEAAAAAAABwQQAA2EEAAAAAAACoQQAA2EEAAAAAAACoQQAA2EEAAAAAAADYQQAA2EEAAAAAAADYQQAA2EEAAAAAAAAEQgAA2EEAAAAAAABAQAAA2EEAAAAAxeCZwOD8BEIAAAAAAABAQAAA2EEAAAAAAAAQQQAA2EEAAAAAAABAQAAA2EEAAAAAAABAQAAA8EEAAAAAAABAQAAA2EEAAAAAAAAQQQAA8EEAAAAAAAAQQQAA2EEAAAAAAABwQQAA2EEAAAAAAAAQQQAA2EEAAAAAAAAQQQAA8EEAAAAAAAAQQQAA2EEAAAAAAABwQQAA8EEAAAAAAABwQQAA2EEAAAAAAACoQQAA2EEAAAAAAABwQQAA2EEAAAAAAABwQQAA8EEAAAAAAABwQQAA2EEAAAAAAACoQQAA8EEAAAAAAACoQQAA2EEAAAAAAADYQQAA2EEAAAAAAACoQQAA2EEAAAAAAACoQQAA8EEAAAAAAACoQQAA2EEAAAAAAADYQQAA8EEAAAAAAADYQQAA2EEAAAAAAAAEQgAA2EEAAAAAAADYQQAA2EEAAAAAAADYQQAA8EEAAAAAAADYQQAA2EEAAAAAAAAEQgAA8EEAAAAAAAAEQgAA2EEAAAAAAAAEQgAA8EEAAAAAAABAQAAA8EEAAAAAMEaCwMZIFEIAAAAAAABAQAAA8EEAAAAAAAAQQQAA8EEAAAAAAABAQAAA8EEAAAAAMEaCwMZIFEIAAAAAAABAQAAA2EEAAAAAxeCZwOD8BEIAAAAAAAAQQQAA8EEAAAAAAABwQQAA8EEAAAAAAABwQQAA8EEAAAAAAACoQQAA8EEAAAAAAACoQQAA8EEAAAAAAADYQQAA8EEAAAAAAADYQQAA8EEAAAAAAAAEQgAA8EEAAAAA"},{"byteLength":960,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAEQgAAAAAAAAAAxkggQjBG4sAAAAAAAAAEQgAAQEAAAAAAGTwjQgjOT8AAAAAAAAAEQgAAQEAAAAAAGTwjQgjOT8AAAAAAAAAEQgAAAAAAAAAAxkggQjBG4sAAAAAAAAAEQgAAQEAAAAAAGTwjQgjOT8AAAAAAAAAEQgAAwEAAAAAAukwmQgjkWj8AAAAAAAAEQgAAwEAAAAAAukwmQgjkWj8AAAAAAAAEQgAAQEAAAAAAGTwjQgjOT8AAAAAAAAAEQgAAwEAAAAAAukwmQgjkWj8AAAAAAAAEQgAAEEEAAAAAmiMpQq0nqUAAAAAAAAAEQgAAEEEAAAAAmiMpQq0nqUAAAAAAAAAEQgAAwEAAAAAAukwmQgjkWj8AAAAAAAAEQgAAEEEAAAAAmiMpQq0nqUAAAAAAAAAEQgAAQEEAAAAAJjkrQhWfIEEAAAAAAAAEQgAAQEEAAAAAJjkrQhWfIEEAAAAAAAAEQgAAEEEAAAAAmiMpQq0nqUAAAAAAAAAEQgAAQEEAAAAAJjkrQhWfIEEAAAAAAAAEQgAAcEEAAAAAAAAsQgAAcEEAAAAAAAAEQgAAcEEAAAAAAAAsQgAAcEEAAAAAAAAEQgAAQEEAAAAAJjkrQhWfIEEAAAAAAAAEQgAAcEEAAAAAAAAsQgAAcEEAAAAAAAAEQgAAkEEAAAAAJjkrQnWwn0EAAAAAAAAEQgAAkEEAAAAAJjkrQnWwn0EAAAAAAAAEQgAAcEEAAAAAAAAsQgAAcEEAAAAAAAAEQgAAkEEAAAAAJjkrQnWwn0EAAAAAAAAEQgAAqEEAAAAAmiMpQhW2xUEAAAAAAAAEQgAAqEEAAAAAmiMpQhW2xUEAAAAAAAAEQgAAkEEAAAAAJjkrQnWwn0EAAAAAAAAEQgAAqEEAAAAAmiMpQhW2xUEAAAAAAAAEQgAAwEEAAAAAukwmQuAo6UEAAAAAAAAEQgAAwEEAAAAAukwmQuAo6UEAAAAAAAAEQgAAqEEAAAAAmiMpQhW2xUEAAAAAAAAEQgAAwEEAAAAAukwmQuAo6UEAAAAAAAAEQgAA2EEAAAAAGTwjQuD8BEIAAAAAAAAEQgAA2EEAAAAAGTwjQuD8BEIAAAAAAAAEQgAAwEEAAAAAukwmQuAo6UEAAAAAAAAEQgAA2EEAAAAAGTwjQuD8BEIAAAAAAAAEQgAA8EEAAAAAxkggQsZIFEIAAAAAAAAEQgAA8EEAAAAAxkggQsZIFEIAAAAAAAAEQgAA2EEAAAAAGTwjQuD8BEIAAAAA"}],"materials":[{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0.5,0.5,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0},"material":0,"mode":6},{"attributes":{"POSITION":1},"material":0,"mode":6},{"attributes":{"POSITION":2},"material":0,"mode":6}]},{"primitives":[{"attributes":{"POSITION":3},"material":1,"mode":6},{"attributes":{"POSITION":4},"material":1,"mode":6},{"attributes":{"POSITION":5},"material":1,"mode":6}]},{"primitives":[{"attributes":{"POSITION":6},"material":2,"mode":1}]},{"primitives":[{"attributes":{"POSITION":7},"material":3,"mode":1}]}],"nodes":[{"mesh":0,"translation":[3,0,0]},{"mesh":0,"translation":[9,0,0]},{"mesh":0,"translation":[15,0,0]},{"mesh":0,"translation":[21,0,0]},{"mesh":0,"translation":[27,0,0]},{"mesh":1,"translation":[33,0,0]},{"mesh":0,"translation":[3,3,0]},{"mesh":0,"translation":[9,3,0]},{"mesh":0,"translation":[15,3,0]},{"mesh":0,"translation":[21,3,0]},{"mesh":0,"translation":[27,3,0]},{"mesh":1,"translation":[33,3,0]},{"mesh":0,"translation":[3,6,0]},{"mesh":0,"translation":[9,6,0]},{"mesh":0,"translation":[15,6,0]},{"mesh":0,"translation":[21,6,0]},{"mesh":0,"translation":[27,6,0]},{"mesh":1,"translation":[33,6,0]},{"mesh":0,"translation":[3,9,0]},{"mesh":0,"translation":[9,9,0]},{"mesh":0,"translation":[15,9,0]},{"mesh":0,"translation":[21,9,0]},{"mesh":0,"translation":[27,9,0]},{"mesh":1,"translation":[33,9,0]},{"mesh":0,"translation":[3,12,0]},{"mesh":0,"translation":[9,12,0]},{"mesh":0,"translation":[15,12,0]},{"mesh":0,"translation":[21,12,0]},{"mesh":0,"translation":[27,12,0]},{"mesh":1,"translation":[33,12,0]},{"mesh":0,"translation":[3,15,0]},{"mesh":0,"translation":[9,15,0]},{"mesh":0,"translation":[15,15,0]},{"mesh":0,"translation":[21,15,0]},{"mesh":0,"translation":[27,15,0]},{"mesh":1,"translation":[33,15,0]},{"mesh":0,"translation":[3,18,0]},{"mesh":0,"translation":[9,18,0]},{"mesh":0,"translation":[15,18,0]},{"mesh":0,"translation":[21,18,0]},{"mesh":0,"translation":[27,18,0]},{"mesh":1,"translation":[33,18,0]},{"mesh":0,"translation":[3,21,0]},{"mesh":0,"translation":[9,21,0]},{"mesh":0,"translation":[15,21,0]},{"mesh":0,"translation":[21,21,0]},{"mesh":0,"translation":[27,21,0]},{"mesh":1,"translation":[33,21,0]},{"mesh":0,"translation":[3,24,0]},{"mesh":0,"translation":[9,24,0]},{"mesh":0,"translation":[15,24,0]},{"mesh":0,"translation":[21,24,0]},{"mesh":0,"translation":[27,24,0]},{"mesh":1,"translation":[33,24,0]},{"mesh":0,"translation":[3,27,0]},{"mesh":0,"translation":[9,27,0]},{"mesh":0,"translation":[15,27,0]},{"mesh":0,"translation":[21,27,0]},{"mesh":0,"translation":[27,27,0]},{"mesh":1,"translation":[33,27,0]},{"mesh":0,"translation":[3,30,0]},{"mesh":0,"translation":[9,30,0]},{"mesh":0,"translation":[15,30,0]},{"mesh":0,"translation":[21,30,0]},{"mesh":0,"translation":[27,30,0]},{"mesh":1,"translation":[33,30,0]},{"mesh":2,"translation":[0,0,0]},{"mesh":3,"translation":[0,0,0]}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67]}]} ================================================ FILE: testdata/match_graph_surface_code.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":9,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0,0.400000005960464],"min":[-0.400000005960464,0,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0.400000005960464,0],"min":[-0.400000005960464,-0.400000005960464,0],"name":"circle_loop","type":"VEC3"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":9,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0,0.400000005960464],"min":[-0.400000005960464,0,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":9,"max":[0.400000005960464,0.400000005960464,0],"min":[-0.400000005960464,-0.400000005960464,0],"name":"circle_loop","type":"VEC3"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":8192,"max":[20.9442710876465,20.9442710876465,39.8058090209961],"min":[-8.9442720413208,-8.9442720413208,-9.80580711364746],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":1336,"max":[20.9442710876465,3,39.8058090209961],"min":[-8.9442720413208,-7,-9.80580711364746],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":1,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":2,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":3,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":4,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":5,"byteLength":108,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":6,"byteLength":98304,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":7,"byteLength":16032,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAMPQkD7D0JA+AAAAAPIwlrLNzMw+AAAAAMPQkL7D0JA+AAAAAM3MzL7yMBazAAAAAMHQkL7E0JC+AAAAAPLkozHNzMy+AAAAAMbQkD6/0JC+AAAAAM3MzD4AAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAAAAAADNzMw+w9CQPgAAAADD0JA+zczMPgAAAADyMJayw9CQPgAAAADD0JC+8jAWswAAAADNzMy+xNCQvgAAAADB0JC+zczMvgAAAADy5KMxv9CQvgAAAADG0JA+AAAAAAAAAADNzMw+"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,zczMPgAAAAAAAAAAw9CQPsPQkD4AAAAA8jCWss3MzD4AAAAAw9CQvsPQkD4AAAAAzczMvvIwFrMAAAAAwdCQvsTQkL4AAAAA8uSjMc3MzL4AAAAAxtCQPr/QkL4AAAAAzczMPgAAAAAAAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAMPQkD7D0JA+AAAAAPIwlrLNzMw+AAAAAMPQkL7D0JA+AAAAAM3MzL7yMBazAAAAAMHQkL7E0JC+AAAAAPLkozHNzMy+AAAAAMbQkD6/0JC+AAAAAM3MzD4AAAAA"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAAAAAADNzMw+w9CQPgAAAADD0JA+zczMPgAAAADyMJayw9CQPgAAAADD0JC+8jAWswAAAADNzMy+xNCQvgAAAADB0JC+zczMvgAAAADy5KMxv9CQvgAAAADG0JA+AAAAAAAAAADNzMw+"},{"byteLength":108,"name":"circle_loop","uri":"data:application/octet-stream;base64,zczMPgAAAAAAAAAAw9CQPsPQkD4AAAAA8jCWss3MzD4AAAAAw9CQvsPQkD4AAAAAzczMvvIwFrMAAAAAwdCQvsTQkL4AAAAA8uSjMc3MzL4AAAAAxtCQPr/QkL4AAAAAzczMPgAAAAAAAAAA"},{"byteLength":98304,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAQEAAAAAAAAAAAAAAEEEAAAAAAAAAAAAAQEAAAAAAAADAQAAAQEAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAQEAAAEBAAAAAAAAAEEEAAAAA6LFpwD02LUEyDxLBAAAAAAAAEEEAAAAAAADAQAAAEEEAAAAAAAAAAAAAEEEAAAAAAAAAAAAAQEAAAEBAAAAAAAAAEEEAAAAAAAAAAAAAEEEAAEBAAADAQAAAQEAAAAAAAADAQAAAEEEAAAAAAADAQAAAQEAAAAAAAADAQAAAEEEAAAAAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAQEAAAAAAAABAQQAAQEAAAAAAAADAQAAAQEAAAAAAAAAAAAAAQEAAAEBAAADAQAAAQEAAAAAAAAAAAAAAQEAAAEBAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAADAQAAAQEAAAAAAAAAAAAAAQEAAAEBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAADAQAAAQEAAAAAAAAAAAAAAQEAAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAQEAAAAAAAAAAAAAAQEAAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAADAQAAAQEAAAAAAAADAQAAAQEAAAEBAAADAQAAAQEAAAAAAAADAQAAAQEAAAEBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAADAQAAAQEAAAAAAAADAQAAAQEAAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAQEAAAAAAAAAAAAAAEEEAAEBAAADAQAAAQEAAAAAAAAAAAAAAEEEAAEBAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAADAQAAAQEAAAAAAAAAAAAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAQEAAAAAAAAAAAAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAADAQAAAEEEAAAAAAADAQOtgL0GW5BzBAADAQAAAEEEAAAAAAABAQQAAEEEAAAAAAADAQAAAEEEAAAAAAADAQAAAQEAAAEBAAADAQAAAEEEAAAAAAADAQAAAQEAAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAEEEAAAAAAADAQAAAQEAAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAADAQAAAEEEAAAAAAADAQAAAQEAAAEBAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAADAQAAAEEEAAAAAAAAAAAAAEEEAAEBAAADAQAAAEEEAAAAAAAAAAAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAEEEAAAAAAAAAAAAAEEEAAEBAAABAQAAAwEAAAEBAAABAQAAAQEEAAEBAAADAQAAAEEEAAAAAAADAQAAAEEEAAEBAAADAQAAAEEEAAAAAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAEEEAAAAAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBAAABAQAAAQEEAAEBAAADAQAAAEEEAAAAAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAADAQAAAEEEAAAAAAADAQAAAEEEAAEBAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAABAQQAAQEAAAAAAAABAQQAAEEEAAAAAAABAQQAAQEAAAAAAAABAQQAAEEEAAAAAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAQEAAAAAAAADAQAAAQEAAAEBAAABAQQAAQEAAAAAAAADAQAAAQEAAAEBAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAABAQQAAQEAAAAAAAADAQAAAQEAAAEBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAABAQQAAQEAAAAAAAADAQAAAQEAAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAQEAAAAAAAADAQAAAQEAAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAABAQQAAQEAAAAAAAABAQQAAQEAAAEBAAABAQQAAQEAAAAAAAABAQQAAQEAAAEBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAABAQQAAQEAAAAAAAABAQQAAQEAAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAQEAAAAAAAADAQAAAEEEAAEBAAABAQQAAQEAAAAAAAADAQAAAEEEAAEBAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAABAQQAAQEAAAAAAAADAQAAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAQEAAAAAAAADAQAAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAABAQQAAEEEAAAAAemx6QT02LUEyDxLBAABAQQAAEEEAAAAAAABAQQAAQEAAAEBAAABAQQAAEEEAAAAAAABAQQAAQEAAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAEEEAAAAAAABAQQAAQEAAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAQEEAAEBANOoyQTTqgkGiUbfAAABAQQAAEEEAAAAAAABAQQAAQEAAAEBAAAAQQQAAQEEAAEBANOoyQTTqgkGiUbfAAABAQQAAEEEAAAAAAADAQAAAEEEAAEBAAABAQQAAEEEAAAAAAADAQAAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAEEEAAAAAAADAQAAAEEEAAEBAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAABAQQAAEEEAAAAAAABAQQAAEEEAAEBAAABAQQAAEEEAAAAAAABAQQAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAEEEAAAAAAABAQQAAEEEAAEBAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAABAQQAAEEEAAAAAAABAQQAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAQEEAAEBANOoyQTTqgkGiUbfAAABAQQAAEEEAAAAAAABAQQAAEEEAAEBAAAAQQQAAQEEAAEBANOoyQTTqgkGiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAAAAAAEBAAAAQQQAAAAAAAEBAAABAQAAAAAAAAEBAAAAQQQAAAAAAAEBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAAAAAAAAQEAAAEBAAADAQAAAQEAAAEBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAAAAAAAAQEAAAEBAAAAAAAAAQEAAAMBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAADAQAAAQEAAAEBAAAAAAAAAQEAAAMBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAABAQAAAAAAAAEBAAABAQAAAwEAAAEBAAABAQAAAAAAAAEBAAABAQAAAAAAAAMBAAABAQAAAAAAAAEBAAABAQAAAAAAAAMBAAABAQAAAAAAAAEBAAABAQAAAAAAAAMBAAABAQAAAAAAAAEBAAABAQAAAAAAAAMBAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAADAQAAAQEAAAEBAAABAQQAAQEAAAEBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAADAQAAAQEAAAEBAAADAQAAAQEAAAMBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAABAQQAAQEAAAEBAAADAQAAAQEAAAMBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAADAQAAAEEEAAEBAAADAQAAAQEAAAMBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAEBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAEBAAABAQAAAAAAAAMBAAAAQQQAAAAAAAEBAAABAQAAAAAAAAMBAAAAQQQAAAAAAAEBAAABAQAAAAAAAAMBAAAAQQQAAAAAAAEBAAABAQAAAAAAAAMBAAAAQQQAAAAAAAEBAAAAQQQAAAAAAAMBAAAAQQQAAAAAAAEBAAAAQQQAAAAAAAMBAAAAQQQAAAAAAAEBAAAAQQQAAAAAAAMBAAAAQQQAAAAAAAEBAAAAQQQAAAAAAAMBAAAAQQQAAAAAAAEBAAABAQAAAwEAAAMBAAAAQQQAAAAAAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAAAQQQAAAAAAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAADAQAAAQEAAAMBAAAAQQQAAAAAAAEBAAABAQAAAwEAAAMBAAAAQQQAAAAAAAEBAAABAQAAAwEAAAMBAAADAQAAAEEEAAEBAAADAQAAAQEAAAMBAAAAQQQAAAAAAAEBAAABAQAAAwEAAAMBAAAAQQQAAAAAAAEBAAABAQAAAwEAAAMBAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAAAAAAQEAAAEBAAADAQAAAQEAAAEBAAAAAAAAAQEAAAEBAAAAAAAAAEEEAAEBAAAAAAAAAQEAAAEBAAAAAAAAAEEEAAEBAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAAAAAAAAQEAAAEBAAAAAAAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAAAAAAAAQEAAAEBAAAAAAAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAAAAAAAAQEAAAEBAAAAAAAAAQEAAAMBAAAAAAAAAQEAAAEBAAAAAAAAAQEAAAMBAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAAAAAAAAQEAAAEBAAAAAAAAAQEAAAMBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAAAAAAAAQEAAAEBAAAAAAAAAQEAAAMBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAADAQAAAQEAAAEBAAABAQQAAQEAAAEBAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBAAAAQQQAAwEAAAEBAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBAAABAQAAAQEEAAEBAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAAAQQQAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAADAQAAAQEAAAEBAAADAQAAAEEEAAEBAAABAQAAAQEEAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAAAAAAAAQEAAAMBAAADAQAAAQEAAAEBAAAAAAAAAQEAAAMBAAABAQAAAwEAAAEBAAABAQAAAAAAAAMBAAADAQAAAQEAAAEBAAAAAAAAAQEAAAMBAAABAQAAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAAAAAAAAQEAAAMBAAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAADAQAAAQEAAAMBAAADAQAAAQEAAAEBAAADAQAAAQEAAAMBAAAAQQQAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAADAQAAAQEAAAMBAAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAAAAAAAAEEEAAMBAAADAQAAAQEAAAEBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAEBAAABAQAAAQEEAAEBAAADAQAAAQEAAAEBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAEBAAABAQAAAAAAAAMBAAADAQAAAQEAAAEBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAAAAAAAAEEEAAMBAAABAQAAAQEEAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAEBAAAAAAAAAEEEAAMBAAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAAAQQQAAwEAAAEBAAAAQQQAAwEAAAMBAAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAQEEAAEBANOoyQTTqgkGiUbfAAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAAAQQQAAQEEAAEBANOoyQTTqgkGiUbfAAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAAAQQQAAQEEAAEBAAAAQQQAAwEAAAMBAAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAAAQQQAAwEAAAMBAsJhCQQAAwEBIKF/AAABAQQAAQEAAAEBAAABAQQAAEEEAAEBAAAAQQQAAwEAAAMBAsJhCQQAAwEBIKF/AAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAABAQQAAQEAAAEBAAADAQAAAQEAAAMBAAABAQQAAQEAAAEBAAADAQAAAQEAAAMBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAMBAAABAQQAAQEAAAEBAAADAQAAAQEAAAMBAAAAQQQAAwEAAAEBAAAAQQQAAwEAAAMBAAABAQQAAQEAAAEBAAADAQAAAQEAAAMBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAABAQQAAQEAAAEBAAABAQQAAQEAAAMBAAABAQQAAQEAAAEBAAABAQQAAQEAAAMBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAABAQQAAQEAAAEBAAABAQQAAQEAAAMBAAAAQQQAAwEAAAMBAsJhCQQAAwEBIKF/AAABAQQAAQEAAAEBAAADAQAAAEEEAAMBAAABAQQAAQEAAAEBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAABAQQAAQEAAAEBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAMBAAABAQQAAQEAAAEBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAAAQQQAAwEAAAMBAAABAQQAAQEAAAEBAAADAQAAAEEEAAMBAAAAQQQAAQEEAAEBAAAAQQQAAwEAAAMBAAABAQQAAQEAAAEBAAADAQAAAEEEAAMBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAAAAAAMBABcM6QRUMq8BAJAHAAAAQQQAAAAAAAMBABcM6QRUMq8BAJAHAAAAQQQAAAAAAAEBANOoyQdGoi8CiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAwEAAAEBAAAAQQQAAwEAAAEBAAABAQAAAwEAAAEBAAABAQAAAQEEAAEBAAABAQAAAwEAAAEBAAABAQAAAQEEAAEBAAAAAAAAAEEEAAEBAAADAQAAAEEEAAEBAAABAQAAAwEAAAEBAAABAQAAAQEEAAEBAAAAAAAAAEEEAAEBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAEBAAABAQAAAQEEAAEBAAADAQAAAEEEAAEBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAEBAAABAQAAAAAAAAMBAAABAQAAAwEAAAEBAAABAQAAAAAAAAMBAAABAQAAAwEAAAEBAAABAQAAAAAAAAMBAAAAAAAAAQEAAAMBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAEBAAABAQAAAAAAAAMBAAABAQAAAwEAAAEBAAABAQAAAAAAAAMBAAABAQAAAwEAAAEBAAABAQAAAwEAAAMBAAABAQAAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAEEEAAEBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAEBAAABAQAAAwEAAAMBAAAAAAAAAQEAAAMBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAEEEAAAAAAADAQOtgL0GW5BzBAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAAAAAAEBAvFxRP9Goi8CiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAAAAAAAAEEEAAEBA0aiLwDTqMkGiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAAAQQQAAwEAAAEBAQs42QQAAwEASctbAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAADAQAAAEEEAAEBAAADAQELONkESctbAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAADAQAAAEEEAAEBAAABAQQAAEEEAAEBAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAADAQAAAEEEAAEBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAABAQQAAEEEAAEBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAAAQQQAAwEAAAEBAAAAQQQAAQEEAAEBAAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAADAQAAAEEEAAEBAAADAQELONkESctbAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAMBAAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAEBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAEBAAABAQAAAwEAAAMBAAAAQQQAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAEEEAAEBAAADAQAAAQEAAAMBAAAAQQQAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAEEEAAEBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAABAQAAAwEAAAMBAAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAAAQQQAAwEAAAMBAAAAQQQAAwEAAAEBAAAAQQQAAwEAAAMBAAABAQQAAEEEAAEBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAAAQQQAAwEAAAMBAAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAEBAAABAQAAAQEEAAMBAAAAQQQAAwEAAAEBAAABAQAAAQEEAAMBAAADAQAAAEEEAAEBAAADAQELONkESctbA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AABAQAAAQEEAAMBAAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAABAQAAAQEEAAEBAAABAQAAAQEEAAMBAAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAADAQAAAEEEAAEBAAADAQELONkESctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAADAQAAAEEEAAAAAAADAQOtgL0GW5BzBAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAEEEAAAAAAADAQOtgL0GW5BzBAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAAAAAAAAEEEAAEBA0aiLwDTqMkGiUbfAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAAAAAAAAEEEAAEBA0aiLwDTqMkGiUbfAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAAAAAAAAEEEAAEBA0aiLwDTqMkGiUbfAAADAQAAAEEEAAAAAAADAQOtgL0GW5BzBAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAAAAAAAAEEEAAEBA0aiLwDTqMkGiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAAAAAAAAEEEAAEBA0aiLwDTqMkGiUbfAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAADAQAAAEEEAAAAAAADAQOtgL0GW5BzBAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAADAQAAAEEEAAEBAAADAQELONkESctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAADAQAAAEEEAAEBAAADAQELONkESctbAAADAQAAAEEEAAAAAAADAQOtgL0GW5BzBAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfAAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAABAQAAAwEAAAEBA3BsTPwAAwEASctbAAABAQAAAQEEAAEBAvFxRPzTqgkGiUbfA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BcOKQQXDOkFAJAHAAAAQQQAAQEEAAMBABcM6QQXDikFAJAHAAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAABAQQAAEEEAAEBANOqCQTTqMkGiUbfAAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAABAQAAAAAAAAMBAAAAQQQAAAAAAAMBAAABAQAAAAAAAAMBAAAAQQQAAAAAAAMBAAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAAAAAAAAQEAAAMBAAADAQAAAQEAAAMBAAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAAAAAAAAQEAAAMBAAAAAAAAAQEAAABBBAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAADAQAAAQEAAAMBAAAAAAAAAQEAAABBBAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAABAQAAAAAAAAMBAAABAQAAAwEAAAMBAAABAQAAAAAAAAMBAAABAQAAAAAAAABBBAABAQAAAAAAAAMBAAABAQAAAAAAAABBBAABAQAAAAAAAAMBAAABAQAAAAAAAABBBAABAQAAAAAAAAMBAAABAQAAAAAAAABBBAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAAAQQQAAAAAAAMBABcM6QRUMq8BAJAHAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAADAQAAAQEAAAMBAAABAQQAAQEAAAMBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAADAQAAAQEAAAMBAAADAQAAAQEAAABBBAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAABAQQAAQEAAAMBAAADAQAAAQEAAABBBAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAADAQAAAEEEAAMBAAADAQAAAQEAAABBBAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAAAQQQAAAAAAAMBAAAAQQQAAwEAAAMBAAAAQQQAAAAAAAMBAAABAQAAAAAAAABBBAAAQQQAAAAAAAMBAAABAQAAAAAAAABBBAAAQQQAAAAAAAMBAAABAQAAAAAAAABBBAAAQQQAAAAAAAMBAAABAQAAAAAAAABBBAAAQQQAAAAAAAMBAAAAQQQAAAAAAABBBAAAQQQAAAAAAAMBAAAAQQQAAAAAAABBBAAAQQQAAAAAAAMBAAAAQQQAAAAAAABBBAAAQQQAAAAAAAMBAAAAQQQAAAAAAABBBAAAQQQAAAAAAAMBAAABAQAAAwEAAABBBAAAQQQAAAAAAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAAAQQQAAAAAAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAADAQAAAQEAAABBBAAAQQQAAAAAAAMBAAABAQAAAwEAAABBBAAAQQQAAAAAAAMBAAABAQAAAwEAAABBBAADAQAAAEEEAAMBAAADAQAAAQEAAABBBAAAQQQAAAAAAAMBAAABAQAAAwEAAABBBAAAQQQAAAAAAAMBAAABAQAAAwEAAABBBAAAAAAAAQEAAAMBAAADAQAAAQEAAAMBAAAAAAAAAQEAAAMBAAAAAAAAAEEEAAMBAAAAAAAAAQEAAAMBAAAAAAAAAEEEAAMBAAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAAAAAAAAQEAAAMBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAQEAAAMBAAAAAAAAAEEEAAMBAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAAAAAAAAQEAAAMBAAAAAAAAAQEAAABBBAAAAAAAAQEAAAMBAAAAAAAAAQEAAABBBAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAAAAAAAAQEAAAMBAAAAAAAAAQEAAABBBAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAQEAAAMBAAAAAAAAAQEAAABBBAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAADAQAAAQEAAAMBAAABAQQAAQEAAAMBAAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAABAQAAAwEAAAMBAAAAQQQAAwEAAAMBAAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAABAQAAAwEAAAMBAAABAQAAAQEEAAMBAAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAABAQAAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAAAQQQAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAADAQAAAEEEAAMBAAABAQAAAQEEAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAAAAAAAAQEAAABBBAADAQAAAQEAAAMBAAAAAAAAAQEAAABBBAABAQAAAwEAAAMBAAABAQAAAAAAAABBBAADAQAAAQEAAAMBAAAAAAAAAQEAAABBBAABAQAAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAAAAAAAAQEAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAADAQAAAQEAAABBBAADAQAAAQEAAAMBAAADAQAAAQEAAABBBAAAQQQAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAADAQAAAQEAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAAAAAAAAEEEAABBBAADAQAAAQEAAAMBAAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAAABAQAAAQEEAAMBAAADAQAAAQEAAAMBAAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAAABAQAAAAAAAABBBAADAQAAAQEAAAMBAAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAAAAAAAAEEEAABBBAABAQAAAQEEAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAAMBAAAAAAAAAEEEAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAABAQQAAQEAAAMBAAABAQQAAEEEAAMBAAABAQQAAQEAAAMBAAABAQQAAEEEAAMBAAAAQQQAAwEAAAMBAsJhCQQAAwEBIKF/AAABAQQAAQEAAAMBAAABAQQAAEEEAAMBAAAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAABAQQAAQEAAAMBAAABAQQAAEEEAAMBAAAAQQQAAwEAAAMBAAAAQQQAAwEAAABBBAABAQQAAQEAAAMBAAABAQQAAEEEAAMBAAAAQQQAAQEEAAMBAAAAQQQAAwEAAABBBAABAQQAAQEAAAMBAAABAQQAAEEEAAMBAAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AABAQQAAQEAAAMBAAABAQQAAEEEAAMBAAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAwEAAAMBAsJhCQQAAwEBIKF/AAABAQQAAQEAAAMBAAADAQAAAQEAAABBBAABAQQAAQEAAAMBAAADAQAAAQEAAABBBAAAQQQAAwEAAAMBAAAAQQQAAAAAAABBBAABAQQAAQEAAAMBAAADAQAAAQEAAABBBAAAQQQAAwEAAAMBAAAAQQQAAwEAAABBBAABAQQAAQEAAAMBAAADAQAAAQEAAABBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAABAQQAAQEAAAMBAAABAQQAAQEAAABBBAABAQQAAQEAAAMBAAABAQQAAQEAAABBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAABAQQAAQEAAAMBAAABAQQAAQEAAABBBAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AABAQQAAQEAAAMBAAADAQAAAEEEAABBBAABAQQAAQEAAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAABAQQAAQEAAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAAAQQQAAAAAAABBBAABAQQAAQEAAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAAAQQQAAwEAAABBBAABAQQAAQEAAAMBAAADAQAAAEEEAABBBAAAQQQAAQEEAAMBAAAAQQQAAwEAAABBBAABAQQAAQEAAAMBAAADAQAAAEEEAABBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAAAQQQAAAAAAAMBABcM6QRUMq8BAJAHAAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAAAAAAMBABcM6QRUMq8BAJAHAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAABAQAAAwEAAAMBAAAAQQQAAwEAAAMBAAABAQAAAwEAAAMBAAABAQAAAQEEAAMBAAABAQAAAwEAAAMBAAABAQAAAQEEAAMBAAAAAAAAAEEEAAMBAAADAQAAAEEEAAMBAAABAQAAAwEAAAMBAAABAQAAAQEEAAMBAAAAAAAAAEEEAAMBAAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAAABAQAAAQEEAAMBAAADAQAAAEEEAAMBAAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAAABAQAAAAAAAABBBAABAQAAAwEAAAMBAAABAQAAAAAAAABBBAABAQAAAwEAAAMBAAABAQAAAAAAAABBBAAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAAABAQAAAAAAAABBBAABAQAAAwEAAAMBAAABAQAAAAAAAABBBAABAQAAAwEAAAMBAAABAQAAAwEAAABBBAABAQAAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAEEEAAMBAAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAAABAQAAAwEAAABBBAAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAAAQQQAAwEAAAMBAsJhCQQAAwEBIKF/AAAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAADAQAAAEEEAAMBAAABAQQAAEEEAAMBAAAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAADAQAAAEEEAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAABAQQAAEEEAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAwEAAAMBAAAAQQQAAQEEAAMBAAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAAAQQQAAwEAAAMBAAAAQQQAAAAAAABBBAAAQQQAAwEAAAMBAAAAQQQAAAAAAABBBAAAQQQAAwEAAAMBAAAAQQQAAAAAAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAAAQQQAAAAAAABBBAAAQQQAAwEAAAMBAAAAQQQAAAAAAABBBAAAQQQAAwEAAAMBAAABAQAAAwEAAABBBAAAQQQAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAEEEAAMBAAADAQAAAQEAAABBBAAAQQQAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAEEEAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAABAQAAAwEAAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAAAQQQAAwEAAABBBAAAQQQAAwEAAAMBAAAAQQQAAwEAAABBBAABAQQAAEEEAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAAAQQQAAwEAAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAABAQAAAQEEAABBBAAAQQQAAwEAAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAAAQQQAAwEAAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAAMBAAADAQAAAQEAAABBBAAAQQQAAwEAAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAABAQAAAQEEAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAAAQQQAAwEAAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAwEAAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAAAAAAAAEEEAAMBAAADAQAAAEEEAAMBAAAAAAAAAEEEAAMBAAAAAAAAAQEAAABBBAAAAAAAAEEEAAMBAAAAAAAAAQEAAABBBAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAAAAAAAAEEEAAMBAAAAAAAAAQEAAABBBAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAEEEAAMBAAAAAAAAAQEAAABBBAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAAAAAAAAEEEAAMBAAAAAAAAAEEEAABBBAAAAAAAAEEEAAMBAAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAADAQAAAEEEAAMBAAABAQQAAEEEAAMBAAADAQAAAEEEAAMBAAADAQAAAQEAAABBBAADAQAAAEEEAAMBAAADAQAAAQEAAABBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAADAQAAAEEEAAMBAAAAAAAAAEEEAABBBAADAQAAAEEEAAMBAAAAAAAAAEEEAABBBAABAQAAAQEEAAMBAAABAQAAAwEAAABBBAADAQAAAEEEAAMBAAAAAAAAAEEEAABBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAADAQAAAEEEAAMBAAADAQAAAEEEAABBBAADAQAAAEEEAAMBAAADAQAAAEEEAABBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAABAQQAAEEEAAMBAAABAQQAAQEAAABBBAABAQQAAEEEAAMBAAABAQQAAQEAAABBBAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AABAQQAAEEEAAMBAAABAQQAAQEAAABBBAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAAMBAAABAQQAAQEAAABBBAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAAMBAAADAQAAAEEEAABBBAABAQQAAEEEAAMBAAADAQAAAEEEAABBBAAAQQQAAQEEAAMBAAAAQQQAAwEAAABBBAABAQQAAEEEAAMBAAADAQAAAEEEAABBBAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAABAQQAAEEEAAMBAAABAQQAAEEEAABBBAABAQQAAEEEAAMBAAABAQQAAEEEAABBBAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AABAQQAAEEEAAMBAAABAQQAAEEEAABBBAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAABAQQAAEEEAAMBAAABAQQAAEEEAABBBAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAAMBAAABAQQAAEEEAABBBAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAQEEAAMBABcM6QQXDikFAJAHAAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAAAQQQAAQEEAAMBABcM6QQXDikFAJAHAAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAABAQAAAQEEAAMBAAAAQQQAAQEEAAMBAAABAQAAAQEEAAMBAAAAQQQAAQEEAAMBAAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAABAQAAAQEEAAMBAAABAQAAAwEAAABBBAABAQAAAQEEAAMBAAABAQAAAwEAAABBBAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAABAQAAAQEEAAMBAAABAQAAAwEAAABBBAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAQEEAAMBAAABAQAAAwEAAABBBAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAABAQAAAQEEAAMBAAABAQAAAQEEAABBBAABAQAAAQEEAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAABAQAAAQEEAAMBAAABAQAAAQEEAABBBAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAQEEAAMBAAABAQAAAQEEAABBBAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAAAQQQAAQEEAAMBABcM6QQXDikFAJAHAAAAQQQAAQEEAAMBAAAAQQQAAwEAAABBBAAAQQQAAQEEAAMBAAAAQQQAAwEAAABBBAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAQEEAAMBAAAAQQQAAwEAAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAQEEAAMBAAAAQQQAAwEAAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAQEEAAMBAAABAQAAAQEEAABBBAAAQQQAAQEEAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAAAQQQAAQEEAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAQEEAAMBAAABAQAAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAAAQQQAAQEEAAMBAAAAQQQAAQEEAABBBAAAQQQAAQEEAAMBAAAAQQQAAQEEAABBBAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAQEEAAMBAAAAQQQAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAQEEAAMBAAAAQQQAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAABAQAAAAAAAABBBAAAQQQAAAAAAABBBAABAQAAAAAAAABBBAAAQQQAAAAAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAAAAAAAAQEAAABBBAADAQAAAQEAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAAAAAAAAQEAAABBBAAAAAAAAQEAAAEBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAADAQAAAQEAAABBBAAAAAAAAQEAAAEBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAABAQAAAAAAAABBBAABAQAAAwEAAABBBAABAQAAAAAAAABBBAABAQAAAAAAAAEBBAABAQAAAAAAAABBBAABAQAAAAAAAAEBBAABAQAAAAAAAABBBAABAQAAAAAAAAEBBAABAQAAAAAAAABBBAABAQAAAAAAAAEBBAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAABAQAAAAAAAAMBAWJ+nPhUMq8BAJAHAAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAADAQAAAQEAAABBBAABAQQAAQEAAABBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAADAQAAAQEAAABBBAADAQAAAQEAAAEBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAABAQQAAQEAAABBBAADAQAAAQEAAAEBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAADAQAAAEEEAABBBAADAQAAAQEAAAEBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAAAQQQAAAAAAABBBAAAQQQAAwEAAABBBAAAQQQAAAAAAABBBAABAQAAAAAAAAEBBAAAQQQAAAAAAABBBAABAQAAAAAAAAEBBAAAQQQAAAAAAABBBAABAQAAAAAAAAEBBAAAQQQAAAAAAABBBAABAQAAAAAAAAEBBAAAQQQAAAAAAABBBAAAQQQAAAAAAAEBBAAAQQQAAAAAAABBBAAAQQQAAAAAAAEBBAAAQQQAAAAAAABBBAAAQQQAAAAAAAEBBAAAQQQAAAAAAABBBAAAQQQAAAAAAAEBBAAAQQQAAAAAAABBBAABAQAAAwEAAAEBBAAAQQQAAAAAAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAAAQQQAAAAAAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAADAQAAAQEAAAEBBAAAQQQAAAAAAABBBAABAQAAAwEAAAEBBAAAQQQAAAAAAABBBAABAQAAAwEAAAEBBAADAQAAAEEEAABBBAADAQAAAQEAAAEBBAAAQQQAAAAAAABBBAABAQAAAwEAAAEBBAAAQQQAAAAAAABBBAABAQAAAwEAAAEBBAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAAAAAAMBABcM6QRUMq8BAJAHAAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAAAAAAQEAAABBBAADAQAAAQEAAABBBAAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAQEAAABBBAAAAAAAAEEEAABBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAAAAAAAAQEAAABBBAAAAAAAAQEAAAEBBAAAAAAAAQEAAABBBAAAAAAAAQEAAAEBBAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAAAAAAAAQEAAABBBAAAAAAAAQEAAAEBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAQEAAABBBAAAAAAAAQEAAAEBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAADAQAAAQEAAABBBAABAQQAAQEAAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAABAQAAAwEAAABBBAAAQQQAAwEAAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAABAQAAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAAAQQQAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAADAQAAAEEEAABBBAABAQAAAQEEAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAAAAAAAAQEAAAEBBAADAQAAAQEAAABBBAAAAAAAAQEAAAEBBAABAQAAAwEAAABBBAABAQAAAAAAAAEBBAADAQAAAQEAAABBBAAAAAAAAQEAAAEBBAABAQAAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAAAAAAAAQEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAADAQAAAQEAAAEBBAADAQAAAQEAAABBBAADAQAAAQEAAAEBBAAAQQQAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAADAQAAAQEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAAAAAAAAEEEAAEBBAADAQAAAQEAAABBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAADAQAAAQEAAABBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBBAABAQAAAAAAAAEBBAADAQAAAQEAAABBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAAAAAAAAEEEAAEBBAABAQAAAQEEAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAABBBAAAAAAAAEEEAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQQAAQEAAABBBAABAQQAAEEEAABBBAABAQQAAQEAAABBBAABAQQAAEEEAABBBAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AABAQQAAQEAAABBBAABAQQAAEEEAABBBAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAABAQQAAQEAAABBBAABAQQAAEEEAABBBAAAQQQAAwEAAABBBAAAQQQAAwEAAAEBBAABAQQAAQEAAABBBAABAQQAAEEEAABBBAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAQEAAABBBAABAQQAAEEEAABBBAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAQEAAABBBAABAQQAAEEEAABBBAAAQQQAAQEEAABBBAAAQQQAAwEAAAEBBAABAQQAAQEAAABBBAABAQQAAEEEAABBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAABAQQAAQEAAABBBAABAQQAAEEEAABBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AABAQQAAQEAAABBBAADAQAAAQEAAAEBBAABAQQAAQEAAABBBAADAQAAAQEAAAEBBAAAQQQAAwEAAABBBAAAQQQAAAAAAAEBBAABAQQAAQEAAABBBAADAQAAAQEAAAEBBAAAQQQAAwEAAABBBAAAQQQAAwEAAAEBBAABAQQAAQEAAABBBAADAQAAAQEAAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAABAQQAAQEAAABBBAABAQQAAQEAAAEBBAABAQQAAQEAAABBBAABAQQAAQEAAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAABAQQAAQEAAABBBAABAQQAAQEAAAEBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAABAQQAAQEAAABBBAADAQAAAEEEAAEBBAABAQQAAQEAAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAABAQQAAQEAAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAAAQQQAAAAAAAEBBAABAQQAAQEAAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAAAQQQAAwEAAAEBBAABAQQAAQEAAABBBAADAQAAAEEEAAEBBAAAQQQAAQEEAABBBAAAQQQAAwEAAAEBBAABAQQAAQEAAABBBAADAQAAAEEEAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAwEAAABBBAAAQQQAAwEAAABBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAABAQAAAwEAAABBBAABAQAAAQEEAABBBAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAwEAAABBBAABAQAAAQEEAABBBAAAAAAAAEEEAABBBAADAQAAAEEEAABBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAAAAAAAAEEEAABBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBBAABAQAAAQEEAABBBAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAABAQAAAwEAAABBBAABAQAAAQEEAABBBAADAQAAAEEEAABBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBBAABAQAAAAAAAAEBBAABAQAAAwEAAABBBAABAQAAAAAAAAEBBAABAQAAAwEAAABBBAABAQAAAAAAAAEBBAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBBAABAQAAAAAAAAEBBAABAQAAAwEAAABBBAABAQAAAAAAAAEBBAABAQAAAwEAAABBBAABAQAAAwEAAAEBBAABAQAAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAEEEAABBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBBAABAQAAAwEAAAEBBAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAADAQAAAEEEAABBBAABAQQAAEEEAABBBAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAADAQAAAEEEAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAABAQQAAEEEAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAwEAAABBBAAAQQQAAQEEAABBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAwEAAABBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAABBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAABBBAAAQQQAAAAAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAABBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAABBBAABAQAAAwEAAAEBBAAAQQQAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAEEEAABBBAADAQAAAQEAAAEBBAAAQQQAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAEEEAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAABAQAAAwEAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAAAQQQAAwEAAAEBBAAAQQQAAwEAAABBBAAAQQQAAwEAAAEBBAABAQQAAEEEAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAAAQQQAAwEAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAABAQAAAQEEAAEBBAAAQQQAAwEAAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAwEAAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAADAQAAAQEAAAEBBAAAQQQAAwEAAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAABAQAAAQEEAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAAAQQQAAwEAAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAwEAAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAwEAAAMBAsJhCQQAAwEBIKF/AAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAAAAAAAAEEEAABBBAADAQAAAEEEAABBBAAAAAAAAEEEAABBBAAAAAAAAQEAAAEBBAAAAAAAAEEEAABBBAAAAAAAAQEAAAEBBAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAAAAAAAAEEEAABBBAAAAAAAAQEAAAEBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAEEEAABBBAAAAAAAAQEAAAEBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAAAAAAAAEEEAABBBAAAAAAAAEEEAAEBBAAAAAAAAEEEAABBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAwEAAAMBAICwmvgAAwEBIKF/AAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAAAAAAAAEEEAAMBAFQyrwAXDOkFAJAHAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AADAQAAAEEEAABBBAABAQQAAEEEAABBBAADAQAAAEEEAABBBAADAQAAAQEAAAEBBAADAQAAAEEEAABBBAADAQAAAQEAAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAAAAAAAAEEEAAEBBAADAQAAAEEEAABBBAAAAAAAAEEEAAEBBAABAQAAAQEEAABBBAABAQAAAwEAAAEBBAADAQAAAEEEAABBBAAAAAAAAEEEAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAADAQAAAEEEAAEBBAADAQAAAEEEAABBBAADAQAAAEEEAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AADAQAAAEEEAAMBAAADAQLCYQkFIKF/AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAABAQQAAEEEAABBBAABAQQAAQEAAAEBBAABAQQAAEEEAABBBAABAQQAAQEAAAEBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAABAQQAAEEEAABBBAABAQQAAQEAAAEBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAABBBAABAQQAAQEAAAEBBAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAABBBAADAQAAAEEEAAEBBAABAQQAAEEEAABBBAADAQAAAEEEAAEBBAAAQQQAAQEEAABBBAAAQQQAAwEAAAEBBAABAQQAAEEEAABBBAADAQAAAEEEAAEBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAABAQQAAEEEAABBBAABAQQAAEEEAAEBBAABAQQAAEEEAABBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAABAQQAAEEEAABBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAABAQQAAEEEAABBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAABBBAABAQQAAEEEAAEBBAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAABAQAAAQEEAABBBAAAQQQAAQEEAABBBAABAQAAAQEEAABBBAAAQQQAAQEEAABBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQAAAQEEAABBBAABAQAAAwEAAAEBBAABAQAAAQEEAABBBAABAQAAAwEAAAEBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQAAAQEEAABBBAABAQAAAwEAAAEBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAQEEAABBBAABAQAAAwEAAAEBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQAAAQEEAABBBAABAQAAAQEEAAEBBAABAQAAAQEEAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQAAAQEEAABBBAABAQAAAQEEAAEBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAQEEAABBBAABAQAAAQEEAAEBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAABAQAAAQEEAAMBAWJ+nPgXDikFAJAHAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAAAQQQAAQEEAABBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAABBBAAAQQQAAwEAAAEBBAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAABBBAAAQQQAAwEAAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAQEEAABBBAAAQQQAAwEAAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAABBBAABAQAAAQEEAAEBBAAAQQQAAQEEAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAQEEAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAQEEAABBBAABAQAAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AAAQQQAAQEEAABBBAAAQQQAAQEEAAEBBAAAQQQAAQEEAABBBAAAQQQAAQEEAAEBBAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAABBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAQEEAABBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAAAQQQAAQEEAAMBABcM6QQXDikFAJAHAAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAABAQQAAEEEAAMBABcOKQQXDOkFAJAHAAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAAAAAAEBBAAAQQQAAAAAAAEBBAABAQAAAAAAAAEBBAAAQQQAAAAAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAAAAAAAAQEAAAEBBAADAQAAAQEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAAAAAABBBsKqqvlZV1cBUVRVAAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAADAQAAAQEAAAEBBAABAQQAAQEAAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAABBBVlVFQVZV1cBUVRVAAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAAAAAAQEAAAEBBAADAQAAAQEAAAEBBAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AADAQAAAQEAAAEBBAABAQQAAQEAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAABAQAAAwEAAAEBBAAAQQQAAwEAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAwEAAAEBBAAAQQQAAwEAAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAAAAAAAAEEEAAEBBAADAQAAAEEEAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAwEAAABBB3o1XQQAAwEAAQ2Q9AAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAAAAAAEEEAAEBBAADAQAAAEEEAAEBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAwEAAABBB9G68vwAAwEAAQ2Q9AAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAAAAAAEEEAABBBVlXVwFZVRUFUVRVAAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAADAQAAAEEEAAEBBAABAQQAAEEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAADAQAAAEEEAABBBAADAQN6NV0EAQ2Q9AADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAABAQAAAQEEAAEBBAAAQQQAAQEEAAEBBAABAQAAAQEEAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAABAQAAAQEEAABBBsKqqvlZVlUFUVRVAAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAAAQQQAAQEEAABBBVlVFQVZVlUFUVRVAAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAABAQQAAEEEAABBBVlWVQVZVRUFUVRVAAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAAAAAAEBBAAAQQQAAAAAAAEBBAABAQAAAAAAAAEBBAAAQQQAAAAAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAAAAAAAAQEAAAEBBAADAQAAAQEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAAAAAAAAQEAAAEBBAAAAAAAAQEAAAHBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAADAQAAAQEAAAEBBAAAAAAAAQEAAAHBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQAAAAAAAAEBBAABAQAAAwEAAAEBBAABAQAAAAAAAAEBBAABAQAAAAAAAAHBBAABAQAAAAAAAAEBBAABAQAAAAAAAAHBBAABAQAAAAAAAAEBBAABAQAAAAAAAAHBBAABAQAAAAAAAAEBBAABAQAAAAAAAAHBBAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAADAQAAAQEAAAEBBAABAQQAAQEAAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAADAQAAAQEAAAEBBAADAQAAAQEAAAHBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAABAQQAAQEAAAEBBAADAQAAAQEAAAHBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAADAQAAAEEEAAEBBAADAQAAAQEAAAHBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAEBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAEBBAABAQAAAAAAAAHBBAAAQQQAAAAAAAEBBAABAQAAAAAAAAHBBAAAQQQAAAAAAAEBBAABAQAAAAAAAAHBBAAAQQQAAAAAAAEBBAABAQAAAAAAAAHBBAAAQQQAAAAAAAEBBAAAQQQAAAAAAAHBBAAAQQQAAAAAAAEBBAAAQQQAAAAAAAHBBAAAQQQAAAAAAAEBBAAAQQQAAAAAAAHBBAAAQQQAAAAAAAEBBAAAQQQAAAAAAAHBBAAAQQQAAAAAAAEBBAABAQAAAwEAAAHBBAAAQQQAAAAAAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAAAQQQAAAAAAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAADAQAAAQEAAAHBBAAAQQQAAAAAAAEBBAABAQAAAwEAAAHBBAAAQQQAAAAAAAEBBAABAQAAAwEAAAHBBAADAQAAAEEEAAEBBAADAQAAAQEAAAHBBAAAQQQAAAAAAAEBBAABAQAAAwEAAAHBBAAAQQQAAAAAAAEBBAABAQAAAwEAAAHBBAAAAAAAAQEAAAEBBAADAQAAAQEAAAEBBAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAQEAAAEBBAAAAAAAAEEEAAEBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAAAAAAAAQEAAAEBBAAAAAAAAQEAAAHBBAAAAAAAAQEAAAEBBAAAAAAAAQEAAAHBBAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAAAAAAAAQEAAAEBBAAAAAAAAQEAAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAQEAAAEBBAAAAAAAAQEAAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAADAQAAAQEAAAEBBAABAQQAAQEAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAABAQAAAwEAAAEBBAAAQQQAAwEAAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAABAQAAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAAAQQQAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAADAQAAAEEEAAEBBAABAQAAAQEEAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAAAAAAAAQEAAAHBBAADAQAAAQEAAAEBBAAAAAAAAQEAAAHBBAABAQAAAwEAAAEBBAABAQAAAAAAAAHBBAADAQAAAQEAAAEBBAAAAAAAAQEAAAHBBAABAQAAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAAAAAAAAQEAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAADAQAAAQEAAAHBBAADAQAAAQEAAAEBBAADAQAAAQEAAAHBBAAAQQQAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAADAQAAAQEAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAAAAAAAAEEEAAHBBAADAQAAAQEAAAEBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAADAQAAAQEAAAEBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBAABAQAAAAAAAAHBBAADAQAAAQEAAAEBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAAAAAAAAEEEAAHBBAABAQAAAQEEAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAEBBAAAAAAAAEEEAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBAAAQQQAAwEAAAHBBAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAQEEAAEBBAAAQQQAAwEAAAHBBAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAABAQQAAQEAAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAABAQQAAQEAAAEBBAADAQAAAQEAAAHBBAABAQQAAQEAAAEBBAADAQAAAQEAAAHBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAHBBAABAQQAAQEAAAEBBAADAQAAAQEAAAHBBAAAQQQAAwEAAAEBBAAAQQQAAwEAAAHBBAABAQQAAQEAAAEBBAADAQAAAQEAAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAABAQQAAQEAAAEBBAABAQQAAQEAAAHBBAABAQQAAQEAAAEBBAABAQQAAQEAAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAABAQQAAQEAAAEBBAABAQQAAQEAAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAABAQQAAQEAAAEBBAADAQAAAEEEAAHBBAABAQQAAQEAAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAABAQQAAQEAAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAHBBAABAQQAAQEAAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAwEAAAHBBAABAQQAAQEAAAEBBAADAQAAAEEEAAHBBAAAQQQAAQEEAAEBBAAAQQQAAwEAAAHBBAABAQQAAQEAAAEBBAADAQAAAEEEAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAwEAAAEBBAAAQQQAAwEAAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAAAAAAAAEEEAAEBBAADAQAAAEEEAAEBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAAAAAAAAEEEAAEBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBAABAQAAAQEEAAEBBAADAQAAAEEEAAEBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBAABAQAAAAAAAAHBBAABAQAAAwEAAAEBBAABAQAAAAAAAAHBBAABAQAAAwEAAAEBBAABAQAAAAAAAAHBBAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBAABAQAAAAAAAAHBBAABAQAAAwEAAAEBBAABAQAAAAAAAAHBBAABAQAAAwEAAAEBBAABAQAAAwEAAAHBBAABAQAAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAEEEAAEBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBAABAQAAAwEAAAHBBAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAABAQQAAEEEAAEBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAABAQQAAEEEAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAwEAAAEBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAEBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAEBBAABAQAAAwEAAAHBBAAAQQQAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAEEEAAEBBAADAQAAAQEAAAHBBAAAQQQAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAEEEAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAABAQAAAwEAAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAwEAAAHBBAAAQQQAAwEAAAEBBAAAQQQAAwEAAAHBBAABAQQAAEEEAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAAAQQQAAwEAAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAABAQAAAQEEAAHBBAAAQQQAAwEAAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAwEAAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAEBBAADAQAAAQEAAAHBBAAAQQQAAwEAAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAABAQAAAQEEAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAwEAAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAAAAAAEEEAAEBBAADAQAAAEEEAAEBBAAAAAAAAEEEAAEBBAAAAAAAAQEAAAHBBAAAAAAAAEEEAAEBBAAAAAAAAQEAAAHBBAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAAAAAAAAEEEAAEBBAAAAAAAAQEAAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAEEEAAEBBAAAAAAAAQEAAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAAAAAAAAEEEAAEBBAAAAAAAAEEEAAHBBAAAAAAAAEEEAAEBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAADAQAAAEEEAAEBBAABAQQAAEEEAAEBBAADAQAAAEEEAAEBBAADAQAAAQEAAAHBBAADAQAAAEEEAAEBBAADAQAAAQEAAAHBBAABAQAAAwEAAAHBBAABAQAAAQEEAAHBBAADAQAAAEEEAAEBBAAAAAAAAEEEAAHBBAADAQAAAEEEAAEBBAAAAAAAAEEEAAHBBAABAQAAAQEEAAEBBAABAQAAAwEAAAHBBAADAQAAAEEEAAEBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAHBBAABAQAAAQEEAAHBBAADAQAAAEEEAAEBBAADAQAAAEEEAAHBBAADAQAAAEEEAAEBBAADAQAAAEEEAAHBBAABAQAAAwEAAAHBBAABAQAAAQEEAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAABAQQAAEEEAAEBBAABAQQAAQEAAAHBBAABAQQAAEEEAAEBBAABAQQAAQEAAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAABAQQAAEEEAAEBBAABAQQAAQEAAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAEBBAABAQQAAQEAAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAEBBAADAQAAAEEEAAHBBAABAQQAAEEEAAEBBAADAQAAAEEEAAHBBAAAQQQAAQEEAAEBBAAAQQQAAwEAAAHBBAABAQQAAEEEAAEBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAHBBAAAQQQAAQEEAAHBBAABAQQAAEEEAAEBBAABAQQAAEEEAAHBBAABAQQAAEEEAAEBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAABAQQAAEEEAAEBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAHBBAAAQQQAAQEEAAHBBAABAQQAAEEEAAEBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAEBBAABAQQAAEEEAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAABAQAAAQEEAAEBBAAAQQQAAQEEAAEBBAABAQAAAQEEAAEBBAAAQQQAAQEEAAEBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQAAAQEEAAEBBAABAQAAAwEAAAHBBAABAQAAAQEEAAEBBAABAQAAAwEAAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQAAAQEEAAEBBAABAQAAAwEAAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAQEEAAEBBAABAQAAAwEAAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQAAAQEEAAEBBAABAQAAAQEEAAHBBAABAQAAAQEEAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQAAAQEEAAEBBAABAQAAAQEEAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAQEEAAEBBAABAQAAAQEEAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAAAQQQAAQEEAAEBBAAAQQQAAwEAAAHBBAAAQQQAAQEEAAEBBAAAQQQAAwEAAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAAAQQQAAQEEAAEBBAAAQQQAAwEAAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAQEEAAEBBAAAQQQAAwEAAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAAAQQQAAQEEAAEBBAABAQAAAQEEAAHBBAAAQQQAAQEEAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAQEEAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAQEEAAEBBAABAQAAAQEEAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAAAQQQAAQEEAAEBBAAAQQQAAQEEAAHBBAAAQQQAAQEEAAEBBAAAQQQAAQEEAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAAAQQQAAQEEAAEBBAAAQQQAAQEEAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAQEEAAEBBAAAQQQAAQEEAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAABAQAAAAAAAAHBBAAAQQQAAAAAAAHBBAABAQAAAAAAAAHBBAAAQQQAAAAAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAAAAAAAAQEAAAHBBAADAQAAAQEAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAAAAAAAAQEAAAHBBAAAAAAAAQEAAAJBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAADAQAAAQEAAAHBBAAAAAAAAQEAAAJBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAABAQAAAAAAAAHBBAABAQAAAwEAAAHBBAABAQAAAAAAAAHBBAABAQAAAAAAAAJBBAABAQAAAAAAAAHBBAABAQAAAAAAAAJBBAABAQAAAAAAAAHBBAABAQAAAAAAAAJBBAABAQAAAAAAAAHBBAABAQAAAAAAAAJBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAABAQAAAAAAAAEBBzI6Kv7OjAsFNXP1AAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAADAQAAAQEAAAHBBAABAQQAAQEAAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAADAQAAAQEAAAHBBAADAQAAAQEAAAJBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAABAQQAAQEAAAHBBAADAQAAAQEAAAJBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAADAQAAAEEEAAHBBAADAQAAAQEAAAJBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAHBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAHBBAABAQAAAAAAAAJBBAAAQQQAAAAAAAHBBAABAQAAAAAAAAJBBAAAQQQAAAAAAAHBBAABAQAAAAAAAAJBBAAAQQQAAAAAAAHBBAABAQAAAAAAAAJBBAAAQQQAAAAAAAHBBAAAQQQAAAAAAAJBBAAAQQQAAAAAAAHBBAAAQQQAAAAAAAJBBAAAQQQAAAAAAAHBBAAAQQQAAAAAAAJBBAAAQQQAAAAAAAHBBAAAQQQAAAAAAAJBBAAAQQQAAAAAAAHBBAABAQAAAwEAAAJBBAAAQQQAAAAAAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAAAQQQAAAAAAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAADAQAAAQEAAAJBBAAAQQQAAAAAAAHBBAABAQAAAwEAAAJBBAAAQQQAAAAAAAHBBAABAQAAAwEAAAJBBAADAQAAAEEEAAHBBAADAQAAAQEAAAJBBAAAQQQAAAAAAAHBBAABAQAAAwEAAAJBBAAAQQQAAAAAAAHBBAABAQAAAwEAAAJBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAAAAAAEBB2lFRQbOjAsFNXP1AAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAAAAAAQEAAAHBBAADAQAAAQEAAAHBBAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAQEAAAHBBAAAAAAAAEEEAAHBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAAAAAAAAQEAAAHBBAAAAAAAAQEAAAJBBAAAAAAAAQEAAAHBBAAAAAAAAQEAAAJBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAAAAAAAAQEAAAHBBAAAAAAAAQEAAAJBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAAAAAAAAQEAAAHBBAAAAAAAAQEAAAJBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAADAQAAAQEAAAHBBAABAQQAAQEAAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAABAQAAAwEAAAHBBAAAQQQAAwEAAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAABAQAAAwEAAAHBBAABAQAAAQEEAAHBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAABAQAAAwEAAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAAAQQQAAwEAAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAADAQAAAEEEAAHBBAABAQAAAQEEAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAAAAAAAAQEAAAJBBAADAQAAAQEAAAHBBAAAAAAAAQEAAAJBBAABAQAAAwEAAAHBBAABAQAAAAAAAAJBBAADAQAAAQEAAAHBBAAAAAAAAQEAAAJBBAABAQAAAwEAAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAAAAAAAAQEAAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAADAQAAAQEAAAJBBAADAQAAAQEAAAHBBAADAQAAAQEAAAJBBAAAQQQAAwEAAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAADAQAAAQEAAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAAAAAAAAEEEAAJBBAADAQAAAQEAAAHBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAHBBAABAQAAAQEEAAHBBAADAQAAAQEAAAHBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAHBBAABAQAAAAAAAAJBBAADAQAAAQEAAAHBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAAAAAAAAEEEAAJBBAABAQAAAQEEAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAHBBAAAAAAAAEEEAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAHBBAAAQQQAAQEEAAHBBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAHBBAAAQQQAAwEAAAJBBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAAAQQQAAQEEAAHBBAAAQQQAAwEAAAJBBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAABAQQAAQEAAAHBBAABAQQAAEEEAAHBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAABAQQAAQEAAAHBBAADAQAAAQEAAAJBBAABAQQAAQEAAAHBBAADAQAAAQEAAAJBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAJBBAABAQQAAQEAAAHBBAADAQAAAQEAAAJBBAAAQQQAAwEAAAHBBAAAQQQAAwEAAAJBBAABAQQAAQEAAAHBBAADAQAAAQEAAAJBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAABAQQAAQEAAAHBBAABAQQAAQEAAAJBB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AAAQQQAAwEAAAHBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAHBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAHBBAABAQAAAwEAAAJBBAAAQQQAAwEAAAHBBAABAQAAAwEAAAJBBAADAQAAAEEEAAHBBAADAQAAAQEAAAJBBAAAQQQAAwEAAAHBBAABAQAAAwEAAAJBBAADAQAAAEEEAAHBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAHBBAABAQAAAwEAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAHBBAAAQQQAAwEAAAJBBAAAQQQAAwEAAAHBBAAAQQQAAwEAAAJBBAABAQQAAEEEAAHBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAHBBAAAQQQAAwEAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAHBBAABAQAAAQEEAAJBBAAAQQQAAwEAAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAwEAAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAADAQAAAQEAAAJBBAAAQQQAAwEAAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAHBBAABAQAAAQEEAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAwEAAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAwEAAAEBBjJGAQQAAwEDPuZ1AAAAQQQAAwEAAAHBBAACYQQAAwEAAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAAAAAAAAEEEAAHBBAADAQAAAEEEAAHBBAAAAAAAAEEEAAHBBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAHBBAAAAAAAAQEAAAJBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAAAAAAAAEEEAAHBBAAAAAAAAQEAAAJBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAAAAAAAAEEEAAHBBAAAAAAAAQEAAAJBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAAAAAAAAEEEAAHBBAAAAAAAAEEEAAJBBAAAAAAAAEEEAAHBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAwEAAAEBBMUaCwAAAwEDPuZ1AAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAAAAAAAAEEEAAEBBs6MCwdpRUUFNXP1AAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAADAQAAAEEEAAHBBAABAQQAAEEEAAHBBAADAQAAAEEEAAHBBAADAQAAAQEAAAJBBAADAQAAAEEEAAHBBAADAQAAAQEAAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAAAAAAAAEEEAAJBBAADAQAAAEEEAAHBBAAAAAAAAEEEAAJBBAABAQAAAQEEAAHBBAABAQAAAwEAAAJBBAADAQAAAEEEAAHBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAADAQAAAEEEAAJBBAADAQAAAEEEAAHBBAADAQAAAEEEAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAADAQAAAEEEAAEBBAADAQIyRgEHPuZ1AAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAABAQQAAEEEAAHBBAABAQQAAQEAAAJBBAABAQQAAEEEAAHBBAABAQQAAQEAAAJBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAABAQQAAEEEAAHBBAABAQQAAQEAAAJBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAQEEAAJBB2lFRQdpRoUHtqLBBAABAQQAAEEEAAHBBAABAQQAAQEAAAJBBAAAQQQAAQEEAAJBB2lFRQdpRoUHtqLBBAABAQQAAEEEAAHBBAADAQAAAEEEAAJBBAABAQQAAEEEAAHBBAADAQAAAEEEAAJBBAAAQQQAAQEEAAHBBAAAQQQAAwEAAAJBBAABAQQAAEEEAAHBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAABAQQAAEEEAAHBBAABAQQAAEEEAAJBBAABAQQAAEEEAAHBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAABAQQAAEEEAAHBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAABAQQAAEEEAAHBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAQEEAAJBB2lFRQdpRoUHtqLBBAABAQQAAEEEAAHBBAABAQQAAEEEAAJBBAAAQQQAAQEEAAJBB2lFRQdpRoUHtqLBBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAAAQQQAAQEEAAJBB2lFRQdpRoUHtqLBBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAAAQQQAAQEEAAJBB2lFRQdpRoUHtqLBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAABAQAAAQEEAAHBBAAAQQQAAQEEAAHBBAABAQAAAQEEAAHBBAAAQQQAAQEEAAHBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQAAAQEEAAHBBAABAQAAAwEAAAJBBAABAQAAAQEEAAHBBAABAQAAAwEAAAJBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQAAAQEEAAHBBAABAQAAAwEAAAJBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAABAQAAAQEEAAHBBAABAQAAAwEAAAJBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQAAAQEEAAHBBAABAQAAAQEEAAJBBAABAQAAAQEEAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQAAAQEEAAHBBAABAQAAAQEEAAJBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAABAQAAAQEEAAHBBAABAQAAAQEEAAJBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAABAQAAAQEEAAEBBzI6Kv9pRoUFNXP1AAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAABAQAAAQEEAAHBB9G68v96Np0EAAHBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAAAAAAAAEEEAAHBBvRsPwd6NV0EAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAAAQQQAAQEEAAHBBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAHBBAAAQQQAAwEAAAJBBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAAAQQQAAQEEAAHBBAAAQQQAAwEAAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAQEEAAHBBAAAQQQAAwEAAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAAAQQQAAQEEAAHBBAABAQAAAQEEAAJBBAAAQQQAAQEEAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAQEEAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAQEEAAHBBAABAQAAAQEEAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAAAQQQAAQEEAAHBBAAAQQQAAQEEAAJBBAAAQQQAAQEEAAHBBAAAQQQAAQEEAAJBBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAAAQQQAAQEEAAHBBAAAQQQAAQEEAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAQEEAAHBBAAAQQQAAQEEAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAAAQQQAAQEEAAEBB2lFRQdpRoUFNXP1AAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAAAQQQAAQEEAAHBB3o1XQd6Np0EAAHBBAABAQQAAEEEAAHBB3o2nQd6NV0EAAHBBAABAQQAAEEEAAEBB2lGhQdpRUUFNXP1AAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAABAQAAAAAAAAJBBAAAQQQAAAAAAAJBBAABAQAAAAAAAAJBBAAAQQQAAAAAAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAAAAAAAAQEAAAJBBAADAQAAAQEAAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAABAQAAAAAAAAJBBAABAQAAAwEAAAJBBAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAABAQAAAAAAAAHBB9G68v70bD8EAAHBBAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAADAQAAAQEAAAJBBAABAQQAAQEAAAJBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAAAAAAHBB3o1XQb0bD8EAAHBBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAAAAAAQEAAAJBBAADAQAAAQEAAAJBBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAADAQAAAQEAAAJBBAABAQQAAQEAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAABAQAAAwEAAAJBBAAAQQQAAwEAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAQEEAAJBB2lFRQdpRoUHtqLBBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAQEEAAJBB2lFRQdpRoUHtqLBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAABAQAAAwEAAAJBBAAAQQQAAwEAAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAAAAAAAAEEEAAJBBAADAQAAAEEEAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAADAQAAAEEEAAHBBAADAQAAAmEEAAHBBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAABAQAAAwEAAAHBBAADgwAAAwEAAAHBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAAAQQQAAwEAAAJBB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2lFRQbOjAsHtqLBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAADAQAAAQEAAAJBBAABAQQAAQEAAAJBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAADAQAAAQEAAAJBBAADAQAAAQEAAAKhBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAABAQQAAQEAAAJBBAADAQAAAQEAAAKhBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAADAQAAAEEEAAJBBAADAQAAAQEAAAKhBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAJBBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAJBBAABAQAAAAAAAAKhBAAAQQQAAAAAAAJBBAABAQAAAAAAAAKhBAAAQQQAAAAAAAJBBAABAQAAAAAAAAKhBAAAQQQAAAAAAAJBBAABAQAAAAAAAAKhBAAAQQQAAAAAAAJBBAAAQQQAAAAAAAKhBAAAQQQAAAAAAAJBBAAAQQQAAAAAAAKhBAAAQQQAAAAAAAJBBAAAQQQAAAAAAAKhBAAAQQQAAAAAAAJBBAAAQQQAAAAAAAKhBAAAQQQAAAAAAAJBBAABAQAAAwEAAAKhBAAAQQQAAAAAAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAAAQQQAAAAAAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAADAQAAAQEAAAKhBAAAQQQAAAAAAAJBBAABAQAAAwEAAAKhBAAAQQQAAAAAAAJBBAABAQAAAwEAAAKhBAADAQAAAEEEAAJBBAADAQAAAQEAAAKhBAAAQQQAAAAAAAJBBAABAQAAAwEAAAKhBAAAQQQAAAAAAAJBBAABAQAAAwEAAAKhBAAAAAAAAQEAAAJBBAADAQAAAQEAAAJBBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAAAAAAAAQEAAAJBBAAAAAAAAEEEAAJBBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAAAAAAAAQEAAAJBBAAAAAAAAQEAAAKhBAAAAAAAAQEAAAJBBAAAAAAAAQEAAAKhBAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAAAAAAAAQEAAAJBBAAAAAAAAQEAAAKhBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAAAAAAAAQEAAAJBBAAAAAAAAQEAAAKhBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAADAQAAAQEAAAJBBAABAQQAAQEAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAABAQAAAwEAAAJBBAAAQQQAAwEAAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAABAQAAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAAAQQQAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAADAQAAAEEEAAJBBAABAQAAAQEEAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAAAAAAAAQEAAAKhBAADAQAAAQEAAAJBBAAAAAAAAQEAAAKhBAABAQAAAwEAAAJBBAABAQAAAAAAAAKhBAADAQAAAQEAAAJBBAAAAAAAAQEAAAKhBAABAQAAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAAAAAAAAQEAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAADAQAAAQEAAAKhBAADAQAAAQEAAAJBBAADAQAAAQEAAAKhBAAAQQQAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAADAQAAAQEAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAAAAAAAAEEEAAKhBAADAQAAAQEAAAJBBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAADAQAAAQEAAAJBBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBAABAQAAAAAAAAKhBAADAQAAAQEAAAJBBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAAAAAAAAEEEAAKhBAABAQAAAQEEAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAJBBAAAAAAAAEEEAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBAAAQQQAAwEAAAKhBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAQEEAAJBBAAAQQQAAwEAAAKhBAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAABAQQAAQEAAAJBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAABAQQAAQEAAAJBBAADAQAAAQEAAAKhBAABAQQAAQEAAAJBBAADAQAAAQEAAAKhBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAKhBAABAQQAAQEAAAJBBAADAQAAAQEAAAKhBAAAQQQAAwEAAAJBBAAAQQQAAwEAAAKhBAABAQQAAQEAAAJBBAADAQAAAQEAAAKhBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAABAQQAAQEAAAJBBAABAQQAAQEAAAKhBAABAQQAAQEAAAJBBAABAQQAAQEAAAKhBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAABAQQAAQEAAAJBBAABAQQAAQEAAAKhBAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAABAQQAAQEAAAJBBAADAQAAAEEEAAKhBAABAQQAAQEAAAJBBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAABAQQAAQEAAAJBBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAKhBAABAQQAAQEAAAJBBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAAAQQQAAwEAAAKhBAABAQQAAQEAAAJBBAADAQAAAEEEAAKhBAAAQQQAAQEEAAJBBAAAQQQAAwEAAAKhBAABAQQAAQEAAAJBBAADAQAAAEEEAAKhBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAABAQAAAwEAAAJBBAAAQQQAAwEAAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAAAAAAAAEEEAAJBBAADAQAAAEEEAAJBBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAAAAAAAAEEEAAJBBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBAABAQAAAQEEAAJBBAADAQAAAEEEAAJBBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBAABAQAAAAAAAAKhBAABAQAAAwEAAAJBBAABAQAAAAAAAAKhBAABAQAAAwEAAAJBBAABAQAAAAAAAAKhBAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBAABAQAAAAAAAAKhBAABAQAAAwEAAAJBBAABAQAAAAAAAAKhBAABAQAAAwEAAAJBBAABAQAAAwEAAAKhBAABAQAAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAEEEAAJBBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBAABAQAAAwEAAAKhBAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAAAAAAAAEEEAAJBBs6MCwdpRUUHtqLBBAAAQQQAAwEAAAJBBjJGAQQAAwECMkchBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAADAQAAAEEEAAJBBAABAQQAAEEEAAJBBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAADAQAAAEEEAAJBBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAABAQQAAEEEAAJBBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAADAQAAAEEEAAKhBAADAQN6NV0Heje9BAAAQQQAAwEAAAJBBAAAQQQAAQEEAAJBBAADAQAAAEEEAAKhBAADAQN6NV0Heje9BAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAKhBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAJBBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAJBBAABAQAAAwEAAAKhBAAAQQQAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAEEEAAJBBAADAQAAAQEAAAKhBAAAQQQAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAEEEAAJBBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAABAQAAAwEAAAKhBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAAAQQQAAwEAAAKhBAAAQQQAAwEAAAJBBAAAQQQAAwEAAAKhBAABAQQAAEEEAAJBBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAAAQQQAAwEAAAKhBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAAAQQQAAwEAAAJBBAABAQAAAQEEAAKhBAAAQQQAAwEAAAJBBAABAQAAAQEEAAKhBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAwEAAAJBBAABAQAAAQEEAAKhBAADAQAAAEEEAAJBBAADAQAAAQEAAAKhB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AADAQN6NV0Heje9BAABAQQAAEEEAAJBB2lGhQdpRUUHtqLBBAAAQQQAAQEEAAJBBAABAQAAAQEEAAKhBAAAQQQAAQEEAAJBBAABAQAAAQEEAAKhBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAQEEAAJBBAABAQAAAQEEAAKhBAADAQAAAEEEAAKhBAADAQN6NV0Heje9BAAAQQQAAQEEAAJBBAABAQAAAQEEAAKhBAADAQAAAEEEAAKhBAADAQN6NV0Heje9BAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAAAQQQAAQEEAAJBBAAAQQQAAQEEAAKhBAAAQQQAAQEEAAJBBAAAQQQAAQEEAAKhBAABAQQAAEEEAAJBB2lGhQdpRUUHtqLBBAAAQQQAAQEEAAJBBAAAQQQAAQEEAAKhBAADAQAAAEEEAAKhBAADAQN6NV0Heje9BAAAQQQAAQEEAAJBBAAAQQQAAQEEAAKhBAADAQAAAEEEAAKhBAADAQN6NV0Heje9BAABAQQAAEEEAAJBB2lGhQdpRUUHtqLBBAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAABAQAAAAAAAAKhBAAAQQQAAAAAAAKhBAABAQAAAAAAAAKhBAAAQQQAAAAAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAAAAAAAAQEAAAKhBAADAQAAAQEAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAAAAAAAAQEAAAKhBAAAAAAAAQEAAAMBBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAADAQAAAQEAAAKhBAAAAAAAAQEAAAMBBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAABAQAAAAAAAAKhBAABAQAAAwEAAAKhBAABAQAAAAAAAAKhBAABAQAAAAAAAAMBBAABAQAAAAAAAAKhBAABAQAAAAAAAAMBBAABAQAAAAAAAAKhBAABAQAAAAAAAAMBBAABAQAAAAAAAAKhBAABAQAAAAAAAAMBBAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAABAQAAAAAAAAJBBzI6Kv7OjAsHtqLBBAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAADAQAAAQEAAAKhBAABAQQAAQEAAAKhBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAADAQAAAQEAAAKhBAADAQAAAQEAAAMBBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAABAQQAAQEAAAKhBAADAQAAAQEAAAMBBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAADAQAAAEEEAAKhBAADAQAAAQEAAAMBBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAAAQQQAAAAAAAKhBAAAQQQAAwEAAAKhBAAAQQQAAAAAAAKhBAABAQAAAAAAAAMBBAAAQQQAAAAAAAKhBAABAQAAAAAAAAMBBAAAQQQAAAAAAAKhBAABAQAAAAAAAAMBBAAAQQQAAAAAAAKhBAABAQAAAAAAAAMBBAAAQQQAAAAAAAKhBAAAQQQAAAAAAAMBBAAAQQQAAAAAAAKhBAAAQQQAAAAAAAMBBAAAQQQAAAAAAAKhBAAAQQQAAAAAAAMBBAAAQQQAAAAAAAKhBAAAQQQAAAAAAAMBBAAAQQQAAAAAAAKhBAABAQAAAwEAAAMBBAAAQQQAAAAAAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAAAQQQAAAAAAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAADAQAAAQEAAAMBBAAAQQQAAAAAAAKhBAABAQAAAwEAAAMBBAAAQQQAAAAAAAKhBAABAQAAAwEAAAMBBAADAQAAAEEEAAKhBAADAQAAAQEAAAMBBAAAQQQAAAAAAAKhBAABAQAAAwEAAAMBBAAAQQQAAAAAAAKhBAABAQAAAwEAAAMBBAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAAAAAAJBB2lFRQbOjAsHtqLBBAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAAAAAAAAQEAAAKhBAADAQAAAQEAAAKhBAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAABAQAAAwEAAAJBBMUaCwAAAwECMkchBAAAAAAAAQEAAAKhBAAAAAAAAEEEAAKhBAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAAAAAAAAQEAAAKhBAAAAAAAAQEAAAMBBAAAAAAAAQEAAAKhBAAAAAAAAQEAAAMBBAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAAAAAAAAQEAAAKhBAAAAAAAAQEAAAMBBAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAAAAAAAAQEAAAKhBAAAAAAAAQEAAAMBBAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAADAQAAAQEAAAKhBAABAQQAAQEAAAKhBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAABAQAAAwEAAAKhBAAAQQQAAwEAAAKhBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAABAQAAAwEAAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAAAQQQAAwEAAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAADAQAAAEEEAAKhBAABAQAAAQEEAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAAAAAAAAQEAAAMBBAADAQAAAQEAAAKhBAAAAAAAAQEAAAMBBAABAQAAAwEAAAKhBAABAQAAAAAAAAMBBAADAQAAAQEAAAKhBAAAAAAAAQEAAAMBBAABAQAAAwEAAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAAAAAAAAQEAAAMBBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAADAQAAAQEAAAMBBAADAQAAAQEAAAKhBAADAQAAAQEAAAMBBAAAQQQAAwEAAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAADAQAAAQEAAAMBBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAAAAAAAAEEEAAMBBAADAQAAAQEAAAKhBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAADAQAAAQEAAAKhBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhBAABAQAAAAAAAAMBBAADAQAAAQEAAAKhBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAAAAAAAAEEEAAMBBAABAQAAAQEEAAKhBAABAQAAAwEAAAMBBAADAQAAAQEAAAKhBAAAAAAAAEEEAAMBBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAAAQQQAAwEAAAKhBAAAQQQAAQEEAAKhBAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAAAQQQAAwEAAAKhBAAAQQQAAwEAAAMBBAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAAAQQQAAQEEAAKhBVlVFQVZVlUFWVd1BAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAAAQQQAAQEEAAKhBVlVFQVZVlUFWVd1BAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAAAQQQAAQEEAAKhBAAAQQQAAwEAAAMBBAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAABAQQAAQEAAAKhBAABAQQAAEEEAAKhBAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAABAQQAAQEAAAKhBAADAQAAAQEAAAMBBAABAQQAAQEAAAKhBAADAQAAAQEAAAMBBAAAQQQAAwEAAAKhBAAAQQQAAAAAAAMBBAABAQQAAQEAAAKhBAADAQAAAQEAAAMBBAAAQQQAAwEAAAKhBAAAQQQAAwEAAAMBBAABAQQAAQEAAAKhBAADAQAAAQEAAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAABAQQAAQEAAAKhBAABAQQAAQEAAAMBBAABAQQAAQEAAAKhBAABAQQAAQEAAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAABAQQAAQEAAAKhBAABAQQAAQEAAAMBBAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAABAQQAAQEAAAKhBAADAQAAAEEEAAMBBAABAQQAAQEAAAKhBAADAQAAAEEEAAMBBAAAQQQAAwEAAAKhBAAAQQQAAQEEAAKhBAABAQQAAQEAAAKhBAADAQAAAEEEAAMBBAAAQQQAAwEAAAKhBAAAQQQAAAAAAAMBBAABAQQAAQEAAAKhBAADAQAAAEEEAAMBBAAAQQQAAwEAAAKhBAAAQQQAAwEAAAMBBAABAQQAAQEAAAKhBAADAQAAAEEEAAMBBAAAQQQAAQEEAAKhBAAAQQQAAwEAAAMBBAABAQQAAQEAAAKhBAADAQAAAEEEAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAAAQQQAAwEAAAKhB3o1XQQAAwEDeje9BAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAABAQAAAwEAAAKhBAAAQQQAAwEAAAKhBAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAAAAAAAAEEEAAKhBVlXVwFZVRUFWVd1BAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAAAAAAAAEEEAAKhBAADAQAAAEEEAAKhBAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAAAAAAAAEEEAAKhBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAAAAAAAAEEEAAKhBVlXVwFZVRUFWVd1BAADAQAAAEEEAAJBBAADAQIyRgEGMkchBAABAQAAAwEAAAKhBAABAQAAAQEEAAKhBAADAQAAAEEEAAKhBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhBAABAQAAAAAAAAMBBAABAQAAAwEAAAKhBAABAQAAAAAAAAMBBAABAQAAAwEAAAKhBAABAQAAAAAAAAMBBAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhBAABAQAAAAAAAAMBBAABAQAAAwEAAAKhBAABAQAAAAAAAAMBBAABAQAAAwEAAAKhBAABAQAAAwEAAAMBBAABAQAAAwEAAAKhBAABAQAAAwEAAAMBBAADAQAAAEEEAAKhBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhBAABAQAAAwEAAAMBBAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAABAQAAAwEAAAJBB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WJ+nPhUMq8BEEgBCAABAQAAAAAAAAKhBsKqqvlZV1cBWVd1BAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAADAQAAAQEAAAMBBAABAQQAAQEAAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAAKhBVlVFQVZV1cBWVd1BAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAAAAAAAAQEAAAMBBAADAQAAAQEAAAMBBAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAABAQAAAwEAAAKhB9G68vwAAwEDeje9BAADAQAAAQEAAAMBBAABAQQAAQEAAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAABAQAAAwEAAAMBBAAAQQQAAwEAAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAABAQAAAwEAAAMBBAABAQAAAQEEAAMBBAABAQQAAQEAAAMBBAABAQQAAEEEAAMBB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AAAQQQAAQEEAAMBBBcM6QQXDikFEEgBCAABAQQAAEEEAAMBBBcOKQQXDOkFEEgBCAAAQQQAAQEEAAMBBBcM6QQXDikFEEgBCAABAQQAAEEEAAMBBBcOKQQXDOkFEEgBCAABAQQAAEEEAAKhBVlWVQVZVRUFWVd1BAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAABAQAAAAAAAAMBBAAAQQQAAAAAAAMBBAABAQAAAAAAAAMBBAAAQQQAAAAAAAMBBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAAAAAAAAQEAAAMBBAADAQAAAQEAAAMBBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAAAAAAAAQEAAAMBBAAAAAAAAQEAAANhBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAADAQAAAQEAAAMBBAAAAAAAAQEAAANhBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAABAQAAAAAAAAMBBAABAQAAAwEAAAMBBAABAQAAAAAAAAMBBAABAQAAAAAAAANhBAABAQAAAAAAAAMBBAABAQAAAAAAAANhBAABAQAAAAAAAAMBBAABAQAAAAAAAANhBAABAQAAAAAAAAMBBAABAQAAAAAAAANhBAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAADAQAAAQEAAAMBBAABAQQAAQEAAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAADAQAAAQEAAAMBBAADAQAAAQEAAANhBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAABAQQAAQEAAAMBBAADAQAAAQEAAANhBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAADAQAAAEEEAAMBBAADAQAAAQEAAANhBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAAAQQQAAAAAAAMBBAAAQQQAAwEAAAMBBAAAQQQAAAAAAAMBBAABAQAAAAAAAANhBAAAQQQAAAAAAAMBBAABAQAAAAAAAANhBAAAQQQAAAAAAAMBBAABAQAAAAAAAANhBAAAQQQAAAAAAAMBBAABAQAAAAAAAANhBAAAQQQAAAAAAAMBBAAAQQQAAAAAAANhBAAAQQQAAAAAAAMBBAAAQQQAAAAAAANhBAAAQQQAAAAAAAMBBAAAQQQAAAAAAANhBAAAQQQAAAAAAAMBBAAAQQQAAAAAAANhBAAAQQQAAAAAAAMBBAABAQAAAwEAAANhBAAAQQQAAAAAAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAAAQQQAAAAAAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAADAQAAAQEAAANhBAAAQQQAAAAAAAMBBAABAQAAAwEAAANhBAAAQQQAAAAAAAMBBAABAQAAAwEAAANhBAADAQAAAEEEAAMBBAADAQAAAQEAAANhBAAAQQQAAAAAAAMBBAABAQAAAwEAAANhBAAAQQQAAAAAAAMBBAABAQAAAwEAAANhBAAAAAAAAQEAAAMBBAADAQAAAQEAAAMBBAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAAAAAAAAQEAAAMBBAAAAAAAAEEEAAMBBAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAAAAAAAAQEAAAMBBAAAAAAAAQEAAANhBAAAAAAAAQEAAAMBBAAAAAAAAQEAAANhBAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAAAAAAAAQEAAAMBBAAAAAAAAQEAAANhBAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAAAAAAAAQEAAAMBBAAAAAAAAQEAAANhBAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAADAQAAAQEAAAMBBAABAQQAAQEAAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAABAQAAAwEAAAMBBAAAQQQAAwEAAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAABAQAAAwEAAAMBBAABAQAAAQEEAAMBBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAABAQAAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAAAQQQAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAADAQAAAEEEAAMBBAABAQAAAQEEAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAAAAAAAAQEAAANhBAADAQAAAQEAAAMBBAAAAAAAAQEAAANhBAABAQAAAwEAAAMBBAABAQAAAAAAAANhBAADAQAAAQEAAAMBBAAAAAAAAQEAAANhBAABAQAAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAAAAAAAAQEAAANhBAABAQAAAAAAAANhBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAADAQAAAQEAAANhBAADAQAAAQEAAAMBBAADAQAAAQEAAANhBAAAQQQAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAADAQAAAQEAAANhBAABAQAAAAAAAANhBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAAAAAAAAEEEAANhBAADAQAAAQEAAAMBBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBAABAQAAAQEEAAMBBAADAQAAAQEAAAMBBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBAABAQAAAAAAAANhBAADAQAAAQEAAAMBBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAAAAAAAAEEEAANhBAABAQAAAQEEAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAAMBBAAAAAAAAEEEAANhBAABAQAAAAAAAANhBAABAQAAAwEAAANhBAABAQQAAQEAAAMBBAABAQQAAEEEAAMBBAABAQQAAQEAAAMBBAABAQQAAEEEAAMBBAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAABAQQAAQEAAAMBBAABAQQAAEEEAAMBBAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAABAQQAAQEAAAMBBAABAQQAAEEEAAMBBAAAQQQAAwEAAAMBBAAAQQQAAwEAAANhBAABAQQAAQEAAAMBBAABAQQAAEEEAAMBBAAAQQQAAQEEAAMBBAAAQQQAAwEAAANhBAABAQQAAQEAAAMBBAABAQQAAEEEAAMBBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAABAQQAAQEAAAMBBAABAQQAAEEEAAMBBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAABAQQAAQEAAAMBBAADAQAAAQEAAANhBAABAQQAAQEAAAMBBAADAQAAAQEAAANhBAAAQQQAAwEAAAMBBAAAQQQAAAAAAANhBAABAQQAAQEAAAMBBAADAQAAAQEAAANhBAAAQQQAAwEAAAMBBAAAQQQAAwEAAANhBAABAQQAAQEAAAMBBAADAQAAAQEAAANhBAAAQQQAAAAAAANhBAAAQQQAAwEAAANhBAABAQQAAQEAAAMBBAABAQQAAQEAAANhBAABAQQAAQEAAAMBBAABAQQAAQEAAANhBAAAQQQAAAAAAANhBAAAQQQAAwEAAANhBAABAQQAAQEAAAMBBAABAQQAAQEAAANhBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAABAQQAAQEAAAMBBAADAQAAAEEEAANhBAABAQQAAQEAAAMBBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAABAQQAAQEAAAMBBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAAAQQQAAAAAAANhBAABAQQAAQEAAAMBBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAAAQQQAAwEAAANhBAABAQQAAQEAAAMBBAADAQAAAEEEAANhBAAAQQQAAQEEAAMBBAAAQQQAAwEAAANhBAABAQQAAQEAAAMBBAADAQAAAEEEAANhBAAAQQQAAAAAAANhBAAAQQQAAwEAAANhBAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAABAQAAAwEAAAMBBAAAQQQAAwEAAAMBBAABAQAAAwEAAAMBBAABAQAAAQEEAAMBBAABAQAAAwEAAAMBBAABAQAAAQEEAAMBBAAAAAAAAEEEAAMBBAADAQAAAEEEAAMBBAABAQAAAwEAAAMBBAABAQAAAQEEAAMBBAAAAAAAAEEEAAMBBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBAABAQAAAQEEAAMBBAADAQAAAEEEAAMBBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBAABAQAAAAAAAANhBAABAQAAAwEAAAMBBAABAQAAAAAAAANhBAABAQAAAwEAAAMBBAABAQAAAAAAAANhBAAAAAAAAQEAAANhBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBAABAQAAAAAAAANhBAABAQAAAwEAAAMBBAABAQAAAAAAAANhBAABAQAAAwEAAAMBBAABAQAAAwEAAANhBAABAQAAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAEEEAAMBBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBAABAQAAAwEAAANhBAAAAAAAAQEAAANhBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAABAQAAAAAAAAMBBWJ+nPhUMq8BEEgBCAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAAAAAAAAEEEAAMBBFQyrwAXDOkFEEgBCAAAQQQAAwEAAAMBBsJhCQQAAwECE8gVCAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAADAQAAAEEEAAMBBAADAQLCYQkGE8gVCAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAADAQAAAEEEAAMBBAABAQQAAEEEAAMBBAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAADAQAAAEEEAAMBBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAABAQQAAEEEAAMBBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAADAQAAAEEEAANhBAADAQELONkFCzhJCAAAQQQAAwEAAAMBBAAAQQQAAQEEAAMBBAADAQAAAEEEAANhBAADAQELONkFCzhJCAADAQAAAEEEAAMBBAADAQLCYQkGE8gVCAAAQQQAAwEAAAMBBAAAQQQAAAAAAANhBAAAQQQAAwEAAAMBBAAAQQQAAAAAAANhBAAAQQQAAwEAAAMBBAAAQQQAAAAAAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAAAQQQAAAAAAANhBAAAQQQAAwEAAAMBBAAAQQQAAAAAAANhBAAAQQQAAwEAAAMBBAABAQAAAwEAAANhBAAAQQQAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAEEEAAMBBAADAQAAAQEAAANhBAAAQQQAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAEEEAAMBBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAABAQAAAwEAAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAAAQQQAAwEAAANhBAAAQQQAAwEAAAMBBAAAQQQAAwEAAANhBAABAQQAAEEEAAMBBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAAAQQQAAwEAAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAABAQAAAQEEAANhBAAAQQQAAwEAAAMBBAABAQAAAQEEAANhBAADAQAAAEEEAAMBBAADAQLCYQkGE8gVCAAAQQQAAwEAAAMBBAABAQAAAQEEAANhBAADAQAAAEEEAAMBBAADAQAAAQEAAANhBAAAQQQAAwEAAAMBBAABAQAAAQEEAANhBAADAQAAAEEEAAMBBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAABAQAAAQEEAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAAAQQQAAwEAAAMBBAABAQAAAQEEAANhBAADAQAAAEEEAANhBAADAQELONkFCzhJCAAAQQQAAwEAAAMBBAABAQAAAQEEAANhBAADAQAAAEEEAANhBAADAQELONkFCzhJCAADAQAAAEEEAAMBBAADAQLCYQkGE8gVCAAAAAAAAEEEAAMBBFQyrwAXDOkFEEgBCAAAAAAAAEEEAAMBBAADAQAAAEEEAAMBBAAAAAAAAEEEAAMBBAAAAAAAAQEAAANhB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AABAQAAAAAAAANhBvFxRP9Goi8A06g5CAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAABAQAAAAAAAANhBvFxRP9Goi8A06g5CAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBAAAQQQAAwEAAANhBAAAQQQAAAAAAANhBAAAQQQAAwEAAANhBAADAQAAAQEAAANhBAABAQQAAQEAAANhBAAAQQQAAAAAAANhBAAAQQQAAwEAAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAAAQQQAAAAAAANhBAAAQQQAAwEAAANhBAADAQAAAQEAAANhBAADAQAAAQEAAAPBBAAAQQQAAAAAAANhBAAAQQQAAwEAAANhBAABAQQAAQEAAANhBAADAQAAAQEAAAPBBAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAAMBBBcM6QRUMq8BEEgBCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAAAAAAQEAAANhBAADAQAAAQEAAANhBAAAAAAAAQEAAANhBAAAAAAAAEEEAANhBAAAAAAAAQEAAANhBAAAAAAAAEEEAANhBAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAAAAAAAAQEAAANhBAAAAAAAAEEEAANhBAABAQAAAAAAAANhBvFxRP9Goi8A06g5CAAAAAAAAQEAAANhBAAAAAAAAEEEAANhBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAAAAAAAAQEAAANhBAAAAAAAAEEEAANhBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAABAQAAAwEAAAMBBICwmvgAAwECE8gVCAAAAAAAAQEAAANhBAAAAAAAAEEEAANhBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAABAQAAAAAAAANhBvFxRP9Goi8A06g5CAAAAAAAAQEAAANhBAAAAAAAAQEAAAPBBAAAAAAAAQEAAANhBAAAAAAAAQEAAAPBBAABAQAAAAAAAANhBvFxRP9Goi8A06g5CAAAAAAAAQEAAANhBAAAAAAAAQEAAAPBBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAAAAAAAAQEAAANhBAAAAAAAAQEAAAPBBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAABAQAAAAAAAANhBvFxRP9Goi8A06g5CAADAQAAAQEAAANhBAABAQQAAQEAAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAADAQAAAQEAAANhBAADAQAAAEEEAANhBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAADAQAAAQEAAANhBAADAQAAAEEEAANhBAABAQAAAwEAAANhBAAAQQQAAwEAAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAABAQAAAwEAAANhBAABAQAAAQEEAANhBAADAQAAAQEAAANhBAADAQAAAEEEAANhBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAADAQAAAQEAAANhBAADAQAAAEEEAANhBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAABAQAAAQEEAANhBvFxRPzTqgkE06g5CAADAQAAAQEAAANhBAADAQAAAEEEAANhBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAADAQAAAQEAAANhBAADAQAAAEEEAANhBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAADAQAAAQEAAANhBAADAQAAAEEEAANhBAABAQAAAQEEAANhBvFxRPzTqgkE06g5CAADAQAAAQEAAANhBAAAAAAAAQEAAAPBBAADAQAAAQEAAANhBAAAAAAAAQEAAAPBBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAADAQAAAQEAAANhBAAAAAAAAEEEAAPBBAADAQAAAQEAAANhBAAAAAAAAEEEAAPBBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAADAQAAAQEAAANhBAAAAAAAAEEEAAPBBAABAQAAAwEAAANhB3BsTPwAAwEBCzhJCAABAQAAAQEEAANhBvFxRPzTqgkE06g5CAADAQAAAQEAAANhBAAAAAAAAEEEAAPBBAABAQAAAQEEAANhBvFxRPzTqgkE06g5CAADAQAAAQEAAANhBAADAQAAAQEAAAPBBAADAQAAAQEAAANhBAADAQAAAQEAAAPBBAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAADAQAAAQEAAANhBAADAQAAAQEAAAPBBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAADAQAAAQEAAANhBAADAQAAAQEAAAPBBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAAAQQQAAAAAAANhBNOoyQdGoi8A06g5CAABAQQAAQEAAANhBAABAQQAAEEEAANhBAABAQQAAQEAAANhBAABAQQAAEEEAANhBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAABAQQAAQEAAANhBAABAQQAAEEEAANhBAAAQQQAAwEAAANhBQs42QQAAwEBCzhJCAAAQQQAAQEEAANhBNOoyQTTqgkE06g5CAABAQQAAQEAAANhBAABAQQAAEEEAANhBAAAQQQAAQEEAANhBNOoyQTTqgkE06g5CAABAQQAAQEAAANhBAADAQAAAQEAAAPBBAABAQQAAQEAAANhBAADAQAAAQEAAAPBBAAAQQQAAwEAAANhB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"},{"byteLength":16032,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,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AADAQDFGgsDPuZ1AAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAAAAAAAAQEAAAHBBvRsPwfRuvL8AAHBBAAAAAAAAQEAAAHBBvRsPwfRuvL8AAHBBAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAAAAAAAAQEAAAHBBvRsPwfRuvL8AAHBBAAAAAAAAQEAAAHBBvRsPwfRuvL8AAHBBAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAAAAAAAAQEAAAEBBs6MCwcyOir9NXP1AAAAAAAAAQEAAAHBBvRsPwfRuvL8AAHBBAAAAAAAAQEAAAHBBvRsPwfRuvL8AAHBBAAAAAAAAQEAAAEBBs6MCwcyOir9NXP1AAABAQQAAQEAAAEBB2lGhQcyOir9NXP1AAADAQAAAQEAAAHBBAADAQAAA4MAAAHBBAADAQAAAQEAAAHBBAADAQAAA4MAAAHBBAABAQQAAQEAAAEBB2lGhQcyOir9NXP1AAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAADAQAAAQEAAAHBBAADAQAAA4MAAAHBBAADAQAAAQEAAAHBBAADAQAAA4MAAAHBBAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAABAQQAAQEAAAEBB2lGhQcyOir9NXP1AAADAQAAAQEAAAHBBAADAQAAA4MAAAHBBAADAQAAAQEAAAHBBAADAQAAA4MAAAHBBAABAQQAAQEAAAEBB2lGhQcyOir9NXP1AAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAADAQAAAQEAAAHBBAADAQAAA4MAAAHBBAADAQAAAQEAAAHBBAADAQAAA4MAAAHBBAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAAAAAAAAQEAAAEBBs6MCwcyOir9NXP1AAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAABAQQAAQEAAAEBB2lGhQcyOir9NXP1AAABAQQAAQEAAAEBB2lGhQcyOir9NXP1AAABAQQAAQEAAAEBB2lGhQcyOir9NXP1AAABAQQAAQEAAAEBB2lGhQcyOir9NXP1AAADAQAAAQEAAAEBBAADAQDFGgsDPuZ1AAAAAAAAAQEAAAHBBvRsPwfRuvL8AAHBBAAAAAAAAQEAAAHBBvRsPwfRuvL8AAHBB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FQyrwFifpz5EEgBCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAAAAAAAAQEAAAMBBFQyrwFifpz5EEgBCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAABAQQAAQEAAANhBNOqCQbxcUT806g5CAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAABAQQAAQEAAANhBNOqCQbxcUT806g5CAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAABAQQAAQEAAANhBNOqCQbxcUT806g5CAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAAAAAAAAQEAAAMBBFQyrwFifpz5EEgBCAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAAMBBAADAQCAsJr6E8gVCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAABAQQAAQEAAAMBBBcOKQVifpz5EEgBCAABAQQAAQEAAANhBNOqCQbxcUT806g5CAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAAAAAAAAQEAAANhB0aiLwLxcUT806g5CAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAAAAAAAAQEAAAPBB6LFpwBhOlj/MgxxCAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAADAQAAAQEAAANhBAADAQNwbEz9CzhJCAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAABAQQAAQEAAANhBNOqCQbxcUT806g5CAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAADAQAAAQEAAAPBBAADAQKr4hD8mOR9CAABAQQAAQEAAANhBNOqCQbxcUT806g5CAABAQQAAQEAAAPBBemx6QRhOlj/MgxxCAABAQQAAQEAAAPBBemx6QRhOlj/MgxxCAABAQQAAQEAAANhBNOqCQbxcUT806g5C"}],"materials":[{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0.5,0.5,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0},"material":0,"mode":6},{"attributes":{"POSITION":1},"material":0,"mode":6},{"attributes":{"POSITION":2},"material":0,"mode":6}]},{"primitives":[{"attributes":{"POSITION":3},"material":1,"mode":6},{"attributes":{"POSITION":4},"material":1,"mode":6},{"attributes":{"POSITION":5},"material":1,"mode":6}]},{"primitives":[{"attributes":{"POSITION":6},"material":2,"mode":1}]},{"primitives":[{"attributes":{"POSITION":7},"material":3,"mode":1}]}],"nodes":[{"mesh":0,"translation":[0,3,0]},{"mesh":1,"translation":[0,9,0]},{"mesh":0,"translation":[6,3,0]},{"mesh":1,"translation":[6,9,0]},{"mesh":0,"translation":[12,3,0]},{"mesh":1,"translation":[12,9,0]},{"mesh":1,"translation":[3,0,3]},{"mesh":1,"translation":[9,0,3]},{"mesh":0,"translation":[0,3,3]},{"mesh":0,"translation":[6,3,3]},{"mesh":0,"translation":[12,3,3]},{"mesh":1,"translation":[3,6,3]},{"mesh":1,"translation":[9,6,3]},{"mesh":1,"translation":[0,9,3]},{"mesh":1,"translation":[6,9,3]},{"mesh":1,"translation":[12,9,3]},{"mesh":1,"translation":[3,12,3]},{"mesh":1,"translation":[9,12,3]},{"mesh":1,"translation":[3,0,6]},{"mesh":1,"translation":[9,0,6]},{"mesh":0,"translation":[0,3,6]},{"mesh":0,"translation":[6,3,6]},{"mesh":0,"translation":[12,3,6]},{"mesh":1,"translation":[3,6,6]},{"mesh":1,"translation":[9,6,6]},{"mesh":1,"translation":[0,9,6]},{"mesh":1,"translation":[6,9,6]},{"mesh":1,"translation":[12,9,6]},{"mesh":1,"translation":[3,12,6]},{"mesh":1,"translation":[9,12,6]},{"mesh":1,"translation":[3,0,9]},{"mesh":1,"translation":[9,0,9]},{"mesh":0,"translation":[0,3,9]},{"mesh":0,"translation":[6,3,9]},{"mesh":0,"translation":[12,3,9]},{"mesh":1,"translation":[3,6,9]},{"mesh":1,"translation":[9,6,9]},{"mesh":1,"translation":[0,9,9]},{"mesh":1,"translation":[6,9,9]},{"mesh":1,"translation":[12,9,9]},{"mesh":1,"translation":[3,12,9]},{"mesh":1,"translation":[9,12,9]},{"mesh":1,"translation":[3,0,12]},{"mesh":1,"translation":[9,0,12]},{"mesh":0,"translation":[0,3,12]},{"mesh":0,"translation":[6,3,12]},{"mesh":0,"translation":[12,3,12]},{"mesh":1,"translation":[3,6,12]},{"mesh":1,"translation":[9,6,12]},{"mesh":1,"translation":[0,9,12]},{"mesh":1,"translation":[6,9,12]},{"mesh":1,"translation":[12,9,12]},{"mesh":1,"translation":[3,12,12]},{"mesh":1,"translation":[9,12,12]},{"mesh":1,"translation":[3,0,15]},{"mesh":1,"translation":[9,0,15]},{"mesh":0,"translation":[0,3,15]},{"mesh":0,"translation":[6,3,15]},{"mesh":0,"translation":[12,3,15]},{"mesh":1,"translation":[3,6,15]},{"mesh":1,"translation":[9,6,15]},{"mesh":1,"translation":[0,9,15]},{"mesh":1,"translation":[6,9,15]},{"mesh":1,"translation":[12,9,15]},{"mesh":1,"translation":[3,12,15]},{"mesh":1,"translation":[9,12,15]},{"mesh":1,"translation":[3,0,18]},{"mesh":1,"translation":[9,0,18]},{"mesh":0,"translation":[0,3,18]},{"mesh":0,"translation":[6,3,18]},{"mesh":0,"translation":[12,3,18]},{"mesh":1,"translation":[3,6,18]},{"mesh":1,"translation":[9,6,18]},{"mesh":1,"translation":[0,9,18]},{"mesh":1,"translation":[6,9,18]},{"mesh":1,"translation":[12,9,18]},{"mesh":1,"translation":[3,12,18]},{"mesh":1,"translation":[9,12,18]},{"mesh":1,"translation":[3,0,21]},{"mesh":1,"translation":[9,0,21]},{"mesh":0,"translation":[0,3,21]},{"mesh":0,"translation":[6,3,21]},{"mesh":0,"translation":[12,3,21]},{"mesh":1,"translation":[3,6,21]},{"mesh":1,"translation":[9,6,21]},{"mesh":1,"translation":[0,9,21]},{"mesh":1,"translation":[6,9,21]},{"mesh":1,"translation":[12,9,21]},{"mesh":1,"translation":[3,12,21]},{"mesh":1,"translation":[9,12,21]},{"mesh":1,"translation":[3,0,24]},{"mesh":1,"translation":[9,0,24]},{"mesh":0,"translation":[0,3,24]},{"mesh":0,"translation":[6,3,24]},{"mesh":0,"translation":[12,3,24]},{"mesh":1,"translation":[3,6,24]},{"mesh":1,"translation":[9,6,24]},{"mesh":1,"translation":[0,9,24]},{"mesh":1,"translation":[6,9,24]},{"mesh":1,"translation":[12,9,24]},{"mesh":1,"translation":[3,12,24]},{"mesh":1,"translation":[9,12,24]},{"mesh":1,"translation":[3,0,27]},{"mesh":1,"translation":[9,0,27]},{"mesh":0,"translation":[0,3,27]},{"mesh":0,"translation":[6,3,27]},{"mesh":0,"translation":[12,3,27]},{"mesh":1,"translation":[3,6,27]},{"mesh":1,"translation":[9,6,27]},{"mesh":1,"translation":[0,9,27]},{"mesh":1,"translation":[6,9,27]},{"mesh":1,"translation":[12,9,27]},{"mesh":1,"translation":[3,12,27]},{"mesh":1,"translation":[9,12,27]},{"mesh":0,"translation":[0,3,30]},{"mesh":1,"translation":[0,9,30]},{"mesh":0,"translation":[6,3,30]},{"mesh":1,"translation":[6,9,30]},{"mesh":0,"translation":[12,3,30]},{"mesh":1,"translation":[12,9,30]},{"mesh":2,"translation":[0,0,0]},{"mesh":3,"translation":[0,0,0]}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121]}]} ================================================ FILE: testdata/measurement_looping.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5625],"min":[0.25,0.5],"name":"tex_coords_gate_M","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.4375],"min":[0.625,0.375],"name":"tex_coords_gate_MPP:X","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.5],"min":[0.625,0.4375],"name":"tex_coords_gate_MPP:Y","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[1,-0,-0],"min":[-7,-8,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":78,"max":[0,2.5,1],"min":[-5.25,-9,-1],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_M","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:X","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:Y","target":34962},{"buffer":4,"byteLength":144,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":5,"byteLength":936,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_M","uri":"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MPP:X","uri":"data:application/octet-stream;base64,AAAwPwAAwD4AACA/AADAPgAAMD8AAOA+AAAgPwAAwD4AACA/AADgPgAAMD8AAOA+AAAwPwAA4D4AADA/AADAPgAAID8AAOA+AAAgPwAA4D4AADA/AADAPgAAID8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:Y","uri":"data:application/octet-stream;base64,AAAwPwAA4D4AACA/AADgPgAAMD8AAAA/AAAgPwAA4D4AACA/AAAAPwAAMD8AAAA/AAAwPwAAAD8AADA/AADgPgAAID8AAAA/AAAgPwAAAD8AADA/AADgPgAAID8AAOA+"},{"byteLength":144,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAwAAAAMEAAACAAACAwAAAwMAAAACAAACAPwAAAIAAAACAAADgwAAAAIAAAACAAACAPwAAAMAAAACAAADgwAAAAMAAAACAAACAPwAAgMAAAACAAADgwAAAgMAAAACAAACAPwAAwMAAAACAAADgwAAAwMAAAACAAACAPwAAAMEAAACAAADgwAAAAMEAAACA"},{"byteLength":936,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACAAADgvwAAQD8AAEA/AADgvwAAQD8AAEC/AADgvwAAQD8AAEA/AADgvwAADMEAAEA/AADgvwAAQD8AAEA/AAAQwAAAQD8AAEA/AADgvwAAQD8AAEC/AADgvwAADMEAAEC/AADgvwAAQD8AAEC/AAAQwAAAQD8AAEC/AADgvwAADMEAAEA/AADgvwAADMEAAEC/AADgvwAADMEAAEA/AAAQwAAADMEAAEA/AADgvwAADMEAAEC/AAAQwAAADMEAAEC/AAAQwAAAQD8AAEA/AAAQwAAAQD8AAEC/AAAQwAAAQD8AAEA/AAAQwAAADMEAAEA/AAAQwAAAQD8AAEC/AAAQwAAADMEAAEC/AAAQwAAADMEAAEA/AAAQwAAADMEAAEC/AABwwAAAQD8AAEA/AABwwAAAQD8AAEC/AABwwAAAQD8AAEA/AABwwAAADMEAAEA/AABwwAAAQD8AAEA/AACIwAAAQD8AAEA/AABwwAAAQD8AAEC/AABwwAAADMEAAEC/AABwwAAAQD8AAEC/AACIwAAAQD8AAEC/AABwwAAADMEAAEA/AABwwAAADMEAAEC/AABwwAAADMEAAEA/AACIwAAADMEAAEA/AABwwAAADMEAAEC/AACIwAAADMEAAEC/AACIwAAAQD8AAEA/AACIwAAAQD8AAEC/AACIwAAAQD8AAEA/AACIwAAADMEAAEA/AACIwAAAQD8AAEC/AACIwAAADMEAAEC/AACIwAAADMEAAEA/AACIwAAADMEAAEC/AABAvwAAgD8AAIA/AABAvwAAgD8AAIC/AABAvwAAgD8AAIA/AABAvwAAEMEAAIA/AABAvwAAgD8AAIA/AACowAAAgD8AAIA/AABAvwAAgD8AAIC/AABAvwAAEMEAAIC/AABAvwAAgD8AAIC/AACowAAAgD8AAIC/AABAvwAAEMEAAIA/AABAvwAAEMEAAIC/AABAvwAAEMEAAIA/AACowAAAEMEAAIA/AABAvwAAEMEAAIC/AACowAAAEMEAAIC/AACowAAAgD8AAIA/AACowAAAgD8AAIC/AACowAAAgD8AAIA/AACowAAAEMEAAIA/AACowAAAgD8AAIC/AACowAAAEMEAAIC/AACowAAAEMEAAIA/AACowAAAEMEAAIC/"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":5},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":0,"translation":[-1,-2,-0]},{"mesh":0,"translation":[-2,-4,-0]},{"mesh":1,"translation":[-4,-6,-0]},{"mesh":2,"translation":[-4,-8,-0]},{"mesh":3,"translation":[0,0,0]},{"mesh":4,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/noise_gates_1.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.625],"min":[0.1875,0.5625],"name":"tex_coords_gate_DEPOLARIZE1","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.625],"min":[0.25,0.5625],"name":"tex_coords_gate_DEPOLARIZE2","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.4375],"min":[0.4375,0.375],"name":"tex_coords_gate_X_ERROR","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.5],"min":[0.4375,0.4375],"name":"tex_coords_gate_Y_ERROR","type":"VEC2"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.5625],"min":[0.4375,0.5],"name":"tex_coords_gate_Z_ERROR","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":18,"max":[1,-0,-0],"min":[-4,-10,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":6,"max":[0,2.5,-0],"min":[-3,1.5,-0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_DEPOLARIZE1","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_DEPOLARIZE2","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X_ERROR","target":34962},{"buffer":4,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Y_ERROR","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Z_ERROR","target":34962},{"buffer":6,"byteLength":216,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":7,"byteLength":72,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_DEPOLARIZE1","uri":"data:application/octet-stream;base64,AACAPgAAED8AAEA+AAAQPwAAgD4AACA/AABAPgAAED8AAEA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AAAQPwAAQD4AACA/AABAPgAAID8AAIA+AAAQPwAAQD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_DEPOLARIZE2","uri":"data:application/octet-stream;base64,AACgPgAAED8AAIA+AAAQPwAAoD4AACA/AACAPgAAED8AAIA+AAAgPwAAoD4AACA/AACgPgAAID8AAKA+AAAQPwAAgD4AACA/AACAPgAAID8AAKA+AAAQPwAAgD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_X_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAAwD4AAOA+AADAPgAAAD8AAOA+AADgPgAAwD4AAOA+AADgPgAAAD8AAOA+AAAAPwAA4D4AAAA/AADAPgAA4D4AAOA+AADgPgAA4D4AAAA/AADAPgAA4D4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_Y_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAA4D4AAOA+AADgPgAAAD8AAAA/AADgPgAA4D4AAOA+AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AADgPgAA4D4AAAA/AADgPgAAAD8AAAA/AADgPgAA4D4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_Z_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAAAD8AAOA+AAAAPwAAAD8AABA/AADgPgAAAD8AAOA+AAAQPwAAAD8AABA/AAAAPwAAED8AAAA/AAAAPwAA4D4AABA/AADgPgAAED8AAAA/AAAAPwAA4D4AAAA/"},{"byteLength":216,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAvwAAAIAAAACAAACgvwAAAMAAAACAAACgvwAAAMAAAACAAACAvwAAgMAAAACAAACAvwAAAMEAAACAAACAvwAAIMEAAACAAACAPwAAAIAAAACAAACAwAAAAIAAAACAAACAPwAAAMAAAACAAACAwAAAAMAAAACAAACAPwAAgMAAAACAAACAwAAAgMAAAACAAACAPwAAwMAAAACAAACAwAAAwMAAAACAAACAPwAAAMEAAACAAACAwAAAAMEAAACAAACAPwAAIMEAAACAAACAwAAAIMEAAACA"},{"byteLength":72,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACA"}],"images":[{"uri":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAdnJLH8AAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAIABJREFUeNrsnXdUVLnbx79DlSaCIlWKAiIIorgWsPxsrGLDgg2xYFkL9l5AwYqKvWJbC1bWrruKiuDq6iq6KDZQUSyAgiIIAjPwvH/sO/c4SxF17h2EfM6ZI95k5klyk5vvTZ4kIiIiMBgMBoPBqFQosSJgMBgMBoMJAAaDwWAwGEwAMBgMBoPBYAKAwWAwGAwGEwAMBoPBYDCYAGBUMs6cOYM3b96wgmAwGAwmABiVidOnTzMBwAAAvHr1Cjdv3uTdTkxMDFJTU8tFnj09PeHu7s5uPkPhDBkyBMeOHas8AuDDhw9wcnKCo6MjMjMzBbN7+/Zt6OvrY8yYMQCA/Px8NGzYEPb29vj48aNg6di2bRtGjx4tcy00NBTjxo3j1W5ubi4CAwPRrFkzhIeHw8vLC1ZWVhg5ciSuXLkid3vr1q2Dq6sr3N3d0a5dOyQkJJQaPyEhAQ4ODnj//j0v+c/Pz0dISAjq16+P2rVrw87ODmvXrhW8/icnJ8PU1LRcdEDbt2/HmjVrUL9+fd5t1atXDwEBAdizZ4/C83379m3cunWL9T4MhZKfn4/IyEh06tTp679MPygnT54kAASATp8+LZjdjRs3EgCysLAgIqKEhAQuHXFxcYKlY8SIEbRz506Za4MHD6awsDDebGZmZpKTkxN5eHhQZmYmjR07lu7evUupqanUrVs3AkCfPn2Sm71Vq1aRmpoaJSYmEhGRp6cn2djYkFgsLjZ+fn4+NWnShKKjo3nJf2FhIXXu3JmqVKlCV69eJSKiuLg4qlq1KoWGhgpa/8+dO8fVO3mW+dcyd+5cmjhxoqA2CwoKqHfv3rRw4UJB7T548IBq1KhBPj4+9OnTJxoyZAh16dKF8vLyyMfHh/T19QV9BkgkEpJIJMSo3Jw4cYJ8fHy+6bs/7AiAjo4O97eamppgdg0NDQEAVlZW3P9FIhEAwMjISLB0/P3332jatKnMtatXr6J58+a82Vy7di3u3LmDhQsXypR/zZo1sW/fPpiamsrNVmFhIYKDg+Hk5ARLS0tuyDUhIQFHjhwp9juzZ89Gp06d0LJlS17yHxkZidOnT6Nv375cOTs4OKBbt24lpokvjI2NuX+rVKmikDa4bds2hIeHY8WKFYLaVVJSwo4dO7B161YcPnxYMLv6+vrQ19fHnj174OnpicaNG8PFxQV9+vTBnj17ULVqVejp6QmWniVLlmDVqlXsFbiSc/DgQfTt2/fb2tKPmulatWpxf5uZmQlm197eHgDg5OTECZHatWtDW1sb1atXFyQNOTk5ePHiBezs7Lhrb9++RWZmJidM+ODGjRsAgIKCgiJhWlpa3zYEVQKvX79GSkoKatSowV0zMTHhhl7/y7lz53D9+nX4+/vzlv87d+5wHdDnpKenw8DAQND6b2trC1VVVTg6Oiqk/T19+hQTJ06Ev78/lJWVFfICMHfuXAwdOhQvX74UxGbNmjXx8OFD3Lp1C61bt8b69etx8OBBNGnSBH/99ReePHnC1VEGQwhyc3Nx+fJldOjQoXIJADMzM+7N29zcXNAHr6amJicAAKBBgwZo0KCBYGmIiYlBo0aNuPxL3/6bNWvGq11pJ7d8+fJiw8eNGwdVVVW52FJRUQEASCQS7pr02Ir/jvi8efMGEyZMwN69e3ntjKRi5Pr16zL3IioqClOnThW0/qupqcHOzk6mHgrJ3LlzIZFI0L17d4U9A/r27QuJRIKFCxcKZjMqKgpr165FaGgo7O3tYWFhgbCwMOzatQuXL19mPRJDUM6cOYN27dp98yi4yo+acTU1NdSsWRMSiQSampqC2VVSUoKjo6NMh9+gQQOkp6fzanfWrFnYtGkTAODTp09QVlZGtWrVuPDPr40ePRpLliyRexoGDRqEbdu24dChQ9DQ0CjyJizPzsjIyAh16tTBq1evuGvPnj0DADRs2FBGFAwdOhRBQUG8C0HplMv9+/dx+/ZtvHnzBtOnT8fp06fh5ORUxAteVVWVV2EotPCU8uLFCxw8eBBt27aFlpaWwp4BOjo6cHFxwY4dOzBv3jxuWoQv4uLi0LZtWygpKeHAgQOIj49HVlYWhgwZAm9vb2zZsgWxsbEKG5VhVD4OHTqEoUOHfvsP/MjOD40bNyZnZ2fB7QYGBlJOTg73//Pnz9ORI0cEs9+zZ0/67bffZK517dpVEGfI4OBgzvkMAK/5Dg8PJ2VlZYqJiaH8/HxydXWlFi1aUGFhoYyj4PDhwwUrezc3NwJAxsbGNHfuXMrKyuLCli5dypVLv3796Ny5c7ymZceOHXT//n3B6//y5csJAE2ePFnhz4AxY8YQAEGcMDMzM2nKlCl04cIF7vnTuHFjIiK6cOECzZw5kzIzMwXL+4IFC2j58uXMC66Skp2dTRYWFiU6RZeFH1oA9OjRg7p161bpbrylpSU9f/5c5pqxsTGlpqYKYv/w4cNUrVo1AkDKyso0d+5c3ryR//jjD+rfvz8NGjSI5s+fL+Pxfvv2bWrQoAFlZ2dzXtErVqyg3r17k7e3N928eVOuKwB27dpFlpaWXCdf3IoLHR0d6tq1a4Wufx4eHgSAtmzZovC0hISEEADq3r274LYNDAyoRo0aCss7EwCVm4MHD9LIkSO/6zd+aAEwfvx48vPzq1Q3PT09nQwMDGSuvXz5kszMzARNx+vXr6lJkyZcZ9i6dWt6+/atoOq3QYMGdPv2be6ar68vOTk5UW5uLkVERJCGhgZdv379u22lpKRQu3btqHnz5vTkyRNydXUlAGRoaEjv3r3j4iUlJZGOjg69fPmyQtdBMzMzAlBkFEpRD0EAZGtrK7jtiIgI+v3333m3c/LkSdLS0iryUVNTIzU1tWLDTp48yXrICk7Pnj250ahv5YfeCbBWrVqCOgCWB2JiYuDi4iJz7caNG2jcuLGg6TA2NsZPP/2EgIAA6OrqIioqCi1bthRsM6Tx48dj6NChcHZ2BgDcvXsXO3bswIABA6Curo727dvDwMAAs2bN+i47r169QtOmTZGeno6IiAjUrl0bK1euhEgkQmpqKiZOnMjFDQkJwcqVK+W6HLI88vbtW24OXtFI/X9SUlIEt92+fXt07NiRdztdunTBx48fi3z8/f2xaNGiYsO6dOnCJsgrMB8/fuRWo3wPKj9yIXy+FLCiI3UCzMvLAxHJOADm5uZCJBJx1/hyAiwOLy8veHt7o3379nj48CFWrFiB+fPn82rz8OHDSE5OxtatW7lrf/zxBwCgTp063DVra2tERUUhOzv7m53VhgwZgufPn2Pjxo3cbzRt2hSjR4/Gxo0bsXv3bnTq1Am1atVCUlISVq9eXeHr4ucrMxSNhoYG90BkVHzy8/PRtm3bYsMuXrwo6J4wiuT48ePw8PD47lVPP7QA+O+bcEVmyZIlWLJkCXr27AkfHx/06NGDC+vcuTOmTJlSYsOQp+rU1tYuct3W1hYbN25E165dedkO+HOeP3+OefPmISoqSmYZpPQN8PMVIVpaWigoKMDbt2+/SQDcvXsX58+fBwBupEHKihUrEBUVhXv37mHUqFGwsLBAREREpaiLBgYGSElJQU5OjsLT8unTJwBA1apVWe9YCSgsLCzxGVNYWFhpyuHQoUOYMmXKd//ODz0FYG1tDWtr60rVAK5fv15kvX9MTIwgUwC//PLLF8WYdP0+HxQUFMDHxwdr1qwpsvGOrq4uAEAsFnPX8vLyAPy7gcu3EBcXx/39+PHjIm+ehw4dgpaWFj58+ICsrCwZ21Lu3btX4eqgdIojIyND4WnJysoCANSuXZv1jpWAKlWq4P9914p8FLUjptB8+PABd+/eRYsWLSqvACAirFu3Dhs3bqw0lf/FixdQVVWVWe+cmJiIGjVqCPIGlJqaisjIyGLDrl27BuDfKQG+CAoKQrNmzYrd9apVq1YAgKSkJJmykW7c9C1ItyAGgICAAOTm5sqEP336FAYGBlBVVUViYiLc3Nxkyufo0aO4dOlShauH0q2WExMTFZ6W58+fA4DgPjCVnZcvX2Ls2LFYu3ZtpXrz/pw1a9Yo5CCwY8eOoVu3bkX2YfnWjvSH5NKlS5wHujw8vX8EDh06RH379pW5duDAAfL19RXEfrt27cjMzIzOnj1LRMQdBnT37l0yMzOjQYMGUUFBAS+2o6OjqWnTppSfn1/iMr3//e9/1KZNGyosLKSYmJgSl+p9DZ6enlw9s7e3p4CAAFq+fDm1b9+e6tevTw8fPqQ//viDdHR0uHhmZmZUp04dsrW1FXRduFBIDyLq37+/wtMyePBgAkBnzpypdF7gS5YsoVWrVinE9qBBg7j6HhISUunK/s8//+Tyf+XKFUFtd+rUiTuM7Hv5YQXAmzdvyNbWlurVqyezFKsiM2XKlCINfvLkybR582ZB7N++fZsmTZpEjRo1IkdHRzI1NaVmzZqRh4cHr0vC3r17R/b29hQfH19qvIyMDBo4cCC5u7uTs7MzrVmz5rtti8ViWrduHTk7O5O2tjbp6upSixYtaNOmTTIbcCQmJtLAgQOpevXqpKenR15eXkX2aqgoiMVisra2JisrK4WnxdbWlszNzb9rMxTG17NhwwbS1tYmZ2dn6tChQ6XLf1paGtnZ2VHdunUpLS1NMLvp6elUp04dmc3QvgcR0f9vsM5gfCV+fn4YNWqUIOfAM8oXYWFhGDhwIO7du8cdkCU0CQkJsLW1xdatWzF8+HB2UxRAdHQ0Nm/ejH379rHC+AFRYkXA+FY8PDy+2cGO8WPTv39/tG/fHuvWrVNYGtatW4dWrVrB19eX3RAFsWfPnlKdgxnlGzYCwGAwvol3797B1dUVu3bt4g5KEooHDx6gW7duiIyMFPQ4cMa/5OTkIDg4GCYmJkwAMAHAYDAqI0lJSRg5ciTWrl0LW1tbQWy+fv0avr6+gtpkyBIeHg4XFxdYWVmxwmACgMFgVFays7Oxdu1adOjQgffleDdv3sSJEycwZcoUbu8HBoPBBACDwVAghYWF8lmbrGAbDAYTAAwGg8FgMCosTEozGAwGg8EEAIPBYDAYDCYAGAwGg8FgMAHAYDAYDAaDCQAGg8FgMBhMADAYDAaDwWACgMFgMBgMBhMADAaDwWAwmABgMBgMBoPxowmA9+/fY9y4cWjYsCEaNWqEAQMG4NWrV4ImPDc3F+vXr4e5uTkyMjIEs5uTk4MZM2bA0tISampqMDc3x/jx4/Hu3TtB7EskEoSEhKBevXrQ0NCAhYUF5syZA7FYrLBKNGzYMIhEIuTm5gpmc+bMmRCJREU+//zzj6B5T05Oxty5c9GpUyeMHTsWu3bt4tVe06ZNi8239GNhYcF7nnfv3o2WLVuiQ4cOcHd3R8uWLbF7927ByvzkyZNo06YNmjVrBktLS3h5eeHRo0fsac6oFJw4cQK+vr7w9fWFvb09HB0dERwcDIlE8vU/Rl9JamoqOTo6kre3N4nFYiIimjNnDpmYmNDTp0+Jb3Jzc2n16tVUp04dAkAA6P379yQEBQUF1KpVK1JWViZra2uqUaMGl4Y6depQcnIyr/YLCwvJ29ub6tWrRz4+PuTq6srZ9/X1JUVw8OBBLg2fPn0SxGZ6ejpVrVqVlJWVZT4dO3YUNO/Lli0jHR0dWrx4MeXl5fFu79atWwSAlJWVqXr16mRoaCjzEYlENG7cOF7TMG3aNDIxMaFHjx5x1+7fv096eno0c+ZM3stg+fLlZGJiQnfv3iUiog8fPlCHDh2oatWqdO3aNWIwKjKBgYHk7e1NBQUFREQkFotp5MiRBIAGDBjw1b/31QKgW7dupKOjQxkZGdy1vLw8MjQ0JDc3NyosLOS1AMRiMb17945SU1NJVVVVUAGwZs0aat++PSUmJnLXDhw4QJqamgSAvL29ebW/f/9+Cg4OlrkWGhrKdcB8C5D/kpiYSHXq1KGqVasKKgDmzJlDS5cuVVgjlEgk1KdPH1JTU6M//vhDMLujRo2iJUuWUHZ2drH3AgD9+eefvNmPj48nkUhEq1evLhI2Y8YMEolElJSUxJv9v/76i5SUlGj79u0y19PS0khTU5MsLCyKLRuGsG0jPDycYmNjK0yebt++TUeOHOE6XUWRkZFBqqqqtGzZMpnrOTk5pKenRwDo77//5k8AREVFEQDy8vIqEubr60sA6Pjx44IViJmZmaACYNCgQZSTk1Pk+qpVqwgAaWlp8Wq/pJtra2tLAOjBgweClb1YLCZXV1c6efIkmZqaCiYA3r17R1ZWVpSVlaWwhiit6//tiPgu740bN5YYHhwcTGZmZrwK8P379xOAYtOxbt06AsDrW7inpycBoMePHxcJ8/HxIQC0du1a1gsrgPT0dFq6dClZWlpS9+7d6dmzZxUmbwkJCfS///2PrKysKCQkROblV0iePn1KAKh27dpF2rl0NDg0NPSrfvOrfAAOHjwIAHBxcSl2bhIA73Ogn6Ompibo3Iufnx80NDSKXO/Xrx8AQCwWg3g8XPGnn34q9nrNmjXh4OCAunXrClYW8+bNQ5MmTdClSxdB78Hq1auRkpKCHj16YNmyZUhOThbU/u7du7Fjxw60bdsWvr6+gtlVUVHB6NGjSww/dOgQ+vTpA5FIxFsaTE1NAQCbN29Gfn6+TFhsbCyMjY3RoEED3uxfvHgRAGBoaFgkrHXr1gCA48ePs0liAYmLi8PIkSNRv359vHnzBhcvXsSxY8cE8UURCmtra0RGRuLo0aOIi4uDtbU1xo0bh/j4eEHTYWlpic6dO0NFRQWFhYVF/PI+b6O8+ADUrl2bANC+ffuKhEVERBAAMjQ0FEwRSf0AhBoBKG3YSyQSUcOGDRUyLFSzZk2KiYkRzGZkZCQ1btyYm/cWagQgIyODqlWrxk15AKAqVarQ/PnzBRme+/jxIxkbGxMAOn/+fLl5Q3ny5AkBoOvXr/Nqp7CwkBwdHQkAdevWjRtuv337NlWrVo1+//133mzn5uZy9/zFixdFws+ePUsAyNjYWLARmVatWpGZmZmMP4RQfPjwgZycnKh27dr08uVLQW0XFBTQsWPHqG3btmRnZ0cbNmygjx8/8mbv5MmTpKWl9VWfo0eP8paeN2/eUFBQEJmYmJCHhwedO3dOoe0/OTmZVFRUyMLCgnJzc/mZAigsLCRlZWUCQJcuXSp2eFraQDMzMyuVAIiLiyMAtGrVKkHtZmVlUZcuXWjbtm2C2UxLS6O6detSfHw8d00oAZCZmUlXrlyh48eP04wZM8jS0pKrc7169eJdBOzZs4frZKKjo8nb25tsbGzI2tqaRo0aRampqQqpf4sXLyZLS0tBbMXHx3MiyNnZmc6cOUMtW7akGzdu8G5bS0uLABT7cD99+jQBIG1tbcEeuiKRiADQrl27BL/nsbGxXN0/ffq0YC8bISEhVKdOHerUqRP98ccfvPt8lWfy8vJo9+7d5OLiQvb29rR582aF+KBMmzaNlJWVv+mlpMwCIC0tjatwxTX2e/fuceF8OgKVRwEwZ84csrCwKNY/gA9evHhBCxcu5HwglJWVafbs2YLY7tatG+3evVvmmpA+AP99KwwKCuIexCtXruTVXo8ePTgBEBQURLGxsXT79m1ubtrS0lIhIsDZ2ZlmzJghmL3ExERycHDg2rtQwrd79+4EgH7++eciYVJn2Fq1aglWDnv27KGgoCBBVoAUR2hoKIWEhPAufJOTk2nMmDFkbGxMfn5+MuKf8S+XL1+m3r17U82aNWnGjBmUlpYmiN2UlBTS1NQs1T9ILgLg5cuXXIO/c+dOqYpUqIdgeRAAqamppK+vTxEREYJ2fImJibR3715ycXHhyn3Lli282l27di0NGjSoyHVFCQApK1euJABkZmbGq526desSANqwYYPM9fz8fLK3tycANHjwYEHzHh8fTwDo1q1bgtm8fv069e7dmwIDA0lJSYkA0OjRo3nviO7cucOtuJkyZQp9+PCBMjMz6cCBA9yzoHPnzqw3kjPPnj0jT09PMjIyooCAAEpJSWGF8h+ysrJo3bp1ZG5uTj///LMgS+KJiCZNmkTTpk375u+XWQB8/Pix1BGAq1evEgASiUQkkUgqjQDo1asXLVy4UGH2JRIJDRo0iACQvb09b3ZiY2PJycmpWO97RQuAwsJCatiwIQGg169f82ZHV1e3xCHo9evXEwCqWrWqoMOiCxYsIFtbW8HsRUREkImJCbfk9LfffqMqVapwIoBvrl27xoleJSUlqlevHq1bt46srKwIAG3atIn1Rjzx9OlTmjx5MtWsWZN8fHwE8zs6deoU6erqftVHqNVojx8/pokTJ5KpqSmNGTOGHj58KOiLoIeHx3f1t1/lBCh90Bfn7HPq1CnBh+AULQBWrFhBI0aMUHjDTEtLIzU1NVJVVeXNhnTpW1k+0k1ahCQwMJAA8OoQJZ37Lq7+379/n8v/hw8fBMu3o6Mj+fv7C2Lr3bt3pKenR9OnT5e5/vvvv3N7cgi1GU96ejo3zHrjxg0CQHp6eoKWfWV/27W1taWWLVtSeHi4YC995YXz589T165dydraWmFLA69evUqHDh36rt9Q+ZoVA61atcL+/fvx5MmTImHPnj0DALi7u1eK5S+HDx/GzZs3ERYWpvC0VK9eHS4uLrxuh9q4cWNkZ2eXuDXlp0+f4OXlBSUlJVSrVk3wMjAxMUH16tVhZGTEmw07OzskJyfjzZs3RcLMzMwAAOrq6tDW1hYkz48ePcLdu3exf/9+Qezt378f79+/h6urq8z1jh07IjAwELNnz8aJEye4JcF8oq+vz/3t7+8PAFi4cCGqVq3K1ubxjLa2Nvz8/DB27FicOXMGa9aswdSpU+Hn54dhw4YppP0LQU5ODvbu3Yu1a9fC0NAQ48ePR9euXaGkpJgjdZ4+ffr9be1r1ILU07a4eeDhw4cTADp16lSFHwE4deoU9ezZk/Lz84sdklcE9evXp379+inEtqKnAIiIfvnlF5oyZQqvNtauXUsAaNSoUUXCXrx4UaKDGp+jHg4ODoLZ8/f3L3EKJDk5mQCQn5+foPdduhV17969K7VHuqK5d+8ejRw5kgwMDGjs2LEKWxHDB2/fvqXp06eTqakpjRgxguLi4spFuuRR3796K2BXV1eqXr26zMM+Ly+PDAwMqGnTpoI2Qum+BEIKgJMnT1LXrl2LXW/56tUr8vHx4c12fn5+sQLj2rVrpK2tTffu3avQAuDp06d04cKFYvPv4ODAez3Iyckhc3Nz0tPTK+ILcejQIRKJRMUukeULe3t7CgoKEsxeZGQkAaCpU6cWCXv48CEBoBMnTgiWnuvXr5OmpiZ5enoqRHxu376d5s+fr5BVAO/fv6fJkydTUFDQV6/95ntqZunSpfTXX39VGAHw559/0tKlSyk9Pb3cpCk/P58WL1783UvAv1oAPHnyhAwNDbmDPwoKCsjPz49MTEzoyZMnghaC9DCe58+fC2IvLCyMVFVVycXFhdzc3LiPq6srOTk5kYqKCq9LoszNzUlXV5dmz55NCQkJ9O7dOzp06BDZ2NjQ2bNnFVYZhRIA0u0uW7ZsSYcPH6bo6GiaP38+NWvWTOZ8Bj6JiYkhHR0dGjBgACfGXr58STY2NrRo0SJB37gACL4JzYgRI6hKlSoUHR3NXcvOzqauXbt+02Ek38qvv/5KNWrUoEWLFilkj/Znz55xPh979+4V3H5AQABnn+8DoBjlj/DwcO7+f88zAN/ypcTEROrduze5urqSm5ub4EM+GzZsoN69e3MF4OrqSkuXLuW1Azpy5Ai33rykj4qKCq/lEBgYSKampqSqqkpVq1YlZ2dnmjlzpsKX5QglAKKjo8nFxYU0NDRIT0+PWrduTaGhocVOxfDJ3bt3qXPnzuTs7ExeXl7UpUsXQc/AkHYAzs7Ogt/rwsJC2rJlCzVp0oQ6duxIAwYMoC5dutDmzZt5H/3bt28fTZ06lbp160bTpk1T6H7zubm51LRpUzIzM6OEhATB7R8/fpx0dHTIxcWFbGxsWI9YyXj69CmZm5tT48aNv2vzIRERj5vXMxgMBoM3kpKS0K9fP1y9epUVBuOrUWJFwGAwGD8me/bswahRo1hBML4JFVYEDAaD8WMhkUiwceNGiMVi+Pj4sAJhfBNsCoDBYDB+MP744w+YmprC0dGRFQaDCQAGg8FgMBhlh/kAMBgMBoPBBACDwWAwGAwmABgMBoPBYDABwGAwGAwGgwkABoPBYDAYTAAwGAwGg8FgAoDBYDAYDAYTAAwGg8FgMJgAYDAYDAaDwQQAg8FgMBgMIfnqw4DEYjFOnjyJM2fOQCwWQ11dHUSEnJwcqKio4KeffsLgwYOho6PDSpfBYDAYjPIKfQW7d+8mNzc32rBhA2VkZBQJLygooN9//508PT3pjz/+IL4ZO3YsXbx4kVcbFy5coJkzZ5Kuri4BIACkoaFBtra25OLiQpaWlmRra0sDBgygs2fPkhBERUXR4MGDqU6dOmRsbEwNGzak5s2b04IFC+j58+d05coVWrhw4XfbefToES1cuJDq1avH5R0AqampkbGxMenr65OlpSW1adOGZs2aRf/88w8v+T18+DBNmDCBNDQ0CACpq6uThYWFzMfU1JTU1dUJAPn7+8vV/oMHD2jBggVkZWXFlYGJiYmMfX19fS5swYIFvN37t2/fUkhICLVo0YIcHBzIycmJmjRpQm3atKFVq1ZRUlISjRkzhvLy8uR2/+vWrcvlzdjYmGbMmEE3btzg4h07dowmTJhAampqXLz//e9/tHz5csrOzpZr/u/cuUOBgYFkaWnJ2TI3N6f58+fTnTt3BGl/0vpQu3Ztmfowb948iomJkZsdafnXqVNHpvznzZvHtbWMjAxauHAhaWtrEwASiUTUpUsXOnDggNzScfDgQRozZgypqqpy6XBxcaGAgAC6ffu23Mv34cOHNHPmTDIyMuLsbd++vczfHzhrtvZMAAAgAElEQVRwoEw9DA4O/up6mJubS4sWLSI3Nzfut/r3719i/GfPntGiRYvIycmJAJCTkxMtWrSIXrx4IdeyiYmJoV9++YXs7e3J1taWLCwsyN7eniZOnEhPnjz56t8rkwDIzs6mHj160LBhw8pUkBKJhGbMmEGhoaG8NcKMjAzS1tamHj16CNLoV69eTQBIS0urSNjly5fJ0dGRAJCPjw+JxWJe0pCZmUl9+vQhZWVlmj59OiUlJXFhOTk5tGvXLjI1NSVlZWWaNGmSXB+60kYQHh5OEomEC4uNjaXx48dzDwcfHx/6+PEjL/kfPXo0ASA3N7cSG+3gwYNp4sSJvNj/+++/uXJ48OBBsQ/shg0b0syZM3mxf+jQIdLR0aEWLVpQdHQ0FRYWcmGpqak0Z84cUlFRIQByffDExsZy+T5x4kSJ8aZNm0YAqEaNGpSfn8+7CJamKTIykhTBP//8w6Xh9OnTvNmJiYnh7Jw8ebLYOI6OjmRoaEhRUVG8pcPX15cAkKGhoVwE5pc4e/Ysl+969erJ1PeSePnyJfcs0tbWlks9nD9/PpeO4ODgL4oXAJSYmCjXssjOziZfX19SU1OjJUuW0Lt377iwhIQE8vb25sLkKgByc3OpRYsW3/RWNWTIELp37x4vlSMkJIQAkLKyMj1//pz3ynjs2LESBQARUXp6OqdYQ0JCeBE89erVIyUlJTp27FiJ8ZKSksjMzIwGDx4sV9vSBvD5m9/n/Pnnn6Snp8epbj46gMDAwFIFgPQNecSIEbzUgZcvX5YqAKRiacKECXK3vWDBAu4tpKCgoFSRAIBu3bolN9tpaWlcvkt7w5W2SUdHR97b4+PHj7k0fcubjzxISUnh0hAbG6swO8uWLSNLS0t6+vQpr/mVdoRNmzYVpHyTkpJITU2NG1k6fvz4F78zdepUbjSkTp06ckuLdARYSUmJfv/991I7agAyL0nfS25uLrVu3ZoA0KFDh0qM5+fnRwBo/PjxZf7tLzoBjhkzBqampggKCvrq6YXVq1fD399f7tMWhYWFWL9+PbS1tVFQUIBNmzbxPlWirKxcari+vj769u0LANi9e7fc7Q8fPhwPHjzA8OHD0b179xLj1apVC1u2bMH79+8FyzsAuLm5ISwsDABw6dIlLF26VO5loKT0ZZ/VGjVqoGvXrgqpAwDg6OhY6v35FiIiIhAQEAArKyvs3Lmz1HLw8vLCoEGD8ObNG17yXZptaVhZ7pNQaaoIaSjNzq+//ooNGzYgMjISVlZWguRXRUVFkPJVVVWFhoYGBgwYAABYtmxZqfGzsrKwbds2DBs2jJd01qtXD4WFhejfvz8SEhJKbQNleVaUlQkTJiAqKgo9evSAl5dXifGCg4NhaGiItWvXYu/evWV7ppYWGBkZiWPHjmHjxo3flHBdXV3UqFEDz549k+uNOH78ODQ0NBAYGAgA2LZtG3JzcxXuTyG96ampqXL93d9//x3h4eEAgGnTpn0xvoeHB8zNzQXPf6dOndCxY0cAwIoVK5CVlaWQ+8CXACgrbdq0kdtv5eTkYPDgwSAiTJ8+Herq6l/8zvTp0yGRSJiDUwVn586d8Pf3x/nz52FpaVlh8zl16lSIRCJcuXIFV69eLTFeaGgoWrVqBTs7O17SsX//fpibmyMjIwPdu3cX5PkWFxeHrVu3AgBGjx5dalxNTU0MHDgQADBr1izk5eV9nwAICAjA9OnToa+vX+xb+NatW9G3b19MmjQJgYGBOHXqFFq0aIFjx47JdEbnz5+Xa6GsWbMG48aNg6+vLzQ1NZGWloYDBw4otJLm5OTgyJEjAIAmTZrI9bdDQ0MBADY2NrC2ti7Td+bNm6eQchg0aBAAIDMzE6dPnxbU9v79+/HPP/8oJN9isRizZs2S+++GhYUhOTkZANCrV68yfcfBwQGdO3dmPWQFZt26dfD398eFCxfK/Ez4UXFwcOBeLEoaBZBIJFi7di2mTp3KWzoMDQ1x/PhxaGlp4cGDBxg4cCCIiNe8h4WFobCwECoqKmjRosUX47du3RoA8PLlS0RGRn67ALh//z5u3LiBkSNHFgnLy8tDr169EB0djb1792LVqlVwcHDAhAkTcPXqVTRv3pyLa2FhUeJwybcQGxuL2NhY+Pj4oFq1avD29uYahFAUFBTI/H39+nV06tQJz549g76+PhYvXixXe9Ib6eDgUObv1KhRQyGNtVmzZtzfN27cEMzu27dvsWHDBoWJv8WLF+PTp09y/+2TJ08CAExNTWFgYMB6PgYWLFiApUuX4uLFi7C1ta0UeZaOfJ44cQKPHj0qEn7w4EEYGxujZcuWvKbD2dkZe/bsgUgkwokTJxAQEMCrvcuXLwMAzM3NoaGh8cX49vb2Rb5bGiVOkhw/fhxt2rSBnp5ekc6vc+fOeP/+Pa5evQpVVVUAgK2tLZ4+fYqffvoJhoaGXHwtLS25Ds+vWbMGw4YNg5aWFgDAz88PW7duxa1bt/DXX3/JiA8+yM7OhpGREQwNDZGdnY2XL19yw61dunTBqlWr5KrI09LS8OHDB4V26l+rkqWkpKTwYuPWrVsyw3zZ2dl49eoV72r8cxo0aACRSAQAyM/PBxFhwoQJcrfz4MEDAED16tVLjbdhwwZcuXIF7969k0njuHHjYGZmJrf09OjRo8RpCHn6nTCKZ8aMGThz5gzc3Nzkel/LO23atIGLiwtiYmKwfPlybNu2TSY8JCQEc+bMESQtPXr0wIIFCzB37lwsWrQIzs7OZR6d+1qko3/VqlUrU/zP40m/+00jADdv3ixWTQUHB+PixYvYsWOHzINAOs//36HH169fw9jYWG5veQcPHoSfnx93zcnJiUunEKMAWlpaePv2LeLi4pCYmIh3794hPDwc9evXx7lz5zBz5kwkJSXJzV5+fj73d1kUoKL53ElJTU2NFxuNGjXCw4cPuc+LFy/w5MkT1K9fX7B8xsbGIjc3F7m5ucjJycHcuXN5sSO9/5mZmaXGGzt2LJYvX46oqCicPXsWGhoaCA4OlnsncfToUZmy//zDxxQIo6jwVFZWxpUrV9ClSxfk5ORUmrxLh/f37t0r07lduHABmZmZ6NGjh2BpmTNnDvr37w8iwuDBg3H37l1e7X0+6lwanz9zy+KIWKIAeP78OerVqydz7cmTJwgMDISnpycaNGggE3bx4sViBUB8fLzcHFQ2b94Md3f3Ir83duxYAEB4eDhvb50loaOjg169euHmzZtwdnbGb7/9hmbNmsnNC1tfX5/rVN++fVvuG+nn+TYxMRHMrpWVFUaNGqWQPFepUgWBgYG87H4pHVF5/fo1CgsLS41ramrKOX82bNiQ9ZYVkAEDBmDPnj1QVlZGZGQkunbtysvUU3nEy8sLlpaWyMvLw5o1a7jrK1aswOTJkwVfDbJjxw40adIE2dnZ6N69O9LT0+Vuw8jICEDZR9c+d0w0NTX9dgHw8ePHIsMOK1euRH5+PiZNmlREnRw7dgwGBgZwcXGRCTtz5gw6dOjw3QUhFouxadMmxMTEwM7OTubj7+8PFRUViMVibNmyRSGVU11dnZv7T05Oxvr16+XWuUjfbO/fv1/uG+nff//N/e3m5iaobXd3d67BKGLkQ7pcSZ5IR7fy8/Px559/fjG+dEqOr9EXRvHIc9nXl+jfvz/27dsHFRUVXLx4Ed26dasUIkBZWZnrezZv3oysrCzcu3cPMTExGDp0qEKE/7Fjx2BqaorExET07du3zG/qZUX6DH3x4sUXRwGlL+lSpA6B3yQANDU1ZZYSEREOHToEY2PjIt6IYWFheP78OX7++WduXhT4d/5aIpF8cf6yLBw6dAg1a9ZEUlJSkaHH+Ph4zJgxAwCwZcsWiMVihVTQz73/5dlZ9+vXDwBw584dJCYmluk72dnZgs6Jf14XpG//7du3F9S2jY2NwgQAgCIjZvJg2LBhXJuSli2j/KGrqyuovT59+nAi4Pz58+jevXu5WAot5eHDh7z87rBhw6Cvr48PHz5gy5YtWLFiBcaMGaOw6VFjY2OcOHECmpqauHDhAqZMmSL3ER9p/1sWr37pMsk6deqUySGyRAFgYWEhM5yemJiItLQ0Gecn4N/lBtL5T3d3d5nfWLx4MTc8/72sWbOm1MIdO3YsVFVVkZycjN9++00hleHzIXoLCwu5/a50MyYAmD17dplGS2bMmCHjPyAEly9fxokTJwAACxcuVNhbaF5eHqZPn14hOhZ7e3uMGTMGwL9DjrGxsay3LWfo6OjIOL8KhZeXFw4cOAAVFRVERETA09OzXIiA/Pz8Mm9E87VoaWlxU30hISE4evSo3PqYb6VRo0b49ddfIRKJ5D4C7ezszL0AfmnDO4lEgp07dwIAli9fXqaNkEoUAD/99BNu3bol81AFgIyMDO5aSkoKgoKC0LZtWwCAq6srF3bu3Dm8fv2aW7/5PURHRyMpKYkriJKUmKenJwBg1apVcr/JZRlVCAkJ+bdQlZS43ajk9XZx4MABaGpq4sCBAwgKCirx7T4vLw8jR47EiBEjyrRpjLzyHhcXh759+6KwsBAjRozgZUhOOrz2pZGN+fPn8zIH/vkcvLyH+kpjxYoV8PDwgEQiQf/+/fHixQtBH3Blzbc0TIiyUcS9SE9PR926ddG1a1cUFhZydjt37szrFMB/lx1/Tq9evXDw4EGoqKjg7Nmz6Ny5M28b1EifA196HgQEBKBx48bfbU8ikRTr9zJ+/Hioq6sjJSUF/fv3L7I8Vjpy/SWfGXmk5XMxxteSwNDQUDg6OuLs2bOljgLOnTsXjx49wuzZs8vuEFnSHsFxcXFkbW0tsx9xjRo1CAD98ssvNHfuXOrWrRu9f/+eO33p3r17lJeXRytWrCAPDw/uUJjr16/T5cuXv2kfZLFYTE2bNiUvL68vxt2yZQu3Z7Y8T8MiIlqxYgUBIE1NTfrw4UORPeKlB9UoKyvzdghSVFQUWVhYcPvh7927l+Lj4ykjI4MeP35MW7dupQ4dOtBff/0lV7u3bt3iyvW/py+mpKTQ4sWLSUdHh7S1tb94WMb3MGzYMAJAlpaWxZ418OnTJ1q0aBFpaWnxciDRtWvXBDn8pTjy8/Np6tSppKamRkZGRhQaGko5OTkyed+1axfp6emRurq6XOv/54feHD16tMR448aN4w4D4utALCmXLl3i0hQdHS3IPbh79y5nc+/evXT+/HmqWbMm73vw37hxg7N76tSpYuOMHDmSi+Po6MjL2QRDhgwhAKSrq0sJCQkyZ1Lk5OTQ/fv3afTo0aSpqSmXUyCvXLlCIpGoyPOWiGj48OGkpKRE8fHxJR5KpaOjI5c9+d+/f1/qOShSCgsLycvLi/B1h+yWOQ1dunQhVVVVCg4OpszMTC7s6dOn5OPjQxoaGrRmzRr5HQbUtm1bmQfdmTNnyNTUlMzMzGj+/PmUm5tLRESRkZFkampKJiYm5O7uTmFhYVzlKCgoIHd392Jv4pf4/fffqVmzZtwRvKNHjy7xJixZskTm2FJ1dXUaMWJEsRXka7hw4QJNnz6dO2ACnx3LaWdnR+bm5qSrq0sODg40bNgwunv3Lq8Pg+zsbFq/fj21bduWjI2NuaN5W7RoQRs2bJCpGN/Lo0ePaMGCBWRjYyOTdwMDA3JwcCB7e3uysrKizp07U0hICKWlpfGS58OHD9PYsWNJSUmJS0P16tWpTp063MfMzIw7Baxv375ytf/gwQMKCgoia2trzr6hoSEFBATI9fjXspCYmEiLFi2iVq1aUe3atcnJyYnq1atHlpaW5O7uTitXrqTk5GS53v/P25WRkRH5+/sXOQ7Yz89P5rhYIY8DtrKyosDAQEGOA16wYAEZGBiQoaEheXt7f/fzpTTu379Pc+bMkTmG2sjIiGbMmCFT77Zs2UK1atWSaaPKysrk4eFBmzdv/u50HDx4kEaNGkXKysoyNkr6dO/e/bvrXVBQEHcMspubGy1dupTevHkj0yZ79eol870zZ87QpEmTqEqVKlxaOnToQMuWLfumevjmzRtasmQJNWnShDuRcOHChfTo0aMSv5OTk0MuLi681YmIiAjy9vYmGxsbcnBwoHr16lGDBg1o5syZ33QCqIhKGU+9desWBg8ejJs3b37zcPKCBQtgbGyM4cOHs8lCBoPBYDDKCUpfcm7w9fXFkCFDvmk+JTQ0FMnJyazzZzAYDAbjRxIAADBp0iQ4OzvD09OzzJvbfPz4EX5+fkhMTJTbengGg8FgMBjyo9QpgM+Jjo6Gv78/WrZsiUGDBhV7CMXjx4+xf/9+REdHY/bs2XI9FpXBYDAYDIYCBADw7/Krc+fOITw8HM+fP4eqqiqUlJQgEolQUFAAKysreHl5oVWrVjJ7BTAYDAaDwfiBBQCDwWAwGIyKgRIrAgaDwWAwmABgMBgMBoPBBACDwWAwGAwmABgMBoPBYDABwGAwGAwGgwkABoPBYDAYTAAwGAwGg8FgAoDBYDAYDAYTAAwGg8FgMJgAYDAYDEY55927d7C2tgbbQBa4ceMGkpKSKr4AiI+Px8KFC2FnZweRSMR9NDQ0oKenBzs7O/j6+iImJob3BN+/fx/jx4+Hvb09zMzMUL16dTg5OWHmzJl49eqV3O1dvHgRs2bNgp6eHpdvZWVlGBoaomrVqrCwsICHhwcOHz4sSKO4f/8++vfvD0NDQ+jq6sLGxgbjxo3DlStXsHLlSly6dEnuNsPCwmTue1k+ffv2/W67586dw6xZs1CtWjXudxs2bIglS5bg5cuXMnETExOxePFiODk5QSQSoVatWpg/fz4eP378zfajo6PRs2dPmXzVq1cPixcvLjb+6dOn0aJFCy5ut27d8OTJk6+2u2zZMpibm3O/U7t2baxcuRIAEBcXB19fX+4MDpFIhI4dOyIsLEzmNy5fvoyRI0dCSUkJqqqqGDFiBJKTk78qHUlJSZg0aRJq1aolUwaGhoaYM2cOsrOzubi//fYbevfuzcWpX78+goKCvuv+R0ZGol27djK2DQwM4O/vjxcvXsjEffLkCUaNGsWVS9WqVTFt2jS8fv1aLm0gKipKpv1ra2sX+SgrK0MkEkFFRQVXr17l7RmQm5sLkUgENTU11KhRg/v8t0z4QF9fH7a2trh27ZpCOqwXL17I5FlNTQ0ikQi5ubmCpkMsFmPQoEGYNGnSj61i6Cu4efMmASAAdOnSJSIiSk9PpwULFpCysjKJRCJatWoV8UFBQQFNnz6dVFVVacqUKZSUlERERBKJhKKiosjNzY20tLRo69atvNhfu3YtAaCqVatSdnY2ERF9+vSJtm/fTtra2gSABg8eTIWFhcQXly5dIg0NDRowYAC9fPmSiIiSkpJo1qxZpKKiQgAoMjJS7nY3b95M9vb2dPPmTcrLy+PyLq0Lf/31F3cvHj16RJ6enuTh4SE3+8HBwZytlJSUUuMmJCQQALp+/brc7M+cOZOzf/HixVLjisViUldXp7Fjx36XzUePHpGSkhIBKLZNTZkyhUvTo0ePSvyd+vXr08KFC78rLbm5udSvXz/O3tSpU4uNl5aWRgBo7NixlJ+fL7fynzZtGmc7ICCg1LjNmzcnCwsLio+Pl2sbOHz4MNWtW5f+/vvvYtv4vXv3qEqVKgSAZs+eTXwibXvt2rUjRbBz506aMGEClQd+/vlnAkCfPn0S1O6yZcvIx8eH6tevT+fPn6cfla8SAKmpqVxDvHv3rkzYnDlzCACJRCK5PnyJiAoLC6lPnz4EgDZs2FBsnLy8POrYsSMBoODgYLkX1LFjxwgA6erqFgnbtm0bVy779+/n5UZJJBIyNzen+vXrk0QiKRJ+5MgREolEvAiA5cuXc538fx9CnwuAz8O8vLzkZj88PJwAkKam5hfj5uXlEQB69+6d3Oy/e/eOtLS0CACtWLGi1LgPHz4kXV1dysjI+G677u7uBID69OlTJOzt27ekqqpKAOjIkSPFfj8nJ4eqVasml7IQi8XUqlUrAkA1a9ak9+/fF4nj5+dHffv2lXv9E4vF1LBhQwJAdnZ2JBaLi42Xnp5Oenp6dO3aNbmnYePGjXTq1Kliw/Lz87n0NWrUSK7ipzwKgPfv35OVlRWvLzvlWQC8fv2azMzMKCUlhS5evEgODg4l1skKJQDevn1bogB49eoVFzZy5Ei5JnLRokUEgP73v/+VGi8pKYk0NDRISUmJzp07J9c0nDx5skQBIJFISF1dnQCQp6cnLzfq9u3bBIB69+5dYpxOnTrxIgD27t1bpLGXJgCIiHbt2iU3+0ePHi2x7IvrLABQVlaWXMtgzJgxBIDs7e1LjTdnzhyaOHGi3ModAGlpadHHjx+LhHft2pUA0MCBA4v9/pEjR6hr165yK4OXL1+Snp4eAaAhQ4bIhO3fv58cHR250TE+6r90lGvp0qXFxhk7dixNnjyZF/tLliwpMW/SEaIqVarQvXv3eH9oK1oAEBF16dKFoqOjK6UAGDBggMyonJeXF61Zs6ZyCwAi4t6Sfv75Z7klMCUlhTQ1NQkAhYeHfzF+z549CQA5OTnJVaGWJgCIiOrUqUMAqEWLFrzcqFu3bnGdQUkPmR07dvAiAEp7CJUkAORJeRAA8fHxJBKJCACdPXu2xDdBY2PjUofkv4bs7GxueiksLKxI+Pr16wkAaWtrF9s59e7dm/bt2yd3MSi972fOnCEiort375KxsbHch92LE1cASENDgxISEmTCrl27RlZWVsUKJT65fPkyN1WzevVqQdueIgXAnj17yM/Pr9IJgOjoaKpfv77MG//z58/JxMSE3r59W3kFwIcPH7iwoUOHyi2By5Yt4363LEOZ27dv5+JfvXpVEAHw6dMnTqSMGjWKlxslFovJ1NSUS8Ovv/5aJM6rV6/o+fPnTADwIACkbz0AqGPHjsWG79+/n9q3by9Xm4MGDSIAxfpU9O7dm7sH/+3oMzMzqUaNGry8kffo0YMAkJmZGT1//pxsbGxKnIaQJ7m5uVSvXj1uNFAq8PPz88nR0bHEIXq+yMzMJCsrK64zFmpIvDwIgMzMTLK0tKSCgoJKIwAkEgk1aNCALly4UCQsKChI7iPfP5QA2LRpExdW0hvS99xgU1PTr3pTBvDdzk9lFQALFiwgAKSmpsbrW9D58+c5RyMA1Lx5c4qKilJIxamMAuDChQucn8v9+/eLhLds2VLuHWFERAQBIBUVFXrz5o2M4K5WrRonAv4rEH799Vfy9vbm5X6kpqZSjRo1OKfYadOmCVbvrl69yr1xSx1+Fy1aVKyfBN8MHTqUAFC1atXoxYsXgrc9RQoAIiJPT88vOsVWJAGwdu3aEn2bPn36RNbW1nTr1q3KIQBu3LhBRP965x8+fJh0dHQIgNzmP6XY2dkRAHJ0dCxT/BcvXvDii1CcAEhKSqIpU6aQSCSi6tWr04kTJ3i/YdevX6e6detyeQRAnTt3LrZDYgJA/jRo0KDYuhUXF0e1atUq1kHzeygoKOBGftatW8dd37lzJ/Xs2ZOioqKKFQju7u50+vRp3u7J4cOHuft/8uRJQevexIkTuY43OjqajIyMKDk5WdA0SOtkSdMzlUEA7N+/n7cRz/ImAN6+fUumpqaljrAeO3aM3NzcKocA6NGjB3l6elKjRo3IwcGBvLy8eBmCMzc3JwDk4uJSpvg5OTlcGvv37y93AaCiokLu7u7k4OBAAEhJSYk2b97MW4dTHHl5eRQcHEy6urpcXlVVVWnRokVMAPAsAHbu3MnNQ6elpXHXR48eTQsWLODFpnQZXLNmzbhrHTp0oKNHj1JhYSFZWloSAFq7di0R/es3Y2hoyKtn8tWrV0lZWZmbCvjw4YNgdS87O5sbeldRUaHNmzcL+tBMSUnhRkD4WPXwowiAjx8/koWFhdxFb3kdAaiIyNUJkA9cXFwIANna2pYp/rt377g0jhkzhrcRgIyMDKpduzYBoBEjRijk5qWlpdGkSZO45WBlWSf9IwqAEydOlFkA5ObmEgDKycnhTXwZGhoSAE5wZWVlUfXq1b+4R8G3cufOHa6sHz9+TCkpKWRgYMDtyTB79mwCQE2aNCEiojVr1tDo0aN57QAtLS0pPDycE6HDhg0TtO7v2bOHE2JCL0fz8PDgpiXludz0RxMARP/6oURERDAB8INS7rcCtrKyAgC8fv0ahYWFX4z/9u1b7u8GDRrwli5dXV0cPnwY6urq2Lp1K/bv389rOeTk5OD+/fsy16pXr46VK1ciNjYWdevWBQAsWbIEaWlpFWrLTQ0NDa4M6Au7LWZnZ0NZWRlVqlThJS1qamoYM2YMAGDDhg0Qi8XYvXs32rdvD0NDQ15sOjo6cnV53759OHDgAHr16gU1NTUAgI+PDwDg77//xuPHj7Fv3z4MGDCAl7RIJBL07dsXkydPRq9evRASEgIA2L59O86dOydYnahWrdq/W5n+/85/QrFp0yacOXMGIpEIO3fuhJ6eXqXeDrdv3744ePAg2yO5Im8FrEi6dOkCAPj48SMePXr0xfixsbEAAGVlZXh4ePCatkaNGnFbtP7yyy9ISEjgzVZmZia2bt1abFi9evVw8uRJqKioQCwW4/bt2xWqktasWZPbfjM1NfWLW4WamJjw2imMHj0aVapUwevXr3Hw4EFs2rQJY8eO5bUMpJ18WFgYwsLCMHDgQC7Mzs4OLi4uAICgoCCkpqbCzc2Nl3RMnz4dxsbGGDduHABg2LBh6NChAwBgxIgRyMrKqrAPy4SEBEydOhUA4Ofnx+W7pHpYGejcuTPOnTsHiUTCelMmAORPjx49uA4gPDz8i/GPHTsGAOjXrx/MzMx4T9+YMWPQt29fZGVloU+fPrzuSX3s2LESR0FsbGxgZ2fHjU5UJGxtbaGlpQUAX9yDPCIiAs2aNeM1PQYGBvD29gYATJkyBQDQsmVLXm0OGDAAysrKePToEdLS0op08FJBsPe5hhUAACAASURBVGfPHvTr148XAXTo0CGcPXu2iBDdunUrtLW1kZSUxJVHRUMikWDgwIHIycmBnZ0dgoODS4wrFouxadOmStGBaGhowNXVFefPn2e96Y/I18wXJCcnc3ORsbGxgnqbAiAjI6NSt1hNSEggdXV1MjQ0lLtXsNQRTUdHp0hYZmYm2djYEADy9fXlpQykZV+So9+bN29IQ0ODbGxsBFmbm5WVxdWFK1eu8G4vICCA22ipJOe227dvk66uLsXExPCenri4OC7/GzduFKQdSLcG9vf3L3ZeXuqUd+fOHbnbvnHjBhkYGJS42kS6KREAQVbDSNujhoaGIGU/b948zulQugKqJLZs2UIrV66sFD4ARP/uOPnfnSGZD0AFdAL8+++/uUYu9KYb0g2BevbsWWwH8P79e3JxcaHq1at/sYF+C6tXr+bWgBfn8fzPP/9wa/QnT54sdw/sz8XXwIEDOQFWWFhIt27doiZNmpC+vr4gnR8R0YMHD7j0HDx4kHd7ubm51KtXLwJArVu3psjISMrMzKSsrCy6desWTZs2jbS1tWnHjh2C1ckOHTqQjo6OYCtApI5vJe002LFjR6pfv77c7Z48eZL09PRKdfTLy8vj1ufr6enxviXuunXruPqXnp7Oq63r169z2xAHBQWVGC8jI4NCQ0NJS0uL1wNiypsA+PTpE5mbm3NOqUwAVDAB8OjRIwoKCiJra2uu0dWqVYsCAwMF63CI/l1nWatWLWrUqBHt3buX7ty5Qzdv3qQ1a9aQmZkZtWvXjp48eSJXmxcuXKAZM2Zw+xwAIFdXVwoODi4yyhAaGsrFMTc3Jz8/P3r9+rXcBMD48ePpxo0btGDBAnJ1dSVjY2OqWrUqWVhY0KhRowTZjOTZs2e0cOFCql+/PpdXExMTmjdvHi/C63MKCgpo586d1KFDBzI0NCQVFRXS1dUle3t7GjNmjOB7IZw5c0auK02+xMePH6lt27YlhoeFhdHixYvlZu/06dPUrl077j7XrFmT5s6dS7m5uTLxoqOjZXYlxP9vWT1q1KgiW/Z+L9HR0TRr1izuTAIA1LRpU1qyZAmlpqbyUu6f13VNTU3S0tIq8tHQ0JDJP19pKY8CgIho4MCBgu8HwQTA9yMiEuAQezkiFouxZ88eTJw4kXM4UldXx4ULF3hzfGIwGIzyQm5uLjQ0NNCuXbtKP/fesWNHnD17Fp8+feJt5Q9zAixHqKqqwtfXF5cuXYKpqSkAIC8vDxcvXmR3k8FgMBiMiioApDRq1Ah37txBnz59AACBgYE4deoUu6MMBoPBYFRkAQAA+vr6OHjwIKKjo+Hm5oZevXph1apVyM/PZ3eWwWAwGIxS+OF8AErj/v37OHToEJ4+fQpbW1u0b9+e9zXhDAaDISRSHwA1NTWZnQhv3LiBWrVqVei8v3jxAg0bNuT+n5mZCbFYXKF9AGJiYtC0adOv+s6VK1fK9J0KJQAYDAaDwWCUDSVWBAwGg8FgMAHAYDAYDAaDCQAGg8FgMBhMADAYDAaDwWACgMFgMBgMBhMADAaDwWAwmABgMBgMBoPBBACDwWAwGAwmABgMBoPBYDAB8EMQERGBLl26oGfPnqwwPiMjIwMrVqyAhYVFpTueVCwWY+nSpejQoQO0tbXRqFEj/PbbbxUyr7GxsRg6dCgsLS3LRXr69+8PQ0NDxMXFKcT+wIEDYWRkhHv37gli78aNGxgyZEi52O735cuX8Pf3h7GxMf755x/2EPxRoW/g2rVrpKGhQQDo4cOHVNEpLCykCRMmkLm5OQGg7t27E+Nfbt68SV5eXqSkpEQAKCIiotLkXSKRUPv27Sk0NJSIiK5evUpVqlQhAHT+/PkKldf169eTs7MzASBDQ8NykSZLS0sCQEeOHFGIfenzIDw8nHdbO3fupPbt2xMAql69ukLL/cqVK+Tj40MikYgA0O3bt9mD8AcF3/rFw4cPk0gkomXLllWawgoPD2cCoATc3NwqnQDYsGEDAaCsrCzu2u7du8nIyIj++uuvCpffx48flysB8OTJEzp9+rTC7MfHx9OJEyeosLBQEHvJycnlQgBIcXBwYALgB+ebpwB69+6NJUuW4Pjx45VmtKR69epsyKgEatSoUenyvH//fqiqqkJbW5u75uPjg+Tk5Ap5CmV5q/+1a9eGh4eHwuzb2Niga9euEIlEgtj7/OS/8kB5Sw9DYB+AGTNmwNHREW/evKkUhaWiosJqDCsbjocPH0JVVZXdY4YgKCsrs/Qw5Numv/cHNm3axEqRUSnJyMiAuro6KwgGg1H5RgAUQWFhIbZu3YrWrVujR48eqFu3Ln766SeEhYUJmo68vDzMnDkTRkZG0NHRgZeXF5KSkgSxnZubi8WLF8PV1RVNmjRB7dq18csvvyAlJUUQ++fPn4e7uzvatGkDNzc3+Pr64v3794KV/YcPHzBz5ky0aNECZmZmMDY2xvDhw5GamipIp29nZwc7OztIJBLk5ORw/x8xYoQg+d+5cyfc3d0xatQoODs7QyQSyXwCAgIEScfZs2fx008/QVNTEy1atMDjx48FqwOJiYmYM2cOjI2NcfPmTcGfQ0lJSQgICICpqSn+/PNPhT0P8/LyMGDAAIhEIhgbG2PmzJmIioqqEJ2TRCLBkSNH8PPPP3Mrr44ePQoXFxdoa2ujTZs2XJ17+vQpPD09oaOjA1NTU2zcuFHu6bl8+TK8vb1hZ2cHALh48SKaN28OTU1NNG/eHAkJCYKUy6lTp9C5c2d07NgRNjY2cHNzw969e7/tx340p4Xx48cTALp37x4REWVnZ5O9vT0BoD///JNX25cvXyb8H3vnHRbV0TXw39JZmiAiRUFEVBCwK/auscbEGqxR7L2baDQx9tiiSey9RI1GjRpfe+81iqIIilIVkN5h5/vDZT9Q7OwicH/P4/PmnTvsmTt37p0zZ845A6JNmzaiZcuWon79+qJ169Yqz29bW1sREhKi1jbExsaKmjVrCm9vb5GWliaEEGL37t0CEI6OjiIuLk6t8lesWCFMTEzEqVOnVGVTpkwRgEacACMjI0W1atXEgQMHhBBCZGZmimXLlqnu/8WLFxobi9ra2sLIyEij43/ChAlCV1dX+Pn5CSGESEtLEw0aNBCA6NWrl0hISBAZGRlqkR0fH69yAly5cqXo0KGDWLRokWjTpo0AhKenp0b64MyZM6Jv376qMXf16lWNPoNLly6JgQMHqrzgz549qxG56enpuToBjhkzRjRt2lSjY18IIRo1aqRWJ8Dvv/9euLi4qByvp0+fLvr16ycWL14sPD09BSAqV64srl27JurVqyfmzp0rpk6dqopQO3PmTJ61ZdOmTaJbt24CEA4ODmLlypWiVatWYubMmaJFixYCENWqVVN7n0+bNk04OTmJwMBA1ZgYNmyYAIS3t7fmogDyCxMTE6Grq5ujbOrUqQIQc+bM0YgCULJkSXH+/Pkc3sC2trYCEF5eXmptQ8+ePUWpUqVEUlKSqiwlJUUj4Wc3btwQOjo6YsGCBTnKMzIyROnSpTWiAHh5eYmpU6e+Vu7m5iYA8fPPPxdaBeDevXtCJpOJpk2b5ig/ePCgAIS5ubla5WcpAHp6emL9+vU5nr+dnZ0ARHBwsMb6w93dPV8UgCxq1aqV7wrAhAkTRIcOHURKSorG71/dCoAQ/x955eDgIG7cuKEqT0xMFKampgIQI0eOVC2GhBBi/vz5AhDDhw/P07YEBQUJQBgaGuYY/+np6cLKykoA4vHjx2rri6NHjwpA/Pnnn6+Ni6zv39q1azUTBZBfjBkzhokTJ+YoMzExASApKUkjbfD09KRu3bqq/+/s7MzcuXMB2Lt3L+np6WqRGxISwrZt22jZsiWGhoaqcn19fU6ePMm6deto3Lix2u572rRpZGRk0L179xzl2traVK9eXe39/uLFC3bs2MHhw4fp2LFjjn96enpUqFBBI9sA+cWFCxcQQmBlZZWjvEqVKgBER0eTkpKi9naYm5vTt2/fHM/f1dVVNUY1RX5HJVhYWOTrVuigQYMIDg5m9+7dhdYXpVixYqoxXrVqVVW5XC5XjbnevXvncMZ1d3cHIDQ0NE/bkjXPWFlZ5Rj/Ojo6VK5cGYCwsDC19cXs2bMBaNasWY5yHR0dhg4dCsCcOXM+6DcLnFvvTz/9BEBaWho7d+5kz549BAUFqV6K/KJLly706dOHpKQkgoODcXR0VMsEoFAocs0E5unpqdbQs8TERA4fPoy+vj52dnavXdeER/D169fJzMxkzJgxfPPNNxQ1sia8V309siYiMzMzDAwM8qVtWQqpJhQQTY65z1G+EIIePXqwZ88e/P39C3V0xtv62MjISNUf2cl6B1JTUzXWFlNTU9W8pA4SEhI4derUGxXfhg0bAuDv7094eDjW1tbv9bsFMhXwunXrqF27NsnJyWzbto3evXvne5sMDAzeu9M/ZQWsbi3zTQQGBpKeno6WVv4Nmaz7f/ToEUWRVq1aYWdnx61bt4iPj1eVP3nyBICuXbvmW9uyYuHzUwkvKshkMkxMTFQOgBkZGVKnfCa8qozkFcHBwarfzvoOZqdUqVKq//4QS3iBUwD69+/P5MmT+eeffxgwYMBnZfpSKBTo6elhY2Ojlt83MzNTWQLehLrykmdpv8nJyURERORL/2Yl3Nm/f/8b61y9erXQflwMDQ05dOgQJUuWZOrUqaoxN2vWLFxdXZk3b570BS4iLF26lCpVqnD27FkmT54sdUghJ+vbn13hz07WPKijo5OrhbZQKABnzpxh3bp1tG3b9rM4ECM7UVFRPHv2jFatWqnNDFujRg0AfHx8OHjw4GvXT58+rbaDURwdHVVm3rdNwOpcAWbtAV6+fJnt27e/dt3Hx4cTJ04U6g+BXC7H2NiYpKQkvv32W7y9vXF1deXSpUtSZrYihIGBAbt27cLMzIyFCxeyZ88eqVMKMTY2Njg7OwPk+qyzFmWtW7f+oEVxgVIAspw67ty5ozKHZGZmcvv2beD/93wiIyM13ra1a9eir6/PzJkz1SajXLlyNGnSRGUJOX/+vOra0aNHGTVqFO3bt1eLbH19fby8vICXzoCvbkMkJCQAL2P01YWtrS3t2rUDoG/fvvz222+qPefLly/To0cPjfkGCCFQKBRkZmZqbIwlJyfTokULvLy8WL16NevXr2fdunVMnjxZ5aCk7nvOizqabE9h4tX7dXJyYt26dar34cGDB/nansL+jN9ncaPO9k6aNAl4mQckMTHxtcWflpbWh1uDClIIYEBAgNDV1RWAaN68uZg4caKoVauW6Nq1qwCEk5OT6NmzpypHQF7j4+MjDAwMhFwuF6tWrVKFnuzatUtYWFiIffv2aaQPbGxsVDHQpUuXFpaWlkJXV1ecPn1arbKjoqJE+fLlBSDs7OzE0qVLxe7du0WvXr2Eg4ODAISbm5uYPn262g5ICQ4OVskChK6urjA2NhaAWL16tUbHYlYbnj9/rhGZd+/eFYDQ1tYWjo6OokKFCsLFxUW4ubkJT09PMWjQIFV+AHXw5MkTAQgjI6PXnm/Tpk01djJeFh4eHgIQR44cyZfvUdu2bTUaBph1GJC+vn6OXA8DBw4UgHB2dhbh4eEau/+sw4DUGXqcFQbYqFGjN4ZhnjhxIkf5P//8IwBRr169PG3LgwcPBCCKFSv22vjPOqlRneNfoVCIXr16qfIixMTECCGEuH37trC3txfz5s0r/HkAtm7dKhwcHIRcLhcdOnQQgYGB4tGjR8LGxkZUqlRJXLhwQa3yAwMDxahRo4STk5OwsLAQlStXFr179xYPHjzQWB88ffpU9OzZU5ibmwtDQ0PRvHlzcenSJY3IjoyMFIMGDRKWlpbC0NBQNGvWTFy5ckV07txZNGnSROzcuVOkp6ertQ3Pnj0TgwcPFtbW1kJPT09UqVJF7Ny5U2P9P3/+fFUMOiDq1KkjFi9eLHx8fNQue+rUqcLW1lbY2NgIuVyuOoY565+5ubkICwvLc7l79+4VDRs2VMnp3LmzOHTokLh9+7YYP368KimOi4uLWLdundrzIYwYMULVFnd3d7Fhw4Z8UwCy5wRRF5s3bxZNmjRR3XO3bt3Ev//+K5KTk0WnTp1yLAhmz56tmhzUwcOHD8Xw4cNVMitVqiR+++23PJezcuVK4ezsLAChpaUlxo4dK27evCmuXr0qhgwZkuP5L1myRAghxIIFC1SLFC0tLTFq1CgREBDwyW3Zt2+faNy4sUpm9+7dxeHDh8WdO3fEhAkTVOO/YsWKYsWKFWpVAlatWiWqVasmihcvLqpVqya++OKLj1aCZaKo2dEkJAooERERfPPNN+zatUsVH51FSkoKgYGBDBo0CG9vb3r16iV1mJpp164dBw8e5L///sPDw0PqEIkCh5bUBRISBQMvLy+aNWv22uQPL53CKlasSJMmTYrk0cz5tSespaWllpwfEhKSAiAhIQHAtWvXOHbsGGfPnn1jsp3//vuPS5cu0bJlS6nD1EBMTEyO5DIJCQk0aNBAIw6YEhLqQDrgW0KiAODi4oKHhweHDh3C0dGRdu3aUaFCBeRyObGxsVy7do3IyEh27NghndOuBlJSUihbtiy6urr8+eefVKpUCT8/v7eGxEpIfO5IPgASEgVoElq5ciV//fUXPj4+JCYmYm5uTrVq1VQhkIU5LWx+M2DAAHbs2IFCoaBRo0b89NNPqtwcEhKSAiAhISEhISFRIJB8ACQkJCQkJCQFQEJCQkJCQqIoIG0YSkhISEhI5AOyrGM0JQuAhISEhISEhKQASEhISEhISEgKgISEhISEhISkAEhISEhISEhICoCEhISEhISEpABISEhISEhISAqAhISERGEmICCAX3/9lcTERI3J9Pb2ZsuWLRq9z/3797N//34yMzOlhy4pABISEhJFFyEE06ZNY9myZfTp0wcjI6NCfb/t2rVDW1ub1q1bExgYKA2AT0RKBCTxSZw9e5YGDRpIHSEhkQ/MnTuX8+fPc/z48SJxvzKZjDZt2pCamkqLFi24efMmxsbG0kCQLAASmubmzZusW7dO6ggJiXwgLi6On3/+mSZNmhS5e2/cuDH+/v6sWLFCGgiSAiChaVJSUhg0aBDSYZISEvnD9evXSU5OJioqqsjde9Y9nz17VhoIkgIgoUlSU1Pp1asXV69elTpDQiKfyEojf+vWrSJ371n3rKUlTWEaUQAUCgUHDx6kY8eOtG7dGiEEc+fOpXTp0sjlcr744gvu3bunkUbfuHGDLl26UKtWLcqXL0+dOnVYs2YNtWvX5tSpU2qXf+HCBfr06YOzszNCCCZMmICZmRnt27dHoVCoXf7Zs2dp06YNHTt2pHz58shkMooVK6aRvhdC0LdvX65duwbAP//8Q5UqVahSpQqhoaFqk7tgwQLc3NyQyWR4enqqys+fP0///v2RyWTIZDLu37+vFvl//PEHVlZWKjn9+/cnODhYdX337t24u7tjbm7OqlWr8kTmvn37cHBwUMmcOXMmAIcOHaJRo0aq8g4dOqhWQpmZmUycOBEtLS08PDy4c+dOnrRl165d1KhRQyXTw8ODu3fvkpqaSufOnZHJZFSrVo0jR46opf9nzJiBoaEhMpkMHR0dJk+eTGxsrOr6oUOHcHFxQV9fX9VPavlgamlhbm6Ou7u7atxXqVIFU1NTZDIZ9vb2GrOKVahQAQAfH58iN3HdvXsXAFdXV2kW/8QP+nsxa9YsUblyZQGIZs2aiZEjR4oOHTqIAQMGCCsrKwEICwsL8eTJE6FO1qxZI6ytrcWpU6dUZVu2bBFaWloCECdPnlSr/GXLlok6deoIQNjZ2Ykff/xRfPnll0JbW1toa2uLyMhItcp/8OCBsLa2FiEhIUIIIRQKhZg1a5YwMzMTmmTPnj0CEH369NGYzAsXLghA1K5d+7Vrrq6uAhC+vr5qk3/z5k0hk8kEIF68ePHadW9vb7F+/fo8lXn37l2hpaUlDA0NRXp6uqo8ISFBWFpaCkD4+fnl+JukpCRRvHhx8fz58zxtS3JysqhVq5YAxNdff60q//XXX4Wnp6dITExU6/P/448/BCCsra1zvd6jRw8xZcoUtclPT08XlSpVEsnJyTnK79y5IwwMDIS2trY4c+aMRt9DGxsbYWFhIfKD/v37i82bN+eL7B9++EEAYvfu3aIgU2AUACGEOHr0qABEiRIlxNatW1XlISEhwt7eXgCie/fuauuss2fPCm1t7Vwfer169TSiAAghxJMnTwQgDAwMxO+//676UJ87d07tsmfOnCmsra1FRkaGqkyhUIi6desWegXA19f3jQpA1vNXpwIghBCtW7cWgNiyZctrk66rq6tIS0tTm8yjR4/mKB8zZowAxIIFC3KU7969WwwePFgt9x8QECCMjY0FII4cOSKCg4OFs7OzSiFVJwqFQnh4eAhAnD17Nse1lJQUYWVlJZ4+fao2+UlJSWL69Om5PndAzJgxQ+MTSLVq1XIoY0VFAThx4oQAxMWLFyUFQFM+AFnhFu7u7nh5eanKbW1t+emnn1Rmy7S0NLU0dtq0aRgbG9OxY8fXrllbW2us07LM7cbGxgwePFhliqpXr57aZaelpREeHk7//v2JiYlR7QVOmDBBMmdpgBEjRgDw+++/5yjfsWMHX3/9Nbq6unkus1+/fgBs2LAh1zG/evXqHOXr1q3D29tbLfdftmxZfvnlFwCGDRtG3759WbRoEba2thrZ8548eTLwMvzt1S2K2rVrU7p0abXJNzQ0ZMqUKTnKRo0axb1792jSpMlr19RNeHg4ERERr/VFUaBJkyZ069aNkydPakTe06dP6dixI3Z2dtSpU4cZM2bw4MGDXOuuW7eOR48eFa4tACGEuHjxomoL4FUiIyMFIADh7++f55pSXFyc0NbWFtWrV8/1eqdOnTRmAYiPj1dtAWgaf39/YWJiIgBhbm4upk6dmuemXskC8PZVqLOzswDEtWvXVOV169YVQUFBapGZmpoqLC0thVwuF7GxsUIIIdLS0kTlypVFjRo1BCBOnz4thBAiLCzsje9IXtKiRQsBiJYtW2p03GVkZAgnJycBiFu3bqnKGzRoIA4ePKjRtuzcuVMAwtLSUiMWkOx9cPbsWTFkyBBx7969fFu95qcFIGtLZuLEiWLZsmUiKipKre98o0aNxObNm4Wvr6/4+++/Ra9evYSxsbGoVauWWLp0qWrr+9atW6Jp06YiMzOz8FkA3kbx4sUxMTEBICMjI88bGhQURGZmplp+uyDh5OTElStXaNKkCdHR0cycOZNy5cqxZs0aaXmuAWQyGcOGDQNg2bJlwEunVGtra0qVKqUWmXp6evTo0YOkpCR27twJwNatW/nyyy8ZPnw4gMrxcOPGjfTt21ft/TB69GgATpw4oXII1QTa2toqa9fs2bMB8PX1JSgoiC+++EJj7Xjy5AkDBw5EJpOxYcMGjVhAsjh37hyLFy+mU6dOuLi4FNl3McsZNCgoSK1WEH9/f1q1akXPnj2pWLEiX331FZs2bSIsLIyhQ4eyb98+nJycMDQ0pHv37ixcuLDgRCfklQVACCGMjIyElpaWapWSl/j4+AhAmJqaFmkLQHaOHz+ucsrStENMflgA7t+/n+8WACGEiI2NFcbGxsLAwEBEREQIb29vcezYMbXKvH37tgBE/fr1hUKhEDVq1BDPnz8XiYmJwszMTBgYGIioqChRpUqVXB0U83r8V61aVUyePFkAolKlSiIlJUVj4yAlJUXY2NgILS0t8eDBAzFy5Egxa9Ysja48sxyBx4wZk2/v/7x588TYsWOFQqEokhaAa9euiVatWomIiAi1yklOTn7N8fNV0tLSPqodBdICkFuoW0REBImJidSsWRNTU9M8b6ijoyM6OjrExcWxf//+Iqv1rly5ktTUVACaNm3KxYsXGTVqFACbNm0q1Peup6cH8NYDTzQRhmlqakrv3r1JSUlhwYIF3Lx5k2bNmqlVpru7O9WrV+fcuXMsWbKEmjVrUqJECeRyOT169CAlJYWhQ4dSqVIlzM3N1dqWYcOGMXLkSObMmcMXX3zB3bt3mT59usbGgb6+PqNHj0ahUDB9+nS2b99O//79NSZ/+vTpXLx4kerVq7+28nz48KHG2jFx4kT+/vtv1q9fX+S+g3FxcbRp04bhw4djaWmpVlkGBgYYGBi8tY6urq7a2/HZWABcXV1fu7Zq1SoBiF27dqlNE+vYsaMAhJOTk3j8+LGq3M/PT5QuXVrjFgAbGxuNa72TJk16TevOao86IzBe5eDBgwIQX375pRDipTe0ukNAExMThZaWlpDL5TnCLbdu3SrMzc1z9Q5XF/fu3ROAkMlk4tdff9WIzN9//10AQldXVzx8+FBVfvPmTZUV6MSJE2ptw+bNm4WXl5fq/wcFBQkTExOhra0tLly4oLHxFxcXJ4oVKyYA0aVLF41a3bS0tISJiUmOZ5DFTz/9pNHvgYeHh3BzcytyFoDly5dr9H1XFwVSAQDEmjVrVOUPHz4UdnZ2YsCAAWrtrEePHqlinw0NDUWbNm1E27ZthZeXl/jyyy81pgCEhoYKQOjp6Yn4+HiNKwBmZmY54o2PHDkidHV1NfoyZJnj5XK5+PXXX0Xnzp1FeHi42uVmOZ9VqFBBjBw5UtSvX1/MnDlTtQVQs2ZNMXfuXI30QfPmzYWRkZGIiYnRiLzo6GhhYGAgOnfu/Nq1GjVqCCcnJ7Wag2/duiVsbGxEdHR0jvKZM2cKQJQtW/a1a+pkypQpAhDHjx/XiLyIiAhha2srgBxh0Fn4+/uLNm3aaHQrQl9fXxgZGRU5BWDixIkCEMuXL5cUAE0rAJ6enmLo0KGibdu2olmzZqJWrVrijz/+0MhelJ+fn2jXrp2Qy+WiVKlSYtasWSIjI0NjPgA7d+4UDRo0UClCnp6eYtu2bRpVobhQ7AAAIABJREFUALJkV6lSRXTs2FG0bdtWXL58WeODd9q0acLY2Fi4u7trJAeCEC9zTrRo0UIYGBgIFxcXVd83atRIdOjQQfzvf//T2J7ovn37xMCBAzXa515eXrk+65UrV4rZs2erTe6uXbuEpaWl0NbWzhHvfufOnRx+KG5ubmL79u0a6YurV6+K8uXLa7TvsywwtWvXzvHP3d1d6OnpiVatWmmsPVl+Ic7OzkVOAViyZIkAhLe3t6QAfC5OgPmJJp0AJSQk8p9x48aJhQsXFtn7P3z4sMa3QD4XBeD06dMC0KjFpTAqADpSYJeEhERBIyEhge3bt3P79u0i2wclS5YEimY+/KzwR00mgCuMfJACkKWwiM/wCFghHUsrIVGoOXjwIHp6ejRs2JBJkybRrVs3LCwsimx/eHh44OHhQVJSUpG79+TkZAB69eolvRiaUgCyTt/KfgrX50J0dPRn2zYJCYlP4+zZs7Rr1w54eSJfxYoVOXfuXJHuE5lMxubNm+nSpQujRo3Czs6uyNz7rFmzmDRpEo0bN5Zejk/gvfIApKSkMH36dFW8+fXr1xkwYACnT5/O9xu4c+cO48ePV7Vl0qRJRTI3toREYcbNzY2aNWtiZmaGl5cXJ0+eVHu+g4JiBTh48CA///wzv/zyiypHSGHl1KlTDB06lDp16kjf+bxQIoVkO5eQkJAo8CQmJqKvr4+OTuF17YqLi1NLorl8m4BlMpmkAEhISEhISBS1FXg+KwBa0iOQkJCQkJAoeuigdJ7LN44dk55CfqI8RU4in1YA0vjPV0Tz5vnbgIEDP+v+2XbuHF7166tPQH73f5FXACQKHQkpKbiPG8f+yZNxK11a6pACir2BAWPs7elcsiSl9PVV5c/T0lgTEsLswEASMzMB6GRlxTfW1nSysgLgbmIiO589Y8ajR5J8iY9CIQSXHz5UrwIgka9IWwCFUavT1iYyPh4dLenxFmSepqQwxs+PcufPs/3ZM1X5prAwpgQEqCY/gN3PnzPI1xeA34OCqHrp0idPfkVdflEn8PlzyigVKglJAZAoAAgh0NfRYXLHjlS0s0Mh+XgWeFIVCnr5+HBGuV3X28aGYrl4ev9Ytiw7nj1j+IMHpOfhcy/q8osqviEhuHzmuQWCQ0P5dvhwho4fT8uvv6bX4MFkZGQwf+lSflm2jAZt2nD91i0A/vPxYdbChXw3YwZf9epFkjKZkKQASBQKMhUKrLy9qTJxIn5hYXSYNw8DLy+uSyuhAk+GEHj5+BCdno6Vnh6Ly5fPcb17yZI0Mjen3717knyJPFUANp0+Tc3vvkPWtSu633zD8iNHVHX+vnwZK29vKo4ezZazZ4lNSmLB/v3YDhqErGtXHIcN46gyXXNSaiqLDhxA1rUrrWfP5qzSYvMplLK1pUzp0jzw92fPli38MGECKzdsoLyTExNGjKB39+4MGD2a5JQUhk+cyKRRo5gzbRrp6en4PnggKQDSMC88aGtpcX3ePG7On09iSgo7x47l4dKlVHV0lDqnEBCSmsoI5Uerr60trYsXB8DN2JhF5cvT6fZtkrKZxSX574dfUhLf3ruH7NgxfnnyJNc6cRkZmJ48ieP58+yLiMA/KYnRfn7Ijh2j4bVr9Lt3jxpXrjAnMBABvEhPZ01ICNrHj1PhwgW8792j7tWrDPT1JTo9vUCMt6eRkdhbWtK7USPOzphBHaXS1bpqVVWdxpUq4VqqFJdnz6ZngwaYyeWMb9+e8z//jIWxMfq6ujR1cwNArq9PDScnejZowKHvv6eBMp//p2JkZIS7qytGcjnlnZz43/HjPHz0iA3bthETG4ujgwPXb93CSC5X5Ug4sH071atUkRQA6bNauLC3tORJRAS7L1/m1N27OJQogVb+hppK5CFbw8PZ8/w5AKtcXbE3MOBvDw+GPXjAQw3khC+M8svL5XxfpgyGWlosffo01+2DNaGhZAhBcwsLvixRgnJyOcNLlQJghpMT61xdWV6xIlP8/Zn9+DEWurp429lho6fHN9bWrHF15VDVqvwbGUnXO3c+u3EVFBVF6iuKiUKhICtM3UBXlz9HjUKup8ewNWuAl9uNw9auZcWAAZjJ5Tn+1tHKirVDhvAgNJRFBw4AEBUfz/x9+1g1aJB6rWUZGVTz8KCvlxcTRoxg26pVKBQK/AICcpwZ8ywiQlIApE9q4aNMiRIYGxjgbm8vdUYhZPD9+0Smp1NKX587np7sjYhQTYqS/I9DVybjG2trwtPS2B4enuNaphCciY7Gw8QE7WzlOq8o1jVNTXEzNubPbH+fvY6Zjg5fW1lx7MULIt/TCrBNzecdRMXHM3bjRtrPncvaEydyXHs1R41DiRL80qsX/968ybZz55i7dy/tq1en4hv8BDrWrMk39eoxfedOHoaFMXj1ahb27o2hnl6e34dCoVD9d7NGjRj13Xdcv3WLp8HBLFmxgqoeHsTFxzN70SKSkpPZsWcPzyUF4O0KwNPgYMZMmUJpNzdkFhaqfyUrVGDKzJkkZtO4d+/fT+c+fVR13OrWZcb8+QW6c6ITExm7cSNlhw/HpHdv7AYNovuSJey9epVz9+/z8+7dn6V8mUxGQxcX7N7jpLSkzExmPn5MtcuXkR07huzYMXrfvfvebVwbGqr6O+cLF5jx6BF+H7kSO/HiBd/5+2N+6pTqN7WPH6fkmTOYnjyJw7lztLl5k7+ePaMou3g9T0tjiHL/1FRHR+UcJ8n/NEobGNDZyoqFT5/mKN8TEcFXH+ANb/KOVLxaMhlG2trvntSUYXjqRFdHh/EdOrB34kQWHjhAWkYGAOExMdjkctbCwObNaebuzrC1awmKinpniOCyfv0wMTSkztSpfFWrFhVsbXO+82fO0GPgQPRKlswxx/QbMYKb2Y56PnHmDL0GD1Zd92zRgrVbthD+/DlnLlzg1Llz+Pr5ATBy4EDq1a5Ns44d+bJHD9q0aIGJsTHb165l/bZtlKlcmZjYWNyVxyhHx8Tw3YwZ/LJsGTWbNSMhMZEvOnemWceOxMTG0mvwYKo0bEhwaChBISE0bNuWsGfPOHXuHIv++IPWXbqw8c8/AUhNTWXukiX8NG8eX3TuTHRMDMvXraN+69YsXbkSBw8PegwcmENh+WwVAPtSpVg8axb+16/T/euvVeW9u3Vj1tSpGGUz+3Rq356VixcDMMzbm5unTzNt4sQC+5ENj4mhxuTJnPH1Zde4ccRt3IjvkiU0d3fHe8UKGkybRqYaH+Knyq9Wtux7yZFrazPV0ZHTNWqgrwwb3B4eTlBKyjv/VgCLsu2Zbq5UiWlly1L+FXPg+9LUwoI55coxw8lJ9XGPb9yYZw0b8rxRI6aXLcvZmBi63rnDt3fvFmklICQ1lUylOXO5iwumGs7/Xljlj3Nw4L/4eI69eKEq2x4ezjclS77zb4+/eIFPQgKD3rAiDktNZeezZ/SytsbwPUJ0NRGGZ2poiK25OWVKlKChiwsbTp0C3h4BML9nT2ISE4lOTHzn7xc3MWHSl18SFR9PfC5e900bNmTrqlVcOXaM4soFi4W5OeuWLaOqh0eOeisWLQLguzFjuHD4MP179sTayop/tm3j9rlzuCh9FPT09Fi5eDExgYHcPH1aNdE3b9QI/+vXee7nx6C+fVW/fejYMUqWKMGEESMYM2QIxkZGzJk2jeiYGIqZmfHjpEm8iI7G1toaPT09Bvbpg5mpKWu3bGHs0KGsXLyYYRMmEBcfz9JVq2hUrx7TJ03C1saGxcuX06ppU/wCAmjbsiV3zp/n7MWL7Prnn89fAchCX1+fzStW0LBuXQA27dhBTC7H7v44bx7dvvqK3+bPR1dX970acD8khGk7dmDl7Y2sa1e0u3VjxLp1HPnvP1WdP8+fp/uSJci6dkXWtSsVRo1i7t69PFfj0b/jN2/mSUQE+ydNopqjIzKZDFNDQ7ybNePK7NlYGBur9cF8qvzSSgep9161aGtjp6+PsbY26UKw+JVVUG78GxnJ02yKQikDgzy5d3vl78iUCgqAgZYW/WxtWVKhAgAbw8LYkS02vChRUk+PbW5udLtzh9iMDErp67PoFa94Sf7HUcPUlAbFirFAqdheiYujmokJem+ZsPdGRDDz8WO2hoezt3Jl+r6yyr0aF8eip0+ZGhDAd2XKsEY5Ib0LTYfhff/VV8zft4+MzEx8g4NzlS2EYNGBA4xs3Zrt58+z//r1t/5mVHw85x88oHXVqkzcsoXgqKhc61Vxd2fH2rXIZDJeREezc+/e1+rMXbKEbl99xewffkArD3OceNaowc8LFtB/xAgaKy0aVT08SE1N5YG/P9f/+w8Lc3NOnz/PP4cO8WWbNty+e5eIyEg2bNvGiTNnaNGkCVEvXnD89Gn+8/Fhw7ZtlCxRAkMDA/T09DA1McHJ0RFTExM6d+jA1Rs3Co4CAKCjo8O21asxL1aM5xERjJkyJcf17X//zenz51n3228f1ICKdnbM6NaNKUoLQ5tq1VjWrx8tK1dW1fmmXj22jx6No1IbXj5gAJM7dsTKzExtHXPg+nUsjI1zNYOVLVmSSV9+qdYH86nyLU1MPtwcKJPhrXzpV4eEEKM0B76JBU+eMCDbR0Inj5wNtd/yO31tbFSWih2v7NV+DPcTExl2/z6yY8do/paXMjQ1Fb3jx7E8fZr1oaGEpKayJiQEk5MnMT55ko7//cdX//2Hy8WLjPbzU5s3vI5Mxg53dxY9fcru588ZpzR79re1peUHKn2S/NwZ6+DA4agofBISWBEczCCls9+b6FiiBFMdHVnn6kqHEiVeu17T1JSx9vasdXVllL39e78nmg7Dc7axoWa5cmw6cwb/8HCcrK1fn4T37qVT7dos6tOHao6ODFm9mtg3bPkJIRi+bh0LevVi5cCBKIRgiNKBMDeaNWrESKWD4NgpU4hPSFBdO3n2LDv37GH1r7/m+ZhyKF2aO+fPk5ScTLVGjVSLW6/Ondm+ezchYWGMGjSIzTt3Ep+QgImxMRkZGRgZGdHXy4u+Xl7s2bwZW2trMjIzqVe7Nn29vJgzbRpjhw59TZ6FuTmmH/F9zlcFAMDOxoZl8+YBsGHbNg4p85j7+PoydsoUdm/ciNzQ8KMakrWiNXuL+dhU+dvmRkZq7xghBBFxcWw8fTrX613q1Pms5Zt85HPwsrbGTl+fhMxM/ggKemO9G/Hx3EtMpLeNjUYHrLZMpkoLG5kH4VQVjYxYUqECOjIZx1+84L/4+Fzr/R4cjIKX2xTf2tpip6+Pt50dnmZmVDQyYm/lyuypXJnVLi78FhREXzXFo893diYsLY1lymezNjSUo0pz9WoXF0zeY29Zkv92OlhaUk4uZ/zDhxhpa1P8Pa2ZeU1+hOFN+fpr5uzZQ3JaGrqv9OXJu3eJjI/nq1q10NbSYs3gwTyLjWXC5s25tn/m33/TtU4dHK2sKF28OHO8vDhw/fpbHRtnTZ1KGXt7QsLCmDJzJgDPIyL4dvhwtq5ahYkaLK+7/vkHYyMj/lyzhspubjxWWn+8Onfm97VrqV65Mp06dGDfv/9SXrk96VGpEqfPn2f91q08i4jgj7VrSUpOplHdugwdP56HAQH4+Pry1759ACQkJKgiEHz9/GjbsmXBUwAAenTpwlft2gEwcPRongYH83Xv3vz+yy84KzvnY/iQUxE1cYJi1kvW748/mLx1K8lpaTmuO1pZ8YUa40g/VX6LbPtnH2oFGKmMHlgaFETqG/wMfgkMZHjp0hhoON1wikJBmLIv3PLoY6Ark9HE3BwLXd3XHMAAkhUKrsTGUsbAAL1Xxp7+K/dfv1gxqpmYsOf5c9UedV7RtWRJWhUvzoBXlIsB9+6RkJmJvYEBC9Voii/M8jOEIEP5vLRkMkaVLs2RqCiGZztLIzNbnay/yf6/7/rdt/G5hOG5lS6Nu709EXFxOcofhIYyY9cu5nh5qcqqOjoyuEULVh8/zr83b+acVC9dIuTFC76qVUtVNrRVKzwcHBixbt0btwKM5HLVXv/va9Zw5cYNeg0ezPABA6iRTfHJS+ITEmjbrRu/r1lDtcqVqeLu/rIPHRzo1L49DevWxdTEhG5ffUWrpk1fLkZNTNi0fDk/zZ9PlQYNKGllhXmxYowbPhw7GxuqN2nCdzNm0LFtWwBS09JY8Ntv/L5mDXVr1aJaNgt3gVIAAFYsXIhl8eIEh4biXq8eHdu0USkFhYVFffrgUKIECiGYt28fFUePZsOpUzlS63o6OxdK+YPs7DDV0eFZWhobw8JeX5mkpPBvVBRD32EaVQcLnjwhKTMTPS0txuZhmKNcW5tBdnZsDw8nNDU1x7XNYWH0+gBLR0xGBiX09N66lfGh1DA15bcKFeh8+zYJr2wvPElJYbK//8vJ0M6O9paWed7vhVm+f1ISvwYFcTAyUuX8962tLT1tbKggl5OUmcmWsDDuJSZyPDpalQhoqdIKsS40lFuvWI5epKezMiSEsLQ0DkZGcuQNE97nGIY3tVMnXJTvdlJqKjN27aLmd9/xLCaGG48fq+r5hYXxWBl++c2SJczft497wcEMXLmSbosX8zw2lsBsoXZn7t0jOS2NFwkJNP/55zdaAlo1bUrPrl1RKBQ079gRgHHDhqntm+Ldqxdn//2XYd7ezJk2LUe/L1+4UPXffyxYkMO3rU2LFgT+9x9h9+/TqX37l98RQ0O2r11L3NOn7P/zT4yV1uriFhZMGDGCYd7eDPP2/mzmuY9SAKxKlFB1TFx8vMo5sDBha27OpVmzVCvxp5GRfPvHH3iMH8+hV7TdwibfTEdHtbe/8MmT184TWPz0Kb1tbDRqGg1KSWH8w4dMCwiguK4uu9zdcf7IaIM3kbXaW5Zt60MAO549o/t7eIGnC8H0R494kpLyWqraT6GdpSVHqlbln8hIfN/geb06JET1nDZWqoRrHm6TFXb55eRyllWowM3atWmu9EQ30tZmU6VKKuWwp40NiU2a8LhePVUioKUVKiCaN2ebmxtVXtnTtdDVZZCdHZnNmnGzdu03+ifkdxheblRzdGRAs2Yv711fn2mdOxO3cSP3Fi/Osegob2PDgcmTETt3ErtxIxO//BLXUqVYNWgQmTt28Pf48ZTJ5hPRuFIl/H79FbFzJ/eXLHlr25fMnk0JS0viExJoWLeuRqy+RZGPtt/a2digrdwjGjJuHHFv2Dv9UA7fuoXnlCm5/nuYB05fH4J1sWL8+913/D1+PM7KFeDdoCDazJlDt8WLSXiPULmCKn+0vT26Mhl+SUnszabFx2ZksCE0lDEaSDKUmJlJq5s3cbt4Eftz51j89CnLXVwIrF+f9rk4W32y0qWvTzdra1aGhKhOmjscFUUTc/O3eoGHpKQw8sEDSp45w6noaO56etLtPRSGd9HG0pJj1aqxv0oVzHV1aW9pyc9OTq9tOzQoVoytbm6qjI/murpcqVWL5RUrUu4TlKSiLl8T5HcY3puwV4MV50MwNDSkuFIBmr1okWpfvqChUCjY/c8/hD97xvnLlz+79n1U8OyziAi8Bgxgx7p19B8xguDQUMZOmcKapUs/uUGtqlRhy4gRuV6rMmEC/+XDQPiqVi3aVa/OqmPH+HHnTiLj49l58SLP4+I4Pm2a2lPt5of8UsrJcEtYGPOfPOFrZQTGiuBgWhQvTtmPdDL8EIy0tTlctSqxGRlUu3yZR8nJXI+Le2OcdV4wxt6eLWFhrA8NZXjp0qwMDmb1O8K27AwMWFqhAulCsDksLM9WK/9GRvJvZOQ7652NieFsTEye90VRl69pvv/qK1rPnk2/Jk3wDQ5WOe9lJ3sY3tJDh/CqX5/21au/8TdfDcNrW60apfIwWiM+ORn38eOZ8vXXKqtBnljjJk6kS8eOHDt1iotXrzJk3Dj+t2tXwVtha2kxavBgRg0eXDgsABkZGXTr14+xQ4fSqX17Fio9Nddu2cKRkycLjWnkWkBATlOdtjbDWrXCb+lSvqxZE4BTd++y/9q1QikfYIKDAwCXY2M5Ex1NuhAsCwpivLJcU5jp6PCXhwf6WlqsDgnJkWo1r6lmYkJDc3N+DQrCJyEBKz09LN9zq2OBszPWenr0vXtXOoZZ4oPJ7zC8j0FXR4fYpKQ8XYRs27WLhwEBTJ84kZWLF6Ojo8PhEyf4U82ZV4siH6wATJw+HZuSJRkxcCAA/Xv2pEXjxgAMGDUqR+xmQWah0nP2VcyNjNgxejTllSb5K0rno8ImH8DD2Fi1dzn/yRO2hYdTTi6nlqmpxp9HNRMTVbKXQb6+aj14Zqy9Pf5JSXS5c4dRH7DVYaStzYZKlbgQG8ui90ikJFEwOR8TQ6fbt5EdO4bF6dMcVzoOvkhPZ7K/PyYnTzIvMJC4d+TRyI38DsP7UAx0dRncogXd69XLk997GBDAdzNmsG31arS1tXF3dVXF0o/5/nuiC4Glp8AqADv37uXwiROvJWNY/euvGBsZ8TQ4mHFTp2qs8clpaUzYvBlZ1670/f13YpR7Yg9CQ3EYOpSJW7YQl5zMF7NmMWr9eqZu387U7dtxGzcOt3HjSH9Lspb/AgPxf8NKU19XV5WoKCv8ZtelS7iNG4esa1cmbtlCUmoqCiFYc/w4Zn368Pvhw6+F8r2ND5WvLrJW+/9GRvJDQMB7r/53PX+O28WLyI4dY+LDhyRlZr7sj5AQzE6d4vegIJI/MJXy0FKl6FayJPGZmXS9c4eUV/7+UXIykx4+ROvYMUqfPavyzA5ITqbhtWu0vXWLm7n4qmQKkSNxT3tLS5wMDXEwMMjhTJYmxGthkakKBSnZ/rZ+sWKMs7fne39/LqgxU6VE/lGvWDF2e3jQw9qalMxMVeprC11dZMDeypWZVKbMR6Unzu8wvPflf7duoe/lRbG+fbn+6BHt585Fq1s3Gk6f/tG/mZqaSrf+/Vkyezals23zTZ80iTL29jyLiGDiJ/x+XhEdE8PcJUvQtbKiVrNmBIWEAPDH2rU4eHhw4PDhwqcAXLt5k+ETJ7Jr40ZVaEMWDqVLM1f5YFZv2sT+//3vgxuSlSRBvMV0+qpZ1VBPj1969aK5uzs62toUU7arTIkStK9enfk9e6rS5/767bfM7N6db5s04dGzZ6wYMOA1DftVWSPXr881375QHtKhp6NDJ09PADp7enL6xx+xt7QkJjERub4+WjIZkfHxHJg8mWGtWn3QKVgfKj8vyBCCV6W1sLCgiokJAjDW1qbtK85B2WOcsz+fzlZWnK5RA3sDA2IyMpBra7/sj/R0DlSpwrDSpd+YDz3rN3Mzo692dcVZLudWfDzD7t/Pca2soSHznJ2Z7+xMZHq66gNsoq1NebmcfypXpuor3toPkpL43t+fS7GxrA0NJSYj42UcuL09o5Wr/+DUVBY8eUJQSgono6PZoMwEuCokhEuxsdxPSuK3oCDClOGDPzs5UV4up9WNG3zv70/IK2GFEoWDFS4ulNTXZ4hyHB6MjMRCV5dm73EI19vI7zC896GctTWj2rTh3uLF1K1QgWPTprF+6FA6f8L3aMj48dSsWvW1kHK5oaEqAd2azZs5qnSUzC/MixVj8ujRzP7hB8KfP6eE8puYkJjIvzt30q5VqwIzhmXixYt3blYeOHyY3kOG8HW7dm909EtLS8PQ1haFQoF5sWKcO3QIV2Xe9reizCa46MABxm3aROuqVfn3u+9yrWo9YADPYmM5Pm1aDgeZe8HBVJs0iatz5uBub8+Sgwf5smZNVerg+ORkVWa8VrNmYWdhwbohQ97arIqjR/MgNJRa5coxs3t3mlSqhI62NqHR0Xy/bRtbzp5l5cCB9FcmhlC9ZL6+NPnxR47+8AOZCgUPw8IY+hED4kPl77p0iR//+ou7QUFM6NCBH7t0wUBPj3UnTjBu0yZme3nRr0mT15WQVauAlyFs5qdOsd3dnXavTPJbw8Pp6ePDWldX+r0SRvRvZCRtb90C4HKtWq9tD5yJjqbJjRscrVqVTOBhUtI78wf8+vQpo/38kAExjRu/tpL6Lz4ez6tXSVEoGGtvzzxn5xzpVQXQ8sYNMoTgSLVqDPH1ZUH58hTT8IE17/UCKsc/QDMLC9a5uhKSmso/yg+3oZYWPW1scDp/nmomJiytUAFnuZw1ISEU19OjkpERPwQEcEp5It771HkXnmZm9Le1JSg1lVSFArmWFpZ6eiwLCkJPJmOeszNVTUxYotzm0JbJ6GJlxbf37uVqYXkXJtratCtRgm1ubmwND8dHuY1op6+Pnb4+X9++zRfFizPP2ZlRDx5wPS7unfXfe+HRvPknPb/jL17Q/MYN5pQrh29iIhsqVeKDdsOVW6mvkpUF8HMnPjmZiqNHs6B3b775mG2A5s1JTklh7JQprFi/niAfH0q9IVSxhLMzkVFRWFtZceX48RxWgvwgMzOTGk2b0rFNG9p/8QUXrlxh+IABH/b+W1jka3yj9o+TJv34pov/Hj3KkPHjmbVwISkpKYSEhREXH0+9WrXQyfYxPXvxIpN+/JG7Sk04JSWFjX/+SVBICBWdnbHIJZ5VtQI7d471J0+y6MABElJSePTsGRFxcejr6lJWGUr118WLzN27l4vKvN9X/P1JSU/H2cYGI319Spia8jwujq1nz9K6alUu+PmpHOWyTOYA28+fZ+2JE+ybOBG5Mp3sm7j5+DGbRozAwtiYzWfOMHnbNmb9/Tcrjx7Fulgx1g4ZQocaNV77O4cSJXiRkMD8f/5BIQQ/dunyUQ/mQ+W7lipFt7p12X7hAqUsLPi6dm1kMhlHbt9mcseOdPb0zNXikXTlCouDgpgeEMDDpCQuxMQQl5GBqY4ONso+cjUy4nBUFIvLl1dNtHeUedJ/fvxYtdd5LiaG2IwMrPX1VTkCHAwNeZGeznxlPoEf33JK4YkXL1gZEsL8wEDSlKv/M9HRRGVk4CSXY6xsv7W+PiX19NgfGcnF2Fg2hoXxKDmZqiZD1imsAAAgAElEQVQmmOjoIAMam5vzQ0AAh6OimF62LI4aiFr4GH569Ej134+Tk2lqbs79xES+DwjgXEwMp6KjScjM5GZ8PGFpaZQ2MMBSVxcvHx8OREbiLJezuHx5fg8OJlWZJfFddd5GJysrFleoQC8fHw5FRXE+JobT0dF4WVvjk5DAtfh4SurpUcHICC8fH84pPfBPREdjrqub43Co9yVNCHwSEhhnb8+8wEBWhYRwLiaGQ1FRGGlrczM+Hv/kZL4vU4Y9ERH4JSW9s/778uN7npr5JsoaGhKamsr8J0/Y7u5OiQ896/4NHvxmn3n4YnZFZfaePTjb2NBcmUHvQ1h8+DB9hg7lmHJV/yQoiNJ2djkm9/sPHzL5p59UYXQJiYls3rmTZ8+fU9/TE718StWspaWFR6VKDB47lpSUFGZ8//0HRwD9NG/eT5+9BUCtZFsBfdK+TGIi5UeOpKqjIzvHjFFtB2QRp9RUf+7W7bVVe14Tm5SE9YAB9GvalN/799dod36wBUJpAVBrf2RkYH3mDP1sbfm9YkWN9cX3/v4sDQrCx9OTMvmoADxKTmbiw4dEpqfzv6pVCUhO5oeAAJZVqECps2dz1N1buTLBKSkMf/BAVWaopaXyl5jq6Eg7S0s8r14FXqbH3eHuToULF/BTOka+T53cMNXR4Wn9+gz29WX7KyctltTTw9XIiJPR0Yy2t8fbzg63ixdz1Mnezo8hpnFjvO/dY5fSrP3qb96vW5fBvr4qS8a76mvCAgAwJSCANSEhNDY3Z8eHToJvsAAUFBRCYNyrF6sHDaJHgwYfZQEo6DTr2BEzU1P+3rTpwyfgfLYAaFFIMDcyom/jxpQqXvy1yR/g+23bKGtlRb8mTdTell/++YfFffuy4sgRzrxy4pa6aejiwvAvvqDf8uXsvXr1o7Yf8rw/njxhcfnyrAgJ4cx7mqHzYtLNEIKapqZ4a/gZ5LZK3OLmRlxGBgHJyRx/8YJtbm7YvcMKBS8jMSq8IaudXFsbbzs7TkZHvzEq4n3qZNHe0hIzHR2O5/KMnqWlcfINz05HJuMba2uSFQqqm5pypFo1Zjk5cb5GDba5ufF9mTL41a1Lf1tbIhs1wuM9z3DobWPzQZN5Vn0DLa3XZI53cOBenToMLlWKwPr1c5xi+Snsi4iglL4+K11c2PnsGfuy7bkXBbRkMlzs7HDXQGKwz5Hzly/Tunlzjp48WSDD4HUK08PQ19XNNR71WkAAa06c4OqcOSoTTaZCwYuEBErkcUjbn+fPU9nBgS516nDz8WP6L1/O7QULPsgB8FOZ0a0bq/LIsvLJ/REeTmVjY7qULMnN+Hj6+/py29PzjQ6AeUGyQsHMx4/5o2JFQlJT8bh0idUhIXn20f8YDLS0WO3iwhc3bnCyevW3HqJUzdSUyWXKqCbWHj4+Oa6X0NPjuzJlGOvgwKzHj/kjOJhXzXjvU+dVsqwkUe8RrWKpq8vkMmVeKp3FinFEGQp3PS6OVIWC0gYGtLh5k1L6+pjo6DCtbFkuxMbS4No1Hr0lI11HKyvKyeUYaWvT09qaTbmcRfGu+ikKBYdfvMgh83FyMj84OpKmUFD36tX3OqDnfZTM/0VFsVxp1epkZcXQ+/dpZG7+WfqbqIty1taUUfpbFSXiExLYe/Agv8yYQUZGBiMnTeLO+fM5zgv47BW4wvRAFEK85jmeqVAwePVqRrZunUNLPXTz5munb30q1x894vaTJ6qjen/p1YuU9HTGbtyo2RV3PlogcvRHXBy3ExLoovTl+MXZmZTMTMYqfTnUgQDGPHjAD46OGGhp4WRoyEwnJ8b5+eGvxtwB77taqmFqyu5sJuvcuBEXx9zAQGY+fkzPVyZ/gIi0NOYEBnI1NpZ6xYqRlssq+X3qvEq4MlrB7D0mr8j0dOYGBjI3MJCOt2/zPJvSkJiZyY34eJIyM/FLSiIxM5MUhQLfxER8ExPf6oew9/lz5gYG8kNAADOyebx/aP1XZaYoFCQrFNyIjyc0NTVHez+GmIwMxvj58Uu23Pi/VaxIXEbGa9EphR0rMzNMDAyKnAIwfc4cJo4cCcDYoUPJyMzkl2XLCpYFp7A8DN+QEE7dvcsVf39uBQaqylcdO8b1R49Iz8xU5QEYtX49w9ety7OUmEmpqSzcv5+mP/2EhbGxKpTxeWwsNsWKseLoUSZv3Uq4BpJYZFkgBrdogXezZvRfvvyD8g/kSX9kZrLwyROa3riBha6uauX5PC0NG319VgQHM9nfn/A8btfVuDha3bjBhdjYHEfxagHxmZm0u3WLw58Y//yxRKWnczM+nu3u7mx/9uyNh9q8ys34eG7Fx2OUiwNnv3v3aGxu/taTCt+nThZHX7wgVaGgxRveC+s3WLHSFAq2hYfn2sZPYX1oKMB7/+6H1v9YtoSFqVJTP85mzbibkIC+lhbbwsMZ5Oub41phxsbcvEgd1vM0OBivAQM4feECqcpvWERUFFaWlvw4bx4r1q9H8Qm+MJqk0NipXOzsuKBMS5ydIS1bMqRly9fKf/322zyTLdfXZ1z79oxTHgmZ3TR2Zc4cza24lRaIrGQhv/TqRaWxYxm7cSPLPzA85ZP6Q1ubcQ4OjHslaVA5uZwr2RKT5DU1lfvPrzLK3v6DMvrlNbcTEhj14AFb3NzQ19KivaUl3e7cYVsuud5z+4yW1NOjZfHibA4LQ0smU21zhaelMdDXlw2urlyOjVU5+L1PnVw/bCkpzHz8mPnOzlyNi8sxgX1jbc0JpZk/tzZqy2QMtLNjsTI0UOsjVhq5/W5TCwti0tO5kYtn/9vqJykUucrMixVPTxsbeuaiUDWzsCCyUaMitxIu/p4+HYUF+1Kl2LZ6dY4yOxsbLhSgBECFTgEoyiSlprL8yBFm7NrF1E6dEEIgk8lyWCDM5HJGt22LdbFiUodpGA9jY05mC/ea4eTEDCen1+q1Kl6c6qamlJPL+a5MGYRSmeppbU2Da9eoZmJCKwsLysvltLW05H9RUex5/pz2lpacqF6daQEB3EtMfGedreHhbzTDz3z8mKCUFDZXqsTTlBQeJScTnZHBX8+e8SwtjSomJrSxtMTBwIAfHB1JFwJdmYxWxYvze3AwbsbGuBsbU0JXl30RETxNSaGzlRUmOjr0tbVlg3KVnh0TbW2+trLCVFkn6wS/knp6NDQ3p/rly9QxM8NOX5/WxYvzIDGRlsWLv7G+55UrTCpTJofM1sWLY66jQ28bGx4lJxPzEWl6JXKnoIQsSuSidBeWMECJj0QDYYASb3kBpfGfr4j8DkMr4GGAAPuvX3/riYRvpRCEAX7S+5/PYYA60eb52wHHuiCRn/O/1P/5/AWQuiA/aXE0n+f/z7x/zm07R32v+m+v1KU6f33k7zeXhmC+Im0BFEJSElIY5z6OyfsnU9qttNQhBRTzJuY4TnXEoun/55ZPj0wneEUwIatCSAn6/6x7hk6GlJlQBruBdiCDjPgMQlaG8HTxU1JDUyX5Eh+MUAgeXn74bgVAQlIAJD4ftHW0iY+MR0tHS+qMAkz0yWiiT0bjPN8ZhwkvHSqD/gji0fRHr9VNDkjGd7Avxh7G6Nvqc6PFDZIeJknyJT6a54HPsSpjJXVEIUaaIQqb1i4EOvo6dJzcEbuKdgiFkDqlgOP/vT/xN196wZfsWhKZTu77BroWusgryrnT7U6eTn5FXX5RJcQ3BDsXu8+6jaHBoQz/djjjh47n65ZfM7jXYDIyMlg6fynLfllGmwZtuHX95WFlPv/5sHDWQmZ8N4NeX/UiOSm5yD9jSQEoRCgyFXhbeTOxykTC/MKY12EeXgZePLr+SOqcgqzUZQju9buHyBAYVTTCYZxDrvXKzihL2PowYi/HSvIl8kQBKOlUkr9++gsvfS+6yrqyvN9ywv3DVXX8LvoxxnUMfcz6sH/hfsIDwvmt9290lXWlq6wr+xfsJyn2/5Wxc9vO0cuoF6Mrjuby35c/uY22pWwpXaY0/g/82bJnCxN+mMCGlRtwKu/EiAkj6N67O6MHjCYlOYWJwycyatIops2ZRnp6Og98H0gKgDTMC9HD1NZi3vV5zL85n5TEFMbuHMvSh0txrOoodU4BJ/5WPIHzAl9OdNPLIi+XM/TKrLYZlm0sCZgWIMn/QJL8krj37T2OyY7x5JcnudbJiMvgpOlJzjueJ2JfBEn+SfiN9uOY7BjXGl7jXr97XKlxhcA5gSAg/UU6IWtCOK59nAsVLnDP+x5X617Fd6Av6dHpBWLMRT6NxLqcNV2md6HHvB4AlK9THuty1qo65euUp3Sl0nx/6Hvaj2uPtZM1wzcNp+aXL09jrdGhBnKz/39WdbrUwbaiLTMvzKT217XzpJ1GRka4ursiN5LjVN6J4/87zqOHj9i2YRuxMbE4ODpw6/ot5EZy1Sm22w9sp0r1KtKcIX1aCxeW9pZEPIng8u7L3D11lxIOJZBpSa7mhYHHPz8m0TcRLUMtXFa7qCIIZLoyXFa78GDEAzITMyX5H4i8/P+xd97xNV9vHH/fmXmz9yaSEIkQO2oXra01SqlRFS1FjRq1tUUptUfNKqrjZ7SlI7ZQe2YIkb1k73nv/f1x4xIZkkiCyOf1yovX/T7f85zv+Z7veZ7znGdo4zDHAaGWkPC14Sjzix+bRW+LRlmgxOhNI0z7maLdQBubiTYAOC52xHWHKw03NeT+F/cJ+ToEiZEE67HWSC2lWAy1wHWbK82ONSPhaAK3B99+6eZWYkQi+blFFROFQqHO8NdzUk8atGrAzwt/Jjvtsen8wdUHGNsY4+LlUuTesRvHoqWnxQ/TilbI+2vDX7w79110jaoveVBBQQFNPJswbNQwPp3xKVv3bUWhUBAcFKzO0goQHxf/2q8pdQpALYSpgymauprYudvVDUYtgiJXgf+H/igVSgw7GWL9oep81n6GPZkBmST8mVDHv5IQSARYDLUgLzaP2J9ii1xTypUkn0lG1kQGT2QZftoXQa+lHrpuusTujy2RRqwvxuwdM5J8kshPKJ8V4Ny+c9VrWUpMZ/fU3Szrs4wT208UHZMn0vsKhAK8v/cm7WEa++bsU42LQslvS35j8KLBxdo1tDJk2NJhXP3jKv/9+h8AydHJ3L94n1YDqj4b6JOpdzt27cjsybO5cfUGkeGRbP5uM02aNSE9LZ1VX68iOyubgwcOEv+wTgEoUwE4e/Is/bv2x0hgpP5zMnXi63lfExURVVQ7Dw5h6vipGAuNMRIYYadnx/wZ84mNjq1052KCYvjfV/9jSsMp6jMlbytv9s7aS/CVx6a+y4cvs2vKLvU51WDBYBZ1XsTvK38nN6vyIUCZyZnsnrqbifUn8oHsA7ytvfnuve+4fOgygecC+W3Jb9X6cirLXyAQ0KhDI4ysjZ7JQ54lJ+TLEC56XsRH4IOPwAe/D/zK3cfo7dHq+847nefB4gdkBVXOASvpRBL3Z9/nlOEpdZvHRcc5Y36Gk3onOWd/jus9rxP3Sxy8pr6NqRdSiVgbAYDTCicM2htg96kddyffreP/nNC01cRsoBnh34YX+T3+YDxmA8rvDS+WlR1cJRAKEOk8u17BozC86oRYIqbv9L58fuhz/vj2DwryVBkSU2JTMLQsmiTGvok9vaf25p9N/3Dv4j3+2fwP7Ya2Q0tPq8S2u4/vjnNbZ3ZO2kl2Wjb75uxj6NdDi9Ds2rKLJvZNisiYUYNGceLvospIfFw8c6bMwURkgpHACBcLF5YtWEZMVAznz5zn3KlzBAWoioyNmzSO1u1a079rf97v9z7denZDV6bL9p+2s2/nPjwcPEhNScXV3VX1rMkpLJ69mHUr1tG1ZVcyMzIZ+NZA+nftT2pKKuNHjKdD0w5ER0YTFRFFrw69iIuJ49ypc2xctZFBbw9i/+79AOTm5vLdsu9Yvmg5A98aSEpyCjs27eDtN95my9otNLFvwrj3x700tQIEScpnZwJc8PkC1q1QVTn6fP7nzFo0q1TaHl49iI2O5X///g9HJ8dndsCHZ2dCC7sVxgyPGQDMPDKT5n1Kzjr14+c/cmTFEWQmMrZGb0UkqXxRkJTYFOa1m4eOoQ7eW71xaOZATnoO538+z75Z+0hPTGfQgkEMWlg9mXSel/+BeQcYsmTIM/lsRZUJUJ4u57TpaRS5CgQSAe2C26Fp+4wKX0q44HaBTH9VYZuWF1qi30b/uZ89Yl0EdyfdRawnpn1Me0TaIhQ5CmL3xXJ38l3kGXIsR1rSeGfjVz6Rjo+g4pkARdoi2txpg1Y9LZQFSgInBhK1JarG+lyb+L+pVKWiyQ7NJmZXDCa9TbjU8hKe/3pi9KZKgb418BZu+9y42uEquk11abS5kfoe33q+ND/ZHMNOhiQdT+Jat2u47nDFapSVagfvcA6rUVbUX1if3JhcLja7iPFbxjTe1VglrMpIBRT3II4rh6/Q67NeNTKuG0dvxLmtM2+OexO/k35kpmQW263nZecxzW0aEk0Jtm62fHbgs7K/5TsRfO75OQ1aNcCzlycDZg8oOv68SWpKKl1bduXB/QdoaGoQnRVdanGh3h17kxCfwCGfQ1hYWVTJc/+671fiH8bz8ZSP+XXfrwwcNpBb128x6cNJnLp2ipDgEPp27svN0Jskxidy8t+T9HmnD595f8bmPZuJDI+kjWsbAqID2LVlF23eaEPLti35dMynWNlYMXTUULq36c7fF/7GxNQELzcvlqxcQv/B/TESvNhMgOU6Apj39TyaNGsCwMGfD1JQSh7t5KRk7gXeY8eBHeUS/uXFkzvZsna1BhaqPPeGlobPJfwB9kzfQ3xYPDN/n0k9z3oIBAK09LToOrYrX1/6ulrPsKqCv7FtxSodimQiNKw1EOmKUOYrCV8d/sx7Eo4mkBP+OBmLpk3VlATVtCtsR6Ba7AGEmkKsxljh8p3qrDFmdwxxB+JeSyuAPEvOg/mqyA5lvpKorVF1/KsIei30MGhvQNhKlTNg2qU0ZJ4yhNLSl8r4Q/GEfBlC7N5YPA55qIX/I6RdTiN8VTjBc4NxmO2A6zbXcvWlpsPwBswZwOFvDiMvkBMZEFkib6mWlMGLBxPpH0n3j7s/s01bN1s6jezE/Uv36Tu9b4k0+gb6rN+5HoFAQG5OLseOHCvZIpqRSVBAEN/v+77KhD9AizYtWLlkJZ9++ClvdFIlPWrSrAm5ubncv3ufm1dvYmhkiO9pX44dOUbPfj3xu+VHQnwC+3bt48yJM3Tu1pmkxCROHz/NnZt32LdrH6bmpmhqaSKVSpHpyajnWA+Znoy+A/ty7fK1l2ItKZcCIBaLWbdjHWKxmHuB99jw7YYS6ZbOX8qw0cNo3rp51XZSJCxiPivLtPYsmvLi6h9X0TXSLWYGAzCvb06/mf2q9cU8L3+Ziazi5iCJAOuxqo8+6vsoClLKLpgStjIM648eLxKlxWdXuB+i0tuxHGWJUEM1H2IPxD43r8zATAInBOIj8OHam6V/lLnRuRyXHue0yWmid0aTG5VL1LYoTspOclL3JDf73+TmgJtcaHSBoClByLPk1To/8lPy1WbiF3EcUpv520+1J/HvRDLuZBC5ORIbb5sy6U37m1Jvbj1cd7hi2te0uFLRUg+7qXa4bnfFbrJdub+Tmg7Ds3SypEHLBpz54Qyx92OxcCxZyMqMVWuLVFNarufQNdZFKBSWuSlr80Yb3h/zvtrinFdCqfB1K9YxcNhA3Ju6V+n7trW3xfe2L9lZ2XT07EhqiiqMdOCwgfz202/ERMXgPdmbn/f8TEZ6BroyXQoKCtDR0WHYqGEMGzWMPQf3YGFlgbxATut2rRk2ahjzl87nk6mfFONnaGSITE/Gy4ByOwG6N3Vn8szJACxftJwH94vGll+9eJV/j/7LnMVzasUuS6lUkhafxundp0u83nZQ25eav5ZMq1J8LYZZoGGtgTxDTsTGiFLp0q+lk+mfieUHljX6XgQiARo2GiohkPD84VQ6DXVw+c4FgVhA0vEk0m+ml0gXuSESFGDUxQir0VZoWGtgPdYa/Tb66DTUweOQBx4HPWj0fSMi1kfgP8qfOryaMOlrgnYDbe5Nv4dIR4TEWPJC+vEiwvDe+eIdDi49SF523nNbUSuKhcsXYmRsRHBQsPrI+RFCH4Syf/f+Mo+fK4sjvx5BR1eHbfu34ebhRlhImFoB2L5hOx7NPej7bl+OHj6Ko7PKst24SWN8T/uyd+de4uPi2b5xO9lZ2Xh19GL6J9MJvhdMwJ0ADv9yGICMjAx1BEJQQBDde3V/KeZ6haIAps+bjnMjZ3Kyc5jy0RT1A+Xn5zP5o8ksX7ccbZ3aURqy2dvNANg4ZiN7Z+0lL7uoRmpWz4ymbzV9afk36dakcgJWIsBukip6IGJtBIrckp1VQleEYjvRFqFmzQaSKHIU5MWoxkLXrWqOYQQSAYadDZEYSYo5gAEoshWkXkpF00ETgbTo7u2RNeIRDN4wQOYp4+HBhyjldVkYXxmFv0CJskCptiDaTrYl8Z9EbCc+rqWhlD+meXTPk/8+q92y8LKE4dm62WLnbkdafFqpfX3kKPh0f8uiL8gvKBKCVxKMjI1Y9M0iAL796lvCQx9/i7Mnz2bWolno6etV+bvPSM9gSK8hbNuwDQ9PD7WFwb6ePX3e7YNXBy9kejIGDBlAlx5dVFYQPRmbftjEN4u+oX3T9piZm2FgaMDEaROxtLakc/POLJ69mF79Vf4bebl5rF+5nm0bttHKqxUenh6vngKgoaHBuu3rEAqFnDt1jh+3/6g2zTg3cn5ptJqqwMhVIzG1N0WpUHJ4+WGmNJzCqV2niqTWdWrjVCv5W3tbI9YTkxeXR8zumGLXc8JzSDyaiM0nNjX+XsJWhiHPkiOUCrGbWnVhjiJtEdbe1sT+FFuseEzMnhgsR5Tf0lGQUoDUVFrmUcZzKy3iqjvuet35Z93PImJNBAl/JpDkkwSA1WgrLIdbou2ijTxLTsyPMWT6Z5J8PFmdCOhRNEL0jmjSbxS1HOUn5RO1JYq8mDwS/kwg8Z/Eki1pL2EY3rtz38WmUcnf9p0Td/hr/V8A/PndnwSeCyxd+VEoObfvHJcOXkKpUPLT3J+IuRdTJu9ho4fRul1rcrJzmD15NgB///E3yUnJvPfBe9Uyl0aMHcHRs0cZO2Es85fOLzLu3276Vv3/lRtXIpE8tgZ169mNm6E3CYwJpM+7fVSWV20ttv+0nfC0cPb/vh8dXR21cvPpjE8ZO2EsYyeMfWnkXIWLAbVs2xLvSd5s+m4T82fMp4FLA7au28qZ62dqpMMrBqxAolGySS4zObPK+BhaGfLVf1+xacwmrh+7TkJ4AhtHb+T3lb8zfMVw9Q69uvAi+Yv1xVh/ZE3Yt2GEfRuG1VirIgtt+OpwLD+wRGIsIS8+r0bee05EDhFrIghbFYbEWILrTle0narW2mQ70ZawlWFErIugwdIGhasYxB2Io+mxpjxYXHZKZWW+kpAvQ8gJy6HxD42rdTweOVwKtYRIjCTkJ9VsdrnaxF+7gTYu64ruoEU6IvU7FGmLsBxuieXwokqgy1oXXNa6lNimxEiCtbc11t5lO/E9CsPrOakni7supuvYroil4jLD8I6sPEKHER14cPXBM8Pwzv54lp2TduLR3aPEMLySUM+zXqk+RG5d3HDr4lY+JU0o4I1hb1SomqBAIODbTd/SybMTx44c4/fffmfx7MVsP7C91MiAOtSQBeAR5n41F/t69qSmpNKvSz9mLZyFmUXNVI2acXAG3wV+V+Jf/9n9q5SXgYUBs4/OZvr/pmPppPr4I/wiWNpzKauHrCYnI6dan/VF8rebYodAIiArKIv4Q48TZhSkFhC9Kxq7z6o/yZA8U871Hte54HaBc3bnCF8dTqNNjXgj9A1M+5hWOT8NKw0shlgQtSVKnVEu8e9EDDsblukFnhOVw91JdzljfobkU8m08WuD+RDz6pkT7Q1o8HUD6i+ur/6t6dGmOMxyQGomrfZ38rrzr2po6WlhaGWIqYMpjTo04tSuU0DpEQCDFg7CzMGMTWM24X/aH68hXmUKYO+t3qQlpPF1z6+xcrHCrF751mkTO5MXNiau7q6MnzIegA/f+5BO3Tqpo9BeNSgUCo78doS42Dgu+l586fpXKQVAS1uL2YtnqzXYkeNG1motqdWAVqzyW8WH6z9Ua8YXfr7A8j7La6Ta3ovgr2GjEoYAYd88zo8euTkS427GaNXXqvbnFumIaPZ3M1r6tkSrvhZKhZK0q2mIdKvPOcnuMzvyk/OJ3hmtet4tkdiML/uoQ9NaE5e1LpgPMSftalq17lRSzqZwf859ThudVidLutzmMqHLQsl7WP3WmNedf3XiRYXhPQ+y07OZ4DCB498fr9J2Zy2chVgspqCggA8/+fDV3WELhYyfPJ7IjEhat2v98vWvsjfqG+irH7A2mmaezDQIIJKI6DGhB2uD1qo9bP1O+XHl9yu1kj+grsGeejGV5DPJKPOVRKyLwH66fY2+C7G+mCa/NEGoISTq+6giqVarGjJPGYYdDIlYE0HGnQykZlIkJuXzAnda6YTUQorfKL+6Msx1qDBeZBhepb9NiZis1Kwq9wXR1tFGJFL199G/dXiJFIDajj++/aPE33UMdZhyYAqWziqT/P1L92slfwDdJroYdzdWWwFi98Wi3UAbvVZ6Nf4+ZJ4ynFc5AxDgHVCt9d7tptqRdT+L24NuYze5/EcdIh0RjXc1JvV8KuGrwus+olqKuANxnLE4g4/Ah3sz7j0OR1WqnFR9BD4EfhJIbmTF05C/yDC8ykCiKaHb+G60e69d3cSoUwBqD0JvhhZJuFFk0mtI8OiuCuPQ1tcmPzefIyuOMEQ0hElOkwi5FqKmve1zm/c13+fAvANkJGVUC//qxKPdfsLRBILnBZdr96/IVRC2IkxVCtXpPOnXHntIJ/kkcULzBMHzgivsuEiXZ9kAACAASURBVGXziQ3mQ8yRp8u5Pfg2ipyiIYrpN9K51PoSPgIfgucHI89QnePnJ+Rze/Bt/nP/j+RTycXaVcqVRRL3mPQxQctRC017TXRcdR7T5SmLhUUqchXIcx7fa/CGAXbT7Lg/5z6p5+vq0tdGmA8xx/0ndxCAlqPWYwuRAAw7GGI7yZaGGxuq81VUBC8yDK8iuPHXDYZpDGOUwSgeXH3Asj7LGCIcwoIOC2r9+4+OjGbgWwMxEhixec1m9e8+x3yw0bVRR8fVagXgUTrgmihq8KQ5VSEvnd+ja2XRVITnzkk7S2xLqVQV6RBLxbR5tw0SDQl9Z/Sl92e9yUrNwtzxsQOYkY0Rvaf2ZsiSIRVKH1wR/lU2zgVKeIqdUTcjZE1loASRrgiTXibF73nqPQk1hNjPsMfuMzsKUgvQcnzsL6Bho4HdVDsclzgiMZKU3o+n3vsjuH6v8v5Pv5FO4ISiIUiypjI8/ueBWF+MSEuk9hWQmEiQmklp9k8zDDsV9azOupulEtb/pRK9PZqClAIEQgF2k+2wm6La/edG5hK2MoyciBySTyYTvaswE+DWKFL/SyUrMIuI9RHkxqh2fI5LHNF21uZaj2vcn3Of3Khc6lC78KgaYfC8YPITH1sAwteE0+DrBs/V9osMwysvLBpY0HNyT1b7r8bFy4X5PvP5ZOcntBlYdeuRXC6vMRlTEVjZWLFt/zZMTE0wMn6cmt7c0pwFyxYw/MPhr8w8Flf2xuhIlZNUTnYOyUnJGBoZVlsnEyMSi/y/fvP6JdIlhKnKgabEpiAvkCMSP58J7fqx68z1mst7X75H486NEYlFJEcns2/OPkKuhTBuy7giwn7IkiFcPnyZvTP38tHmj1Qf6eo/Gb1mdLXyz8/N59jaY+ydtRfz+uZ8duAz6nnWU1sglvVeRt8Zfen1Wa9SlRBlvpK82Dxyo3ORecqKWQHuDL+j2v0/ddT3ZC2A3MhcNKwe73oclzgSfzieezPvqQuohK8Ox2WNS5nPnR2mSnQiz5BTkFaAWO/xNBXJRLj/4s7lNpeJ3hGN2ECM03IndVy4hrUGTiucuDv5LuZDzNGqr0Xy6WRknjI0LIvvyLRdtHFa7oTT8qI5FWw/tS2itNhPty9m/bAeZ431uOKOWkINIW3vVDxTpFFXI1x3uJIblUv8EVXkhVBLiOVwS3wdfZF5ynBZ64K2kzZR26KQGkvRaaxD8LxgtWWjPDTPgn4bfaw+tCI3IhdFrgKhthCpiZSIdREIpAKcljshayYj/DvVMYdAJMBskBn+o/1Jv55e4ecWyUSY9jbFbZ8bsXtjybiToX6XGtYa3HrnFsZvGeO0XPVe066mPZO+JuC0won4P+K5N/0erjtdid4Rjfkg83JV+isLLzIMryIKwPBvhpOdno3P9z5YuVjRcWTHKms//mG8epMZGx2Li6vLSyU4DQwN+OLLL/jyiy/pN7AfGpoaHNhzgIXLF75Siqxo5sKZFerxhbMX2LVlF+tWrCMnR7X4+572JSkhCUdnR3R0dCrUgQeUHlsdExTDP5v+4cD8A6QnqhYW/9P+ZKVloSXTwshKpX1dPnyZvzf8zb9b/kWpUJKXlUfg2UDSE9JxaOqAWFJxPSfkegif/vApuka6nNlzhn2z9vG/r/7Hv1v+xcDCgI+3f0yLvi2KDqZEhL2HPTsn78Stixv+p/xxbuusPq+vLv4isQiXdi7kpOdw/9J9Bi0YhERTojb/aWhr8N5X7yHVKu40dCnrEhGrIwheEEzWvSxSzqeohe4jganjqkPi34k4r3ZWC9qM26o86SFLQihIKywhei6FgtQCNCw0kBhLEEgE6HroEjQ5CKMuRiSfSka/rT7aziUfWySdSCJqSxSh34SizFPt/pPPJFOQWIC2o7Z6R69hoYHUXErC7wmkXkglZncM2Q+ykTWTIZaJ0WuuR/KJZBL+TMB8iDlhy8OoP6/+S1k58MGix/M/OyQbwy6GZAZmEjwnmJRzKSSfSkaeISf9ejp5MXlo2moiMZFwZ9gdEv5IQNtJG+fVzkRuiESRqygXTVkwe9cMl9Uu3Blxh8RjiaT4ppB8OhmLYRZk3Mkg/Uo6UnMpOi463Bl2h5RzKaScTSH5RDISQ0kRhbDclqc8JRl3MrCbZkfo8lCitkaRci6FxGOJiHREpF9PJ/t+Ng5zHIg/GE9WUNYz6cuL+gvrV958qilE006T4LnB6DbRJcknCYdZDhVqozkl102p7qO9qkJCeAIHvz6IpZMl7m9WPEd/fYqOf3ZWNhtXbWTpgqXERKmsFZfOXyI9LR1be1u18/nLgCbNmnBgzwFSUlLIzcnF1t4Wp4YVS862fNHyRS/yGcpVDrg6UZ5ywK8avh//PXdO3MGztycjV9VciGRedh7Tm0zHvau72gKx1Xsro9eMVisET+NROeDqRMD4AJJPJGPS20TtyFfdyH6QzX/u/6HXUo+Gmxui01Dnhc2HjNsZXO95nXpf1MNmvA15D/O4/d5tGm1pxHnn80VoPQ55kBOZw92Jj+vbC7WEKLJVgrve3HqY9DbhcpvLKrPjYHPcD7hz3uU8WUFZ5aYp0RyoJ+aN8DcIGB9A3E9FKy1KzaXouOqQfDIZuyl2WI+15oLbhaIC8Yl+VgadUjrhP9afh78+LLFNr0Av1VwqtGQ8i748eFQO+Hlwo/cNknySaBvYFi2HioXHllUO+FWAUqFkhO4IvL/3pv377St8/5u8+Uo//4WzFxjcczBDRgxh5caVFbf6vQrlgOtQMQxaOIiYezE17hkr1ZIyftt4fL73IeBsAKd3n6bt4LalCv+aguNCR7LuZWHxnkWN8dSqr4XFCAuE2sIXKvwBdN11abSlEQl/qI6o0q+l02hTo3JlMtRtoouOS8n9F2mLsB5rTfLJ5FKjIspD8wgmfUwQ64tJPl78qCAvLo/kkyUfIQjEAiyGWqDIVqDXXA/Pfzxx/MqRFr4tcNvnhsMcB7yCvLD60IqOCR3RbVI+XxjLDywrJMwf0Qs1hcV42k+3p61/W2zG2/BG6BtFqlg+Lww7GSLSFVVY+NcGCIQCrBtZY+dux+uItu3b4tzQmRZtWryS/RdThyrHI4H7IvKku3Z05c2P3mTzh5vx7O1ZpedyldYyHxUMqmF1U6QpemG56osJ154mxO2PI2pbFAKRAOO3jEul1fPUw2GWg1qw3nn/TlFFz1SKw2wH7KfaE/JVCJEbI4uVxC0PTTGlqVCA5SU+O6GOxESiNncbdDAg6R9VDv20q2kochVo2mpyvdt1NGw0EMvE1J9fn9TzqVxpf4XsB9mltmvW3wztBtqIdERYDLcg5oeyndZKolfkKEj6O6koz5Bs6s2rhyJPwWWvy+Uq0FOH8sGigQVmDmav7fNLNaQIha/mXrpOAagGPPJef1HJYAYtHMS/W/59aWJz1eOgqHm+L1NCHufvnLngdoE3wsp2ykq7lkboslAAEv5MKL4bj88jdGkoBu0NMGhnoHbGqyjN08iNVUUriPXFFCQXlEmbn5Cv7qNwlRCzgY8FgDxTTvq1dORZcrKCstBpqIMiR0FmwLNrdTw89FBt0i9LUXgWvTxTXoynIltB+rX0YsWennuelbPiX22Fvpk+mjLN1/b5FQrFSxepUO7NWZ24rlqkJ6ZzcudJAHz3+5IYmfhaWSCKCYrEx2l1Y/fHVio5SmWQejGVlDMpZNzIKLUSW42/F2MJYn1xmXUFis2n6+mk30gv0bPcf4w/hp0My6xUWB6aR0j6NwlFrgLjbiVbJ6QWJWeeU+QpiN0X+9ze70/j0bwpb7sVpa8KJJ9K5uHBhxSkFhCxLqLGimO9TDC0NHxtC/WcOXGGe4H38DnmQ2R4ZJ0F4HWHzFhGn2l96DOtz2trgXha6NlPs8d+Ws2mD9ZvrU/rGy9X7u20K2nkxeaR6Z9ZJMFQEZSwjkrNpRh3NyZmTwwCoUCt2OXF5hEwLgDXXa6kXkxVO/iVh6Yk5ITnEPJlCE7fOJF2OY3skMc7aouhFiSdSCq1jwKRAOtx1oSvDi95a1EenaeEdo26GJGfkl8kmVR56BVZipJ5VvGWx7CTIa0utnqt1zxdY93X9tk7dOnAg6QHr2z/6xSAWmiBeFRRzHe/L4ZWhhjbGNcNzEsAvRZ6dErpVOp14x7G6DXXQ7uBNg6zHVTJl7RVZ9tX2l9B5inDqIcR2s7amPQyIfGvRB4efIhJHxOan2hO8PxgMv0zn0kTuze21HDAkC9DyInIofGexuSE55D9IJuC5ALifokjLy4PWVMZJj1N0LTXpN68eijzlQgkAox7GBO5IRJdN1103XWRmEqIPxxPTngOZgPNEMvEWI2yInpXdDGeIpkIs3fMEOupaLQbaKsVH8MOhlxsfhH9tvpoWGtg/LYxmXczMe5uXCr9pTaXcJjpUISn8dvGiA3FWH5gqXqmlIK6CVlFeFVCFutQgg5dFwb4eqMmwgDrUMb8F9TN/xeJqggDfB686mGAAFd/v0rzPs0rN/6veBjg8+JFhwGKSTZ8sSPgM6huFXqhGkDd+L9gHbxuCF4kuv37Yvm/AvL/7Nm9NGjQEkvLknN4NGcQ/FJZDez1nn7KqizQUCkFoA61Djk5GUyb5s6sWb9ja+tWNyC1AIaGHWne/FThoqFAoSjuIS8UaiEQCFEq5Vy50oHU1PN1/Ovw3AgKOk+rVv3rBuIlwO3btzl48CA7d+4kNDQUADs7O8aMGcM777yDu3vFsjHWKQC1ECKRmPT0BITCutdbWyCRmJKVdZc7d0aQlnaFp4P6dXRcad36KgKBJqGhy6tc+L3u/F/vDUUmGho6dQPxEsDd3R13d3c6depEx46qHC+7d++mU6dOlWqvLgywlkGpVCIWa9C//yysrRuiVCrqBqUWQCo1JShoGmlpl4sJP4FAgpvbjwiFmqSnX+PBg4V1/OtQh1oMa+vHmSzt7CqfhbFui1iLoFDI+egjC4yMrHBwaMry5X25ceMvvvrqAvXrN68boFcYYrE+ycknS7zm6LgYmawZCkUOd+6MQKnMr+NfhypBZmYyurqFRdcuH2b16sG4uHihrf24KE9q6kOCgi5gaenMihU3kEq1+PzzZmRnp2Fj44pQ+Dgvg7//GTIzk/n44+107jzmufp26NDPLFu2EG/vSXz33TKmTfuCfv0GsXXrOsRiMX/99Ttff72a5s1bk5mZwebNazA0NOLIkV/58MNP6NPn3Vf2vYhEj8f0ebIQ1ikAtQhCoYjly69ibGzDqlWDmTr1Z1JS4jAxsa0bnFccoaHLSvzdwOAN7O0/B+D+/VlkZvrX8a8gsrKCCA1dSnT0LpycvsHefkYxmoKCNM6etUEqNcbZ+Tt0dBoTGbme8PA1GBi0R1u7ARkZtzAzexcHh1nk5yfz8OH/CAz0RkurAQYG7cnM9EdX140GDZYjkRi+EvMuMjIAGxtVKe/09ASmTfuN5s17F6FZurQnAoGQTz7ZiVSqSidtZeXCxIk/IBY/Th4VFHSBK1d+p2nTt55b+AP06zeIyZM/QiqV8vff5xGJxMydO41PP52Os3Mj9PT08fYezpUr95g1azJDh47Ey6sDVlY2/Pzzj6+0AlBlMqNuaa1dMDGxIz4+jIsXf8PP7xSmpvYIBHWvuTZCJJLRuPEPCARCkpKOEx6+to5/JaCt7YyDwxyEQi3Cw9eWaEGIjt6GUlmAkdGbmJr2Q1u7ATY2E9UWCFfXHTRsuIn7978gJORrJBIjrK3HIpVaYmExFFfXbTRrdoyEhKPcvj34lZljUVEBWFs3KhxvcTHhf+LEdq5fP0bv3p/h4uKl/r1Zs7eLCP+8vGw2bBiFlpYMb+/vq6RvAoEATU0tmjTxxMLCClNTM06c+JsrVy6yb98uMjMzaNiwMTk52Rw58iuNGzcB4K23+rBjx4G6BaROAaidMDV1QFNTFzs797rBqMVwcVmDllY9CgpS8PMbxTOr/dTxL0OYSLCwGEpeXiyxsT8VuaZUyklOPoNM1gQQPXFPUQOqnl5LdHXdiI3dXyKNWKyPmdk7JCX5kJ+fUO6+nT27l5iYoGody5s3/+GLL9py586JUhWAjh2LljZPTIxk9+6pWFo6M2TIkiLXnqbdv/8LYmKCGDlyNcbGNtX2HNnZWXTq9CbDho1i0qTP2bnzF6RSDeRyOYGBfmq6hw9j6xaQiigAvr6nMTISYGQkwMREhI2NbrE/ExMRRkYCTE3FXLpUO71wIyP9WbNmKB99ZM7IkfpMmuTEjh2fcveuL3/8sQo/v1NVzvP69aMsWtSFkSP1GT3aiJkzPfnttyVERNxh9eohJWrGjRp1wMiociVPs7Lucv/+bM6cscTHR4CPj4Do6B3lvt/Pb4T6vqtXOxMW9g1yeVal+pKUdIL792dz6pShus3jx0WcOWPOyZN6nDtnz/XrPYmL+6XGBdCLVfL6Y2U1GoDAwAnk5kbW8X9OaGraYmY2kPDwb4v8Hh9/EDOzAeVuRyyWPUPZECISld+rPijofKW/5fIJ/7+JiLhDly4f8uuvi4tcS0tLQCYrOZPo5s1jycnJYMKEXWrTf0m4e9eXo0fXFJr+R5dIs3z5IrV8MTeXlihfHl13dbUmJeVxaeonC/F07tydcePeJzDQj4iIMNau/QaALl2688UXU4mKiiAuLoYDB/ao70lJSWbx4tmsW7eCrl1bkpmZwcCBb9G/f1dSU1MYP34EHTo0JTo6kqioCHr16kBcXAznzp1i48ZVDBr0Nvv37wYgNzeX775bxvLlixg48C1SUpLZsWMTb7/9Blu2rKVJE3vGjXv/pSkeVG4FIDExngYNXDh+/BLx8QVERmYU+Tt+/BISicrkM3nyTFq18qp1i66//2lmzWqBQCBk+fJr7N6dyoIFJ9DSkrFwYSd++GFalfM8fPgbli3rQ7Nmb7NlSxQ7diTw8cc7CA29ybRp7ly48HOJ99Wv71lpntraLjRosJTGjXerfwsLW1kuAZubG0Vs7IFCk6Eunp7/YG//OSJR5dKFGhl1oUGDpTg6Li5cXPXo1CmdDh3i6NjxIfXrLyAl5Sy3bw/Gz2/0a6EESKXmuLqqzKhxcQeIjd1Xx7+KYG8/jfT0myQlPc7QGBv7E+bmQ8uhrB4nI+MO1tbepXwbMcTF/YyFxQiEQq1y96m6w/A8PHrQu/dUunQZQ3JyDAEBZ555z/Hj27h582969/4MZ+e2pdLl5WWzcePoZ5r+ExPj6dWrP7duhREXl1dMvixfvla9udmwYScGBob4+BwjNTWZAwd+IDU1pVCRWIehoRHdurXhgw/eoUeP3giFQlas2ICRkTFt2rjy8ccjGTx4uJq3j88xTE3N+fTTGXz88Wfo6Ogyf/5SUlKS0dc3YObMhSQnJ2FhYYVUKmXkyHHo6enz44/b+eSTqaxevYUZMyaQnp7G1q1radeuIzNnLsDS0opNm1bTpUsPgoOD6N69F76+t7lw4SxHjvz6Uqwl5XYCTEiI58svv6VZs5bFruXn5+PtPZzc3Bw8PDyZOXNhhTrh67ufc+f2c/Xq72rzkZfXEJo1exuACxd+4dy5fVy+fAiADh1G4OU1BE/PXjU2UAqFnPXrP8Dc3JGJE39Qe7YaG9sydOjXODq25Ntvq9apJDr6Lvv3z6FbN2/69n3smOTg0JRp035l164pHD26psR7jY2f3/FPR6cRQqFKqcvMDCA+/ndMTfuWeU94+HcIhRrI5flIpeYIBJIqGQtNzUehLgK1MiEUamJlNQZQ4u8/lpiY3ZiYvIW5+XvlbregIIWYmB8IC1tJTk4EMllTWre+/gwz4wPOn3dGqZRjbj4Ic/P30NV1JyHhD0JCviQ/Pwmp1BxtbWfk8gzy8xORyTxxcJiJvn6b5x4LV9cdSCQm5OZGERDwcY0vGrWZv55eCwwM2hMWthIjozdJS7uETOap/g5KQnz8IVJSzpGd/QAPj0PFvpG0tMuEh68iI8MPB4fZ2NpOeCkVS4FAyIABs/n11yXMm/cveXnZaGholyALwvnhh2lYWbnw3ntfltnmvn2ziYm5x8cf7yjT9J+ensa6dTswMCjuHBkWFsLs2VMAGDt2Ap07dwfgzTffJja2aHVRExNT9uw5WKwNc3NLfv75aIm8W7RoQ9euLfH3v80XX6iOMpo0aUZubi7379/lzp2bGBoa4et7mpCQ+7zzznv4+d0iISGefft2FVoeupGUlMjp08fR1ZVx795dTE3N0dTUQiqVIpPpUa+eIwB9+w7k2rXL9O//4n1Byq0ApKWl0r595xKvLV06n1u3rqOhocnmzXuQSCq26LdrN5SmTd9i9Ggj9PXNmTBhV5HrbdsOonnz3gwfro2OjgETJ/5Q4wMVHn6bhIRw2rQZWCSs5RFatRpA06ZvVSnP69ePoVDIsbVtXOL1YcOWcubMnhKvyWQmz28eEkoQCrUwMxtAdPQuwsK+KVMBkMvTiYrahrX1h4SHryl2Rvp8i1PpJV4tLUcRGDgBhSKX2NgDFVIAxGIDbG0nIRbr4+c3ivT0GyQmHsPY+O1S7wkLW4lSKVcLI5FIVQ3Nzu4zsrNDiIhYh5PTSiwthxd+O5e4fv0trlz5k2bNjmJkVPn8pzY2H2Ni0hNQ4uc3moKC5Br9Dl4H/vb2U7l5cwAZGXeIjNyMk9OKMulNTftjaNipDKWiJXZ2UyvVl5oOw2vffji//LKIoKALSKVaWFm5lGr6/+STnUgkmqX2PTDwHMeOraNZs7dLNf0/gp2dQ4nCX6FQ8PHHH5CRkY6TU0MWLfqmyt+3ra09vr63mTt3Gh07enLpUiD6+gYMHDiM3377CT09Pby9J/Pzz3to1MgNXV0ZBQUF6OjoMGzYqMK1eBS5ubnI5QW0bt0OV1f3QqtPLomJ8UX4GRoa8YIzAD9e48tLOGXKLLS0imuD//13Tn3OsmDBMlxcXCu5w5MV/qtbitlPC6FQ9MIyUj16YdevHyMysuRQozZtqjqvvornv/9uITs7vcQxedor9xG0tGRVuCBOBwSkpPiWmWEtMnIrhoYd0NZuWMM7FxEaGjaF1qiESrUhkRijp9cCgJCQpWWYNB+SlHQcqdQMgUCkFv6P2zEqQQC0wsFhNkplPsHBCyr9nNraTjg5rQQgImI9SUn/lvE9VX3o5+vC38SkL9raDbh3bzoikQ4SyYurpllSGN6CBSeZMeOQ+k9Hx6DEMLzVqwOYOfN3NV2/fjPJykotMwxPJBLTv/9Mfv11cREHwMfm8q3cuvUvvXtPLdH0n52dVij4sso0/T+ie4TZsxeX2J81a5bz33/nEIvFbN68B01NrSof4yNHfkVHR5dt2/bj5uZBWFgIAAMHDmP79g14eDSnb993OXr0MI6OqnoIjRs3wdf3NHv37iQ+Po7t2zeSnZ2Fl1dHpk//hODgewQE3OHwYVWRhIyMDLUMCQoKoHv3XrwMeK4ogIyMdD7++AMUCgUdO3bF23sStRV2du4YGVmTm5vJ3LlenD69uxhN06Y9MDevX2U8PTx6IBAICQ+/zZw5rbh3779iNN27l2wCbdKkW5X1Q0enMcbGKutGaOg3pShIBURErC1UFmoWCkUOeXkxAOjqVr72gYFBOwwM2pGScpaUFN8SaSIi1mJj80mFjza0tBoUKhCV8z4WCMS4uf2ISKRNZmYg9+7NLINWgo1N1ZrGazt/pbIApbKg8H4htraTSUz8B1vbiU/QyNU0j+558t9ntVsZvIgwvE6dRhMefpvTp39QKx+gMv3v2TO90PS/pIRv4w6hoTcAlek/NvY+I0euLtGB8cyZH5/57LduXWfZMpXCPGPGfJo1a1Et60dGRjpDhvRi27YNeHh44u7etHDjU48+fd7Fy6sDMpkeAwYMoUuXHoUWVj02bfqBb75ZRPv2TTEzM8fAwJCJE6dhaWlN587NWbx4Nr169S8c/1zWr1/Jtm0baNXKCw8PT14GPJcCMHv2ZMLCQtDXN2DDhl0IBLW3splIJGbChN1IJJpkZaWyYcMo5s71KuIwY2hohYmJXZXxtLFxVX9oUVGBzJ3rxbp1w4mLe6CmcXJqUyPP7+Cg8kGIjz9CVtbdYtfj4g6goWGJgUH7Gn83YWErkcuzEAqllTa1Pn7OWYWKTnErgFyeQVzcAaytx1a43UdZ7AwNO1eqX/XqzUVPrxVKZQF+fiNKLIbzCFZWo8nLi6/SMa7N/LOy7hMRsYaEhD/Vzn9WVqOxtByOtrYLcnkWMTE/kpnpT3LyceLjDxfeo3JMi47eQXr6jSJt5ucnERW1hby8GBIS/iQx8Z8y+/AyheFJJBr06TOdwMBzGBnZqC2gmzZ9SE5OZomm/4KCPPbtm42trRsBAWf466/STf8BAWeJjb1fZh9yc3Pw9h5Ofn4+zZu3ZurUOdW2fowYMZajR88yduwE5s9fWkSOffvtJvX/V67cWOR4u1u3nty8GUpgYIw6qZCWljbbt/9EeHga+/f/jo6OykJoZGTMp5/OYOzYCYwd+/w+IE9GEcjl8kq3U+lD2j//PMTevTsBWLFiA1ZWNtR2uLt3ZdGi06xf/wHR0XcJCrrAggUd8fTsxYgRK4qZy6oCAwbMwcjIml27ppCZmcLZs3u5cOEXunUbz6BBC9Tng9UNQ8PO6Ok1Jy3tKqGhK3B13faUEP6WevW+qNH3kZMTQUTEGsLCViGRGOPquhNtbafnatPEpFehQ9+fZGTcQle3yROL8fdYWLxfoRAuuTyLiIh1RESsx9CwA05OyyvcJz29VuqxffBgcWExnBI+ZrE+5uaDcXZexc2b/apsnGs7f23tBri4rHtK4dehceMfCv+vjaXlcLVPxyO4uKzFxWVtKULUCGtr71IjAooK/7+JiPBTh+G5uXVRX6upMLyn8eab47h920ctDM+c+YHbt33Q0THk8OHlxYR/aOhNQImOjgEbN45BqVSSzNBwjAAAIABJREFUmZnCihVFqwimpcUTFPQfH320qUz+CxfO5O5df7S1ddi8eU+R1Ld1gIiICPX/o6OjcXR0rDkFID4+jilTPioUUEMYOHBYlT1YSkpssUnz2Jz24mMnGzRoxcqVt/jzz+84ePBrsrJSuXbtT27e/IfBgxcyYEDVa6odO46kadO3+emnuZw8uYOCgjyOHVuLr+9+PvlkZ41FQ9jbT+f27aHExv6Io+MSNDQsAVX4U0FBGqamA6q9D3J5Jtev9yAnJ4rMTD8EAiGNGm0qFMy6VWFsxt7+c/z8RhAaugw3t32Fcy+fyMgttGzpW65WYmJ28fDhzyQm/o1EYoKnpw+Ghp0qlZXR1XW72qHSwWEWDg7Fzd8CgbBIaFlGxu0qG/PXnX91w8OjBx4ePVAqFRw5soKAgDM0atShzHseheH16TOtSsLwnoaGhjajR68psgY9bVVQ7dQzmTbNne7dP+bdd+cCsG7d/ecaj9Onfdi6VaWQLVmyEkdHJ15VKBQKjhz5jbi4WC5e9KV163bP1d6T5YAfYeTIkYwaNYoBAwbUTDngiRPHkJiYgKWldRETSVXAwMCCGTMOlXjtvfdejtIFYrGUfv0+p0uXD/nf/77ir7/WI5fns3//F+Tn5zJ48KIq56mvb4a391Z69/6MH3+cydWrv5OWFs833/RjzpxjVXrmXxrMzAahpTWb7OxQIiLW0KDBssLd/0rs7afWSMphkUiHZs3+pqAglYsXPcnOfkBa2tVy7bTKCwuL93jwYB5xcT/j6LgELS1HYmP3YWzco9wOYZaWo7C0fJ9r17qRlHSc/Pz4So/Pf/+92IyOrzv/msKLDMMrCebmz95V5uZm8fBhCJGRflW0AUzmk09GoVQq6datJ6NHj3+l36lQKGT8+MmMHz+5Stp7VA54/vz5VdO/it6wY8cm/v33aJGEDK8DcnOzinn/y2TGjBy5ipUrb6rDZQ4eXEp6ekKV8IyJuUdWVmqR36ytGzFz5hE+//wwmpq6KBRyfvzx8xpaoETY2X0GQGTkZuTydDIz/UhPv6rOylZzSpg+TZr8glCoQVTU90XSrz7/c4qxs5uGUikvdHpUEh7+Hfb2FU30JKBx4z1IJCYEBHiTkxNGHepQFtq3H05s7H2Cgi4QHX23xsLwKgs9PVOcnNrQokXVHPlMm/YxMTFRGBubsG7d9roJUd0KSkWIg4PvMW+eysv7o48m0qlT6bvOqKiIWjVQ2dlpHD9esglNJZR/RyQSI5fnExJyvUp4PnhwtdTUwi1a9GXMmHWFO/Cb5Ofn1sg4WFl9iERiREFBKpGRWwgLW4mNzScVymxWVZDJPHF2XgVAQIA3WVn3qqxta+sPkUpNiYnZTVTU9+jquj+RjKj80NCwpHHjnRQUpHLnzvvq/AF1eLWRkPAnZ8/a4O8/hoCA8QQEjOfmzf74+AgICKj8rvVFheE9D9q2HUSLFn2eu51fftnLwYOqLKKrVm3BzMzitZEvL70CUFBQgLf3cLKzs3ByasjChaU7M+Xn57Njx6ZaN1iXLh0q1Q/B0tIJKytV/PuTSTqeFxcv/lbqtebNVR+dVKpVJOSnOiES6WBjM75Q8fiWhw8PYmPz4jKb2dh8grn5EOTydG7fHoxCkVM1H4ZQC1vbSSgUuQQGTijx3LlsPE70YWLSG1vbiaSk+PLgwYK6VacWID8/mZYtz+PquoNGjTbTqNFmlMp8tLTq4ey88rnaflnC8MrCsWNrGTJExMiR+ly+fIi1a4czZIiI0aMNiYoKrHB7UVERzJihWkeGDh1Jnz7vlErr73+bs2dPvtD3L5fL8fR0ZOHCmXz11Vy++moutrYypk59tY4syq0AfPvtl1y7dqlcCRn27duJiYlphTqSmZlcqLlmlrIDT0ehkJOTk/HCBis+PpRDh0quS56WFk9cXDCWlk44OlZdvKqv70/4+58u8VpQ0AUAvLzeq5YQTFUMc3GFx9Z2EkKhBnl5sVhYDEUqNS12nwqKKu2L6t/ibbq6fo+2thPp6TcIDKycMlJQkEpBQepTysUERCIZxsZvoaPTuIhwl8vTUSrlyOUZT7WTUiggkor87uS0Al1dd0JCviYmZk+dBH3Foa/fsohFKCrqexIT/8LVdddzO6O+DGF4z4KDQzN6957K2rVBNG7cmU8/3cOcOcfo0GEEBgYWFfy2lUyYMIq0tFRsbe1Ztqzsss6rVy9VZ9p7UUhKSmT9+p0sXLicL774EienhmhpaTF//tJXah6Xy6vu2rVLfPvtV0DZCRnS0lI5dOhnvvhiKnv3Hi53J86fP8D58yrTT0pKLJs3j6VNm4Hq1LoXL/6Gr6+qROejGHwvr8Ho6Zmxf/8c/PxOsXz5Vezs3Ll924c9e2YwevQadHWNWb9+BHl52cyZ8xempvakpj7km2/64eU1hG7dvMsMnykJ+/d/QWRkAH37zsDevglKpZLQ0Bt8//14pFItpkz5qUqd4eTyfL76qgf9+8/mzTc/wtDQCrk8n+vXj7FlyzgcHJrywQcrq2Vy5OSEI5dnUFCQhlisp/5dKjXH0nIE0dE7Soy7z8kJL1TmYlEq5WWm8S0vsrPDCsejeH9EIhnu7r9w+XIboqN3IBYb4OS0vFypiAsKUomLO0BExHpyciLQ1XXH2LgnOjoNkUgMsbEZVyS6ITHxLx4+PERBQVrhYjoOM7OBhaGDv6uFe0SEqiaCmVl/pFILhEJN3N1/4uLFFvj5jeThw/9hafl+kb4YGXXF1XUHublRxMcfUVsiLC2H4+vriEzmiYvLWrS1nYiK2oZUaoyOTmOCg+eRnHwKoFw0zxZubbCy+pDc3AgUilyEQm2kUhMiItYhEEhxclqOTNaM8PDvAJVviJnZIPz9R5OeXvHjL5FIhqlpb9zc9hEbu5eMjDsAaGhYo6Fhza1b72Bs/BZOTsu5e3cyaWlXn0lf3dDWdnliboYSFDQNW9vJGBp2qJL2X3QY3rPQqFF7GjVqT25uFr6++9HQ0KZfv5l4eHSvcFsbN67izJkTCIVCNm7cjUymVyJdTEwUGzeu5vDhX1i/fucLFZx6evq0bKk6gklNTWHevOksXrzyuXzijh8/zpgxY7C2tqZv376FcyubH3/8keDgYK5du8akSZO4d+8eY8eOJTExET8/P5YsWUKnTp0KZfWzaZ6EICnp2UmJ27VzJyBA9ZFpaWmXuNtUKBTk5DxOznH3bhympmbPfGgfn+c1xeUwd247unQZQ48eE/jtty/p3v1jdezsw4chzJ7dkoULT2Fr60Zycgy+vvvp3btiCWNSUmI5eHApHTqM4MaNv7hx4xgPH4aQk5OJrq4hTZu+zTvvfFGlta5VSo8SPT0zrl37g1u3/iUjI4ns7HRMTR3w8hpCnz7TKqzEPImtW4v/lpUVRFzcAaKjd5OdHYyBQTtMTPpgZTVGvdvPzAwkOHguTZr8+oRwPEZi4r9ERm5Sm+KNjLphbNytcDdd8YqASUknSEr6h4iIjcjl6YUCygszs35YWn6AVGpRZBcWEDAOUBUPMjXti4PDHHW44ssIH5+i35KHxxFycsK5e/dxBjorqzHqcsz16y/A2PhtLl9WJYBq0OBrbGwmcu6cjVopKQ9NaTAzexdHx0VcudLpibTKAtzcfiQiYgOpqeexs5uKtfUYLlxwe0IgOiOVmpGScu45TN9p+PuP4eHDX0t89jfeCMfP7wO1IvMs+vIJ2ufNya7k6tUu5OXF0rr1dYRCzQrdPW5c6dfi4oLL5Yn/IhEXF8ynnzagVasBTJ/+vwrf37hxFJ6ejuTm5iIQCEpMN69ScvLJy8sDwNm5Ef/95//SjMHUqeO5dy+Q338/VeF7DZ/SF/r27YudnR3r169X/7Zjxw7GjFGlbl60aBHHjh3jv/9UWWHnzJnD+vXriYyMRE9Pr9w0FbIA+Pq+vDG1EokmU6b8xLx57YiPD6Njx5FFEmeYmdVj+PBvWLt2OEuXXuKffzYycGDFQygMDCzUcbGOji3UMa/ViXbtHhe1cXfvWmNjqq3tTL1686hXb16pNDo6DYsIfwBj47cxNn5b7ZhXFTAy6lJYEnjZM2mtrT/C2vqjV9y4XPyI48kIh6edCNPTbyAWy5BKLdTCvTw0JZoDxXq4um4nIGD8UzUVlAQFTUVHx7XUPmZlBZGTE1Gtz65QZFWIviYQHr6GlJRztGx5vsLC/1l42YW/an2tj6amLvXqNavU/ZaW1sTE5LyyX+vVqxfZv383p09fq5L2hMLi1uOhQ4c+YS0rak1t2rQp6enpxMbGqoV7eWjU/KgFsLR04s03x3Ht2p9YWDQodr1z5zGYmdVjyZJueHkNQSSSUIc6vArQ1W2Cjo5LiddEIm2srceSnHyy1AiI8tA8golJH8RifZKTjxe7lpcXp05nXMyMKBBjYTEUhSIbPb3meHr+g6PjV7Ro4Yub2z4cHObg5RWEldWHdOyYUCS7Ytnf9QdlpvwtjV4o1CzG095+Om3b+mNjM5433gitEkUxK+su9+/PwcFhFnp6LV/L+SkQCLCzc8fBoelr9+xyuZypU8czYcJUnJ0bVQuPW7ducffu3VLmXxbbtm2jc+fOODk5VYqmVigAERF+mJs7Ym3diH37ZpVI07PnJHJzM7G1daMOdXiZoafniYPDLOrVm4u7e/EdrVRqioPDbN54I4yEhKNcv/4WT0YdlJfmaWhpORQK+8Rn9lEiMSnMyjcLD48jSKXmAKSlXUWhyEVT05br17vx4MFCkpL+RlPTjtTU81y50r7EWhKPd5T9cXCYhaPjEurXf3ZCrZLoFYqcYjwjItajoWGNQpHH5ctexMcffq53pFTKuXPnA3R0XKhfv6hFMS3t8jPHujbBysrllbBWVDW2bFlDWloq06c/tgY/fBj73O1eu3aNZcuW8eWXXxbZ/T9CfHw8S5cuxd7enp49e/LXX38VO5YvDw08Ry2AlwWpqXFcuXKYAQPm0LJlP6ZPb0KTJt1o1qzn07pqnWSpwyuBtLRrhIaqjjwSEv4sYTceT2joUgwM2mNg0E7tjFdRmqeRm6tavMRifQoKksukzc9PUPdRKFyFmdnAJ3ZGmaSnX0MuzyIrKwgdnYYoFDlkZgY8sw8PHx5Sn+lnZz+oNL1cnlmMp0KRTXr6NXJzo5/7HYWGLiUj4watWl0pVhkyJmbPa2UR0NMzrbGaJC8LoqMjWbp0ATt2HCgSEffnn4eeO3uhp6cns2apNrK9ehVP825qasrs2bM5e/Ysvr6+TJkypVI0r7wF4OrVP5g/v4M6IYZEokHDhm+wdu1wzp3bp6bLzEzh1q1/SUgIJzDwHHWow6uC9PTrpKffKLEAkb//GAwNO2FpOaLU+8tD8whJSf+iUORibFxygq8nHS6fhEKRR2zsvgoVSSrfIqvy9C5vuxWlrywyMwN58GAxGhpWhIevwd9/bOHfaC5ebEZubtRrNUd1dAzQ1NR9rZ557txpaGpqcunSeXUegAkTRnHixN9VyqdZs2Y0bdqUzMzi4fE7duzg1KlT7NlTeljxs2heaQtA8+a9i9TH1tDQYcqUn0qcoEOHfsXQoV/VSZQ6vOQQlCB4zTE27k5MzB4EAqE6zDQvL5aAgHG4uu4iNfUiWVlBqhbKQVMScnLCCQn5Eienb0hLu0x2doj6moXFUJKSTpTaR4FAhLX1OMLDV5eytxBW6tmNjLqQn59Cevq1CtGrHAaF1bLn0dFpSNeueXVT9Yl1t6SaBbUZO3YcqJZ2lSUE5cXFxfHPP/8wYsQIFAqFuhSwhYUFW7duZdSoUbRu3RpnZ+dChfzZNLVCAahDHWoTjI17oKfXHG3tBjg4zAaUiETaWFgM58qV9shknhgZ9UBb2xkTk16FOQkOYmLSh+bNTxAcPJ/MTP9n0sTG7kWhKDl1dEjIl+TkRNC48R5ycsLJzn5AQUEycXG/kJcXh0zWFBOTnmhq2lOv3jyUynwEAgnGxj2IjNyAru7/2TvvuCrL94+/z4HDlD0EGYIg4iAHguLelpor98AytCxLzVXOMtNSy4a/0kzN1CL3yPymGTkKQVFQUZbsLRvZHM7vjwcOHDYKgnk+r5cvD89zP8/nnPsZ93Vf93V9ri60auWERGLCw4enyM+PxtR0EqqqOrRp8yrx8T9W4VRR0cHUdCKqqrq0afMqWlr2csPHwGAAPj7O6Om5oa5ugZHRS+TkBGNkNKLG9r6+vbGxWanAaWT0EqqqBpibu5f+pgzlDddI0NLSeyqFwP7r+OOPP/Dz8yMsLIzNmzcjEonIzc3l4MGDXLlyhZs3b/LHH38QEhLC2bNnefHFF5kwYQJnzpxhyJAhbNiwgU6dOtXZZubMmairqwsmdH10AJoST6oDoMSToTodACWe5v2vjE1pTjy5DsCToTYdgGcF3t5HcHOb/Jj9/3zffwbNXEtPJJPJmjlc9Ugzs09uVv4poud7AGj22+85h+h5v/+aeQQafqGZO6CZv8CwYZ81K39ZsN3zimdyCeDy5fvs3fsXV68GkZtbiJmZPhoaEkaN6o67+0BiY1Px8gpk9eqmkQS9f/kyf+3dS9DVqxTm5qJvZoZEQ4Puo0Yx0N2d1NhYAr28mLh6tXKEUUIJJZRoAB4+fIifnx9+fn5kZwvqn5MmTaJnz/rVWPn111+5dUuQpG7Xrh0dOnSgT58+SCRK/ZfKeKYWbrKz85g6dTtDhnxE69b6eHl9SHz8Lm7e/IyLF9dhbW1Mnz5rGDBgPamp2Y3On5edzfapU/loyBD0W7fmQy8vdsXH89nNm6y7eBFja2vW9OnD+gEDyE5NVd5dDcCePXvQ19fH19f3ueRXQgklBJiYmPDiiy8yZcqUCpO+y/XyFmZlZXH79m0A1NTUeP311xk4cKBy8H/WDYDMzFx69VrF0aPXOHZsKZ99NhMrq3LJX01NNdzdB+Lt/Qnm5gakpTVu1cDczExW9erFtaNHWXrsGDM/+wwjKyv5fjVNTQa6u/OJtzcG5uY8SktT3l0NgKamJvr6+vLglDKEhoYyefJk+vTpQ7du3VBTU0MkEiESibh79+5/hl8JJZRQhKmpKSoqKqioqJCcnMz9+3XrSFy9elUuhaujo1NFFrclQVUVPv0Uvv0W1NRgxgz46y+wtIT//Q/efRcmToS1a0FPT9i3bBns3l01dmLjRihbzdPVhQsXYMECKFMWLjt+9Woh7uSHH8DY+BkyADw8dnL/fhweHkMZN65mkQ0rKyN27ZpPenpOo/Lv9PAg7v59hnp44DJuXI3tjKysmL9rFznp6fU+d9++fTl27FhpZcFITp8+zeHDh7l+/TqHDx+mf//+8rY6Ojq4u7uTnJxMZmYmP/74o/zfiRMnKCoqQk1NDQcHBzZu3IhMJiMuLo5Tp07h5+fH+fPn6du3b4viB5gxYwaRkZF07dpVvi0wMBBnZ2eGDx/Ov//+i7+/PzExMUyYMKHR76/m5v8vws7Ojl27dnHixAn5tiVLlnD48OHngl+JJ5ydisVIJBK6dRNkhi9dulRr+4KCAq5fv46Li4v8+JaM4mK4exdu3oTCQvD1hfBwiI2FsDBhwD5+HLZvh8xMCAmBkyeFv5cuLT+Pvj688AIMHFjmBYHgYLhyBUqzAeXHHzsmBH4fPSqc55kwAM6du8XRo0Jlo+XLx9bZftSo7lhbGzca/61z57h2VFAbG7t8eZ3tu48ahbG1db3P/88//7B9u5A/vXDhQsaOHcuUKVPo378/0dHRXLp0iSVLlgCQnZ3NTz/9xNWrV0lISODVV1+V/5swYQJLliyhVatWhISEsHbtWkpKSjhw4ADjxo2jd+/eFBUVcfHiRbp06dJi+GvCtm3bMDU1ZX6FUOnWrVvz66+/KgzUTYXm5n9acHNz48KFC8hkMjw9PfH09MTb25txtRi69UFCQgJSqRRNTc0Kz/I5du/e3aL4lWjZGDBgACKRiKioKKKiomps5+vri62tLaampv+J3923L7z2WvnAXoa2bSE6usJ40x08PKAa1eAaceECDBnyjBgA338v5Aq2b2+Ovb1ZvY5Zv77xovv/LM2VM2/fHjN7+3odM3n9+gZx5OfnV7tt2bJl/PLLL2zdupUePXrI95WVxqyMvXv3kpVVVhVORlFRkXxfUVERX375Jerq6sycObNF8VeHpKQk4uLiCAlRFK+RSCS8+eabTX7fNTf/04K3tze//ioIm0ybNo1p06Zx7Ngxjh8/ruD9aShyc3OJjY1V2BYUFMSFCxdaFL8SLRutW7eWC9jU5AUoKSnhn3/+eaL7pbnQrRuMH1/Vrf/PP7BvHyQllW8bOxbeeUfRA9ChA/TpIxgGreopyCiTQX7+M2IAeHkFAtC5s2W9jzE21mk0/kAvoQqaZefO9T5Gx7jxPBAbNmxARUWFd955p9Z2r7zyCqamphQXF9dy4WXymXxL4Y+NjeXjjz/GxsZGXsMa4MUXXyQ/P5++ffvi6amo8Dh69GhatxYK0Hz++eeoq6sjEon48ktB837//v2YmZkhEomYNWsWoaGh8sHG0dERNzc3+eDQ3Pwtwx1ZXMWQE4vFTJ069YnOW6ZI1tL5lWj5XgCA+/fv8/Dhwyr7AwIC0NHRwdbW9pn7bf7+gmu/Jk2cu3eFdX2A06chMVFw+QM4OQnHnjwprOtXiJuUQ0sLjIwUtw0ZIngBGmwA3Llzhw0bNmBraysPhmrbti0fffQRd+7cafTOSUnJJjMzt3RQ133qFyc7JYXczEwAdBtxUG8IgoODSUlJoXfv3grbzc3N5evvp06dqjJIVYaGhgbLly8nMzOTgwcPthj+wMBArly5UsW9t3DhQmbMmEFKSgrTp0+nd+/eXLwolKq1srLCxMQEgKVLl8qLXQwfLujYz5kzh48//hiAqVOnykthurm5YWZmxunTp7G0tGwR/C0ZZYbaqlWr2Lp1K99//z0nT55EU1OTdu3acerUKfz9/QFwdHTk77//5rfffqv2XB07dmTv3r0Ka/ItnV+JlgE7OzssLCyQyWRcvny5yv4rV64wsLKvvIVDVVWY/XfqJAQBdu8OtrZgYQF2djBqlBAEuHkzqKiAoyP06CF4AD76CMaMEYICy4L/MjNh8WKhXYcOwpKAuzt8+aUQC+DoCC+/DJMnw4gRsGLFYxgATk5OrFu3jv3798u37d+/n/Xr1+Pk5NTonVRYWD4z0NRUe/ozowqubrUKa4lPG6mpqVXWtiquwY8bN45PP/202mNdXV1ZvXo1P/zwAyEhIfTo0YPoiotIzcw/cuRIRo0aVeU4sVjMoUOHOHToEJaWlvj4+DBs2DBGjhxZxS3/5ptvIhKJOHTokHzbpEmTEIlE7Nu3T74tKCiI9u3bywfvlsDfErF48WLy8/PZv38/jo6OrF27luXLl/PGG2/g5ubGsGHDCA8PVxhMg4KC+OOPmouhhIWFkZWVpbAm31L5lWi5XoBbt24peBDDwsIoKCigcwM8tC3D6yYM4O+9JwQBHjkCQ4dCXBy89BJs2SIEAS5ZAunpMGgQHD4MOTkwfDj89hvMmQMJCcL5LlwQPANBQcL+1avhp5+EqP+y47duFXhWrBCCBR97CcDCwkL+2boBAW8NhaFhK8RiwcR5+DDrqV+kVoaGiEqjSbOqcT09LRgYGJCSklJrm7Nnz1a73dfXl08++YRZs2bxzjvvEB4e3uL4K6ffVcSMGTMICQnh008/RV9fn/Pnz9OzZ08Fd72trS1Dhgzhp59+QiqVAnDy5Ens7e05c+YMCaVPyZ49exSC+loKf0vBwoUL2bx5M6ampri6uhIUFERYWBgjR45ELBbz0ksvIZPJ0NWt3htXW652UVERSRUXNFsA/8W0NCYEBCD680+0vbxIqxCzUh3WPHiA6M8/sbpyhe9iY+tsXxfSLqYRMCGAP0V/4qXtRVFa7ed7sOYBf4r+5IrVFWK/i62zfV0QKht+yF9/afDnnyJCQhbXeUx8/I/8+aeIixdVCAv7gMzMa0/l3nRycsLAwIDi4mKuXi2v6nr58mX69ev33KtaPg4e2wComF/ZlOkWGhoSunQRDIx7957+mqlEQwPr0oj12Hv3muUi2dvbY2pqWq3rqyKuXbtGZGTkM8lf3cNbJugheH80WblyJWFhYYwbN47s7GwWLFig0H7evHnExcVx/vx5ZDIZR44c4ddff6W4uJg9e/ZQVFTEnTt35GlCLYm/pWDHjh188MEHvPnmm/IlveLiYgwMDPj000+JjIwkJSXlsV+2dYm5PG3+oYaGeDo5oSISkSuV8n1czaV8C0pK5PvnWliwwNISwycUmDEcaoiTpxMiFRHSXClx39fMX1JQIt9vMdcCywWWSAyfjF9b25F27T7EzEwIIY+L201RUW2GvoyoqG0A6Oj0wN5+M3p6vZ/OYCUW069fPwB8fHwoKCiQB+rWVyVQ8XwlzZ6Hr/h9BPe8VCrM0vfuFb5HPePOFTBlipBKWD45g+peO89EEOC0aX1KX8hRREQk19OyLWg0nfk+06YBEHX7NskREfU6piAnp9H4V61aRWFhoTxVry7Mnj27Ufu/ufh3796tkEUAYGRkxOHDh7GxscHf318heGzChAkYGRnJ13lHjx5N9+7d6d27N7t37+bUqVMNSi1rbv6Wgr59+/LZZ5+xcuVK7lUygqVSaZOLrTQ1v7pYjKOWFsYSCTtiYiiq4bn9OTERy1JPkU4j/maxuhgtRy0kxhJidsQgK6qeP/HnRNQtBX4Vncbtc4nECF3dnkiluURHf11ju4cPf0NFRVhCUVXVe+r3oouLC1paWuTn5+Pj48Ply5dxc3N7LKW/khJxs+fhK34fYeBPTYXPP4e5cyEmBn7+ueH9dOaMYMiU4d13hWDDZ9IAeOutkVhYGJYORr/U2b6oSMrKlQcV4geeBCPfegvD0iWPX1atqrO9tKiIgytXKsQP1PlJX/OeAAAgAElEQVQSqsYFraqqyvr165k1axYeHh4KLz+JRFLtTT98+HDMzMzks1qJRFKvNc/m5q8O2dnZCmvqZVBTU8PKygobGxtUVVUVts+ePZvTp0/zf//3f8ydOxeA+fPnEx0dzQcffFCv9MOWwv80UfY7Kv6eMvTs2RMtLS10dHTo0aMHxsbGaGlpYWtrS2JiIra2tlhYWNCxY0cGDhyIiYmJ/N4oCxSu6Gmpbvbe3PwaKiq8YWlJXEEBR2pYptgZG8uCJgrcVNFQwfINSwriCkg6Uj1/7M5YLBc0XeCopeVbqKjoEBu7A6m0+iyhqKgt2NisbLb7VE1NjV69egFC4F9gYCBubm5NZHg2fR5+9YZJ+eerV4UgwYYiL0/x7wcPoLrVqmfCANDT08LTczFaWup4ev7Dhg1Ha5xdFxQUMX/+LubNG4a6euPoP2vp6bHY0xN1LS3+8fTk6IYNNfIXFRSwa/58hs2bh6SWdeWK6NevHytWrABg06ZN7N+/nx9++IELFy5gZWVFjx49OHDgQKnbTYd58+YxZMgQbG1tOXLkiDwS/8yZM/z222+cOXMGBwcHNm3ahFgsZvz48cyYMaNGK7m5+aFch6CyvsDChQsV1tUBfvnlF65du8YXX3xR5TweHh4UFhYyZMgQueExdepU9PT0GDBgQI1rx0+b/+7duxhX8AE+fPiQRYsWsXXrVlxdXZk1axZFRUWsWbMGMzMzYmJiuHbtGnp6enz++efyY0aPHo23t3fpzCNLno1QhsjISPr378+YMWPYsGEDQ4cOJTAwUKGNm5sb00vfXsuXL8eqgsQ1wLFjx3j06BF3796lZ8+e/PXXX8ydO5ecnBwuXrzIxYsXuX37Nq+++iqXLl0iMzOTgQMH0rZtW0aOHEnXrl3p378/tra2DB8+HCcnJ4WMkubmL8PblpZIRCK2VxMgeyk9HQdtbczr+Uw/1gD8tiUiiYjo7VX50y+lo+2gjbp50/FLJAZYWs6nqCid2NhdVfZnZv6Lioo2rVp1eyrv/ZKSkmrfs3379kVVVZXs7Gy6du2KtrZ2Fa8Q1L/SaHPm4deFIUPK0wM3bxai/M+cgX79hKWGXbugLKFq6VIhZbAynJ3Bx0c4porh/ay4Ifv1c+TcuVW4u+9g/frDnD8fwIIFI3B1tcfUVI+UlGy8vO5y+LA3GzZMpWvXto3K79ivH6vOnWOHuzuH168n4Px5RixYgL2rK3qmpmSnpHDXywvvw4eZumEDbRugFHf16lWFoJa6ZqW7d++ul5rZBx98wAcffNDi+X///Xd++OEHALZu3Yq2tjbOzs4A5OTkMGfOHN577z3s7e3Jzc3F1NSU8+fPM2jQoCrn6ty5MyNGjODtt98uN+C0tJg9ezazZs1qMfxRUVGkVigYtWPHDvr168fkyZNZuHAh27ZtQyKRsHr1ar777jvU1NTo3bs3s2fPlr/gTExMGDRokHwGdOjQIX7++WfWrFkjNy5sbGxwdnbGxsaGxYsXs379epYtW8a5c+fk3N7e3gwdOrTG6xMbG0unCtOQ70uFscpQeVmjYjZI5T4aUs20p7n5y2Curs6U1q05lJjI1YwM+unry/d9FRPDKhsbEhvg1WvwUoS5Oq2ntCbxUCIZVzPQ71fOH/NVDDarbChMLGzS96y19XvExHxDdPQXWFm9g1isXsGY/Awbm6dXPjcjI4PCwkIKCgoUPJStWrWie/fu3Lhxo1rhn4yMDPm7qqSkpM4YtbI8fHt7qC6UoHIefpcugsv/33/L8/ATE4W0vilThLV7hQmkFlR2gpbl4deEF1+EwYMFT8O2baCtLWQIuLpCWprgmXj9deEc48cLx5w6JWyvDD+/Wjx/PEMYMKAj9+59wb59f3P8uA/Llx8kNTUbIyMd7OxaM316X44dW4qOTtOk+XQcMIAv7t3j73378Dl+nIPLl5OdmoqOkRGt7ezoO306S48dQ1NHByXqj1GjRlWbhlfmWWgoqksF++abb1oU/9ChQzE0NKwwC+nGokWL0NLSYvTo0bi7uwNC8OGECRPw9PSU7z948CArVqzA39+f7t27y2dL6enpzJgxg127drG6hlLUjx49ok2bNsqbrgYssbbmUGIi26Oj5QZAZF4eqUVF9NTV5bc6MmGeeABeYk3ioUSit0fLDYC8yDyKUovQ7alLym9Ny6+u3gYzs9nEx+8hIWE/FhbzSw3h+xQWJmNgMIjc3LAm/Q4pKSkEBARw8+ZNZDIZe/fupVOnTvTs2VM+2x8wYAB5eXkKXrTg4GBCQ0Pl2TkFBQXs27eP9u3bVxsnIBaX0K2bEHxXUx6+gwP07w8bNijm4Z88CV99JQTtvf9+mYcE1q0TDIOyPPzgYGHmvXJleR6+k5MQkFfqdK0W//sfXKuUXNG/P0ydKhgBDXVEVV4SeCYNAMGaUuftt0fy9tsjm4VfXUuLkW+/zcgKM7zmRIcOHVi3bh3+/v5s3br1qXL379+fr776ivbt23P79m0WL17M9evXlaNIDUhOTmb8+PHY29uzaNEi+SAPQgChVCrlrbfeol27duzaVe6CdXd3Z+nSpSxYsAAzMzOkUin+/v54eXmxaNGi0pnJacaOHYuWlhaDBw9m5cqVCuvpgYGB7N27F11dXdY3UKb6eYKzri599fU5+fAhEXl52GpqsiM2loVPSbRJ11kX/b76PDz5kLyIPDRtNYndEYvlwqcnGmVjs4KEhH1ERm6hTZvXEYlUiIraQtu2K54Kv7GxMUOHDq3VK2RiYlLFo9ehQwc6dOjAmDFj6sVTUiJWGISPHBH+gZCHX4bjx8u8SeXbSvW+qKg5VZaHX3E/CLn4lY8v46kvtLXhl1+EWb9UWj7rf9I4c7HykX92YW5ujouLCxMnTnzqZS8tLCzYunUr3377LcuWLaNt27b88ccf8gDApkJAQAATJ05k8eLFvPjii7i6uvLXX38BwtrfqVOnGD16NG+88QaBgYH07duXVq1a0a9fP7liXEORmprKvHnzeOONN5g5cyaOjo4KM/rr168zb948unfvTmZmJtOnT0dHR4eOHTsqqCNmZ2cTERHB9evXOXjwoFwbACAmJoZJkyYRHBxMv379GD58uFzGtn///qSmpvLll18yevRoZs+eLRfiKnNvXrhwgRs3bnD58mUMDQ05duyYwm/o3Lkzc+fOZf369TXGQSghYLG1NSUyGd/ExJAjlXIhNZWJT7HAjPVia2QlMmK+iUGaIyX1QiqmE58ev5aWAyYmE8jLe0BS0mEKCuLIyvLD1HT8f/J6W1gIbvKlS4WBdelScHMTZumZmTBrFuzZA6++WvXYb78VVPrKYGgoqPTNmQPe3oIrv1s3yMgQzj1+POzcWcegLFY8JwgBiSYm8PAhtGkjtGnVCrKzy6P9u3atutRQF1SVj/uzi4SEBA4ePMjatWufOveQIUMYPXq0fB3bx8eHW7duMWLECH4qM3kbGbm5uQwdOpR33nlHPovt1asXM2fOJCEhgdDQUKKjo/n9998ZNWoUn332GYsWLSIgIIDPPvuMgQMHcufOnQYLV82ePZu8vDy8SmtCrFy5knfffZdhw4bRunVroqKiOHbsGPr6+ixfvpwRI0YwcOBA1qxZw/Tp09HQ0GD8+PHY2dkpDPojRoyQfz569CivvfYa+vr6fPzxxxw4cID8/Hy0tLTk9QTOnDnDihUrmDVrFu3bt+fff/8FBGW00aNHy5cxzMzM2LBhwxPr6Jdh9OjRbNu2DSMjI/m1FYlEdOvWDX9/f5ZWjIiqzyxXV5eFCxcyePBguXTy0+JXU1Pj66+/ZsqUKTx69EjItaqECSYmtNXQYE98PKZqasw0N0flKYrMmEwwQaOtBvF74lEzVcN8pjkilacrcmNj8z7JyceIjPyUrKwbWFsvBv6bQjtxcRARAZcuwY0bwjYdHWFwTU8Xguz+/hsCAqDiimDHjmBuDhMmCGl9ICwbFBbC/v1CsF63bkKMQUaGsGwAUKomXu3A/8orgm7/pElC2mCZ9tzNm8L2P/4QvA5du4KVFVy+LAQHXrsGO3YIrv6+fYXURHV1GDlS+G329sLnf/9VzDJQGgD/ARQ9oRrZ4+DXX39ViJj39/cnPT2dgoKCJuPMy8tDKpXK170BevToga+vL7m5uTg6OtKuXTveffddcnJyOH36NCoqKkyZMoW8vDy2b9/Ojh072LJlS4N4MzIy6Nu3rwInQEREBB07dmTSpEl88cUXBAcHs3HjRrlkcps2bRg3bhybNm1i/HjF2dOJEycUqtKlpqbSu3dvZsyYQUFBARs2bEBLS0u+393dXe5dsbS0ZP78+XTv3p2EhAQWL17Mxo0bFVyo3t7ebN26lYkTJ3Lr1i1iYmKYNm3aY3lozp49y0svvUS/fv1YtmyZfLtIJKqzQFR1sLOzw9LSst5yyI3Jv2TJEm7dusWOHTuYNm0aZZESFSPGVUQiFlpZsTw0lM2RkURWuPZNhYr8IhURVgutCF0eSuTmSPpG9n3qz7eubk8MDYeSlnaR4uIM7O03/+ffo25ugjfAza18Xb8M9vZQWs9LYdvrrwt5+mUGwLlzgn5Ax45CPECpcxIVFcEbYGwszNyr8wKU6QBUtzyQkiLEI5ShYkhRabwyUJ4RIDwf5Z9rWsF6bAOgYpWtiilSSjwfqJwu16pVK6RSaa1a7E8KIyMj0tLSEIlEhIWFceDAAfksuLCwEC0tLblL3N7eXmFZ5J133mH79u34+Pg0mPeff/5BJBKRnp7OgQMH+N///lelD8RiMQYGBgr1EsaOHYu1tTV+fn5VBGtaVcoX2rhxo8IgXhn29vbYV5AE++qrrwBhGahyidSePXsqDCg1lVBtCKqr8CiTydizZ0+Dz3Xr1i2uXbtGnz59njr/rVu3OH/+PABr1qxh9bBhSGWyKtH9HhYWfBQezgwzMwwqBI9lln6PzOLiRruvZVJZleh+Cw8Lwj8Kx2yGGRKDcv7izGKF/xsLxcWZFBdnVvECpKVdxMrqXcRiNYW2wv8Z/6l3mre34AFITy/fJpEIyn1TpgjFd8pgaiqk/amoCDN+V1dBSCg1VcgkWLBAEALy8BCMAqlUCOwDKC1pUCPatoVPPxVSC8uSrUxMBDXB6pYhqjeyhXP88IPgNagJjx0DEBMTI/8cHx//xJ0fHp7EypWHEIunYmW1AH//SAAePEhiwID1jB69mVu3Ijh58jqtW89DVXUax4+Xv8yDg+Pp2HEJr7zyOSEhCTx4kISDwyJWrDjImjWerFnjSf/+61BTmy4/d2XscHdnh7s7nmvW4LlmDQdXrGC6RMIXkycT6e/Pql69mCIS8eu6deQ/egQI1QK3T5nCUicnAv/+m1AfH7aMG8cUkYhPXnyR1NKSr6HXrvG6sTEHli+Xb/svYcaMGXzyySfyFJymQkxMDNOnT+fAgQNyN3J90LZtWyQSyWN5KPLz81mxYgVLly5l2LBhDdLyt7e3p6SkpIqXxtXV9Zm/5m+++SY5OTmYm5uzbds2bt68yZw5c0hJScHd3b3KtorBWY1Rpvdx+MsG/zL8nZ7O/Pv3iS8oYGlICNdKK3/qq6ryWps2LCrVJCiWyfghLo4tpVLX+xMSGqUWQPrf6dyff5+C+AJCloaQeU3gV9VXpc1rbbBaJPDLimXE/RBH5BaBP2F/QqPUAsjNDSEq6nOSko4SFbWtVApYGAENDYdhaDgcS8s3ykwV4uP3EhYmRM5lZ9/iwYN1ZGZeo6gohVu3RhIT8zUxMTsIClqAl5cuubmhte6riLi4OL777js2btzIb7/9xo8//sjBgwfJzFQ0TAoKCqrEuACkp6dz8uRJ3n//fY4ePcpff/3F6dOn+fHHH7lbXYJ8Dbh8WfAECN5VYRB++FAxiK9vX8HlfvKkIBW8ZImwfWRpbPrXXwtZANWV5614/uoQFSUYDTExgsTwxo2waBH8/ntDDDqwtlb0AjSKB+DOnTucOHFCocLZnDlzePXVV5kwYcJjVwRs1641n302ExMTXdau9URXV4hm0NHRwMHBnF275qOiIqZ7d1tUVcW8/PJnmJmV58na2pri7NyO/fvfRkVFzLVrofz22/s4OJiXGinpfPfdeT76aArdutlU+x2chg5l4Jw58r8Pvf8+uiYmzNu5Ex0jI5YdP857nTujpqmJRukMTsfYGF1TU9Z89RUG5gLX8pMn2TJ2LMkREeiX1owvzM9n3MqVjF2+/D83+JuYmNCzZ0/eeOONJuUJDw+nV69efP755woR9PWFSCRqcL3woqIiBg8eTKdOndhbmuBbuRJgXZxmZmZoaGgobNfT06tSXbGlw9zcXJ5j3759e3R1ddm5cycpKSk8ePCAefPmER8fz9tvv82NGzfQ1tZW2Pak5cIbm18kEjHIwIBBBgbsqUZu7esOHcpflCIRHhYWeFR6c8uAgwkJLAsNpb++Pvs7dyZbKmXy7du8bmFBb11dNkdGsj8hAR9XV1x1dfk+Lo6zKSl84eCAwSADDAYZ0GlPVf4OX5fzi1RFWHhYYOGhyF+SX8KDtQ+I2BhB99+7Y/SSEcVZxdyZdgf9fvoYDjEkcHYgmvaadDnUBYmhhLQLaYQsC6Hz/s7oaDnQtu1S2ratPo6iR4+KBpOINm3m0qbN3GqM5Bi6dDmERGKMVJqDr29POnbchZZW+1r3KXg9LCzkXq4xY8ZQUlLC999/Lzf2y+Dn54efnx/Dhw9XCGg1MDDghRde4Nq1a4wfP16eBRMXF8f3339PWlqavKKg4n0FNjaCi97SUhg4k5OFtXNjYyFt79VXhTV9U1O4c0dYoz9/XqjMl5MjBPe9/LIwS//9dzh0SFij//prITPAyEhIGSwuFtICv/22Lg971W2nT9f/WYmKErQJ6kKDDQAnJyd5SeCmwNKlY/jjD39ef30n58+vYdWqX9i2bTYqKuXOijFjnJkxox9vvPE9N29uQSJR4fPPz7B69UR5u44dLdDTK19DnTv3WxwdLVixomYtdpcK67RBV69yZts2Vp45g46RkWARW1gwe+tW9i1aRJ+pU2ndrh33Ll2iXY8e8sG/7MXy1r59vNe5M8c2bmT4G29w7cgRXv+///vPDf4SiYSVK1eydOnSRqt9UBN+/vlnUlJSqi38UXlGWXmJIjQ0lMLCQiZNmtQgTh8fH3x8fKo1OOriLCkpISgoqEZOGxubZ+paJyQk8H7p4qhIJJK/A4qKikhMTCQjI0MhrqG6bS2Jf8aMGYoyb48BETDL3BxbTU1m3r1LsUxGcE4O79vYMKo0R31f584kFRZyNSODnjo6JBQUcMTJCbVGKKIm1hBj97EdjwIekROUg9FLRoglYgz6G2DzgXB/dTnUhdtTbiOWCHwFCQW8cOwFtOy1Gu3e0NAoV28MCnoTff3+8gJDte2rzmCW/zaxmHbt2nHp0iVkMhkikQiZTEZ6ejpWVlb4+PhUCSKtTvTHwsKCESNGcO7cOZydnasoByYkVC8ABIrKfhVidrlypfxzWJhi9H3FdfgyVJSGqVDBut545RWhjoCmplBQqEMHIQVQJhNiE8r+Li4WihpB/VIEW1waoEgkYs+eBfj5hTNkyEe8/fZI9PW1q7T76qvXSErK5LPPThIcHE9JiYyOHS0qzLDKb+4dO/7H1atB/PTTQgVDojK09ITiFnnZ2exwd2fovHl0r5gQCgydNw8HNze+f+MNigoKuHLoEIOqkV/SMTZm3s6dnNi8mb3vvsuMzf+9IBoVFRXWrl3Lli1b5PW5NTU1m6w6pHmpkbVixQrOnz/P6tWr5S/348ePc7QsEgdBJ7xizfCPP/6YUaNGMXHixAZxlgXNbd++nbNnz/LNN9/IiyJdvXqV/6tg1MXFxSmkGu7ZswexWFxj3n3rUu/QswiZTMYvv/yi8HdlA7C6bS2Fv127dtjZ2TXa9+mrr880MzNeu3ePgEeP5IN/mZGwp1MnPo+K4oOwMDwsLBpl8K8Ix+8cidoWRV5kHrG7FGsG6Lrq0npya0LfD6UwsZCSvJJGHfwV3fg/kJ0dQIcOXzVoX3WQSqWEhYXh6OgoNwyCgoLo3LkzAwYMwMfHp97xZ506daK4uJjg4OBn5hlr00aQ/l2+XIj0ByHK39dXUCP08BCWHyr+/eGHDeNokVkA1tbGLFz4Il9//TsGBtWLKxsb6/D116/x6qvfcvduDPv3Vy/MExKSwMqVh/jyy1exs6vfC/fHxYtRUVXFfdu2ave/sXs3y5yc+GTkSObt3FljaVLXCROw69mTxLAw1LS0mqy/VFRUmrQkc02c33//PT4+PvKoeF1dXcaNG9dkBW9mz57NH3/8wblz50hJSeHTTz+lV69ezJgxA29vb7777jt5WysrK9566y0MDAyIjo7Gzs6OH374ocFlZO3t7dm0aRNbt25l8eLFLFmyhIMHD9KzZ0+uX7/Oe++9pzCgf/vtt2hqapKamkpRURFXr15VUCurCP0KUrPPIkJCQtDS0mpy7YfG5jcxMcHd3Z2PPvqIj2oRm2ko1traYn75MnOrUVpso67Om5aWnE1JYfPj1HetA+oW6rT7qB23X7mN7RpbVPUVX+12G+y41u0aJQUldNzZsUmux6NHdwkL+4CePa8gFmvWe19l5OTk4OfnR3JyMp07d1Yo9hMbG8vw4cORyWScO3eO27dvK2QF1YSyoNtHpbFbzwLi4+HLL4XP4eHl23NzheI+WVnCP2trxb+feQMgPDyJ4mIpLi72eHjs5M8/q89znzatLx9+eIQePWyrLfxTXCxl1qyvGTy4M/Pm1e9Bv37qFJf27+fjq1dR19aufubWrh0DZs8mJToaC0fHGs/le+IEfadN4+jHH3Ny82ZeaeR8fT09PaZPn46trS1jx47Fz8+vSaPwK+LQoUNMnTpVXvGuDJ988kmTcaqpqXH48OFqXjyPKlxzITrawcFBru//pKiupkFSNa5jLS2tKjr1tcHAwOCZeRmJxeJqjad169bJr3l1YlQ1CVTVVJWvqflNTEzYtGkTW7ZsoW3bxq0Xsic+noNduvBWUBC3e/dGr4ISY1R+Phbq6hioqvJFdDTLGpkbhMyBoDeDMJ1QNbZErCnGfJY5MqkMkWrj5/NLpTncuTMZB4dtaGuXvxPT0i6ip9e7xn3VoWItjopITk4mLS2Nv//+W34tvb2962UA5OTkAEIxs2cRZTGPjT2PbHEGQF5eIRs3Hufbbz2Ii0vjhReWsXv3xRoHcA0NSY2z348/PkZERDKnT6+s5IpKk5cXrojM5GR2zZvHhA8+oH2FamGP0tJQ19JCUiGQS6KhgaiWWXd8cDBhvr7M2LwZXRMT/u/VV3GdOBGrzp0bra8yMzPZuXMnO+uSlmoCTJs2jWnTpqHE48OoNLakpWPcuHGMHDmStm3bsmPHDvLz8xGLxXTt2pWcnBy0tLSYNGkSFhYWLFiwgJ07d2JqalplW5k73snJibFjx9Y7ILOx+PX19bly5QodOnTAw8NDOHk9hIjqg+PJyfTW08NVV5e/0tN5JziYn0qf9VyplJ8SElhra8swQ0N6+voyytiYTjVMMJoMTajjExy8EKk0l6KidKKjvwRkZGZ6Y27uXuu+huD27dtMnjxZ/r7Py8vjk08+ITY2Fss6pJrv37+PRCLBoWIyfQtGdbbxmDFw61aZQVzZQK7fOVq0ASCTyViyZD9r176ChoYEO7vWbNw4jaVLf2Lw4M7Y21d19ZWUyKpNKfL1DWPTphP8+utihWyB9PQczp27hYdHVYNip4cHRlZWTKoU4Oi1dy9jKrh6AWQlJchqSGXKSU/n2MaNLCjNUe47fTr/eHryzaxZfHLtWr3LBCvRcJSl+RUWFj513oZyPiuSvKdOneLUqVO1tpk1a5aCNntSUlKVbWW4c+cOkydPfur86enpOFby2Mkq14Bt6ISlpITvYmM5kZzM6dIKoIMNDBgfEIC1hgajjI1ZHhLCW6XphHqqqnTR1uaV27fZ26kToNdo1ynltxRkUhnJR5MxnaToBShIKCDLN4uSohIK4gpQt2jcd1CnTvuq2Srkxhkbj6lxHwRWGQOqi9t49OgRMplMYbKnqamJo6Mj//zzj1z1srpjk5OT+fPPPxk3blyVAMCWiLZthbLDjo6wapWQEWBkJBQrGjVKkCru1k34OzBQ2Fb2d5mB8MILwvHDhwulgCtqG7RIA+D69QesXv0LiYkZSKUlFSwbEdnZeYwZ8ylfffUaI0cKD1lxsZTff79FeHgSf/wRwIsvduOFFwS3WlGRlNmzv8HIqBU3b0Zw82ZE6Uu6iLNnb7JtW1XL89L+/fidOYPb5Mkc+egj+faHkZEkhYfzcgUFslAfH+5dvkxWcjIB58/TtUJ46LWjR/ll1SrsXV0pyMlBVU2NnPR0dIyMuHH6NF9MmsSMzZux6tJFOVo3Mnx9feVBeWfPnmXHjh289tprTfrQx8bGsnv3bm7fvk1RURFr167l1VdfrVeA2bPwMlKidmiKxbxnbc17FeSlx5mYKBgW/7i4yD/rqaryVzXu7caA8RhjhsmqN2jUzdXperpri+7LuLg4QkNDSUpK4v79+/Lgv6ysLE6cOIGqqiqZmZnolQZrZ2Zmkp+fT2BgIFZWVnTo0IGAgABAqMipq6tLTk4OqampzJgxo1GDPpsSUVFCymBNeO894V9NfwveEiEzoE5Pg6ypc7fqxJFmZp/crPxTRP9Nfe2GeH2UaD6Invf77wk9AE+K4ReauQOa+QsMG/ZZs/K/X1nz93l7/mXDhsmUD0Dz4QLDUfZ/M/b/hSMo8fwa4M87Jh9p5glYc1/+Zv4Ck5v59yuLASmhRANx+fJ99u79i6tXg8jNLcTMTB8NDQmjRnXH3X0gsbGpeHkFsnr1RCV/E+D+5cv8tXcvQVevUpibi76ZGRINDbqPGsVAd3dSY2MJ9PJi4urVSv4nQMe0I3YAACAASURBVEhCAke8vTlw+TLBpXLv5gYGuA8YwKTevelZ6lI/df06XoGBfHf+PIWlWTiDOndmdI8evDViBFqPGfOUEJKA9xFvLh+4THywwG9gbsAA9wH0ntQbu54C//VT1wn0CuT8d+cpLhT4Ow/qTI/RPRjx1gjUtR6TPyEEb+8jXL58gPh4QT/AwMCcAQPc6d17EnZ2gnrQ9eunCAz04vz57yguFuKAOnceRI8eoxkx4i3U1bVa7LtMaQAooUQ9kZ2dh4fHTo4d82Hp0pfx8voQKyshkj8vr5AjR7zp02cNiYkZvPvuS0r+RkZedjY7PTzwOXaMl5cu5UMvL4xKg+sK8/LwPnKENX36kJGYyEvvvqvkf0I4mJuzeuJEXnZ2pmuphPmu+fN5uVIMwzgXF8a5uKCmqsrW06cx1tHh/Jo1SGpIAa0vzB3Mmbh6Is4vO7O8q8A/f9d8nF9W5HcZ54LLOBdU1VQ5vfU0OsY6rDm/BhXJE/KbOzBx4mqcnV9m+XIhfmL+/F04O7+syO8yDheXcaiqqnH69FZ0dIxZs+Y8KiqSFv9OkxsAuVIpH4WHcykjg6KSEu7l5JBfGuWePXgwrVRUOJSYyO64OC6lp6MpFuOorU1eSQnqYjETTUxYbmODplhMUE4Ox5OT2RARQUFJCW01NDBVUyOpsJDuOjq8b2NDbz3F6FdprpTwj8LJuJRBSVEJOfdyKMkX+AdnD0allQqJhxKJ2x1H+qV0xJpitB21KckrQawuxmSiCTbLbRBriskJyiH5eDIRGyIoKShBo60GaqZqFCYVotNdB5v3bdDrXYlfmkt4+EdkZFyipKSInJx7lJTkC/yDs1FRaUVi4iHi4naTnn4JsVgTbW1HSkryEIvVMTGZiI3NcsRiTXJygkhOPk5ExAZKSgrQ0GiLmpophYVJ6Oh0x8bmffT0eivwK/u/efu/LmRm5uLmtprg4HiOH1/GuHEuCvs1NdVwdx/I4MFd6NNnDWlpjSs48rzz52ZmstrNjfjgYJYdP47LOEVJbzVNTQa6u9Nl8GDW9OnDo7Q0JX8jwcLQsNrPlWFWKmxlbmDwxIN/RRhWSNk2tKiZX78028vA3OCJB38FfkOLaj9X4dc3k3sJnoXBHypIAY8NCCCmoIBLzs749epFbP/+vFKpWMlMMzM2lrp95lpYcLNXL+65uTHH3Jz14eGMvnULGeCorc0qW1v6ld4QN3r1wtfVlX9cXAjJzWXAjRtcqHSDBowNoCCmAOdLzvTy60X/2P6YvqLIbzbTDLuNAr/FXAt63eyF2z03zOeYE74+nFujb4EMtB21sV1li34/gb/XjV64+rri8o8LuSG53Bhwg7QLlfgDxlJQEIOz8yV69fKjf/9YTE1fUeQ3m4mdnVCy1cJiLr163cTN7R7m5nMID1/PrVujARna2o7Y2q5CX7+fwN/rBq6uvri4/ENubgg3bgwgLU1x7VvZ/83b/3XBw2Mn9+/H4eExtMrgVxFWVkbs2jWf9PScRn1Qn3f+nR4exN2/z1APjyqDX0UYWVkxf9cucmrKe1LyNxgqFVLvxLUEjZbtEzdyYKm4gny7SFzzucv21dbmsfjF5caESFSz9kvZvtratEgD4HpWFhfT0lhta4t66cU2kkj4uUsXHCpJD+mrKq4aiIAl1tZ01dHBKz2d31NSamxrqa7OJnt7imQyVoWFybdnXc8i7WIatqttEasL/BIjCV1+7oKWgyJ/ZYlLRGC9xBqdrjqke6WT8ntKjW3VLdWx32SPrEhG2KoK/FnXSUu7iK3tasRiYb1IIjGiS5ef0dJSFI5QVa0s3yrC2noJOjpdSU/3IiXl9xrbqqtbYm+/CZmsiLCwVfLtyv5v3v6vC+fO3eLo0WsALF8+ts72o0Z1x9rauNEe0ued/9a5c1wrrfNQn2qa3UeNwrhCWp6SXwklajEAkksFTK5UshrVxGJmVahyVxu6lOY0R+TlNbhdYbLAn35FkV+sJshX1gfaXYTz5kXkNbhdYWGywJ9+pZLlp4a5+az68WsLef15eRENbqfs/+bt/7rw/fd/AtC+vXm1YlTVYf36xgvvfd75/yyVVzZv3x6zeuroT66hAJOSXwklKhkAvfX00FJR4Z3gYDZFRFBcITd7sqmpfFZaG0JzcwHo3KpV7e1KB56K7fR666GipULwO8FEbIpAVlzObzrZVD4rrQ25oQJ/q8618+eF5lVpp6fXGxUVLYKD3yEiYhMyWXE5v+lk+ay0Vv7cUOG8rWqX+s3Lq9pO2f/N2/91wctLUCvr3Nmy3scYGzee5vjzzh/o5SV4sBogo61jbKzkV0KJOqAKgrt5X6dOzLp7l9UPHnAwMZEt7dszxtgYx3qole2MjcU3K4vRxsYMrqXASWJhIR+EhSERiRQqYkmMJHTa14m7s+7yYPUDEg8m0n5Le4zHGKPtWDd/7M5YsnyzMB5tjMHgmvkLEwsJ+yAMkUSE/eYK/BIjOnXax927s3jwYDWJiQdp334LxsZjFIpX1Mgfu5OsLF+MjUdjYDC4Zv7CRMLCPkAkkmBvX14eWNn/zdv/tSElJZvMzNzSQe3pS/c+7/zZKSnkZmYCoNsMg9rzzl8ZE7ZuRV1SfYBbek5Ok/NvnbAViXr1/DnpT4F/6wQkkuonJDk56Y3Od+fOHU6cOMG+ffuIjIwEwNramrlz58pLm9e238nJqW4DAGBK69Y4aGkx7/59bmRl8bK/P8MNDdnVsSO2mlXLN3pnZLA8NJTo/HyKZTK+dXRkvkX1EZIbwsMpAcJzc3HT0+NXJyc6VFrbbj2lNVoOWtyfd5+sG1n4v+yP4XBDOu7qiKZtVf4M7wxCl4eSH52PrFiG47eOWMyvnj98QziUQG54Lnpuejj96oRWh0r8raegpeXA/fvzyMq6gb//yxgaDqdjx11oalYtWpKR4U1o6HLy86ORyYpxdPwWC4v51fOHbwBKyM0NR0/PDSenX9HSUtRpVPZ/8/Z/zUZDuTdCU1Ptqb9wn3f+4gr1FdQ0NZX8zYwTy5fTzcam2n1fnj3Lkv37m5R/+Ynl2HSrnv/sl2fZv6SJ+ZefwMamW/X8Z79k//4ljcrn5OSEk5MTgwYNYuDAgQDs37+fQYMGKbSpbX+9DACAbjo6+Li4sDc+njUPHnAhLQ1XX1+8XVywrzRguOnrs7V9+3qRrGvXDmNJ3WkROt10cPFxIX5vPA/WPCDtQhq+rr64eLugZV8pGM5Nn/Zb68ffbl07JMb14NfphouLD/Hxe3nwYA1paRfw9XXFxcUbLS3FtTd9fTfat99aP/5265BI6rbelf3fvP1fHQwNWyEWiygpkfHwYdZTf+E+7/ytDA0RicXISkrIevhQya/EcwmLCpM762oCPOvaXxPEILiGU4uKhA0iER4WFgT16cM4ExNSiopY8+BB084yEgspShX4RWIRFh4W9Anqg8k4E4pSiniwpon5CxMpKkoV+EViLCw86NMnCBOTcRQVpfDgwZom5Vf2f/P2f23Q0JDQpYvwQN27F6vkf8qQaGhgXVo4K/bePSW/Es8lVCroKoiriQmra3+tBkBkXh4XK+WF66uq4unkhL6qKv7Z2U364/Ii80i7qMivqq+Kk6cTqvqqZPs3MX9eJGlpFxX5VfVxcvJEVVWf7Gz/JuVX9n/z9n9dmDatDwC3b0cREZFcr2NycgoardDR887fZ9o0AKJu3yY5on7ZGwU5OUp+ZaEtJepjAADsT0ioav2LxVhpaNCumrWnhtxc9WmbsL8qv1hDjIaVBprtngJ/QtW1I7FYAw0NKzQ12zU5v7L/m7f/a8Nbb43EolSBbNWqX+psX1QkZeXKgwrr50r+x8fIt97CsNTF+cuquvUbpEVFHFy5UmH9XMmvhBK1GAC/p6SwOCSER1KpfOfPiYmE5ubyYYU6ypmlxR7Si+t+uBvSNuX3FEIWhyB9VM6f+HMiuaG52H1Yzl+cKZyrOL3uczakbUrK74SELEYqLZcwTUz8mdzcUOzsPiw/Z3Fm6f91R3w2pK2y/5u3/2uDnp4Wnp6L0dJSx9PzHzZsOFqjUVFQUMT8+buYN28Y6uqNIwf6vPNr6emx2NMTdS0t/vH05OiGDTXyFxUUsGv+fIbNm4fkMYvQKPkVUVKBS1oqT16t4VG6r7Y2jwNZSTl/ibTmc5ftq63NY/HLys9XUiKtmb90X21tWhoUggC/io7mh7g4OmlrUyiTYSKR8LezM666QvrPocREvoqOBsAzMZFfEhMBmG9hwUobG9ppapJZXMzGiAi2R0cjLb1xFgYF8aalJRMrSdtWRvRX0cT9EId2J21khTIkJhKc/3ZG11XgTzyUSNTWKOHzL4kk/pKIXm892n3YDqORRvLzRG6OJGJjBNJc4ULcn38fq3etMJ1YB3/0V8TF/YC2didkskIkEhOcnf9GV9e1dEA6RFycIMqRlHSEpKQjaGk5oKpanvMslebx6NFtRCIVuSRkSMhS2rR5DVPT2quj1dT/nbS12RQRwZcxMTwsteoPJyVhKJGw1NoaW01NfkpIYN2DB0Tl5zPG2BhrDQ0uZ2QAsDQkhEEGBnwSGcl2Bwfm1CAuVFP/S0wlBLoHknCg3EuQfCKZ6C+iMZlggqatJul/pxO6LJQsvyx0uunQ6oVWZFwW+EOWhmAwyIDITyJx2O6A+Rzzx+r/iIhP5PEASUmHycj4B4nEELFYndzcUIqK0jAzm46t7TqSk4+RkXEZAD+/IYhEYvr0CUMsfrxI9n79HDl3bhXu7jtYv/4w588HsGDBCFxd7TE11SMlJRsvr7scPuzNhg1T6dq1baM+qM87v2O/fqw6d44d7u4cXr+egPPnGbFgAfauruiZmpKdksJdLy+8Dx9m6oYNtO3aVcnfSIhJTVX47NyuXbXtokpVSBMzMiiWSlFtpHoAqTGpCp/bOVfPnxIl8GckZiAtlqKi2kj8qTEKn9u1c65hEiOMTRkZiUilxaiotPxaeyLZsGGP5R+9++gR/W7cILO4mGsuLvSqUFzmkVRKJ29vTnftSjed2gVBHqccfElhCdd7XSfbP5uOuzti4VE1/SxgfAB6vfSw+cCGRv8CQEDAOBwcvkBT005he3DwO8TE7MDO7mNsbesOXrvA8HpzZhUXM8jPj1vZ2Xxqb8/KSuk4Q2/eZKaZGXPbtKly7MmHD5kQEMCytm0Vsgca8vOD3g4i9ttYzGaa0eVgl6oD+JfRpF1Io+uprohUFfW4H558SMCEANoua6uYPdCAL5Caep7U1P/h4PCFwvb8/Ci8vbugoqKNm1sgEomRwn4fn248enSXgQPTUFVVzGW/cKFh9dBzcwvYt+9vjh/34f79OFJTszEy0sHOrjXTp/dl9uwB6Og0XbrWf43/CA1TDCzIzeXvffvwOX6cuPv3yU5NRcfIiNZ2dvSdPp0Bs2ejqaPTZL//v8Y/+UjN939IQgKH//2Xg1euyMsBm+nrM2/oUMb27KlQDvjPO3fYdeECRaUezPqWAz5Sy+VPCEng38P/cuXgFXk5YH0zfYbOG0rPsT0VygHf+fMOF3ZdQFok8Ne7HHAtXyAhIYR//z3MlSsH5eWA9fXNGDp0Hj17jlUoB3znzp9cuLALqVQIpq5vOeDJ9bz9IyMjsbW1LZ0IRWBT6d1f1/5GNwAAfk1KYtqdOwwyMMCrQonIVwMDGWtiUueM/wnGX7JvZePr4otmO0163+2NWK088jE/Kp+7s+/i/Ldz3YUhHvMLREd/gbX1ewrb0tMv4ec3GB2d7ri6+iAS1W0BNsQAAHiQl0fXa9fQEIsJ6tNHnt73Y3w8t7Kz+apD9fntj6RSzC9fxsvZmZ66uo/186WPpHh39qYgoQC3u24KdQJkUhk3+t3ghWMvoN5GvdpjL5tfxtnLGd2euo/V/4mJv2BoOBg1tYpytDJu3hxGWtpfvPDCsWq9LOHhH5GVdZ1u3X6r2v8NNACUaFw01ABQonFRmwHwVK5/c1/+Zv4CzW0APFHZoqmtWzPR1JS/09PZW2oh7ouPR1dVtV6D/5NAp7sOVu9akRuaS9SWKEXLdWkIDtsdGr0qVEWYmc1UHOCkOdy7NxeRSJXOnffVa/B/HNhparLJ3p7UoiLeCwkBIDAnh30JCVV0AUpkMsYGBDDtzh2Cc3J4y9ISiUjE0pAQXHx9ufuoYSVbVVqp0OGbDsiKZAS9FaToJtwRQ+sprRUGf1mJjICxAdyZdoec4Bws37JEJBERsjQEXxdfHt1tGL+h4ZBKg7+gApiW9hetW09VGPxTUn7D19eVmJgdGBu/hJHRCJKSPLl5cyghIUtQQgkllHje8cR1C//P0REDiYRloaFcTEtjb3w82+opUPPEg+EGOzSsNIj4JIK8B4LGfOq5VNRM1dB1blrZUjW11gp/h4WtJC8vnHbt1tKq1QtNyr3Q0pI+enocSEjg5MOHvH7vHns7dUKtUv6nDHiQm4tfdja/JCXxsKiIC2lpeGdmEpSTQ1qp9kBDYDLWBJPxJqRdTCPxZyEGpDCxkOQjyVi9Y0XlL5D7IJdsv2ySfkmi6GERaRfSyPTOJCcoh6K0oifq87y8SEJDV6CmZoqj4w6FfUVFqeTmhpCWdp6UlP+RlxdBWtpFHj26S05OsPLJV0IJJZ57PPE01UxNjc/bt2fuvXu87O/P7d69qwxETYWyGWnA+ACCFgbR9URXwjeE0+33bk+1E9PT/yYm5lt0dLpjY/NB01ttIhF7OnWim48Pr9y+zY+dOmFXTaqgikhEoJub/O+hN2/ydYcOLGv7ZAFajt84knYxjZD3QjAebUzo8lDsNtlVWfcXqYhwCyznvzn0Jh2+7kDbZY0RICbj/v3XkUof0bnzj1WU/szN52BuPqfUG3CWrCw/HBy207HjbuVTr4QSSijRGB4AgNfatKGTtjZ5JSUEl1ale1owGSfMSFP/l8qtF29h4WGBxEDy1Pgruv47dWo613+VQVhbm9fbtKFEJuN2PVz5R5KS+CstjfWNoCqobqmO3cd2FCYVEjAuAERgMMCg1mOSjiSR9lcaD9Y3jqpgbOx3pa7/KZiavlJr29DQFURGbpaXHVZCCSWUeJZQUiG1UiqVNnh/kxoAR5OTcdTWRkMsZkFQkEIu+1MZDL9xRKQqIj82nzZz2zxV7tDQFeTlRWBruxodna5PjfdBXh6BOTl01NZme3Q0N+tQC1QRCbNzQ0njGEdWC63Q7qRN+qV07D6xq7O9SEXglxg+OX9eXgShoStRUzPB0fH/6uYWqQAiJBJD5ZtECSUqISAqCvcdO9CePZtbFZQGS2QyfvPzw3rBAs74+RGamMjKQ4cQT52K1YIF+JdWn3uQlMSA9esZvXkzPqGhbD19GpWpU2n/7rvcrHC+P+/cQWPmTNb++itplSYt/v/zZ3Xv1Sx2XExuZvkkMic9h3Nfn2Ol80rCfMMI9Qlly7gtTBFN4ZMXPyE1VkgRDL0WyuvGr3Ng+QFSY1NJepDEIodFHFxxEM81nniu8WRd/3VMV5tOpH9klT7w9/8fq1f3ZvFiR3JzM8v5c9I5d+5rVq50JizMl+vXTzJvXmumTVPFx+e4vF18fDBLlnTk889fISEhhKSkByxa5MDBgyvw9FyDp+ca1q3rz/TpakRGNlzZNCYmpgJXfIP314Qnnq6G5ebyTUwM57t3Z0tUFOsePGB1WFiN0ehNAXVLdcTqYiT6EhA9vQcnPd2L2Njv0NHphq3tqqfGW1BSwtx79/ihY0cSCwsZeOMG8+7dw9fVVT7QV0aXVq0A6N5IKUoiFRGatprk3Mupl8elVReBX6f7k/LLuHdPcP136rS3XkV+WrXqglis8dS8M0oo8Syha9u2/LRwIWdv3mT81q3c+PRTTHR1EYtEjHF25rebN3m5NMvrs5kzMdHVZa2nJ7qly446Gho4mJuza/58VMRierVvT1JmJj9duoRd6/K4HUtDQ94bM4aPp06t8h26vdgNA3MDVnRfwf+3d+dxUVf748dfwzDMMCqbwgjIpoC7ueUSeutqy7eyxBQt03LLn6V1Na9paZq5Ua6ZaZnltTIrr2ZZ1yVNb6mZmIoLBsq+oyIg2zDb7w8QQYYBXJq6vp+Ph49H8Vne8znnfM7n8znn8zln5dMrmf7tdBQOChq5N6LfuH5cSL5AcI/yCcGmbZvG24+/TU5iDm46NwDKSssYOH0gj097vPKGYMZ3M/AOLR9z5HLGZXav2c3QuUOtzibYufP/4e7uzSuvdGHlyqeZPv1bFAoHGjVyp1+/cVy4kExwcPl4JA4Ojrz11mO4uV17IdnLK4iWLbsxceIGHByUnDt3mBkzvsPbO7TiWpHB7t1rGDp0bq2zCVpTdTrgq5599llGjRrFoEGDAGwur2s64JtqASitciFSOzgwPSCAto0asSotjV/z8/+nTxqTqZCYmLEVTf//QqGoeREsKro9k3f8IzaWCb6+hGi19HVzY7SPD8euXGF5xSBN1gQ7O6NxcKCVVmuX9HIOdsZB44C21c3FT01dzeXL+9DpItDpan5DU1qajMlUvRuqUaMONcZruNaCk8nIke8SFvY6paXlLyWWlRlZt24vzz77HufOZbFmzW602hGMGbOGkpIy8vKKCA9fzPz5W8jMvMzSpdtRKIayZMl2zBWjlq1fv49evWZy8mQyW7b8ygsvrGPVqp2sWrWT+++fR+fO0ygtNZCfX2xz/1FR523+vtTUS7c1flxcBiNGvItSOYzDh8+V34DqDfy//7eWsWPXcOZMKosXf4tO9xzx8dmV6XrwYCz33juH2NgMm/Ezz53j3ZEjeT0sDENpKVA+Be7edet479lnyc/O5vNXX2XLvHnsXLWKnatW8UJAAB9OmEDqmTNM79qVKW3bciG5/Eug/JwcZvbuzfcrVpCXnc3uNWsYodWyZswYykpKKMrLY3F4OFvmz+dKxQA3te3/9wMHePXuu9nw8rXPfQtzc/nw+efZsXIlCb/9dlvj13V8p/butfn7Lmdl1Sv+VeF3342niwtDli6t/J4fwPG6d7qmDhhAnzZtGPv++xhMJl7btIklI0eirLLevGHDcNVqmb5xY+Xfln//PbOHDKn9YqR04JF/PELswVi+mPXFtb87OKCo8mCjUCh4Yf0LFFwoYMv8LVzOuMzhzYcrL/4Avm19Ky/+AKvHrMa3jS8DXxlYe3wHJY888g9iYw/yxRezao3frdsA+vQZztq1/6/yu//t25fyxBMzcXAoH3zI17dt5cUfYPXqMfj6tmHgwFcaVN917NiR2bNnk5iYiMViwWKxkJCQwOzZsyunCra1/La2ALwYG8uEFi0IqbioODk48EHbttx79Cjjzp7ltx49/rAXAv9oV5v+W7acY7Xp32DIJTd3D40atbulcTdmZWEGnmp+7e5zcUgI3128yOz4eMI9PWtMHQzlLw62dHamxS0aHrTBLQYOCpxbOqNucePxS0oSOX++vOm/dWvrTf8ZGRtqDMCk1YbUOhxwSIg3U6c+xqBBixk58l2++moKTk6OhIf3wGg0ExLSnJCQ5jRr1oQZMz7HaDSRmnqJxx/vzpgxfy+vEKc+RmJiDseOJeBQ8enpxYtX+OabV9DpXDGZzAwePA6A06dTmTdvCz///CYajQqNRsXzzz9Y5/5r+31+fk1ve/z161/gzJlUTp5MplevEFQqRzw8GrNgwVM4OCho396PjRt/5pFHFnLo0HyaNm1CWFhrHnigE61b+1BcrK81vndICI9NncriQYN4d+RIpnz1FY5OTvQID8dsNOKq09F76FCCunQBYO+6dTRyc2PUihWoNBqmbtnCq3ffTWlFF5jZZKJ3RASPTp4MwIPPP0+TZs34fMYMTEYjl1JT6f744/x9zJjKMmBr/32efpr/vPMOXkFBPPziizT28KBj//74deiAb5s2tz1+Xfu39fvcmzevV/zKm3QnJ7555RXufvVVJv/rX7w3dmwtXWoKPnr+eTpMnUq/uXNZOXo0bo0a1djXugkT6Dd3Lk/36UNCTg5De/dGU0cXpE9rHyZ/OZnIRyMJ7BxI76G9ra7XpFkTnnv/OZYPW07qmVRe+PiF6ue867U6cOeqnfx+4HeWRC/BQWn7euTj05rJk78kMvJRAgM707v3UKvrjR79DlOmtGPbtrfo3TsCi8WMr2/bKnXOtYHxdu5cxe+/H2DJkujKG4Q/ixu+Oi9MTCSltJThzat/l93XzY3HPT05XVjItHPn/pCDsBgsmEvMlWPP3265uT+SlvY+TZrcRVDQzBrLzeYSYmNfsjqJzc3Yk5vL9HPnWB4aWu3vHioVrwYGUmI28/Tp0+hrGYu7hUZDI+WtK4CVY/3XM901LTQoG91ofAsxMWMwmYpo3XoVTk6eNdbIz/+Fy5f3Vg7BfJWTk2ed/f9LljxDTk4+r7zymdXlERG9uf/+jowatZqtW3+tvDhW3oQtHsmxY4l89dUv/PrrOUJCvNHpyiuBLl2CKlqE9ERELGPZsmcIDfVu0P7r+n23M75KpWTjxpeYOXMTCQnZfPjhHsaPv7/yZgNgyJBehIf3IDx8MXp99c876xP/mSVLyM/J4bNXaj4hXb04pp4+zabXXmPKV1+h0mjKm16Dghjx9tusHDECY1kZu1ev5uEXX6y2fe+ICDrefz+rR43i161ba1z8bO0fYPq337ItMpKj335b47fd7vj12b+t31ef+FX5eniwbdo0Pv7xRz7cu7fW9fybNWPS//0fxxMTca/oXrzeve3a8dz99zP2/feJTkqifz2eSAHuevAunln2DKtHryY5OrnW9XoM6kGr7q3IOp+Fk9b6EN+ZcZlsnL6RUctHoWulq1/8ux7kmWeWsXr1aJKTo63fgDRpxujRK9m6dT5ffTWHxx77p/X4mXFs3DidUaOWo9O14s+mwTcAe3Nz+ftvvzEzPp7YoiK+zqn+ZvW/c3KIJVFxMgAAIABJREFUrnjBY2VqKsNPn+ZoQcHtuxj/mMvZ8WexmC0Uny8mflY8V47dzulryz8/AwsGQx5Hj/YlKqpX5b9ff+3GTz95k5W1kUaN2t+SiOeLixkdE8ODx46RXVbG6rQ0qg7feKSggO0V43AfKSjg3t9+Y2tOzTfeb9XTf9HZIhLnJZL/a3k3T9yUOC5+d7HO7W7m6T8j419cvrwfhUJJSsqyamkeFdWLQ4dCiYoKQ6Op2b/n6OiKUmn73QO12pFvvnmFHTtOsGbNbqvrvP32CHbuPEHLljUrEmdnJz777EVeeuljdu48QXj43TXWmTBhLX36tOHpp/s2eP91/b7bHb9duxbMnPkETzyxhEaNNAQF1RzoKzJyOAEBnjz77HtWJ6uxFd9RreaVb77hxI4d7F6zpsZyfVERyyIieHbZMnyue7/o72PG4BUUxLwHHuCeYcNQWnnKHPH225zYuRNdLePY29q/V1AQM7Zv54Px44k/erTGtrc7fl37r+v31Sd+tQtrcDAfv/ACkz76iEOx1sfMSMjOxmgycXdwMOPef7/Wfb0REcG5zEyeDAtr0Pn+8IsP03dEX94e+DYFF61fP458fYSwJ8PITc9l26JtNbtpjSZWjlhJ+7+3p/9z/RsW/+EX6dt3BG+/PZCCAut1W1jYk3h6BhIU1BWVysropyYjK1eOoH37v9O//3N/ypbsBncB9PfwoL9H7U9TQ7y8GHKbRwGs9vTbzwOPfh60W9/uD4qoICws8Q/NpGCtlvXt2rG+nfVj7OHiwt6uXevcT/NbdAPQqG0jgl4PIuj1oAZtp25+4/F9fEbj4zP6hrZVKpugVDaqcz03t0bs2PEaffq8jtbK+OGrV+9i27ZpPP30Sv72t7YEBFRvhejevRWtW/vQzcpkJevW7eXEiSSOHFlUa/y69l/X77vd8f/xj0eYMmWD1ZuLq03D69e/wCOPLOTVVz+ncWNNg+I3cnPjtR07eL1PH9TXdWOtnTCB0Hvuoe+IEVa3feSll/h02jT8OnSwunzX6tVM27aNlU8/Tdu//Q3P68bCqGv/QV27MmnDBpYMGsQj//hHjTi3O35d+6/r99UV/3pPhYVxJjWVwUuX8re2bat3xZWVMX/rVlaPG0d6bi6d/vlPPty7l+f617zIXm3yd1A0/O3ssavGMv/B+awYtoLQ3tVbPTNiMzh/5DzDFw3HxdOF90a9R48neuDX/tpgZFvmbSEnMYfp306v/tCYnouHb91fBI0du4r58x9kxYphhIZa74pQqTQ41NLNvWXLPHJyEpk+/dvrWpDT8fDw/Wt3AYi/Hg9H+74F7+hhn/hKpQalsn4vH/r5NeW772Ywbdqn1f7+wQc/EB7egwce6MTUqY/x9NMrMRpNVi+C1zt9OpVXX/2cr756GWfn8qbKixevEF2lebO++6/t9/0R8RX1qMRVKiVbtvyTXbuiOXcuq97xr2rq58eM777j02nTrrU6rltH0vHjjHn33cq/ndm3D0vVri4bv+2HDz6gR3g4nR54gMemTmXl009jqjJFdr32D9z10EM8OX8+X8yaZS3hb2/8eqR9bb+vrvhXGa77fHvesGHc07o18dnXXu60WCxM2bCB1wcPRqNS0UqnY/6TTzL1k084XzE7bFVXpxI2W+qecsZsNlf7nl2pUjJ1y1TysvOqt0BeLmLL/C0MnVvePx/2VBid/68z7454F0NF99P5I+f5euHXjP9gPG7N3apte3zH8frFV6qYOnULeXnZtbcHW6pvU9lqe/4IX3+9kPHjP6j2tUBR0WWOH9/x1+0CEH9dLva+AXCxT3yFwgkHB43VZYWFpezeHc2uXdFculTeddSxoz9ffjkFtdqRzMzLTJ36Cd99dww/v/JZBvv378DBg7E888yqam++HzuWSHx8Njt3nqjcl15vICJiGZ06BbBr1wlWrPiexYu/5dFHFxEU5FXn/k+dSrH5+6q6HfGvHp/JZGbr1l8B2Lz5FwyGaxeL3buj+fHH05w/X34BcHFx5j//eRVvb7c645cWFhK9ezfRu3ZVvpXu37EjU778Eke1mvTff2f9Sy/ROiyMPWvX8v2KFWyZN4/vli1DUfHkVZSXx8kffuBiSgq/HzhQ+bsuZ2byydSpHPvuO5r6lT8Zdujfn9iDB1n1zDNkx8fb3H9eVhbnDh/ml82bMVUMm33vs88yZPbs6hek2xS/zuPLyLD5++oTH6BIr+fzAwfYc+oU+8+cqXbD98mkSXSpmGQmKj6ehxYs4FBsLKYqFz0HhYIrJSUMiIxkV/S1PvNLV66wft8+ADYdPEjadV8dVHU54zIHNh7g+H+OkxaTVvn3xh6NmbF9RuVLfYf/fZjXer4GFtAX6Ssv6k2aNiHpRBLLhiwj6UQS7458l8ZNG5N4LLFyHIBPp33KrLBZePjUfPq/fDmDAwc2cvz4f0hLu/b1VuPGHsyYsb3aS31Xm/ePHv2W7OwEoqN3kZx8ssoyA+++O5LGjZuSmHischyATz+dxqxZYXh4+Pxprgk3NRvgrXCjswH+r/yAhs4GeDM2ZWVV+3rgjz78rE1ZNH+q+R+e/gZDLgUFUTRt+lDN9JfZAO1KZgO0L5kN8K8xG6DcAMgNwP+mOzz/H/jhgTs7+e/0AviAnH53dPrb+filC0AIIYT4Ezp16hRvvvkmQUFBKBQKFAoFAQEBzJ07l1OnTtW5vC4yNqoQQgjxJ3R1tL/77ruPe++9F4ANGzZw3333VVvH1nJpARBCCCH+onx9r3026O/v3+DlcgMghBBC/AUpq4zgam3cgbqW16ZGF0ChycSKlBQO5eXRXK1GqVDgolTSzcWFAqORoTodW3NymBIXh4XyoX9LzGYKTSYmtmjBaB8f4oqL+SQzkwWJifio1XR3cSGttJSmKhVzWrYkzM2t1h9kKjSRsiKFvEN5qJurUSgVKF2UuHRzwVhgRDdUR87WHOKmxIEF3Pq6YS4xYyo00WJiC3xG+1AcV0zmJ5kkLkhE7aPGpbsLpWmlqJqqaDmnJW5hNuKbCklJWUFe3iHU6uYoFEqUShdcXLphNBag0w0lJ2crcXFTAAtubn0xm0swmQpp0WIiPj6jKS6OIzPzExITF6BW++Di0p3S0jRUqqa0bDkHN7faR8Wyd/rbPX6hiRUrUjh0KI/mzdUolQpcXJR06+ZCQYGRoUN1bN2aw5QpcVgs0LevGyUlZgoLTUyc2ILRo32Iiyvmk08yWbAgER8fNd27u5CWVkrTpirmzGlJWJit4y9kRcoKDuUdorm6OUqFEhelC91culFgLGCobihbc7YyJW4KFiz0detLibmEQlMhE1tMZLTPaOKK4/gk8xMWJC7AR+1Dd5fupJWm0VTVlDkt5xBmI//tXf7tnf52P/8LC0lZsYK8Q4dQN2+OQqlE6eKCS7duGAsK0A0dSs7WrcRNmQIWC259+2IuKcFUWEiLiRPxGT2a4rg4Mj/5hMQFC1D7+ODSvTulaWmomjal5Zw5uNkYFc/+9Y+9y/+dnf5/tGo3AKmlpTx4/DiPNWvG9s6dK6eWzS4rY8CJEwz09MRDpWKcry8bs7IoMZvZUTGO9cLERMbExGC0WHjO15d5rVqxKCmJkd7eRAYHY7BYCI+Opv+xYxzt0aNyetqqSlNLOf7gcZo91ozO2ztXziFfll3GiQEn8BzoicpDhe84X7I2ZmEuMdNlR3n8xIWJxIyJwWK04PucL63mtSJpURLeI70JjgzGYrAQHR7Nsf7H6HG0R+X0tNXil6Zy/PiDNGv2GJ07b6+YRx7KyrI5cWIAnp4DUak88PUdR1bWRszmErp0KR/UITFxITExY7BYjPj6PkerVvNISlqEt/dIgoMjsVgMREeHc+xYf3r0OErjxjVH9LJ3+ts9fmopDz54nMcea8b27Z1RVuR/dnYZAwacYOBATzw8VIwb58vGjVmUlJjZUZH/CxcmMmZMDEajheee82XevFYsWpTEyJHeREYGYzBYCA+Ppn//Yxw92oMOHawdfyoPHn+Qx5o9xvbO21FW5H92WTYDTgxgoOdAPFQejPMdx8asjZSYS9hRkf8LExcyJmYMRouR53yfY16reSxKWsRI75FEBkdisBgIjw6n/7H+HO1xlA5W8t/e5d/e6W/38z81leMPPkizxx6j8/btKCqeqsqyszkxYACeAwei8vDAd9w4sjZuxFxSQpcdFef/woXEjBmDxWjE97nnaDVvHkmLFuE9ciTBkZFYDAaiw8M51r8/PY4epbGVEf3sX//Yu/zf2elvD5VtBRZg+OnTuDk68lZISLV55XVOTmzt1InCKiNFqa9rZng5IAClQsHHGRkAKABVlX2oFAom+/ujN5vZaGXEKCxwevhpHN0cCXkrpPLkB3DSOdFpaydMhdfiO6irxw94OQCFUkHGx+XxUYBCVWUKSZUC/8n+mPVmsjZaiY+F06eH4+joRkjIW5WZD+DkpKNTp62YTIVVmlmqD8UaEPAyCoWSjIyPr0asNkWwQqHC338yZrOerKyN1g7frulv9/gWGD78NG5ujrz1VkjlxQdAp3Ni69ZOFFbJf/V1+f/yywEolQo+rsh/hQJUVfJfpVIwebI/er2ZjRutHb+F4aeH4+boxlshb1VWfuXHr2Nrp60UVsl/9XX5/3LAyygVSj6uyH8FClRV8l+lUDHZfzJ6s56NVvLf3uXf3ulv9/PfYuH08OE4urkR8tZblRef8vg6Om3diqmwyvl/3bDaAS+/jEKpJOPjj6+e8CiqjNmvUKnwnzwZs15P1saNf8L6x97l/85Of7vfAPx0+TIH8vIY7eODtUEn/TQam2P8X92miY3Z5mytc/mny+QdyMNntA/WfoDGT4PXEBtzDFRso2yivKF1Ll/+iby8AxXjzdf8ARqNH15eQ6hr57Ynnal9HXunv93j/3SZAwfyGD3ax+qop35+GobYyP+r2zSxkf+21vnp8k8cyDvAaJ/RKKykgJ/GjyE28v/qNk1s5L+tdexd/u2d/nY//3/6ibwDB/AZPdrqsLsaPz+8bMxlf3UbZZMmN7SO/esfe5f/Ozv97X4DsKNimMZuNhKwu4tLrcsWJiVhsliY6OdndXmp2czi5GRcHR0Z4e1dY/mlHeXxm3SrPb5L99rjJy1MwmKy4DfRenxzqZnkxck4ujriPcJK/Es7KiqnbrXHd+lee/ykhVgsJvz8JlqPby4lOXkxjo6ueHvXnPDD3ulv9/gV+d/NRv53t5H/CxcmYTJZmFhL/peWmlm8OBlXV0dGjLB2/Dsqjr+bjePvbuP4F2KymJhYS/6XmktZnLwYV0dXRljJf3uXf3unv93P/4qm5CbdbJz/3W2c/wsXYjGZ8JtYy/lfWkry4sU4urribWXCH/vXP/Yu/3d2+ttL5TsAaaWlQPnc8vWVqdcTmZREQkkJerOZfd26cZ+7e7V1juTnsyAxkbNFRYRqtaxp0wZ/Tc1x2UvTyuOrPOofX5+pJykyiZKEEsx6M932dcP9vurx84/kk7ggkaKzRWhDtbRZ0waNv5X4pWkVTZUe9Y+vzyQpKZKSkgTMZj3duu3D3f2+6vHzj5CYuICiorNotaG0abMGjabmZxr2Tn+7x6/If48G5H9mpp7IyCQSEkrQ683s29eN+67L/yNH8lmwIJGzZ4sIDdWyZk0b/P2tHX9axfF7NOD4M4lMiiShJAG9Wc++bvu477r8P5J/hAWJCzhbdJZQbShr2qzB30r+27v82zv97X7+p1Wc/x4NOP8zM0mKjKQkIQGzXk+3fftwv+776/wjR0hcsICis2fRhobSZs0aNFY+07J//WPv8n9np7/dbwC0Fc2yV0ymem/srVYzIzDQ5jo9XF2ZGVT3tLFKbXl805X6x1d7qwmcYTu+aw9XgmbWI37FbHEm05X6x1d7Exg4w3Z81x4EBc2sc1/2Tn+7x6/I/ysNyH9vbzUz6sj/Hj1cmTmzPsevrTj+Kw04fm9m1JH/PVx7MLMe+W/v8m/v9Lf7+V8x/bDpSgPOf29vAmfUcf736EHQzJl/gfrH3uX/zk5/e6nsArjHtXy2o2MFBXb5Ia73lMcvOGan+K73lMcvOGaX+PZOf7vHr8j/Y8fsdfz3VBz/sTuy/Ns7/e1+/t9Tcf4fs1P+273+sXf5v7PT3+43AEN1Olqo1axKS8NkZe5ms8XCJmtv798iuqE61C3UpK1Kw2KqGd9itpC16TbG1w1FrW5BWtoqLJaaTyEWi5msrE23Lb6909/u8YfqaNFCzapVaZis5L/ZbGHTptt5/ENpoW7BqrRVmKzkv9liZtNtzH97l397p7/dz/+hQ1G3aEHaqlVYrLSCWcxmsjZt+h+uf+xd/u/s9Lf7DYBWqWRzp07EFhUx7NQpssvKKlfKMxp5IyGB/lX6Z/RmMyU2mostgMFisblO9SYgJZ02d6IotohTw05Rln0tvjHPSMIbCXj0vxbfrDdjKrGxbwtYDBbb61zXBNSp02aKimI5dWoYZWXX5nk3GvNISHgDD4/+VSpEPSZTCbZ+gMViqGOda+yd/naPr1WyeXMnYmOLGDbsFNlV8j8vz8gbbyTQv0r+6/VmSmzkrcUCBoPF5jrVj1/L5k6biS2KZdipYWRXyf88Yx5vJLxB/yr5rzfrKbGRtxYsGCwGm+v8mcq/vdPf7ue/VkunzZspio3l1LBhlGVXOf/z8kh44w08+lc5//V6TCU28tZiwWIw2F7nT1X/2Lv839npXxez2Vz53yYrdWpdy2tTbSCgXq6uRPfqxZsJCfQ8cgQvJycCNBqCtVqmBQTgoVKRazCwOSeHqIIC9GYzq1JTGezlhXeV7zLjiotZn5GB2WJh24UL9HR1JUKnq/ZduNVmmF6u9IruRcKbCRzpeQQnLyc0ARq0wVoCpgWg8lBhyDWQszmHgqgCzHozqatS8Rrshdr7WvziuGIy1mdgMVu4sO0Crj1d0UXoqn0XbL0ZqBe9ekWTkPAmR470xMnJC40mAK02mICAaahUHhgMueTkbKagIAqzWU9q6iq8vAajVl97s7i4OI6MjPVYLGYuXNiGq2tPdLqIat+FWmPv9Ld7/F6uREf34s03E+jZ8wheXk4EBGgIDtYybVoAHh4qcnMNbN6cQ1RUAXq9mVWrUhk82AvvKvkfF1fM+vUZmM0Wtm27QM+erkRE6Kp9l279+HsR3SuaNxPepOeRnng5eRGgCSBYG8y0gGl4qDzINeSyOWczUQVR6M16VqWuYrDXYLyr5H9ccRzrM9ZjtpjZdmEbPV17EqGLqPZd9J+x/Ns7/e1+/vfqRa/oaBLefJMjPXvi5OWFJiAAbXAwAdOmofLwwJCbS87mzRRERWHW60ldtQqvwYNRV/mypTgujoz167GYzVzYtg3Xnj3RRURU+y79z1n/2Lv839npb0tqamrlf2dkZNCqVasGLa+NwnL//RZ7NkE8cIdPSP3Dn2BGdDsnwB2d/w/88MCdnfx3egF8QE6/Ozr96zj+U6dO8fXXX7N+/XqSkpIACAoKYtSoUQwaNAjA5vKOHTvWvwVACCGEEH8OV6cDnj17ts11bC23xeq0Qel6PW8nJ+O4dy/u+/fzUUYG+UZjjfX2X77Mk6dOodizB8WePUyOi+OiwQDAr/n5hEVF4b5/P4uSkihuQL+EPl1P8tvJ7HXcy373/WR8lIExv2b8rM+z+Nn3Z/Yo9hAVFkXez3mVy0qSSogeFM1e5V5iX4pFn6FvUMLo9ekkJ7/N3r2O7N/vTkbGRxiN+VbXPXkygoMHg4mOHsTJk0M4eXIIJ048yp49Cn75pT1mc8NixxUXM/3cOdQ//ohizx4eOn6cM0VFACSVlDAuJgbHvXuZFBtLgpU+rvrm362MedPbxxUzffo51OofUSj28NBDxzlzpmL7pBLGjYvB0XEvkybFkpBQc/vDh/Pp1SsKhWIPzZv/xOefX3thrLjYxMyZ8SgUe3jkkeMcPWr9TfP9l/fz5KknUexRoNijYHLcZC4aLlaU518JiwrDfb87i5IWUWwqtrqPiJMRBB8MZlD0IIacHMKQk0N49MSjKPYoaP9Le/T1LAs3WrbNejMZ6zMqt014M4GS+Pr1Q37+eRa+vj+jUOwhLCyKn6vETEoqYdCgaJTKvbz0UiwZVWLm5xtZsiQZH5/ybYOCDvLDD7mVab9sWQoKxR4efvh4tX3eqmMuTSnlzDNn2KPYwx7FHpKXJFerL7I+z2Jfo30canOInK05tcY36/UkzJ3Lj2o1exQKYsaMofj8+WvH+csv/NKuHftdXUleuhRzlfdkAIrOnOFHtZpjDzzAySFDKv8dDApij0JB5qefNqgeSE19j//+tyknTjxWWa+cPDmEffsa8+OPWoqL42oeg1lPRsZ6fv7Zlz17FCQkvElJSXy9Y95I+Y0rjmP6uemof1Sj2KPgoeMPcaboTMW5n8S4mHE47nVkUuwkEkoSbMY/GRHBweBgogcNqky/E48+yh6Fgl/at8esrxk///Bhonr1Yo9CwU/Nm5P1+eeVy0zFxcTPnMkehYLjjzxCwdGjdabBjdbnN5Jf9ma1BcBXreaVgADWpafTWqtlrI+P1Y3vc3fnPnd3fNRqlqek0L1JE5pV9LP0dHWlmZMT+9u04a4mDRv6UO2rJuCVANLXpaNtrcVnrPX4zYc3RxuiJap3FM6Bzrj1vTbLl3OgMx79PHDt5Urg9MAGJ4xa7UtAwCukp69Dq22Nj8/YWtfVaPzo0OFTHBw0VS5ok1EoHGnffkONcaPrEqrV8lZICL3d3HgiOho/tZr2jRoBEOjsjL9Gw/tt2jCuyhzQN5J/tzLmTW8fquWtt0Lo3duNJ56Ixs9PTfv2FdsHOuPvr+H999swbpz17Xv1cmX37i506HAYB4fyt9qv0mqVPPmkjuPHC/j++y7U9irCfe73cZ/7ffiofViespzuTbrTTNWsojz3pJlTM/a32c9dTe6qNR39NH582uFTNFXKwuS4yTgqHNnQfkONMdRrc6Nl20HtgM9oH/J/ySdrUxYtZ7esd7kbPrw5ISFaeveOIjDQmb5VYgYGOtOvnwe9erky/bqYrq6O/POfAQwe7EX37kdQqxX06+demfbduzdhxAhvPv20/W05Zo2/hvaftMdYYOTCNxfwfNwTR9drVZsuQkfy0mS6/tDV5kBDDmo1LefMwdHVlbgpU3Dt3RttcPC14+zdm0bt29Nu3brKz9aqKrt4kfaffIJu2LAqNycpHO7YEc+BA/EeObJB9YDZXESPHr/h7HzteC9c+IacnC2Ehi5Hqw2teQwOanx8RpOf/wtZWZto2bJhT4Y3Un5DtaG8FfIWvd1680T0E/ip/WjfqH3FuR+Iv8af99u8zzjfcXXG1/j50eHTT3GoMlhY3OTJKBwdab9hQ405AKD83YEuu3dzuEMHcHBAN3Ro5TKlVovuyScpOH6cLt9/D3W8h3Qz9fmN5Je92Zw42EmhqDHpizVvhYTQ3cWF6efPVz5pLk5OZqhO1+CLf1UKJ0WNST+u53K3C/5T/Mn+IpuCI9ee7EyFJnJ/yCXgnwE3lUAKhVOdF/BmzQZUKyyXL/+XlJSVBAZOtzl8ZF3CPT15yd+f9ZmZHM4vb304lJ9PVllZrRfSG8m/WxnzprcP9+Sll/xZvz6Tw4crtj+UT1ZWWa0X/8qy4OLImjVtSE4u5Z13Uqoti4xM4oMP2tbn/OetkLfo7tKd6eenk1/R6rM4eTFDdUNtXvwBBjQbUK3y/O/l/7IyZSXTA6fbHEr1VpdtByeHOs8da+6+24UpU/z54otsjlSJWVho4ocfcvmnjZhBQc589FE7YmOLWbasPP0vXTLw9tvJrF3b9rYfc5vVbXB0cSRuavUnrdT3UgmaFVTvUQb9XnoJlx49SHjjDYxVxsUo+O03NC1aWL34AygcHfEMD7/2B4uFmDFjUKhUtP3ggwbnRZMm3atdTAyGi5w9Ox43t774+b1ku2J3cGrwg8fNlt9wz3Be8n+J9ZnrOZx/uOLcP0RWWVa9Lv4AzQYMqHbxv/zf/5KyciWB06fbHArY0cWFNmvWUJqcTMo771RblhQZWZ7+9Tn5b6I+v5n8+lPeAFgz4vRphp06Ve1vKoWC9e3acdFgYNq5c/ycl0dSSQlPN29+y3/w6RGnOTWsevyWc1ui8ddwdvxZLMbydxoT5iYQNCuo2qxit4LFYuDgwVakpa2u/JuHR79rFZWpkJiY0TRu3JGgoNk3HW9hq1YEaTSMi4khXa9nQWIiS0Nr3kmuTU8n8MAB9FU+B7ndMa2VhYZsX2v8ha0ICtIwblwM6el6FixIZOlSK/FHnGbYdWXh0UebERGhY86cBJKTy4eX3b79Al26NMHPT1Ov+CqFivXt1nPRcJFp56bxc97PJJUk8XTzp62kwQiGnbr2xNevSlkoNBUyOmY0HRt3ZPYNloX6lO3Ck4X81+O/XDl+5ZaU8blzW+Lvr2H8+LMYK2LOnZvArFlBlbMErl2bTmDgAfR6c40buKeeas6cOfGcO1fMhAlnWbo0FGdnh1t6zOlr0zkQeABzlfhqHzXBi4K5+N1Fcv5d3tSvz9CT/2s+XoO86n/T7+BAuw8/pCwnh/jXXis/781mEufNo+Xcude62tau5UBgYGWztFtYWLUn1NT33iN3717avPceTjpdg/Ohar0CcPbs85hMRbRvvx6F4lp6pqev5cCBwAZ3NVpT3/K7Nn0tgQcCa3QJLGy1kCBNEONixpGuT2dB4gKWhi6t/zH3q1KXFhYSM3o0jTt2JOi6Pu7r0x6g2aOPoouIIGHOHEqTk8ufwLdvp0mXLmhqmaOkrnS3VZ+fPj2CU1XO/frm11/6BsBbrcbHSjNMh8aNmRUUxLr0dGbHxzeowm9Q07y3GrVP9fhKrZI2q9twJfoKKctTKDxZiFlvxqWHy234BUo0Gv9ax4yOi5vZXodwAAAP5UlEQVRKaWlaRVOR001H0yqVfNSuHTFFRYRFRbE8NBRnK0/17o6OBDg746hQ/GExaysL9d2+1vhaJR991I6YmCLCwqJYvtz6BcTbW42PT834K1e2RqVS8MILv1NcbOLDDzOYPLlh4293aNyBWUGzWJe+jtnxs2utxLzV3viorXexTI2bSlppGhvab8DpBstCfcq2g9YBTYAGZSPlLSnhWq2S1avbEB19heXLUzh5shC93kyPKjHd3R0JCHDG0bFmeXv33dY0aeJI795RDBrkRevW2lt+zI7ujjgHOKO4Lr7vBF9ce7sS+1IsxgIj5187T/DC4AanQeNOnfB/+WXS1qwh/9dfSX//fZo/9RSOLlV/gzvOAQEoHGv2pJbEx3N++nS8hgyp1iVwo7KyNpGT829CQt7G2bn6J16Oju44OwegUNzad7ptlV93R3cCnANwvC6mVqnlo3YfEVMUQ1hUGMtDl+Ps4HxD8eOmTqU0La286d+pevza0r71ypUoVCp+f+EFTMXFZHz4If6TJ99wGtiqz9Vqb9S1nPu28qs2e/fuJSAggHvuuYfIyEgiIyOZM2dO5Sd9x44do0+fPuh0OmbOnMmECRPo27cv+/fvr9xHfdaplo4NTZDFISG1LpsRGMg7KSmklJZittyerwtDFluP3/Thpuie1JHwRgK5e3Pp+EXH2xJfoXCgW7d9VpddurSL9PS1tGw5lyZNOt+ymPe6uzPA05OdFy/W+oQfodMRcQNPGTcT01ZZqM/2NuPf686AAZ7s3HmxxlNmZfxaykLz5k5ERoYwYcJZHnroOJGRwVYvVHWZETiDd1LeIaU0BbOltjRYbPXvuy7tYm36Wua2nEvnmywLdZVtbbCWnsd73tJy/vDDTXnySR1vvJHA3r25fHFdzIgIHRER1stb06Yqpk8PZOrUuAbNLdCQY9ZF6NBZia9wUNB2bVt+7forJx45QbNHm+EcdGMXoFZvvEHOv/9NzJgxNO7QgY5ffnndb4hAFxFRs5XQbObMs8+ibNyYtmvW3HRe6PWZxMZOwsOjHy1aPF9juU4XgU4XcUvzv67yG6GLIKKWmPe638sAzwHsvLiz3i+91qhLd+0ife1aWs6dS5PONePXlvZOzZsTEhnJ2QkTOP7QQwRHRlq9QavXb6ijPg+p5dyvK79q079/f+666y78/f2ZUWWOg4CA8m6vrl278sADD2A0GlmwYAEAr732Go8//jhpaWm4uLjUa52bagGwZUVKChNatCCptJRZ8fH80UKXhGIqNuHayxVHtz/2C0ejMY+YmLE0adKVoKDXbum+D+Xn46tW4+nkxNiYGKtD9d5qNxvzprc/lI+vrxpPTyfGjo2xOjytLePH+xIaqkWpVBAW5naD5XkFE1pMIKk0iVnxs+q9XZ4xj7ExY+napCuv3aKyYI+yvWRJKMXFJnr1csWtATEvXTJw8GAeDz/clFdeOUdamv4PPebGHRrj86wP+Ufyb+odIAdnZ1q9+SZFMTG0eL7+FXnK0qXkHTxImzVrUDVrdtP5cPbsc5jNBtq1+xhrc9Xfajdbfg/lH8JX7YunkydjY8ZaHVrYZl2al0fM2LE06dqVoNcaHt93/Hi0oaEolErcwsL+8Pr8ZvLLwUpL6VNPPXWtdUxZvZWvc+fOXLlyhawqw7TXZ51bfgPwc14e2WVlzG/VikktWvBOaipH/uCJZVRNy1/ycdD88f0tsbEvYjBcoH37Dbe0Ke5CWRmLk5J4JzSU99q0IaqggOUpKbf1WG425k1vf6GMxYuTeOedUN57rw1RUQUsX96wY1YowN1dheYGy8LPeT+TXZbN/FbzmdRiEu+kvsORgiP12vbF2Be5YLjAhvYbajSR/pXKdtOKmA1JQ4sFJk36nSVLQvjgg7aYzRaef/7sH37MqqYqFA6KOkf/q3s/TSt+Q/3eHymKiSH+9ddpPnw4Xk88cdN5kJHxERcvfk9o6FI0moA/JN9vpvxeKLvA4qTFvBP6Du+1eY+ogiiWpyxvWF364osYLlyg/YYNN/b0rlCgcnevd57dyvr8VufXyZMniY2NtbqsuLiYdevW8fe//52QWlpj61rnltQm2WVlLElOZmFFX8WC4GD81GrGnDlD2S14Ke3P7sKFb8jM/IyWLefSuHGHastKS1PIzz90Q/s1WyxMjI1lWWgoTg4OhHt6MtjLi9nx8ZwvLr4tx3KzMW96e7OFiRNjWbYsFCcnB8LDPRk82IvZs+M5f774D8nP7LJsliQvYWGrhRXleQF+aj/GnBlDmbnM5rbfXPiGzzI/Y27LuXS4riyklKZw6AbLwl/F/PmJDB2qIyjIGT8/DYsWBfPddxerjcvwv8piNHLm2WdReXjQ+t13ayxv6GQ2paUpxMW9TNOmD+Hr+1zNcpr95S0/hpspv2aLmYmxE1kWugwnByfCPcMZ7DWY2fGzOV98vn516TffkPnZZ7ScO5fGHa6rS1NSyD90+8+fG63Pb1V+HTt2jMjISObPn1/t6f/a77vAokWLCAgI4JFHHmHnzp0ornv3qz7r1HkDoLdYMFzXdDs5Lo5JVe5ICk0mnjp1iuUVFT5AY6WSpaGhnCkq4vWb6Aqw6C1YDNXjx02OI3aS9Tsic1n5zYZZf+tuOiwWPRaLocr/mzh6tC+ZmeWDelz91MPVtScBAdOu35qEhDk4O4fcUOwpcXGEe3oS5HytD3Nl69aYgFExMRir5M2mrCz6HD1arb/dWv7dypjXl4WGbm81/pQ4wsM9CarSb7tyZWtMJhg1KqbyrXSAyZPjmFRLWQAoKzPX+v5AbQpNhTx16imWhy6vfPGpsbIxS0OXcqboDK/Hv37d+TCZSbGTALhouMj4s+Pp6dqTadeVBQsW5iTMIeQGy4Ktsl0cV8zhTocpqhg46ep61587DVVWEdNaGm7alEWfPkerLfv3v3NITy9lUJU37l94oQWdOjXmxRdjG9wVYOuYszZlcbTP0VrPdXNZxfHfZG/Z1cF+rA1Ak7VpE0f79KlclrhwIQVHj9J27VpUHh41WgauHD/ekJqHmJgxgIJ27dZZedL8V2X1nZW1iaNH+1T7CsBsrl5v1UdDyu+mrE30OdqnWh//lLgphHuGE+QcVOXcX4kJE6NiRmG02B6MzHDxImfHj8e1Z08Cpk2r0bSUMGcOzhVPsdenvbV8q22Zzd/QgPo8Lm4ysRXnfkPyqy5du3ZlxowZzJo1i88++6zGck9PT1599VXuvvtuDh48iJOT0w2tA7W8BJiu1/NldjYJJSVcMhhYm57OMJ0OV0dHMvR6DBUXmU8yM5mbkECGXk9UQQEtKyr9AqOx8hvwxcnJlJrNTPb3r3ZRsHnjka4n+8tsShJKMFwykL42Hd0wHY6ujugz9JgNNU/6ojNFpK1NK7/T+jKbRu0aWX1JqL70+nSys7+kpCQBg+ES6elr0emG4eCgobQ0BYPhUkUhmEJZWQ4ajR8nTw6uWgQpKvodozGfdu3WN7D5OY858fHsu3yZmY6OFJtMaCv6dXZeuoQCOJiXx+MnTjAzKIgwNzdyDQZSSksxWixctJF/tzJm1bJwI9tXi/9zHnPmxLNv32VmznSkuNiEVlux/c5LKBRw8GAejz9+gpkzgwgLcyMjQ4/BSlnIySnjq6+yOXOmCJVKwYcfpjNkiBfu7ra/A/8k8xPmJswlQ59BVEEULZ1bVpTngsrvmhcnL6bUXMpk/8kEOQeRoc/AYDZUVoA5ZTn4afwYXKUsmDHze9Hv5BvzWd/AslCfsm0qNFGaWoqx0IhZbyb7q2wufn8RY4GRhDkJeD/jjXOrhr0Id+ZMEWsrYn75ZTbt2jWq9tJfbq6BlJRSjEYLGRklLFqUxEcfZTBwoCdJSSUEBpbH++mnPEpKzOTmGrj//t+YPbslw4c3v+ljNuQaKE0pLf9MsMqHIBazhewvsrnw9QUsZgvxs+LxHuWNNkTb4HTP/fFHUletKn/6XbGivE+5T58qvyGX0pQULEYjRYmJJM6fj7JRI9LXrSN93bWLgOnKFfIOHSJ02bIGNCV/TG7uXjSaAH7/fdJ1dVMmBQVH6N07puKilUtpaQoWixGzGbKzv+Lixe8xGgtISJiDt/cz9XoTvSHlN9eQS0ppCkaLkSN5R5gTP4d9l/cx03EmxaZitEptxbm/EwUKDuYd5PETjzMzaCZhbtb75eOmTKEsJweNnx8nBw+u2ixI0e+/Y8zPp9369TXSnipfIpXl5JD91VcUnTmDQqUi/cMP8RoyBJW7e73SvSH1uV6fgbni3G9IfjVEly5dys+HoiIaVQysdtXHH39Mx44d+fTTTxlZyyBTda0jkwHJZEDYOQHu6PyXyYDu8AIokwHd2el/3fEPHDgQPz8/VlXceJZ3HWSze/duRo4cyZtvvsl3333HkSPl7yN9/fXXjBo1iqioKEIrPr2vzzr16gIQQgghxO23a9cufvvtN/bt28eiRYuIjIxk9uzZ9O7dm379+nHs2DF27dpFXFwc33//PSaTiUGDBjF48GD69evHxx9/zOHDh+tcR1+la0RaAKQFQB5BpAVAWgCkBUBaAP4ELQBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEHe6/w9fw3EBXkQ95QAAAABJRU5ErkJggg=="}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":4},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":5},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":6},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":7},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":0,"translation":[-0,-2,-0]},{"mesh":1,"translation":[-1,-0,-0]},{"mesh":1,"translation":[-1,-4,-0]},{"mesh":1,"translation":[-1,-8,-0]},{"mesh":1,"translation":[-1,-10,-0]},{"mesh":2,"translation":[-2,-0,-0]},{"mesh":2,"translation":[-2,-2,-0]},{"mesh":2,"translation":[-2,-4,-0]},{"mesh":3,"translation":[-3,-0,-0]},{"mesh":3,"translation":[-3,-2,-0]},{"mesh":3,"translation":[-3,-8,-0]},{"mesh":4,"translation":[-3,-4,-0]},{"mesh":4,"translation":[-3,-6,-0]},{"mesh":4,"translation":[-3,-10,-0]},{"mesh":5,"translation":[0,0,0]},{"mesh":6,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/noise_gates_2.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.4375],"min":[0.5,0.375],"name":"tex_coords_gate_E:X","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.5],"min":[0.5,0.4375],"name":"tex_coords_gate_E:Y","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.5625],"min":[0.5,0.5],"name":"tex_coords_gate_E:Z","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.4375],"min":[0.5625,0.375],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","type":"VEC2"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.5],"min":[0.5625,0.4375],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.5625],"min":[0.5625,0.5],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":22,"max":[1,-2,-0],"min":[-3,-10,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":6,"max":[0,0.5,-0],"min":[-3,-0.5,-0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:X","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:Y","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:Z","target":34962},{"buffer":4,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","target":34962},{"buffer":7,"byteLength":264,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":8,"byteLength":72,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_E:X","uri":"data:application/octet-stream;base64,AAAQPwAAwD4AAAA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/AADgPgAAED8AAOA+AAAQPwAA4D4AABA/AADAPgAAAD8AAOA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_E:Y","uri":"data:application/octet-stream;base64,AAAQPwAA4D4AAAA/AADgPgAAED8AAAA/AAAAPwAA4D4AAAA/AAAAPwAAED8AAAA/AAAQPwAAAD8AABA/AADgPgAAAD8AAAA/AAAAPwAAAD8AABA/AADgPgAAAD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_E:Z","uri":"data:application/octet-stream;base64,AAAQPwAAAD8AAAA/AAAAPwAAED8AABA/AAAAPwAAAD8AAAA/AAAQPwAAED8AABA/AAAQPwAAED8AABA/AAAAPwAAAD8AABA/AAAAPwAAED8AABA/AAAAPwAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","uri":"data:application/octet-stream;base64,AAAgPwAAwD4AABA/AADAPgAAID8AAOA+AAAQPwAAwD4AABA/AADgPgAAID8AAOA+AAAgPwAA4D4AACA/AADAPgAAED8AAOA+AAAQPwAA4D4AACA/AADAPgAAED8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","uri":"data:application/octet-stream;base64,AAAgPwAA4D4AABA/AADgPgAAID8AAAA/AAAQPwAA4D4AABA/AAAAPwAAID8AAAA/AAAgPwAAAD8AACA/AADgPgAAED8AAAA/AAAQPwAAAD8AACA/AADgPgAAED8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","uri":"data:application/octet-stream;base64,AAAgPwAAAD8AABA/AAAAPwAAID8AABA/AAAQPwAAAD8AABA/AAAQPwAAID8AABA/AAAgPwAAED8AACA/AAAAPwAAED8AABA/AAAQPwAAED8AACA/AAAAPwAAED8AAAA/"},{"byteLength":264,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AAAAgAAAgMAAAACAAAAAgAAAAMAAAACAAACAvwAAgMAAAACAAACAvwAAAMAAAACAAACAvwAAwMAAAACAAACAvwAAgMAAAACAAAAAwAAAAMEAAACAAAAQwAAAwMAAAACAAAAQwAAAwMAAAACAAAAAwAAAgMAAAACAAAAAwAAAwMAAAACAAAAAwAAAAMEAAACAAACAPwAAAMAAAACAAABAwAAAAMAAAACAAACAPwAAgMAAAACAAABAwAAAgMAAAACAAACAPwAAwMAAAACAAABAwAAAwMAAAACAAACAPwAAAMEAAACAAABAwAAAAMEAAACAAACAPwAAIMEAAACAAABAwAAAIMEAAACA"},{"byteLength":72,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAIAAAACAAABAwAAAAIAAAACAAAAgwAAAAL8AAACAAABAwAAAAIAAAACAAAAgwAAAAD8AAACAAABAwAAAAIAAAACA"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":4},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":5},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":6},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":7},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":8},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-2,-0]},{"mesh":0,"translation":[-0,-4,-0]},{"mesh":0,"translation":[-1,-2,-0]},{"mesh":1,"translation":[-1,-4,-0]},{"mesh":2,"translation":[-1,-6,-0]},{"mesh":3,"translation":[-2,-4,-0]},{"mesh":4,"translation":[-2,-8,-0]},{"mesh":5,"translation":[-2,-6,-0]},{"mesh":3,"translation":[-2,-10,-0]},{"mesh":6,"translation":[0,0,0]},{"mesh":7,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/noise_gates_3.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.625],"min":[0.5,0.5625],"name":"tex_coords_gate_PAULI_CHANNEL_1","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.625],"min":[0.5625,0.5625],"name":"tex_coords_gate_PAULI_CHANNEL_2","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":16,"max":[1,-0,-0],"min":[-2,-8,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":6,"max":[0,2.5,-0],"min":[-3,1.5,-0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_PAULI_CHANNEL_1","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_PAULI_CHANNEL_2","target":34962},{"buffer":3,"byteLength":192,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":4,"byteLength":72,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_PAULI_CHANNEL_1","uri":"data:application/octet-stream;base64,AAAQPwAAED8AAAA/AAAQPwAAED8AACA/AAAAPwAAED8AAAA/AAAgPwAAED8AACA/AAAQPwAAID8AABA/AAAQPwAAAD8AACA/AAAAPwAAID8AABA/AAAQPwAAAD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_PAULI_CHANNEL_2","uri":"data:application/octet-stream;base64,AAAgPwAAED8AABA/AAAQPwAAID8AACA/AAAQPwAAED8AABA/AAAgPwAAID8AACA/AAAgPwAAID8AACA/AAAQPwAAED8AACA/AAAQPwAAID8AACA/AAAQPwAAED8AABA/"},{"byteLength":192,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAvwAAAIAAAACAAACAvwAAAMAAAACAAACAvwAAgMAAAACAAACgvwAAwMAAAACAAACgvwAAwMAAAACAAACAvwAAAMEAAACAAACAPwAAAIAAAACAAAAAwAAAAIAAAACAAACAPwAAAMAAAACAAAAAwAAAAMAAAACAAACAPwAAgMAAAACAAAAAwAAAgMAAAACAAACAPwAAwMAAAACAAAAAwAAAwMAAAACAAACAPwAAAMEAAACAAAAAwAAAAMEAAACA"},{"byteLength":72,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACA"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":3},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":4},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":0,"translation":[-0,-2,-0]},{"mesh":0,"translation":[-0,-4,-0]},{"mesh":0,"translation":[-0,-6,-0]},{"mesh":1,"translation":[-1,-0,-0]},{"mesh":1,"translation":[-1,-2,-0]},{"mesh":1,"translation":[-1,-4,-0]},{"mesh":1,"translation":[-1,-8,-0]},{"mesh":2,"translation":[0,0,0]},{"mesh":3,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/repeat.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5],"min":[0.0625,0.4375],"name":"tex_coords_gate_H","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.4375],"min":[0.3125,0.375],"name":"tex_coords_gate_RX","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":8,"max":[1,-0,-0],"min":[-6,-6,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":54,"max":[0,2.5,1],"min":[-4.25,-7,-1],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_RX","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":4,"byteLength":648,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_H","uri":"data:application/octet-stream;base64,AAAAPgAA4D4AAIA9AADgPgAAAD4AAAA/AACAPQAA4D4AAIA9AAAAPwAAAD4AAAA/AAAAPgAAAD8AAAA+AADgPgAAgD0AAAA/AACAPQAAAD8AAAA+AADgPgAAgD0AAOA+"},{"byteLength":96,"name":"tex_coords_gate_RX","uri":"data:application/octet-stream;base64,AADAPgAAwD4AAKA+AADAPgAAwD4AAOA+AACgPgAAwD4AAKA+AADgPgAAwD4AAOA+AADAPgAA4D4AAMA+AADAPgAAoD4AAOA+AACgPgAA4D4AAMA+AADAPgAAoD4AAMA+"},{"byteLength":96,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAPwAAAIAAAACAAADAwAAAAIAAAACAAACAPwAAAMAAAACAAADAwAAAAMAAAACAAACAPwAAgMAAAACAAADAwAAAgMAAAACAAACAPwAAwMAAAACAAADAwAAAwMAAAACA"},{"byteLength":648,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACAAADgvwAAQD8AAEA/AADgvwAAQD8AAEC/AADgvwAAQD8AAEA/AADgvwAA2MAAAEA/AADgvwAAQD8AAEA/AABQwAAAQD8AAEA/AADgvwAAQD8AAEC/AADgvwAA2MAAAEC/AADgvwAAQD8AAEC/AABQwAAAQD8AAEC/AADgvwAA2MAAAEA/AADgvwAA2MAAAEC/AADgvwAA2MAAAEA/AABQwAAA2MAAAEA/AADgvwAA2MAAAEC/AABQwAAA2MAAAEC/AABQwAAAQD8AAEA/AABQwAAAQD8AAEC/AABQwAAAQD8AAEA/AABQwAAA2MAAAEA/AABQwAAAQD8AAEC/AABQwAAA2MAAAEC/AABQwAAA2MAAAEA/AABQwAAA2MAAAEC/AABAvwAAgD8AAIA/AABAvwAAgD8AAIC/AABAvwAAgD8AAIA/AABAvwAA4MAAAIA/AABAvwAAgD8AAIA/AACIwAAAgD8AAIA/AABAvwAAgD8AAIC/AABAvwAA4MAAAIC/AABAvwAAgD8AAIC/AACIwAAAgD8AAIC/AABAvwAA4MAAAIA/AABAvwAA4MAAAIC/AABAvwAA4MAAAIA/AACIwAAA4MAAAIA/AABAvwAA4MAAAIC/AACIwAAA4MAAAIC/AACIwAAAgD8AAIA/AACIwAAAgD8AAIC/AACIwAAAgD8AAIA/AACIwAAA4MAAAIA/AACIwAAAgD8AAIC/AACIwAAA4MAAAIC/AACIwAAA4MAAAIA/AACIwAAA4MAAAIC/"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":3},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":4},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":0,"translation":[-0,-2,-0]},{"mesh":0,"translation":[-0,-4,-0]},{"mesh":1,"translation":[-1,-4,-0]},{"mesh":0,"translation":[-2,-0,-0]},{"mesh":0,"translation":[-2,-2,-0]},{"mesh":0,"translation":[-2,-6,-0]},{"mesh":0,"translation":[-3,-6,-0]},{"mesh":2,"translation":[0,0,0]},{"mesh":3,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/repetition_code.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5625],"min":[0.3125,0.5],"name":"tex_coords_gate_R","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"control_x_line_cross","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.5625],"min":[0.375,0.5],"name":"tex_coords_gate_MR","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5625],"min":[0.25,0.5],"name":"tex_coords_gate_M","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":26,"max":[1,-0,-0],"min":[-9,-8,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":30,"max":[0,2.5,1],"min":[-7.25,-9,-1],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_R","target":34962},{"buffer":2,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":3,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":4,"byteLength":48,"byteOffset":0,"name":"control_x_line_cross","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MR","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_M","target":34962},{"buffer":7,"byteLength":312,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":8,"byteLength":360,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_R","uri":"data:application/octet-stream;base64,AADAPgAAAD8AAKA+AAAAPwAAwD4AABA/AACgPgAAAD8AAKA+AAAQPwAAwD4AABA/AADAPgAAED8AAMA+AAAAPwAAoD4AABA/AACgPgAAED8AAMA+AAAAPwAAoD4AAAA/"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_x_line_cross","uri":"data:application/octet-stream;base64,AAAAAM3MzL4AAAAAAAAAAM3MzD4AAAAAAAAAAAAAAADNzMy+AAAAAAAAAADNzMw+"},{"byteLength":96,"name":"tex_coords_gate_MR","uri":"data:application/octet-stream;base64,AADgPgAAAD8AAMA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+AAAQPwAA4D4AABA/AADgPgAAED8AAOA+AAAAPwAAwD4AABA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_M","uri":"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/"},{"byteLength":312,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAvwAAAIAAAACAAACAvwAAAMAAAACAAACAvwAAgMAAAACAAACAvwAAwMAAAACAAAAAwAAAgMAAAACAAAAAwAAAAMAAAACAAAAAwAAAAMEAAACAAAAAwAAAwMAAAACAAACgwAAAAIAAAACAAACgwAAAAMAAAACAAACgwAAAgMAAAACAAACgwAAAwMAAAACAAADAwAAAgMAAAACAAADAwAAAAMAAAACAAADAwAAAAMEAAACAAADAwAAAwMAAAACAAACAPwAAAIAAAACAAAAQwQAAAIAAAACAAACAPwAAAMAAAACAAAAQwQAAAMAAAACAAACAPwAAgMAAAACAAAAQwQAAgMAAAACAAACAPwAAwMAAAACAAAAQwQAAwMAAAACAAACAPwAAAMEAAACAAAAQwQAAAMEAAACA"},{"byteLength":360,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACAAABwwAAAgD8AAIA/AABwwAAAgD8AAIC/AABwwAAAgD8AAIA/AABwwAAAEMEAAIA/AABwwAAAgD8AAIA/AADowAAAgD8AAIA/AABwwAAAgD8AAIC/AABwwAAAEMEAAIC/AABwwAAAgD8AAIC/AADowAAAgD8AAIC/AABwwAAAEMEAAIA/AABwwAAAEMEAAIC/AABwwAAAEMEAAIA/AADowAAAEMEAAIA/AABwwAAAEMEAAIC/AADowAAAEMEAAIC/AADowAAAgD8AAIA/AADowAAAgD8AAIC/AADowAAAgD8AAIA/AADowAAAEMEAAIA/AADowAAAgD8AAIC/AADowAAAEMEAAIC/AADowAAAEMEAAIA/AADowAAAEMEAAIC/"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":2},"material":1,"mode":6}]},{"primitives":[{"attributes":{"POSITION":3},"material":2,"mode":6},{"attributes":{"POSITION":3},"material":3,"mode":3},{"attributes":{"POSITION":4},"material":3,"mode":1}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":5},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":6},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":7},"material":4,"mode":1}]},{"primitives":[{"attributes":{"POSITION":8},"material":5,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":0,"translation":[-0,-2,-0]},{"mesh":0,"translation":[-0,-4,-0]},{"mesh":0,"translation":[-0,-6,-0]},{"mesh":0,"translation":[-0,-8,-0]},{"mesh":1,"translation":[-1,-0,-0]},{"mesh":2,"translation":[-1,-2,-0]},{"mesh":1,"translation":[-1,-4,-0]},{"mesh":2,"translation":[-1,-6,-0]},{"mesh":1,"translation":[-2,-4,-0]},{"mesh":2,"translation":[-2,-2,-0]},{"mesh":1,"translation":[-2,-8,-0]},{"mesh":2,"translation":[-2,-6,-0]},{"mesh":3,"translation":[-3,-2,-0]},{"mesh":3,"translation":[-3,-6,-0]},{"mesh":1,"translation":[-5,-0,-0]},{"mesh":2,"translation":[-5,-2,-0]},{"mesh":1,"translation":[-5,-4,-0]},{"mesh":2,"translation":[-5,-6,-0]},{"mesh":1,"translation":[-6,-4,-0]},{"mesh":2,"translation":[-6,-2,-0]},{"mesh":1,"translation":[-6,-8,-0]},{"mesh":2,"translation":[-6,-6,-0]},{"mesh":3,"translation":[-7,-2,-0]},{"mesh":3,"translation":[-7,-6,-0]},{"mesh":4,"translation":[-8,-0,-0]},{"mesh":4,"translation":[-8,-4,-0]},{"mesh":4,"translation":[-8,-8,-0]},{"mesh":5,"translation":[0,0,0]},{"mesh":6,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/single_qubits_gates.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.4375],"min":[0,0.375],"name":"tex_coords_gate_I","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.4375],"min":[0,0.375],"name":"tex_coords_gate_X","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.5],"min":[0,0.4375],"name":"tex_coords_gate_Y","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.5625],"min":[0,0.5],"name":"tex_coords_gate_Z","type":"VEC2"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.625],"min":[0.0625,0.5625],"name":"tex_coords_gate_C_XYZ","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.625],"min":[0.125,0.5625],"name":"tex_coords_gate_C_ZYX","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5],"min":[0.0625,0.4375],"name":"tex_coords_gate_H","type":"VEC2"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5625],"min":[0.0625,0.5],"name":"tex_coords_gate_H_XY","type":"VEC2"},{"bufferView":9,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.4375],"min":[0.0625,0.375],"name":"tex_coords_gate_H_YZ","type":"VEC2"},{"bufferView":10,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.5625],"min":[0.125,0.5],"name":"tex_coords_gate_S","type":"VEC2"},{"bufferView":11,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.4375],"min":[0.125,0.375],"name":"tex_coords_gate_SQRT_X","type":"VEC2"},{"bufferView":12,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.4375],"min":[0.1875,0.375],"name":"tex_coords_gate_SQRT_X_DAG","type":"VEC2"},{"bufferView":13,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.5],"min":[0.125,0.4375],"name":"tex_coords_gate_SQRT_Y","type":"VEC2"},{"bufferView":14,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.5],"min":[0.1875,0.4375],"name":"tex_coords_gate_SQRT_Y_DAG","type":"VEC2"},{"bufferView":15,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.5625],"min":[0.1875,0.5],"name":"tex_coords_gate_S_DAG","type":"VEC2"},{"bufferView":16,"byteOffset":0,"componentType":5126,"count":8,"max":[1,-0,-0],"min":[-6,-6,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":17,"byteOffset":0,"componentType":5126,"count":6,"max":[0,2.5,-0],"min":[-3,1.5,-0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_I","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Y","target":34962},{"buffer":4,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Z","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_XYZ","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_ZYX","target":34962},{"buffer":7,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H","target":34962},{"buffer":8,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_XY","target":34962},{"buffer":9,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_YZ","target":34962},{"buffer":10,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_S","target":34962},{"buffer":11,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_X","target":34962},{"buffer":12,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_X_DAG","target":34962},{"buffer":13,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_Y","target":34962},{"buffer":14,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_Y_DAG","target":34962},{"buffer":15,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_S_DAG","target":34962},{"buffer":16,"byteLength":96,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":17,"byteLength":72,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_I","uri":"data:application/octet-stream;base64,AACAPQAAwD4AAAAAAADAPgAAgD0AAOA+AAAAAAAAwD4AAAAAAADgPgAAgD0AAOA+AACAPQAA4D4AAIA9AADAPgAAAAAAAOA+AAAAAAAA4D4AAIA9AADAPgAAAAAAAMA+"},{"byteLength":96,"name":"tex_coords_gate_X","uri":"data:application/octet-stream;base64,AACAPQAAwD4AAAAAAADAPgAAgD0AAOA+AAAAAAAAwD4AAAAAAADgPgAAgD0AAOA+AACAPQAA4D4AAIA9AADAPgAAAAAAAOA+AAAAAAAA4D4AAIA9AADAPgAAAAAAAMA+"},{"byteLength":96,"name":"tex_coords_gate_Y","uri":"data:application/octet-stream;base64,AACAPQAA4D4AAAAAAADgPgAAgD0AAAA/AAAAAAAA4D4AAAAAAAAAPwAAgD0AAAA/AACAPQAAAD8AAIA9AADgPgAAAAAAAAA/AAAAAAAAAD8AAIA9AADgPgAAAAAAAOA+"},{"byteLength":96,"name":"tex_coords_gate_Z","uri":"data:application/octet-stream;base64,AACAPQAAAD8AAAAAAAAAPwAAgD0AABA/AAAAAAAAAD8AAAAAAAAQPwAAgD0AABA/AACAPQAAED8AAIA9AAAAPwAAAAAAABA/AAAAAAAAED8AAIA9AAAAPwAAAAAAAAA/"},{"byteLength":96,"name":"tex_coords_gate_C_XYZ","uri":"data:application/octet-stream;base64,AAAAPgAAED8AAIA9AAAQPwAAAD4AACA/AACAPQAAED8AAIA9AAAgPwAAAD4AACA/AAAAPgAAID8AAAA+AAAQPwAAgD0AACA/AACAPQAAID8AAAA+AAAQPwAAgD0AABA/"},{"byteLength":96,"name":"tex_coords_gate_C_ZYX","uri":"data:application/octet-stream;base64,AABAPgAAED8AAAA+AAAQPwAAQD4AACA/AAAAPgAAED8AAAA+AAAgPwAAQD4AACA/AABAPgAAID8AAEA+AAAQPwAAAD4AACA/AAAAPgAAID8AAEA+AAAQPwAAAD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_H","uri":"data:application/octet-stream;base64,AAAAPgAA4D4AAIA9AADgPgAAAD4AAAA/AACAPQAA4D4AAIA9AAAAPwAAAD4AAAA/AAAAPgAAAD8AAAA+AADgPgAAgD0AAAA/AACAPQAAAD8AAAA+AADgPgAAgD0AAOA+"},{"byteLength":96,"name":"tex_coords_gate_H_XY","uri":"data:application/octet-stream;base64,AAAAPgAAAD8AAIA9AAAAPwAAAD4AABA/AACAPQAAAD8AAIA9AAAQPwAAAD4AABA/AAAAPgAAED8AAAA+AAAAPwAAgD0AABA/AACAPQAAED8AAAA+AAAAPwAAgD0AAAA/"},{"byteLength":96,"name":"tex_coords_gate_H_YZ","uri":"data:application/octet-stream;base64,AAAAPgAAwD4AAIA9AADAPgAAAD4AAOA+AACAPQAAwD4AAIA9AADgPgAAAD4AAOA+AAAAPgAA4D4AAAA+AADAPgAAgD0AAOA+AACAPQAA4D4AAAA+AADAPgAAgD0AAMA+"},{"byteLength":96,"name":"tex_coords_gate_S","uri":"data:application/octet-stream;base64,AABAPgAAAD8AAAA+AAAAPwAAQD4AABA/AAAAPgAAAD8AAAA+AAAQPwAAQD4AABA/AABAPgAAED8AAEA+AAAAPwAAAD4AABA/AAAAPgAAED8AAEA+AAAAPwAAAD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_X","uri":"data:application/octet-stream;base64,AABAPgAAwD4AAAA+AADAPgAAQD4AAOA+AAAAPgAAwD4AAAA+AADgPgAAQD4AAOA+AABAPgAA4D4AAEA+AADAPgAAAD4AAOA+AAAAPgAA4D4AAEA+AADAPgAAAD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_X_DAG","uri":"data:application/octet-stream;base64,AACAPgAAwD4AAEA+AADAPgAAgD4AAOA+AABAPgAAwD4AAEA+AADgPgAAgD4AAOA+AACAPgAA4D4AAIA+AADAPgAAQD4AAOA+AABAPgAA4D4AAIA+AADAPgAAQD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_Y","uri":"data:application/octet-stream;base64,AABAPgAA4D4AAAA+AADgPgAAQD4AAAA/AAAAPgAA4D4AAAA+AAAAPwAAQD4AAAA/AABAPgAAAD8AAEA+AADgPgAAAD4AAAA/AAAAPgAAAD8AAEA+AADgPgAAAD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_Y_DAG","uri":"data:application/octet-stream;base64,AACAPgAA4D4AAEA+AADgPgAAgD4AAAA/AABAPgAA4D4AAEA+AAAAPwAAgD4AAAA/AACAPgAAAD8AAIA+AADgPgAAQD4AAAA/AABAPgAAAD8AAIA+AADgPgAAQD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_S_DAG","uri":"data:application/octet-stream;base64,AACAPgAAAD8AAEA+AAAAPwAAgD4AABA/AABAPgAAAD8AAEA+AAAQPwAAgD4AABA/AACAPgAAED8AAIA+AAAAPwAAQD4AABA/AABAPgAAED8AAIA+AAAAPwAAQD4AAAA/"},{"byteLength":96,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAPwAAAIAAAACAAADAwAAAAIAAAACAAACAPwAAAMAAAACAAADAwAAAAMAAAACAAACAPwAAgMAAAACAAADAwAAAgMAAAACAAACAPwAAwMAAAACAAADAwAAAwMAAAACA"},{"byteLength":72,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACA"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":4},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":5},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":6},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":7},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":8},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":9},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":10},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":11},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":12},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":13},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":14},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":15},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":16},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":17},"material":2,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":1,"translation":[-0,-2,-0]},{"mesh":2,"translation":[-0,-4,-0]},{"mesh":3,"translation":[-0,-6,-0]},{"mesh":4,"translation":[-1,-0,-0]},{"mesh":5,"translation":[-1,-2,-0]},{"mesh":6,"translation":[-1,-4,-0]},{"mesh":7,"translation":[-1,-6,-0]},{"mesh":6,"translation":[-2,-0,-0]},{"mesh":8,"translation":[-2,-2,-0]},{"mesh":9,"translation":[-2,-4,-0]},{"mesh":10,"translation":[-2,-6,-0]},{"mesh":11,"translation":[-3,-0,-0]},{"mesh":12,"translation":[-3,-2,-0]},{"mesh":13,"translation":[-3,-4,-0]},{"mesh":9,"translation":[-3,-6,-0]},{"mesh":14,"translation":[-4,-0,-0]},{"mesh":14,"translation":[-4,-2,-0]},{"mesh":6,"translation":[-4,-4,-0]},{"mesh":6,"translation":[-5,-0,-0]},{"mesh":6,"translation":[-5,-6,-0]},{"mesh":15,"translation":[0,0,0]},{"mesh":16,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/surface_code.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5625],"min":[0.3125,0.5],"name":"tex_coords_gate_R","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5],"min":[0.0625,0.4375],"name":"tex_coords_gate_H","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"control_x_line_cross","type":"VEC3"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.5625],"min":[0.375,0.5],"name":"tex_coords_gate_MR","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5625],"min":[0.25,0.5],"name":"tex_coords_gate_M","type":"VEC2"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":210,"max":[1,-32,-32],"min":[-17,-40,-40],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":9,"byteOffset":0,"componentType":5126,"count":30,"max":[0,-29.5,-31],"min":[-15.25,-41,-41],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_R","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H","target":34962},{"buffer":3,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":4,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":5,"byteLength":48,"byteOffset":0,"name":"control_x_line_cross","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MR","target":34962},{"buffer":7,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_M","target":34962},{"buffer":8,"byteLength":2520,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":9,"byteLength":360,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_R","uri":"data:application/octet-stream;base64,AADAPgAAAD8AAKA+AAAAPwAAwD4AABA/AACgPgAAAD8AAKA+AAAQPwAAwD4AABA/AADAPgAAED8AAMA+AAAAPwAAoD4AABA/AACgPgAAED8AAMA+AAAAPwAAoD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_H","uri":"data:application/octet-stream;base64,AAAAPgAA4D4AAIA9AADgPgAAAD4AAAA/AACAPQAA4D4AAIA9AAAAPwAAAD4AAAA/AAAAPgAAAD8AAAA+AADgPgAAgD0AAAA/AACAPQAAAD8AAAA+AADgPgAAgD0AAOA+"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_x_line_cross","uri":"data:application/octet-stream;base64,AAAAAM3MzL4AAAAAAAAAAM3MzD4AAAAAAAAAAAAAAADNzMy+AAAAAAAAAADNzMw+"},{"byteLength":96,"name":"tex_coords_gate_MR","uri":"data:application/octet-stream;base64,AADgPgAAAD8AAMA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+AAAQPwAA4D4AABA/AADgPgAAED8AAOA+AAAAPwAAwD4AABA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_M","uri":"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/"},{"byteLength":2520,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AAAAwAAACMIAAADCAAAAwAAAEMIAAADCAAAAwAAACMIAABDCAAAAwAAAEMIAABDCAAAAwAAACMIAACDCAAAAwAAAEMIAACDCAAAAwAAAGMIAAADCAAAAwAAAIMIAAADCAAAAwAAAGMIAABDCAAAAwAAAIMIAABDCAAAAwAAAGMIAACDCAAAAwAAAIMIAACDCAAAAwAAACMIAAAjCAAAAwAAAAMIAAAjCAAAAwAAACMIAABjCAAAAwAAAAMIAABjCAAAAwAAAGMIAAAjCAAAAwAAAEMIAAAjCAAAAwAAAGMIAABjCAAAAwAAAEMIAABjCAABAwAAACMIAAADCAABAwAAACMIAAAjCAABAwAAACMIAABDCAABAwAAACMIAABjCAABAwAAAGMIAAADCAABAwAAAGMIAAAjCAABAwAAAGMIAABDCAABAwAAAGMIAABjCAABAwAAAAMIAABDCAABAwAAAAMIAAAjCAABAwAAAAMIAACDCAABAwAAAAMIAABjCAABAwAAAEMIAABDCAABAwAAAEMIAAAjCAABAwAAAEMIAACDCAABAwAAAEMIAABjCAABAwAAAIMIAABDCAABAwAAAIMIAAAjCAABAwAAAIMIAACDCAABAwAAAIMIAABjCAACAwAAACMIAABDCAACAwAAACMIAAAjCAACAwAAACMIAACDCAACAwAAACMIAABjCAACAwAAAGMIAABDCAACAwAAAGMIAAAjCAACAwAAAGMIAACDCAACAwAAAGMIAABjCAACAwAAAAMIAAADCAACAwAAAAMIAAAjCAACAwAAAAMIAABDCAACAwAAAAMIAABjCAACAwAAAEMIAAADCAACAwAAAEMIAAAjCAACAwAAAEMIAABDCAACAwAAAEMIAABjCAACAwAAAIMIAAADCAACAwAAAIMIAAAjCAACAwAAAIMIAABDCAACAwAAAIMIAABjCAACgwAAACMIAAADCAACgwAAAAMIAAADCAACgwAAACMIAABDCAACgwAAAAMIAABDCAACgwAAACMIAACDCAACgwAAAAMIAACDCAACgwAAAGMIAAADCAACgwAAAEMIAAADCAACgwAAAGMIAABDCAACgwAAAEMIAABDCAACgwAAAGMIAACDCAACgwAAAEMIAACDCAACgwAAACMIAAAjCAACgwAAAEMIAAAjCAACgwAAACMIAABjCAACgwAAAEMIAABjCAACgwAAAGMIAAAjCAACgwAAAIMIAAAjCAACgwAAAGMIAABjCAACgwAAAIMIAABjCAAAgwQAACMIAAADCAAAgwQAAEMIAAADCAAAgwQAACMIAABDCAAAgwQAAEMIAABDCAAAgwQAACMIAACDCAAAgwQAAEMIAACDCAAAgwQAAGMIAAADCAAAgwQAAIMIAAADCAAAgwQAAGMIAABDCAAAgwQAAIMIAABDCAAAgwQAAGMIAACDCAAAgwQAAIMIAACDCAAAgwQAACMIAAAjCAAAgwQAAAMIAAAjCAAAgwQAACMIAABjCAAAgwQAAAMIAABjCAAAgwQAAGMIAAAjCAAAgwQAAEMIAAAjCAAAgwQAAGMIAABjCAAAgwQAAEMIAABjCAAAwwQAACMIAAADCAAAwwQAACMIAAAjCAAAwwQAACMIAABDCAAAwwQAACMIAABjCAAAwwQAAGMIAAADCAAAwwQAAGMIAAAjCAAAwwQAAGMIAABDCAAAwwQAAGMIAABjCAAAwwQAAAMIAABDCAAAwwQAAAMIAAAjCAAAwwQAAAMIAACDCAAAwwQAAAMIAABjCAAAwwQAAEMIAABDCAAAwwQAAEMIAAAjCAAAwwQAAEMIAACDCAAAwwQAAEMIAABjCAAAwwQAAIMIAABDCAAAwwQAAIMIAAAjCAAAwwQAAIMIAACDCAAAwwQAAIMIAABjCAABAwQAACMIAABDCAABAwQAACMIAAAjCAABAwQAACMIAACDCAABAwQAACMIAABjCAABAwQAAGMIAABDCAABAwQAAGMIAAAjCAABAwQAAGMIAACDCAABAwQAAGMIAABjCAABAwQAAAMIAAADCAABAwQAAAMIAAAjCAABAwQAAAMIAABDCAABAwQAAAMIAABjCAABAwQAAEMIAAADCAABAwQAAEMIAAAjCAABAwQAAEMIAABDCAABAwQAAEMIAABjCAABAwQAAIMIAAADCAABAwQAAIMIAAAjCAABAwQAAIMIAABDCAABAwQAAIMIAABjCAABQwQAACMIAAADCAABQwQAAAMIAAADCAABQwQAACMIAABDCAABQwQAAAMIAABDCAABQwQAACMIAACDCAABQwQAAAMIAACDCAABQwQAAGMIAAADCAABQwQAAEMIAAADCAABQwQAAGMIAABDCAABQwQAAEMIAABDCAABQwQAAGMIAACDCAABQwQAAEMIAACDCAABQwQAACMIAAAjCAABQwQAAEMIAAAjCAABQwQAACMIAABjCAABQwQAAEMIAABjCAABQwQAAGMIAAAjCAABQwQAAIMIAAAjCAABQwQAAGMIAABjCAABQwQAAIMIAABjCAACAPwAAAMIAAADCAACIwQAAAMIAAADCAACAPwAACMIAAADCAACIwQAACMIAAADCAACAPwAAEMIAAADCAACIwQAAEMIAAADCAACAPwAAGMIAAADCAACIwQAAGMIAAADCAACAPwAAIMIAAADCAACIwQAAIMIAAADCAACAPwAAAMIAAAjCAACIwQAAAMIAAAjCAACAPwAACMIAAAjCAACIwQAACMIAAAjCAACAPwAAEMIAAAjCAACIwQAAEMIAAAjCAACAPwAAGMIAAAjCAACIwQAAGMIAAAjCAACAPwAAIMIAAAjCAACIwQAAIMIAAAjCAACAPwAAAMIAABDCAACIwQAAAMIAABDCAACAPwAACMIAABDCAACIwQAACMIAABDCAACAPwAAEMIAABDCAACIwQAAEMIAABDCAACAPwAAGMIAABDCAACIwQAAGMIAABDCAACAPwAAIMIAABDCAACIwQAAIMIAABDCAACAPwAAAMIAABjCAACIwQAAAMIAABjCAACAPwAACMIAABjCAACIwQAACMIAABjCAACAPwAAEMIAABjCAACIwQAAEMIAABjCAACAPwAAGMIAABjCAACIwQAAGMIAABjCAACAPwAAIMIAABjCAACIwQAAIMIAABjCAACAPwAAAMIAACDCAACIwQAAAMIAACDCAACAPwAACMIAACDCAACIwQAACMIAACDCAACAPwAAEMIAACDCAACIwQAAEMIAACDCAACAPwAAGMIAACDCAACIwQAAGMIAACDCAACAPwAAIMIAACDCAACIwQAAIMIAACDC"},{"byteLength":360,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAA8MEAABDCAABAwAAA8MEAABDCAAAgwAAA9MEAABDCAABAwAAA8MEAABDCAAAgwAAA7MEAABDCAABAwAAA8MEAABDCAAD4wAAA+MEAAPjBAAD4wAAA+MEAACTCAAD4wAAA+MEAAPjBAAD4wAAAJMIAAPjBAAD4wAAA+MEAAPjBAAB0wQAA+MEAAPjBAAD4wAAA+MEAACTCAAD4wAAAJMIAACTCAAD4wAAA+MEAACTCAAB0wQAA+MEAACTCAAD4wAAAJMIAAPjBAAD4wAAAJMIAACTCAAD4wAAAJMIAAPjBAAB0wQAAJMIAAPjBAAD4wAAAJMIAACTCAAB0wQAAJMIAACTCAAB0wQAA+MEAAPjBAAB0wQAA+MEAACTCAAB0wQAA+MEAAPjBAAB0wQAAJMIAAPjBAAB0wQAA+MEAACTCAAB0wQAAJMIAACTCAAB0wQAAJMIAAPjBAAB0wQAAJMIAACTC"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":3},"material":1,"mode":6}]},{"primitives":[{"attributes":{"POSITION":4},"material":2,"mode":6},{"attributes":{"POSITION":4},"material":3,"mode":3},{"attributes":{"POSITION":5},"material":3,"mode":1}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":6},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":7},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":8},"material":4,"mode":1}]},{"primitives":[{"attributes":{"POSITION":9},"material":5,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-32,-32]},{"mesh":0,"translation":[-0,-36,-32]},{"mesh":0,"translation":[-0,-40,-32]},{"mesh":0,"translation":[-0,-34,-34]},{"mesh":0,"translation":[-0,-38,-34]},{"mesh":0,"translation":[-0,-32,-36]},{"mesh":0,"translation":[-0,-36,-36]},{"mesh":0,"translation":[-0,-40,-36]},{"mesh":0,"translation":[-0,-34,-38]},{"mesh":0,"translation":[-0,-38,-38]},{"mesh":0,"translation":[-0,-32,-40]},{"mesh":0,"translation":[-0,-36,-40]},{"mesh":0,"translation":[-0,-40,-40]},{"mesh":0,"translation":[-0,-34,-32]},{"mesh":0,"translation":[-0,-38,-32]},{"mesh":0,"translation":[-0,-32,-34]},{"mesh":0,"translation":[-0,-36,-34]},{"mesh":0,"translation":[-0,-40,-34]},{"mesh":0,"translation":[-0,-34,-36]},{"mesh":0,"translation":[-0,-38,-36]},{"mesh":0,"translation":[-0,-32,-38]},{"mesh":0,"translation":[-0,-36,-38]},{"mesh":0,"translation":[-0,-40,-38]},{"mesh":0,"translation":[-0,-34,-40]},{"mesh":0,"translation":[-0,-38,-40]},{"mesh":1,"translation":[-1,-34,-32]},{"mesh":1,"translation":[-1,-38,-32]},{"mesh":1,"translation":[-1,-34,-36]},{"mesh":1,"translation":[-1,-38,-36]},{"mesh":1,"translation":[-1,-34,-40]},{"mesh":1,"translation":[-1,-38,-40]},{"mesh":2,"translation":[-2,-34,-32]},{"mesh":3,"translation":[-2,-36,-32]},{"mesh":2,"translation":[-2,-34,-36]},{"mesh":3,"translation":[-2,-36,-36]},{"mesh":2,"translation":[-2,-34,-40]},{"mesh":3,"translation":[-2,-36,-40]},{"mesh":2,"translation":[-2,-38,-32]},{"mesh":3,"translation":[-2,-40,-32]},{"mesh":2,"translation":[-2,-38,-36]},{"mesh":3,"translation":[-2,-40,-36]},{"mesh":2,"translation":[-2,-38,-40]},{"mesh":3,"translation":[-2,-40,-40]},{"mesh":2,"translation":[-2,-34,-34]},{"mesh":3,"translation":[-2,-32,-34]},{"mesh":2,"translation":[-2,-34,-38]},{"mesh":3,"translation":[-2,-32,-38]},{"mesh":2,"translation":[-2,-38,-34]},{"mesh":3,"translation":[-2,-36,-34]},{"mesh":2,"translation":[-2,-38,-38]},{"mesh":3,"translation":[-2,-36,-38]},{"mesh":2,"translation":[-3,-34,-32]},{"mesh":3,"translation":[-3,-34,-34]},{"mesh":2,"translation":[-3,-34,-36]},{"mesh":3,"translation":[-3,-34,-38]},{"mesh":2,"translation":[-3,-38,-32]},{"mesh":3,"translation":[-3,-38,-34]},{"mesh":2,"translation":[-3,-38,-36]},{"mesh":3,"translation":[-3,-38,-38]},{"mesh":2,"translation":[-3,-32,-36]},{"mesh":3,"translation":[-3,-32,-34]},{"mesh":2,"translation":[-3,-32,-40]},{"mesh":3,"translation":[-3,-32,-38]},{"mesh":2,"translation":[-3,-36,-36]},{"mesh":3,"translation":[-3,-36,-34]},{"mesh":2,"translation":[-3,-36,-40]},{"mesh":3,"translation":[-3,-36,-38]},{"mesh":2,"translation":[-3,-40,-36]},{"mesh":3,"translation":[-3,-40,-34]},{"mesh":2,"translation":[-3,-40,-40]},{"mesh":3,"translation":[-3,-40,-38]},{"mesh":2,"translation":[-4,-34,-36]},{"mesh":3,"translation":[-4,-34,-34]},{"mesh":2,"translation":[-4,-34,-40]},{"mesh":3,"translation":[-4,-34,-38]},{"mesh":2,"translation":[-4,-38,-36]},{"mesh":3,"translation":[-4,-38,-34]},{"mesh":2,"translation":[-4,-38,-40]},{"mesh":3,"translation":[-4,-38,-38]},{"mesh":2,"translation":[-4,-32,-32]},{"mesh":3,"translation":[-4,-32,-34]},{"mesh":2,"translation":[-4,-32,-36]},{"mesh":3,"translation":[-4,-32,-38]},{"mesh":2,"translation":[-4,-36,-32]},{"mesh":3,"translation":[-4,-36,-34]},{"mesh":2,"translation":[-4,-36,-36]},{"mesh":3,"translation":[-4,-36,-38]},{"mesh":2,"translation":[-4,-40,-32]},{"mesh":3,"translation":[-4,-40,-34]},{"mesh":2,"translation":[-4,-40,-36]},{"mesh":3,"translation":[-4,-40,-38]},{"mesh":2,"translation":[-5,-34,-32]},{"mesh":3,"translation":[-5,-32,-32]},{"mesh":2,"translation":[-5,-34,-36]},{"mesh":3,"translation":[-5,-32,-36]},{"mesh":2,"translation":[-5,-34,-40]},{"mesh":3,"translation":[-5,-32,-40]},{"mesh":2,"translation":[-5,-38,-32]},{"mesh":3,"translation":[-5,-36,-32]},{"mesh":2,"translation":[-5,-38,-36]},{"mesh":3,"translation":[-5,-36,-36]},{"mesh":2,"translation":[-5,-38,-40]},{"mesh":3,"translation":[-5,-36,-40]},{"mesh":2,"translation":[-5,-34,-34]},{"mesh":3,"translation":[-5,-36,-34]},{"mesh":2,"translation":[-5,-34,-38]},{"mesh":3,"translation":[-5,-36,-38]},{"mesh":2,"translation":[-5,-38,-34]},{"mesh":3,"translation":[-5,-40,-34]},{"mesh":2,"translation":[-5,-38,-38]},{"mesh":3,"translation":[-5,-40,-38]},{"mesh":1,"translation":[-6,-34,-32]},{"mesh":1,"translation":[-6,-38,-32]},{"mesh":1,"translation":[-6,-34,-36]},{"mesh":1,"translation":[-6,-38,-36]},{"mesh":1,"translation":[-6,-34,-40]},{"mesh":1,"translation":[-6,-38,-40]},{"mesh":4,"translation":[-7,-34,-32]},{"mesh":4,"translation":[-7,-38,-32]},{"mesh":4,"translation":[-7,-32,-34]},{"mesh":4,"translation":[-7,-36,-34]},{"mesh":4,"translation":[-7,-40,-34]},{"mesh":4,"translation":[-7,-34,-36]},{"mesh":4,"translation":[-7,-38,-36]},{"mesh":4,"translation":[-7,-32,-38]},{"mesh":4,"translation":[-7,-36,-38]},{"mesh":4,"translation":[-7,-40,-38]},{"mesh":4,"translation":[-7,-34,-40]},{"mesh":4,"translation":[-7,-38,-40]},{"mesh":1,"translation":[-9,-34,-32]},{"mesh":1,"translation":[-9,-38,-32]},{"mesh":1,"translation":[-9,-34,-36]},{"mesh":1,"translation":[-9,-38,-36]},{"mesh":1,"translation":[-9,-34,-40]},{"mesh":1,"translation":[-9,-38,-40]},{"mesh":2,"translation":[-10,-34,-32]},{"mesh":3,"translation":[-10,-36,-32]},{"mesh":2,"translation":[-10,-34,-36]},{"mesh":3,"translation":[-10,-36,-36]},{"mesh":2,"translation":[-10,-34,-40]},{"mesh":3,"translation":[-10,-36,-40]},{"mesh":2,"translation":[-10,-38,-32]},{"mesh":3,"translation":[-10,-40,-32]},{"mesh":2,"translation":[-10,-38,-36]},{"mesh":3,"translation":[-10,-40,-36]},{"mesh":2,"translation":[-10,-38,-40]},{"mesh":3,"translation":[-10,-40,-40]},{"mesh":2,"translation":[-10,-34,-34]},{"mesh":3,"translation":[-10,-32,-34]},{"mesh":2,"translation":[-10,-34,-38]},{"mesh":3,"translation":[-10,-32,-38]},{"mesh":2,"translation":[-10,-38,-34]},{"mesh":3,"translation":[-10,-36,-34]},{"mesh":2,"translation":[-10,-38,-38]},{"mesh":3,"translation":[-10,-36,-38]},{"mesh":2,"translation":[-11,-34,-32]},{"mesh":3,"translation":[-11,-34,-34]},{"mesh":2,"translation":[-11,-34,-36]},{"mesh":3,"translation":[-11,-34,-38]},{"mesh":2,"translation":[-11,-38,-32]},{"mesh":3,"translation":[-11,-38,-34]},{"mesh":2,"translation":[-11,-38,-36]},{"mesh":3,"translation":[-11,-38,-38]},{"mesh":2,"translation":[-11,-32,-36]},{"mesh":3,"translation":[-11,-32,-34]},{"mesh":2,"translation":[-11,-32,-40]},{"mesh":3,"translation":[-11,-32,-38]},{"mesh":2,"translation":[-11,-36,-36]},{"mesh":3,"translation":[-11,-36,-34]},{"mesh":2,"translation":[-11,-36,-40]},{"mesh":3,"translation":[-11,-36,-38]},{"mesh":2,"translation":[-11,-40,-36]},{"mesh":3,"translation":[-11,-40,-34]},{"mesh":2,"translation":[-11,-40,-40]},{"mesh":3,"translation":[-11,-40,-38]},{"mesh":2,"translation":[-12,-34,-36]},{"mesh":3,"translation":[-12,-34,-34]},{"mesh":2,"translation":[-12,-34,-40]},{"mesh":3,"translation":[-12,-34,-38]},{"mesh":2,"translation":[-12,-38,-36]},{"mesh":3,"translation":[-12,-38,-34]},{"mesh":2,"translation":[-12,-38,-40]},{"mesh":3,"translation":[-12,-38,-38]},{"mesh":2,"translation":[-12,-32,-32]},{"mesh":3,"translation":[-12,-32,-34]},{"mesh":2,"translation":[-12,-32,-36]},{"mesh":3,"translation":[-12,-32,-38]},{"mesh":2,"translation":[-12,-36,-32]},{"mesh":3,"translation":[-12,-36,-34]},{"mesh":2,"translation":[-12,-36,-36]},{"mesh":3,"translation":[-12,-36,-38]},{"mesh":2,"translation":[-12,-40,-32]},{"mesh":3,"translation":[-12,-40,-34]},{"mesh":2,"translation":[-12,-40,-36]},{"mesh":3,"translation":[-12,-40,-38]},{"mesh":2,"translation":[-13,-34,-32]},{"mesh":3,"translation":[-13,-32,-32]},{"mesh":2,"translation":[-13,-34,-36]},{"mesh":3,"translation":[-13,-32,-36]},{"mesh":2,"translation":[-13,-34,-40]},{"mesh":3,"translation":[-13,-32,-40]},{"mesh":2,"translation":[-13,-38,-32]},{"mesh":3,"translation":[-13,-36,-32]},{"mesh":2,"translation":[-13,-38,-36]},{"mesh":3,"translation":[-13,-36,-36]},{"mesh":2,"translation":[-13,-38,-40]},{"mesh":3,"translation":[-13,-36,-40]},{"mesh":2,"translation":[-13,-34,-34]},{"mesh":3,"translation":[-13,-36,-34]},{"mesh":2,"translation":[-13,-34,-38]},{"mesh":3,"translation":[-13,-36,-38]},{"mesh":2,"translation":[-13,-38,-34]},{"mesh":3,"translation":[-13,-40,-34]},{"mesh":2,"translation":[-13,-38,-38]},{"mesh":3,"translation":[-13,-40,-38]},{"mesh":1,"translation":[-14,-34,-32]},{"mesh":1,"translation":[-14,-38,-32]},{"mesh":1,"translation":[-14,-34,-36]},{"mesh":1,"translation":[-14,-38,-36]},{"mesh":1,"translation":[-14,-34,-40]},{"mesh":1,"translation":[-14,-38,-40]},{"mesh":4,"translation":[-15,-34,-32]},{"mesh":4,"translation":[-15,-38,-32]},{"mesh":4,"translation":[-15,-32,-34]},{"mesh":4,"translation":[-15,-36,-34]},{"mesh":4,"translation":[-15,-40,-34]},{"mesh":4,"translation":[-15,-34,-36]},{"mesh":4,"translation":[-15,-38,-36]},{"mesh":4,"translation":[-15,-32,-38]},{"mesh":4,"translation":[-15,-36,-38]},{"mesh":4,"translation":[-15,-40,-38]},{"mesh":4,"translation":[-15,-34,-40]},{"mesh":4,"translation":[-15,-38,-40]},{"mesh":5,"translation":[-16,-32,-32]},{"mesh":5,"translation":[-16,-36,-32]},{"mesh":5,"translation":[-16,-40,-32]},{"mesh":5,"translation":[-16,-34,-34]},{"mesh":5,"translation":[-16,-38,-34]},{"mesh":5,"translation":[-16,-32,-36]},{"mesh":5,"translation":[-16,-36,-36]},{"mesh":5,"translation":[-16,-40,-36]},{"mesh":5,"translation":[-16,-34,-38]},{"mesh":5,"translation":[-16,-38,-38]},{"mesh":5,"translation":[-16,-32,-40]},{"mesh":5,"translation":[-16,-36,-40]},{"mesh":5,"translation":[-16,-40,-40]},{"mesh":6,"translation":[0,0,0]},{"mesh":7,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/test_circuit_all_ops.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.4375],"min":[0,0.375],"name":"tex_coords_gate_I","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.4375],"min":[0,0.375],"name":"tex_coords_gate_X","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.5],"min":[0,0.4375],"name":"tex_coords_gate_Y","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[0.0625,0.5625],"min":[0,0.5],"name":"tex_coords_gate_Z","type":"VEC2"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.625],"min":[0.0625,0.5625],"name":"tex_coords_gate_C_XYZ","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.625],"min":[0.125,0.5625],"name":"tex_coords_gate_C_ZYX","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5625],"min":[0.0625,0.5],"name":"tex_coords_gate_H_XY","type":"VEC2"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5],"min":[0.0625,0.4375],"name":"tex_coords_gate_H","type":"VEC2"},{"bufferView":9,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.4375],"min":[0.0625,0.375],"name":"tex_coords_gate_H_YZ","type":"VEC2"},{"bufferView":10,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.4375],"min":[0.125,0.375],"name":"tex_coords_gate_SQRT_X","type":"VEC2"},{"bufferView":11,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.4375],"min":[0.1875,0.375],"name":"tex_coords_gate_SQRT_X_DAG","type":"VEC2"},{"bufferView":12,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.5],"min":[0.125,0.4375],"name":"tex_coords_gate_SQRT_Y","type":"VEC2"},{"bufferView":13,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.5],"min":[0.1875,0.4375],"name":"tex_coords_gate_SQRT_Y_DAG","type":"VEC2"},{"bufferView":14,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.5625],"min":[0.125,0.5],"name":"tex_coords_gate_S","type":"VEC2"},{"bufferView":15,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.5625],"min":[0.1875,0.5],"name":"tex_coords_gate_S_DAG","type":"VEC2"},{"bufferView":16,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":17,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.45254835486412,0.45254835486412],"min":[0,-0.45254835486412,-0.45254835486412],"name":"control_zswap_line_cross","type":"VEC3"},{"bufferView":18,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":19,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.45254835486412,0.45254835486412],"min":[0,-0.45254835486412,-0.45254835486412],"name":"control_xswap_line_cross","type":"VEC3"},{"bufferView":20,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.625],"min":[0.3125,0.5625],"name":"tex_coords_gate_ISWAP","type":"VEC2"},{"bufferView":21,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.625],"min":[0.375,0.5625],"name":"tex_coords_gate_ISWAP_DAG","type":"VEC2"},{"bufferView":22,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.625],"min":[0.4375,0.5625],"name":"tex_coords_gate_SWAP","type":"VEC2"},{"bufferView":23,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.4375],"min":[0.6875,0.375],"name":"tex_coords_gate_SQRT_XX","type":"VEC2"},{"bufferView":24,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.4375],"min":[0.75,0.375],"name":"tex_coords_gate_SQRT_XX_DAG","type":"VEC2"},{"bufferView":25,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.5],"min":[0.6875,0.4375],"name":"tex_coords_gate_SQRT_YY","type":"VEC2"},{"bufferView":26,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.5],"min":[0.75,0.4375],"name":"tex_coords_gate_SQRT_YY_DAG","type":"VEC2"},{"bufferView":27,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.5625],"min":[0.6875,0.5],"name":"tex_coords_gate_SQRT_ZZ","type":"VEC2"},{"bufferView":28,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.5625],"min":[0.75,0.5],"name":"tex_coords_gate_SQRT_ZZ_DAG","type":"VEC2"},{"bufferView":29,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":30,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"control_x_line_cross","type":"VEC3"},{"bufferView":31,"byteOffset":0,"componentType":5126,"count":3,"max":[0,0.400000005960464,0.346410155296326],"min":[0,-0.200000032782555,-0.346410185098648],"name":"circle_loop","type":"VEC3"},{"bufferView":32,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":33,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.4375],"min":[0.5,0.375],"name":"tex_coords_gate_E:X","type":"VEC2"},{"bufferView":34,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.5],"min":[0.5,0.4375],"name":"tex_coords_gate_E:Y","type":"VEC2"},{"bufferView":35,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.5625],"min":[0.5,0.5],"name":"tex_coords_gate_E:Z","type":"VEC2"},{"bufferView":36,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.4375],"min":[0.5625,0.375],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","type":"VEC2"},{"bufferView":37,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.5],"min":[0.5625,0.4375],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","type":"VEC2"},{"bufferView":38,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.5625],"min":[0.5625,0.5],"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","type":"VEC2"},{"bufferView":39,"byteOffset":0,"componentType":5126,"count":12,"max":[0.25,0.625],"min":[0.1875,0.5625],"name":"tex_coords_gate_DEPOLARIZE1","type":"VEC2"},{"bufferView":40,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.625],"min":[0.25,0.5625],"name":"tex_coords_gate_DEPOLARIZE2","type":"VEC2"},{"bufferView":41,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5625,0.625],"min":[0.5,0.5625],"name":"tex_coords_gate_PAULI_CHANNEL_1","type":"VEC2"},{"bufferView":42,"byteOffset":0,"componentType":5126,"count":12,"max":[0.625,0.625],"min":[0.5625,0.5625],"name":"tex_coords_gate_PAULI_CHANNEL_2","type":"VEC2"},{"bufferView":43,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.4375],"min":[0.4375,0.375],"name":"tex_coords_gate_X_ERROR","type":"VEC2"},{"bufferView":44,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.5],"min":[0.4375,0.4375],"name":"tex_coords_gate_Y_ERROR","type":"VEC2"},{"bufferView":45,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.5625],"min":[0.4375,0.5],"name":"tex_coords_gate_Z_ERROR","type":"VEC2"},{"bufferView":46,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.4375],"min":[0.625,0.375],"name":"tex_coords_gate_MPP:X","type":"VEC2"},{"bufferView":47,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.5],"min":[0.625,0.4375],"name":"tex_coords_gate_MPP:Y","type":"VEC2"},{"bufferView":48,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.5625],"min":[0.625,0.5],"name":"tex_coords_gate_MPP:Z","type":"VEC2"},{"bufferView":49,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.4375],"min":[0.375,0.375],"name":"tex_coords_gate_MRX","type":"VEC2"},{"bufferView":50,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.5],"min":[0.375,0.4375],"name":"tex_coords_gate_MRY","type":"VEC2"},{"bufferView":51,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.5625],"min":[0.375,0.5],"name":"tex_coords_gate_MR","type":"VEC2"},{"bufferView":52,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.4375],"min":[0.25,0.375],"name":"tex_coords_gate_MX","type":"VEC2"},{"bufferView":53,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5],"min":[0.25,0.4375],"name":"tex_coords_gate_MY","type":"VEC2"},{"bufferView":54,"byteOffset":0,"componentType":5126,"count":12,"max":[0.3125,0.5625],"min":[0.25,0.5],"name":"tex_coords_gate_M","type":"VEC2"},{"bufferView":55,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.4375],"min":[0.3125,0.375],"name":"tex_coords_gate_RX","type":"VEC2"},{"bufferView":56,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5],"min":[0.3125,0.4375],"name":"tex_coords_gate_RY","type":"VEC2"},{"bufferView":57,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.5625],"min":[0.3125,0.5],"name":"tex_coords_gate_R","type":"VEC2"},{"bufferView":58,"byteOffset":0,"componentType":5126,"count":12,"max":[0.6875,0.625],"min":[0.625,0.5625],"name":"tex_coords_gate_MXX","type":"VEC2"},{"bufferView":59,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.625],"min":[0.6875,0.5625],"name":"tex_coords_gate_MYY","type":"VEC2"},{"bufferView":60,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.625],"min":[0.75,0.5625],"name":"tex_coords_gate_MZZ","type":"VEC2"},{"bufferView":61,"byteOffset":0,"componentType":5126,"count":12,"max":[0.875,0.625],"min":[0.8125,0.5625],"name":"tex_coords_gate_MPAD","type":"VEC2"},{"bufferView":62,"byteOffset":0,"componentType":5126,"count":120,"max":[1,-2,-0],"min":[-25,-34,-32],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":63,"byteOffset":0,"componentType":5126,"count":30,"max":[0,0.5,1],"min":[-17.25,-35,-33],"name":"buf_red_scattered_lines","type":"VEC3"},{"bufferView":64,"byteOffset":0,"componentType":5126,"count":96,"max":[-0.75,-1.20000004768372,0.5],"min":[-23.25,-1.60000002384186,-32.5],"name":"buf_blue_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_I","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Y","target":34962},{"buffer":4,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Z","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_XYZ","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_C_ZYX","target":34962},{"buffer":7,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_XY","target":34962},{"buffer":8,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H","target":34962},{"buffer":9,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H_YZ","target":34962},{"buffer":10,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_X","target":34962},{"buffer":11,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_X_DAG","target":34962},{"buffer":12,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_Y","target":34962},{"buffer":13,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_Y_DAG","target":34962},{"buffer":14,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_S","target":34962},{"buffer":15,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_S_DAG","target":34962},{"buffer":16,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":17,"byteLength":48,"byteOffset":0,"name":"control_zswap_line_cross","target":34962},{"buffer":18,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":19,"byteLength":48,"byteOffset":0,"name":"control_xswap_line_cross","target":34962},{"buffer":20,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ISWAP","target":34962},{"buffer":21,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ISWAP_DAG","target":34962},{"buffer":22,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SWAP","target":34962},{"buffer":23,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_XX","target":34962},{"buffer":24,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_XX_DAG","target":34962},{"buffer":25,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_YY","target":34962},{"buffer":26,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_YY_DAG","target":34962},{"buffer":27,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_ZZ","target":34962},{"buffer":28,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_ZZ_DAG","target":34962},{"buffer":29,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":30,"byteLength":48,"byteOffset":0,"name":"control_x_line_cross","target":34962},{"buffer":31,"byteLength":36,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":32,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":33,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:X","target":34962},{"buffer":34,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:Y","target":34962},{"buffer":35,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_E:Z","target":34962},{"buffer":36,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","target":34962},{"buffer":37,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","target":34962},{"buffer":38,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","target":34962},{"buffer":39,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_DEPOLARIZE1","target":34962},{"buffer":40,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_DEPOLARIZE2","target":34962},{"buffer":41,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_PAULI_CHANNEL_1","target":34962},{"buffer":42,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_PAULI_CHANNEL_2","target":34962},{"buffer":43,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_X_ERROR","target":34962},{"buffer":44,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Y_ERROR","target":34962},{"buffer":45,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_Z_ERROR","target":34962},{"buffer":46,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:X","target":34962},{"buffer":47,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:Y","target":34962},{"buffer":48,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPP:Z","target":34962},{"buffer":49,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MRX","target":34962},{"buffer":50,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MRY","target":34962},{"buffer":51,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MR","target":34962},{"buffer":52,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MX","target":34962},{"buffer":53,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MY","target":34962},{"buffer":54,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_M","target":34962},{"buffer":55,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_RX","target":34962},{"buffer":56,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_RY","target":34962},{"buffer":57,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_R","target":34962},{"buffer":58,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MXX","target":34962},{"buffer":59,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MYY","target":34962},{"buffer":60,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MZZ","target":34962},{"buffer":61,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_MPAD","target":34962},{"buffer":62,"byteLength":1440,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":63,"byteLength":360,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962},{"buffer":64,"byteLength":1152,"byteOffset":0,"name":"buf_blue_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_I","uri":"data:application/octet-stream;base64,AACAPQAAwD4AAAAAAADAPgAAgD0AAOA+AAAAAAAAwD4AAAAAAADgPgAAgD0AAOA+AACAPQAA4D4AAIA9AADAPgAAAAAAAOA+AAAAAAAA4D4AAIA9AADAPgAAAAAAAMA+"},{"byteLength":96,"name":"tex_coords_gate_X","uri":"data:application/octet-stream;base64,AACAPQAAwD4AAAAAAADAPgAAgD0AAOA+AAAAAAAAwD4AAAAAAADgPgAAgD0AAOA+AACAPQAA4D4AAIA9AADAPgAAAAAAAOA+AAAAAAAA4D4AAIA9AADAPgAAAAAAAMA+"},{"byteLength":96,"name":"tex_coords_gate_Y","uri":"data:application/octet-stream;base64,AACAPQAA4D4AAAAAAADgPgAAgD0AAAA/AAAAAAAA4D4AAAAAAAAAPwAAgD0AAAA/AACAPQAAAD8AAIA9AADgPgAAAAAAAAA/AAAAAAAAAD8AAIA9AADgPgAAAAAAAOA+"},{"byteLength":96,"name":"tex_coords_gate_Z","uri":"data:application/octet-stream;base64,AACAPQAAAD8AAAAAAAAAPwAAgD0AABA/AAAAAAAAAD8AAAAAAAAQPwAAgD0AABA/AACAPQAAED8AAIA9AAAAPwAAAAAAABA/AAAAAAAAED8AAIA9AAAAPwAAAAAAAAA/"},{"byteLength":96,"name":"tex_coords_gate_C_XYZ","uri":"data:application/octet-stream;base64,AAAAPgAAED8AAIA9AAAQPwAAAD4AACA/AACAPQAAED8AAIA9AAAgPwAAAD4AACA/AAAAPgAAID8AAAA+AAAQPwAAgD0AACA/AACAPQAAID8AAAA+AAAQPwAAgD0AABA/"},{"byteLength":96,"name":"tex_coords_gate_C_ZYX","uri":"data:application/octet-stream;base64,AABAPgAAED8AAAA+AAAQPwAAQD4AACA/AAAAPgAAED8AAAA+AAAgPwAAQD4AACA/AABAPgAAID8AAEA+AAAQPwAAAD4AACA/AAAAPgAAID8AAEA+AAAQPwAAAD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_H_XY","uri":"data:application/octet-stream;base64,AAAAPgAAAD8AAIA9AAAAPwAAAD4AABA/AACAPQAAAD8AAIA9AAAQPwAAAD4AABA/AAAAPgAAED8AAAA+AAAAPwAAgD0AABA/AACAPQAAED8AAAA+AAAAPwAAgD0AAAA/"},{"byteLength":96,"name":"tex_coords_gate_H","uri":"data:application/octet-stream;base64,AAAAPgAA4D4AAIA9AADgPgAAAD4AAAA/AACAPQAA4D4AAIA9AAAAPwAAAD4AAAA/AAAAPgAAAD8AAAA+AADgPgAAgD0AAAA/AACAPQAAAD8AAAA+AADgPgAAgD0AAOA+"},{"byteLength":96,"name":"tex_coords_gate_H_YZ","uri":"data:application/octet-stream;base64,AAAAPgAAwD4AAIA9AADAPgAAAD4AAOA+AACAPQAAwD4AAIA9AADgPgAAAD4AAOA+AAAAPgAA4D4AAAA+AADAPgAAgD0AAOA+AACAPQAA4D4AAAA+AADAPgAAgD0AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_X","uri":"data:application/octet-stream;base64,AABAPgAAwD4AAAA+AADAPgAAQD4AAOA+AAAAPgAAwD4AAAA+AADgPgAAQD4AAOA+AABAPgAA4D4AAEA+AADAPgAAAD4AAOA+AAAAPgAA4D4AAEA+AADAPgAAAD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_X_DAG","uri":"data:application/octet-stream;base64,AACAPgAAwD4AAEA+AADAPgAAgD4AAOA+AABAPgAAwD4AAEA+AADgPgAAgD4AAOA+AACAPgAA4D4AAIA+AADAPgAAQD4AAOA+AABAPgAA4D4AAIA+AADAPgAAQD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_Y","uri":"data:application/octet-stream;base64,AABAPgAA4D4AAAA+AADgPgAAQD4AAAA/AAAAPgAA4D4AAAA+AAAAPwAAQD4AAAA/AABAPgAAAD8AAEA+AADgPgAAAD4AAAA/AAAAPgAAAD8AAEA+AADgPgAAAD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_Y_DAG","uri":"data:application/octet-stream;base64,AACAPgAA4D4AAEA+AADgPgAAgD4AAAA/AABAPgAA4D4AAEA+AAAAPwAAgD4AAAA/AACAPgAAAD8AAIA+AADgPgAAQD4AAAA/AABAPgAAAD8AAIA+AADgPgAAQD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_S","uri":"data:application/octet-stream;base64,AABAPgAAAD8AAAA+AAAAPwAAQD4AABA/AAAAPgAAAD8AAAA+AAAQPwAAQD4AABA/AABAPgAAED8AAEA+AAAAPwAAAD4AABA/AAAAPgAAED8AAEA+AAAAPwAAAD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_S_DAG","uri":"data:application/octet-stream;base64,AACAPgAAAD8AAEA+AAAAPwAAgD4AABA/AABAPgAAAD8AAEA+AAAQPwAAgD4AABA/AACAPgAAED8AAIA+AAAAPwAAQD4AABA/AABAPgAAED8AAIA+AAAAPwAAQD4AAAA/"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_zswap_line_cross","uri":"data:application/octet-stream;base64,AAAAAGu0575rtOe+AAAAAGu05z5rtOc+AAAAAGu0575rtOc+AAAAAGu05z5rtOe+"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_xswap_line_cross","uri":"data:application/octet-stream;base64,AAAAAGu0575rtOe+AAAAAGu05z5rtOc+AAAAAGu0575rtOc+AAAAAGu05z5rtOe+"},{"byteLength":96,"name":"tex_coords_gate_ISWAP","uri":"data:application/octet-stream;base64,AADAPgAAED8AAKA+AAAQPwAAwD4AACA/AACgPgAAED8AAKA+AAAgPwAAwD4AACA/AADAPgAAID8AAMA+AAAQPwAAoD4AACA/AACgPgAAID8AAMA+AAAQPwAAoD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_ISWAP_DAG","uri":"data:application/octet-stream;base64,AADgPgAAED8AAMA+AAAQPwAA4D4AACA/AADAPgAAED8AAMA+AAAgPwAA4D4AACA/AADgPgAAID8AAOA+AAAQPwAAwD4AACA/AADAPgAAID8AAOA+AAAQPwAAwD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_SWAP","uri":"data:application/octet-stream;base64,AAAAPwAAED8AAOA+AAAQPwAAAD8AACA/AADgPgAAED8AAOA+AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAAQPwAA4D4AACA/AADgPgAAID8AAAA/AAAQPwAA4D4AABA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_XX","uri":"data:application/octet-stream;base64,AABAPwAAwD4AADA/AADAPgAAQD8AAOA+AAAwPwAAwD4AADA/AADgPgAAQD8AAOA+AABAPwAA4D4AAEA/AADAPgAAMD8AAOA+AAAwPwAA4D4AAEA/AADAPgAAMD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_XX_DAG","uri":"data:application/octet-stream;base64,AABQPwAAwD4AAEA/AADAPgAAUD8AAOA+AABAPwAAwD4AAEA/AADgPgAAUD8AAOA+AABQPwAA4D4AAFA/AADAPgAAQD8AAOA+AABAPwAA4D4AAFA/AADAPgAAQD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_YY","uri":"data:application/octet-stream;base64,AABAPwAA4D4AADA/AADgPgAAQD8AAAA/AAAwPwAA4D4AADA/AAAAPwAAQD8AAAA/AABAPwAAAD8AAEA/AADgPgAAMD8AAAA/AAAwPwAAAD8AAEA/AADgPgAAMD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_YY_DAG","uri":"data:application/octet-stream;base64,AABQPwAA4D4AAEA/AADgPgAAUD8AAAA/AABAPwAA4D4AAEA/AAAAPwAAUD8AAAA/AABQPwAAAD8AAFA/AADgPgAAQD8AAAA/AABAPwAAAD8AAFA/AADgPgAAQD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_ZZ","uri":"data:application/octet-stream;base64,AABAPwAAAD8AADA/AAAAPwAAQD8AABA/AAAwPwAAAD8AADA/AAAQPwAAQD8AABA/AABAPwAAED8AAEA/AAAAPwAAMD8AABA/AAAwPwAAED8AAEA/AAAAPwAAMD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_ZZ_DAG","uri":"data:application/octet-stream;base64,AABQPwAAAD8AAEA/AAAAPwAAUD8AABA/AABAPwAAAD8AAEA/AAAQPwAAUD8AABA/AABQPwAAED8AAFA/AAAAPwAAQD8AABA/AABAPwAAED8AAFA/AAAAPwAAQD8AAAA/"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_x_line_cross","uri":"data:application/octet-stream;base64,AAAAAM3MzL4AAAAAAAAAAM3MzD4AAAAAAAAAAAAAAADNzMy+AAAAAAAAAADNzMw+"},{"byteLength":36,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAM/MTL6sXLE+AAAAAMvMTL6tXLG+"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":96,"name":"tex_coords_gate_E:X","uri":"data:application/octet-stream;base64,AAAQPwAAwD4AAAA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/AADgPgAAED8AAOA+AAAQPwAA4D4AABA/AADAPgAAAD8AAOA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_E:Y","uri":"data:application/octet-stream;base64,AAAQPwAA4D4AAAA/AADgPgAAED8AAAA/AAAAPwAA4D4AAAA/AAAAPwAAED8AAAA/AAAQPwAAAD8AABA/AADgPgAAAD8AAAA/AAAAPwAAAD8AABA/AADgPgAAAD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_E:Z","uri":"data:application/octet-stream;base64,AAAQPwAAAD8AAAA/AAAAPwAAED8AABA/AAAAPwAAAD8AAAA/AAAQPwAAED8AABA/AAAQPwAAED8AABA/AAAAPwAAAD8AABA/AAAAPwAAED8AABA/AAAAPwAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:X","uri":"data:application/octet-stream;base64,AAAgPwAAwD4AABA/AADAPgAAID8AAOA+AAAQPwAAwD4AABA/AADgPgAAID8AAOA+AAAgPwAA4D4AACA/AADAPgAAED8AAOA+AAAQPwAA4D4AACA/AADAPgAAED8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Y","uri":"data:application/octet-stream;base64,AAAgPwAA4D4AABA/AADgPgAAID8AAAA/AAAQPwAA4D4AABA/AAAAPwAAID8AAAA/AAAgPwAAAD8AACA/AADgPgAAED8AAAA/AAAQPwAAAD8AACA/AADgPgAAED8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_ELSE_CORRELATED_ERROR:Z","uri":"data:application/octet-stream;base64,AAAgPwAAAD8AABA/AAAAPwAAID8AABA/AAAQPwAAAD8AABA/AAAQPwAAID8AABA/AAAgPwAAED8AACA/AAAAPwAAED8AABA/AAAQPwAAED8AACA/AAAAPwAAED8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_DEPOLARIZE1","uri":"data:application/octet-stream;base64,AACAPgAAED8AAEA+AAAQPwAAgD4AACA/AABAPgAAED8AAEA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AAAQPwAAQD4AACA/AABAPgAAID8AAIA+AAAQPwAAQD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_DEPOLARIZE2","uri":"data:application/octet-stream;base64,AACgPgAAED8AAIA+AAAQPwAAoD4AACA/AACAPgAAED8AAIA+AAAgPwAAoD4AACA/AACgPgAAID8AAKA+AAAQPwAAgD4AACA/AACAPgAAID8AAKA+AAAQPwAAgD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_PAULI_CHANNEL_1","uri":"data:application/octet-stream;base64,AAAQPwAAED8AAAA/AAAQPwAAED8AACA/AAAAPwAAED8AAAA/AAAgPwAAED8AACA/AAAQPwAAID8AABA/AAAQPwAAAD8AACA/AAAAPwAAID8AABA/AAAQPwAAAD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_PAULI_CHANNEL_2","uri":"data:application/octet-stream;base64,AAAgPwAAED8AABA/AAAQPwAAID8AACA/AAAQPwAAED8AABA/AAAgPwAAID8AACA/AAAgPwAAID8AACA/AAAQPwAAED8AACA/AAAQPwAAID8AACA/AAAQPwAAED8AABA/"},{"byteLength":96,"name":"tex_coords_gate_X_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAAwD4AAOA+AADAPgAAAD8AAOA+AADgPgAAwD4AAOA+AADgPgAAAD8AAOA+AAAAPwAA4D4AAAA/AADAPgAA4D4AAOA+AADgPgAA4D4AAAA/AADAPgAA4D4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_Y_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAA4D4AAOA+AADgPgAAAD8AAAA/AADgPgAA4D4AAOA+AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AADgPgAA4D4AAAA/AADgPgAAAD8AAAA/AADgPgAA4D4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_Z_ERROR","uri":"data:application/octet-stream;base64,AAAAPwAAAD8AAOA+AAAAPwAAAD8AABA/AADgPgAAAD8AAOA+AAAQPwAAAD8AABA/AAAAPwAAED8AAAA/AAAAPwAA4D4AABA/AADgPgAAED8AAAA/AAAAPwAA4D4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MPP:X","uri":"data:application/octet-stream;base64,AAAwPwAAwD4AACA/AADAPgAAMD8AAOA+AAAgPwAAwD4AACA/AADgPgAAMD8AAOA+AAAwPwAA4D4AADA/AADAPgAAID8AAOA+AAAgPwAA4D4AADA/AADAPgAAID8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:Y","uri":"data:application/octet-stream;base64,AAAwPwAA4D4AACA/AADgPgAAMD8AAAA/AAAgPwAA4D4AACA/AAAAPwAAMD8AAAA/AAAwPwAAAD8AADA/AADgPgAAID8AAAA/AAAgPwAAAD8AADA/AADgPgAAID8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_MPP:Z","uri":"data:application/octet-stream;base64,AAAwPwAAAD8AACA/AAAAPwAAMD8AABA/AAAgPwAAAD8AACA/AAAQPwAAMD8AABA/AAAwPwAAED8AADA/AAAAPwAAID8AABA/AAAgPwAAED8AADA/AAAAPwAAID8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MRX","uri":"data:application/octet-stream;base64,AADgPgAAwD4AAMA+AADAPgAA4D4AAOA+AADAPgAAwD4AAMA+AADgPgAA4D4AAOA+AADgPgAA4D4AAOA+AADAPgAAwD4AAOA+AADAPgAA4D4AAOA+AADAPgAAwD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MRY","uri":"data:application/octet-stream;base64,AADgPgAA4D4AAMA+AADgPgAA4D4AAAA/AADAPgAA4D4AAMA+AAAAPwAA4D4AAAA/AADgPgAAAD8AAOA+AADgPgAAwD4AAAA/AADAPgAAAD8AAOA+AADgPgAAwD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_MR","uri":"data:application/octet-stream;base64,AADgPgAAAD8AAMA+AAAAPwAA4D4AABA/AADAPgAAAD8AAMA+AAAQPwAA4D4AABA/AADgPgAAED8AAOA+AAAAPwAAwD4AABA/AADAPgAAED8AAOA+AAAAPwAAwD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MX","uri":"data:application/octet-stream;base64,AACgPgAAwD4AAIA+AADAPgAAoD4AAOA+AACAPgAAwD4AAIA+AADgPgAAoD4AAOA+AACgPgAA4D4AAKA+AADAPgAAgD4AAOA+AACAPgAA4D4AAKA+AADAPgAAgD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_MY","uri":"data:application/octet-stream;base64,AACgPgAA4D4AAIA+AADgPgAAoD4AAAA/AACAPgAA4D4AAIA+AAAAPwAAoD4AAAA/AACgPgAAAD8AAKA+AADgPgAAgD4AAAA/AACAPgAAAD8AAKA+AADgPgAAgD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_M","uri":"data:application/octet-stream;base64,AACgPgAAAD8AAIA+AAAAPwAAoD4AABA/AACAPgAAAD8AAIA+AAAQPwAAoD4AABA/AACgPgAAED8AAKA+AAAAPwAAgD4AABA/AACAPgAAED8AAKA+AAAAPwAAgD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_RX","uri":"data:application/octet-stream;base64,AADAPgAAwD4AAKA+AADAPgAAwD4AAOA+AACgPgAAwD4AAKA+AADgPgAAwD4AAOA+AADAPgAA4D4AAMA+AADAPgAAoD4AAOA+AACgPgAA4D4AAMA+AADAPgAAoD4AAMA+"},{"byteLength":96,"name":"tex_coords_gate_RY","uri":"data:application/octet-stream;base64,AADAPgAA4D4AAKA+AADgPgAAwD4AAAA/AACgPgAA4D4AAKA+AAAAPwAAwD4AAAA/AADAPgAAAD8AAMA+AADgPgAAoD4AAAA/AACgPgAAAD8AAMA+AADgPgAAoD4AAOA+"},{"byteLength":96,"name":"tex_coords_gate_R","uri":"data:application/octet-stream;base64,AADAPgAAAD8AAKA+AAAAPwAAwD4AABA/AACgPgAAAD8AAKA+AAAQPwAAwD4AABA/AADAPgAAED8AAMA+AAAAPwAAoD4AABA/AACgPgAAED8AAMA+AAAAPwAAoD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_MXX","uri":"data:application/octet-stream;base64,AAAwPwAAED8AACA/AAAQPwAAMD8AACA/AAAgPwAAED8AACA/AAAgPwAAMD8AACA/AAAwPwAAID8AADA/AAAQPwAAID8AACA/AAAgPwAAID8AADA/AAAQPwAAID8AABA/"},{"byteLength":96,"name":"tex_coords_gate_MYY","uri":"data:application/octet-stream;base64,AABAPwAAED8AADA/AAAQPwAAQD8AACA/AAAwPwAAED8AADA/AAAgPwAAQD8AACA/AABAPwAAID8AAEA/AAAQPwAAMD8AACA/AAAwPwAAID8AAEA/AAAQPwAAMD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_MZZ","uri":"data:application/octet-stream;base64,AABQPwAAED8AAEA/AAAQPwAAUD8AACA/AABAPwAAED8AAEA/AAAgPwAAUD8AACA/AABQPwAAID8AAFA/AAAQPwAAQD8AACA/AABAPwAAID8AAFA/AAAQPwAAQD8AABA/"},{"byteLength":96,"name":"tex_coords_gate_MPAD","uri":"data:application/octet-stream;base64,AABgPwAAED8AAFA/AAAQPwAAYD8AACA/AABQPwAAED8AAFA/AAAgPwAAYD8AACA/AABgPwAAID8AAGA/AAAQPwAAUD8AACA/AABQPwAAID8AAGA/AAAQPwAAUD8AABA/"},{"byteLength":1440,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AABAwAAAAMIAAADCAABQwAAAiMEAAIDBAABQwAAAiMEAAIDBAABAwAAAAMAAAACAAABAwAAAgMAAAACAAABAwAAAwMAAAACAAABAwAAAAMEAAACAAABAwAAAIMEAAACAAABAwAAAQMEAAACAAABAwAAAYMEAAACAAABAwAAAgMEAAACAAABAwAAAkMEAAACAAACAwAAAAMIAAADCAACIwAAAiMEAAIDBAACIwAAAiMEAAIDBAACAwAAAAMAAAACAAACAwAAAgMAAAACAAACAwAAAwMAAAACAAACAwAAAAMEAAACAAACAwAAAIMEAAACAAACAwAAAQMEAAACAAACAwAAAYMEAAACAAACAwAAAgMEAAACAAACAwAAAkMEAAACAAACAwAAAoMEAAACAAACAwAAAsMEAAACAAACgwAAAAMIAAADCAACowAAAiMEAAIDBAACowAAAiMEAAIDBAACgwAAAAMAAAACAAACgwAAAgMAAAACAAACgwAAAwMAAAACAAACgwAAAAMEAAACAAACgwAAAIMEAAACAAACgwAAAQMEAAACAAACgwAAAYMEAAACAAACgwAAAgMEAAACAAACgwAAAkMEAAACAAACgwAAAoMEAAACAAACgwAAAsMEAAACAAACgwAAAwMEAAACAAACgwAAA0MEAAACAAACgwAAA4MEAAACAAACgwAAA8MEAAACAAACgwAAAAMIAAACAAACgwAAACMIAAACAAADAwAAAgMAAAACAAADAwAAAAMAAAACAAADAwAAAwMAAAACAAADAwAAAgMAAAACAAADAwAAAYMEAAACAAADIwAAAMMEAAACAAADIwAAAMMEAAACAAADAwAAAAMEAAACAAADAwAAAQMEAAACAAADAwAAAYMEAAACAAADgwAAAAMAAAACAAADgwAAAgMAAAACAAADgwAAAAMEAAACAAADgwAAAIMEAAACAAAAQwQAAAMAAAACAAAAUwQAAiMEAAIDBAAAUwQAAiMEAAIDBAAAQwQAAAMIAAADCAAAQwQAAgMAAAACAAAAQwQAAAMAAAACAAAAgwQAAAMAAAACAAAAkwQAAiMEAAIDBAAAkwQAAiMEAAIDBAAAgwQAAAMIAAADCAABAwQAAAMIAAADCAABEwQAAiMEAAIDBAABEwQAAiMEAAIDBAABAwQAAAMAAAACAAABAwQAAgMAAAACAAABAwQAAwMAAAACAAABAwQAAAMEAAACAAABAwQAAIMEAAACAAABAwQAAQMEAAACAAABAwQAAYMEAAACAAABwwQAAAMIAAADCAAB0wQAAiMEAAIDBAAB0wQAAiMEAAIDBAABwwQAAAMAAAACAAACAPwAAAMIAAADCAADIwQAAAMIAAADCAACAPwAAAMAAAACAAADIwQAAAMAAAACAAACAPwAAgMAAAACAAADIwQAAgMAAAACAAACAPwAAwMAAAACAAADIwQAAwMAAAACAAACAPwAAAMEAAACAAADIwQAAAMEAAACAAACAPwAAIMEAAACAAADIwQAAIMEAAACAAACAPwAAQMEAAACAAADIwQAAQMEAAACAAACAPwAAYMEAAACAAADIwQAAYMEAAACAAACAPwAAgMEAAACAAADIwQAAgMEAAACAAACAPwAAkMEAAACAAADIwQAAkMEAAACAAACAPwAAoMEAAACAAADIwQAAoMEAAACAAACAPwAAsMEAAACAAADIwQAAsMEAAACAAACAPwAAwMEAAACAAADIwQAAwMEAAACAAACAPwAA0MEAAACAAADIwQAA0MEAAACAAACAPwAA4MEAAACAAADIwQAA4MEAAACAAACAPwAA8MEAAACAAADIwQAA8MEAAACAAACAPwAAAMIAAACAAADIwQAAAMIAAACAAACAPwAACMIAAACAAADIwQAACMIAAACA"},{"byteLength":360,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAIAAAIDBAABAwAAAAIAAAIDBAAAgwAAAAL8AAIDBAABAwAAAAIAAAIDBAAAgwAAAAD8AAIDBAABAwAAAAIAAAIDBAABcwQAAgL8AAIA/AABcwQAAgL8AAATCAABcwQAAgL8AAIA/AABcwQAADMIAAIA/AABcwQAAgL8AAIA/AACKwQAAgL8AAIA/AABcwQAAgL8AAATCAABcwQAADMIAAATCAABcwQAAgL8AAATCAACKwQAAgL8AAATCAABcwQAADMIAAIA/AABcwQAADMIAAATCAABcwQAADMIAAIA/AACKwQAADMIAAIA/AABcwQAADMIAAATCAACKwQAADMIAAATCAACKwQAAgL8AAIA/AACKwQAAgL8AAATCAACKwQAAgL8AAIA/AACKwQAADMIAAIA/AACKwQAAgL8AAATCAACKwQAADMIAAATCAACKwQAADMIAAIA/AACKwQAADMIAAATC"},{"byteLength":1152,"name":"buf_blue_scattered_lines","uri":"data:application/octet-stream;base64,AABAv83MzL8AAAA/AABAv5qZmb8AAAA/AABAv83MzL8AAALCAABAv5qZmb8AAALCAABAv5qZmb8AAAA/AABAv5qZmb8AAALCAABAv5qZmb8AAAA/AAAQwJqZmb8AAAA/AABAv5qZmb8AAALCAAAQwJqZmb8AAALCAAAQwM3MzL8AAAA/AAAQwJqZmb8AAAA/AAAQwM3MzL8AAALCAAAQwJqZmb8AAALCAAAQwJqZmb8AAAA/AAAQwJqZmb8AAALCAAAwwM3MzL8AAAA/AAAwwJqZmb8AAAA/AAAwwM3MzL8AAALCAAAwwJqZmb8AAALCAAAwwJqZmb8AAAA/AAAwwJqZmb8AAALCAAAwwJqZmb8AAAA/AACowJqZmb8AAAA/AAAwwJqZmb8AAALCAACowJqZmb8AAALCAACowM3MzL8AAAA/AACowJqZmb8AAAA/AACowM3MzL8AAALCAACowJqZmb8AAALCAACowJqZmb8AAAA/AACowJqZmb8AAALCAAC4wM3MzL8AAAA/AAC4wJqZmb8AAAA/AAC4wM3MzL8AAALCAAC4wJqZmb8AAALCAAC4wJqZmb8AAAA/AAC4wJqZmb8AAALCAAC4wJqZmb8AAAA/AAAEwZqZmb8AAAA/AAC4wJqZmb8AAALCAAAEwZqZmb8AAALCAAAEwc3MzL8AAAA/AAAEwZqZmb8AAAA/AAAEwc3MzL8AAALCAAAEwZqZmb8AAALCAAAEwZqZmb8AAAA/AAAEwZqZmb8AAALCAAAMwc3MzL8AAAA/AAAMwZqZmb8AAAA/AAAMwc3MzL8AAALCAAAMwZqZmb8AAALCAAAMwZqZmb8AAAA/AAAMwZqZmb8AAALCAAAMwZqZmb8AAAA/AAA0wZqZmb8AAAA/AAAMwZqZmb8AAALCAAA0wZqZmb8AAALCAAA0wc3MzL8AAAA/AAA0wZqZmb8AAAA/AAA0wc3MzL8AAALCAAA0wZqZmb8AAALCAAA0wZqZmb8AAAA/AAA0wZqZmb8AAALCAABcwc3MzL8AAAA/AABcwZqZmb8AAAA/AABcwc3MzL8AAALCAABcwZqZmb8AAALCAABcwZqZmb8AAAA/AABcwZqZmb8AAALCAABcwZqZmb8AAAA/AACCwZqZmb8AAAA/AABcwZqZmb8AAALCAACCwZqZmb8AAALCAACCwc3MzL8AAAA/AACCwZqZmb8AAAA/AACCwc3MzL8AAALCAACCwZqZmb8AAALCAACCwZqZmb8AAAA/AACCwZqZmb8AAALCAACWwc3MzL8AAAA/AACWwZqZmb8AAAA/AACWwc3MzL8AAALCAACWwZqZmb8AAALCAACWwZqZmb8AAAA/AACWwZqZmb8AAALCAACWwZqZmb8AAAA/AAC6wZqZmb8AAAA/AACWwZqZmb8AAALCAAC6wZqZmb8AAALCAAC6wc3MzL8AAAA/AAC6wZqZmb8AAAA/AAC6wc3MzL8AAALCAAC6wZqZmb8AAALCAAC6wZqZmb8AAAA/AAC6wZqZmb8AAALC"}],"images":[{"name":"gates_image","uri":"data:image/png;base64,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E2JjY9H8uR/56pw9exZmZmbSq8/G8APAaoANGr+sGuC7egCurhvw77//4ty5c3j06BG6du0KR0dH2bqf+/fv4/jx4xAKhfj444/lcmGkpKTg+PHjSEpKgqWlJfr3749nz57h7NmzePToEVq0aAFdXV1ZoSwrKyt069ZNbgTuXa8G2OAdgNqWA6776CMbND7LAb/+6ZeUlARfX1+sX78eRUVFWLJkCSZPnowWLVrUyzEXFRVh+/bt+Oabb5CYmIhRo0Zh1qxZ6FbDbpAyf/75JywtLevt2NgBYAfgbesANCR2ANgBYAfgPzICQOwAsAPADgA7AERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERPT/gnx31HLEbHgAAAAASUVORK5CYII="}],"materials":[{"doubleSided":false,"name":"gates_material","pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"name":"black","pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"name":"white","pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"name":"white","pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"name":"black","pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"name":"white","pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"name":"black","pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"name":"black","pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"name":"gray","pbrMetallicRoughness":{"baseColorFactor":[0.5,0.5,0.5,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"name":"black","pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"name":"black","pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"name":"red","pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"name":"blue","pbrMetallicRoughness":{"baseColorFactor":[0,0,1,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":4},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":5},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":6},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":7},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":8},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":9},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":10},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":11},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":12},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":13},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":14},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":15},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":16},"material":1,"mode":6},{"attributes":{"POSITION":17},"material":2,"mode":1}]},{"primitives":[{"attributes":{"POSITION":18},"material":3,"mode":6},{"attributes":{"POSITION":18},"material":4,"mode":3},{"attributes":{"POSITION":19},"material":4,"mode":1}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":20},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":21},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":22},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":23},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":24},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":25},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":26},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":27},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":28},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":29},"material":5,"mode":6},{"attributes":{"POSITION":29},"material":6,"mode":3},{"attributes":{"POSITION":30},"material":6,"mode":1}]},{"primitives":[{"attributes":{"POSITION":31},"material":7,"mode":2},{"attributes":{"POSITION":31},"material":8,"mode":4}]},{"primitives":[{"attributes":{"POSITION":32},"material":9,"mode":6}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":33},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":34},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":35},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":36},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":37},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":38},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":39},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":40},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":41},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":42},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":43},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":44},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":45},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":46},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":47},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":48},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":49},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":50},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":51},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":52},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":53},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":54},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":55},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":56},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":57},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":58},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":59},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":60},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":61},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":62},"material":10,"mode":1}]},{"primitives":[{"attributes":{"POSITION":63},"material":11,"mode":1}]},{"primitives":[{"attributes":{"POSITION":64},"material":12,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-32,-32]},{"mesh":1,"translation":[-0,-2,-0]},{"mesh":2,"translation":[-0,-4,-0]},{"mesh":3,"translation":[-0,-6,-0]},{"mesh":4,"translation":[-1,-32,-32]},{"mesh":5,"translation":[-1,-2,-0]},{"mesh":6,"translation":[-1,-4,-0]},{"mesh":7,"translation":[-1,-6,-0]},{"mesh":8,"translation":[-1,-8,-0]},{"mesh":9,"translation":[-2,-32,-32]},{"mesh":10,"translation":[-2,-2,-0]},{"mesh":11,"translation":[-2,-4,-0]},{"mesh":12,"translation":[-2,-6,-0]},{"mesh":13,"translation":[-2,-8,-0]},{"mesh":14,"translation":[-2,-10,-0]},{"mesh":15,"translation":[-3,-32,-32]},{"mesh":16,"translation":[-3,-2,-0]},{"mesh":17,"translation":[-3,-4,-0]},{"mesh":17,"translation":[-3,-6,-0]},{"mesh":18,"translation":[-3,-8,-0]},{"mesh":18,"translation":[-3,-10,-0]},{"mesh":19,"translation":[-3,-12,-0]},{"mesh":19,"translation":[-3,-14,-0]},{"mesh":16,"translation":[-3,-16,-0]},{"mesh":15,"translation":[-3,-18,-0]},{"mesh":20,"translation":[-4,-32,-32]},{"mesh":20,"translation":[-4,-2,-0]},{"mesh":21,"translation":[-4,-4,-0]},{"mesh":21,"translation":[-4,-6,-0]},{"mesh":22,"translation":[-4,-8,-0]},{"mesh":22,"translation":[-4,-10,-0]},{"mesh":23,"translation":[-4,-12,-0]},{"mesh":23,"translation":[-4,-14,-0]},{"mesh":24,"translation":[-4,-16,-0]},{"mesh":24,"translation":[-4,-18,-0]},{"mesh":25,"translation":[-4,-20,-0]},{"mesh":25,"translation":[-4,-22,-0]},{"mesh":26,"translation":[-5,-32,-32]},{"mesh":26,"translation":[-5,-2,-0]},{"mesh":26,"translation":[-5,-4,-0]},{"mesh":27,"translation":[-5,-6,-0]},{"mesh":26,"translation":[-5,-8,-0]},{"mesh":28,"translation":[-5,-10,-0]},{"mesh":27,"translation":[-5,-12,-0]},{"mesh":26,"translation":[-5,-14,-0]},{"mesh":27,"translation":[-5,-16,-0]},{"mesh":27,"translation":[-5,-18,-0]},{"mesh":27,"translation":[-5,-20,-0]},{"mesh":28,"translation":[-5,-22,-0]},{"mesh":28,"translation":[-5,-24,-0]},{"mesh":26,"translation":[-5,-26,-0]},{"mesh":28,"translation":[-5,-28,-0]},{"mesh":27,"translation":[-5,-30,-0]},{"mesh":28,"translation":[-5,-32,-0]},{"mesh":28,"translation":[-5,-34,-0]},{"mesh":29,"translation":[-6,-2,-0]},{"mesh":30,"translation":[-6,-4,-0]},{"mesh":31,"translation":[-6,-6,-0]},{"mesh":32,"translation":[-6,-8,-0]},{"mesh":33,"translation":[-6,-14,-0]},{"mesh":34,"translation":[-6,-12,-0]},{"mesh":35,"translation":[-6,-32,-32]},{"mesh":36,"translation":[-7,-2,-0]},{"mesh":36,"translation":[-7,-4,-0]},{"mesh":37,"translation":[-7,-6,-0]},{"mesh":38,"translation":[-7,-8,-0]},{"mesh":38,"translation":[-7,-10,-0]},{"mesh":39,"translation":[-7,-32,-32]},{"mesh":40,"translation":[-8,-2,-0]},{"mesh":41,"translation":[-8,-4,-0]},{"mesh":42,"translation":[-9,-32,-32]},{"mesh":43,"translation":[-9,-2,-0]},{"mesh":44,"translation":[-9,-4,-0]},{"mesh":44,"translation":[-10,-32,-32]},{"mesh":44,"translation":[-10,-2,-0]},{"mesh":45,"translation":[-11,-32,-32]},{"mesh":46,"translation":[-11,-2,-0]},{"mesh":47,"translation":[-11,-4,-0]},{"mesh":48,"translation":[-11,-6,-0]},{"mesh":49,"translation":[-11,-8,-0]},{"mesh":50,"translation":[-11,-10,-0]},{"mesh":50,"translation":[-11,-12,-0]},{"mesh":51,"translation":[-11,-14,-0]},{"mesh":52,"translation":[-11,-16,-0]},{"mesh":53,"translation":[-11,-18,-0]},{"mesh":54,"translation":[-12,-32,-32]},{"mesh":54,"translation":[-12,-2,-0]},{"mesh":54,"translation":[-12,-4,-0]},{"mesh":54,"translation":[-12,-6,-0]},{"mesh":55,"translation":[-12,-8,-0]},{"mesh":55,"translation":[-12,-10,-0]},{"mesh":56,"translation":[-12,-12,-0]},{"mesh":56,"translation":[-12,-14,-0]},{"mesh":7,"translation":[-14,-32,-32]},{"mesh":28,"translation":[-15,-32,-32]},{"mesh":26,"translation":[-15,-2,-0]},{"mesh":13,"translation":[-16,-2,-0]},{"mesh":47,"translation":[-19,-32,-32]},{"mesh":39,"translation":[-20,-32,-32]},{"mesh":47,"translation":[-21,-32,-32]},{"mesh":57,"translation":[-22,-32,-32]},{"mesh":57,"translation":[-22,-2,-0]},{"mesh":57,"translation":[-23,-32,-32]},{"mesh":58,"translation":[0,0,0]},{"mesh":59,"translation":[0,0,0]},{"mesh":60,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105]}],"textures":[{"name":"gates_texture","sampler":0,"source":0}]} ================================================ FILE: testdata/tick.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":12,"max":[0.125,0.5],"min":[0.0625,0.4375],"name":"tex_coords_gate_H","type":"VEC2"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.5625],"min":[0.125,0.5],"name":"tex_coords_gate_S","type":"VEC2"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":12,"max":[0.1875,0.4375],"min":[0.125,0.375],"name":"tex_coords_gate_SQRT_X","type":"VEC2"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":4,"max":[1,-0,-0],"min":[-12,-2,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":30,"max":[0,2.5,1],"min":[-6.25,-3,-1],"name":"buf_red_scattered_lines","type":"VEC3"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":32,"max":[0.25,0.800000011920929,0.5],"min":[-9.25,0.400000005960464,-0.5],"name":"buf_blue_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":1,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_H","target":34962},{"buffer":2,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_S","target":34962},{"buffer":3,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_X","target":34962},{"buffer":4,"byteLength":48,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":5,"byteLength":360,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962},{"buffer":6,"byteLength":384,"byteOffset":0,"name":"buf_blue_scattered_lines","target":34962}],"buffers":[{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_H","uri":"data:application/octet-stream;base64,AAAAPgAA4D4AAIA9AADgPgAAAD4AAAA/AACAPQAA4D4AAIA9AAAAPwAAAD4AAAA/AAAAPgAAAD8AAAA+AADgPgAAgD0AAAA/AACAPQAAAD8AAAA+AADgPgAAgD0AAOA+"},{"byteLength":96,"name":"tex_coords_gate_S","uri":"data:application/octet-stream;base64,AABAPgAAAD8AAAA+AAAAPwAAQD4AABA/AAAAPgAAAD8AAAA+AAAQPwAAQD4AABA/AABAPgAAED8AAEA+AAAAPwAAAD4AABA/AAAAPgAAED8AAEA+AAAAPwAAAD4AAAA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_X","uri":"data:application/octet-stream;base64,AABAPgAAwD4AAAA+AADAPgAAQD4AAOA+AAAAPgAAwD4AAAA+AADgPgAAQD4AAOA+AABAPgAA4D4AAEA+AADAPgAAAD4AAOA+AAAAPgAA4D4AAEA+AADAPgAAAD4AAMA+"},{"byteLength":48,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AACAPwAAAIAAAACAAABAwQAAAIAAAACAAACAPwAAAMAAAACAAABAwQAAAMAAAACA"},{"byteLength":360,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACAAABwwAAAgD8AAIA/AABwwAAAgD8AAIC/AABwwAAAgD8AAIA/AABwwAAAQMAAAIA/AABwwAAAgD8AAIA/AADIwAAAgD8AAIA/AABwwAAAgD8AAIC/AABwwAAAQMAAAIC/AABwwAAAgD8AAIC/AADIwAAAgD8AAIC/AABwwAAAQMAAAIA/AABwwAAAQMAAAIC/AABwwAAAQMAAAIA/AADIwAAAQMAAAIA/AABwwAAAQMAAAIC/AADIwAAAQMAAAIC/AADIwAAAgD8AAIA/AADIwAAAgD8AAIC/AADIwAAAgD8AAIA/AADIwAAAQMAAAIA/AADIwAAAgD8AAIC/AADIwAAAQMAAAIC/AADIwAAAQMAAAIA/AADIwAAAQMAAAIC/"},{"byteLength":384,"name":"buf_blue_scattered_lines","uri":"data:application/octet-stream;base64,AACAPs3MzD4AAAA/AACAPs3MTD8AAAA/AACAPs3MzD4AAAC/AACAPs3MTD8AAAC/AACAPs3MTD8AAAA/AACAPs3MTD8AAAC/AACAPs3MTD8AAAA/AACgv83MTD8AAAA/AACAPs3MTD8AAAC/AACgv83MTD8AAAC/AACgv83MzD4AAAA/AACgv83MTD8AAAA/AACgv83MzD4AAAC/AACgv83MTD8AAAC/AACgv83MTD8AAAA/AACgv83MTD8AAAC/AADYwM3MzD4AAAA/AADYwM3MTD8AAAA/AADYwM3MzD4AAAC/AADYwM3MTD8AAAC/AADYwM3MTD8AAAA/AADYwM3MTD8AAAC/AADYwM3MTD8AAAA/AAAUwc3MTD8AAAA/AADYwM3MTD8AAAC/AAAUwc3MTD8AAAC/AAAUwc3MzD4AAAA/AAAUwc3MTD8AAAA/AAAUwc3MzD4AAAC/AAAUwc3MTD8AAAC/AAAUwc3MTD8AAAA/AAAUwc3MTD8AAAC/"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,1,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":2},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":3},"material":0,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4},"material":1,"mode":1}]},{"primitives":[{"attributes":{"POSITION":5},"material":2,"mode":1}]},{"primitives":[{"attributes":{"POSITION":6},"material":3,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":0,"translation":[-1,-0,-0]},{"mesh":0,"translation":[-2,-0,-0]},{"mesh":0,"translation":[-2,-2,-0]},{"mesh":0,"translation":[-3,-0,-0]},{"mesh":0,"translation":[-4,-0,-0]},{"mesh":0,"translation":[-4,-2,-0]},{"mesh":0,"translation":[-5,-0,-0]},{"mesh":1,"translation":[-6,-0,-0]},{"mesh":0,"translation":[-7,-0,-0]},{"mesh":0,"translation":[-8,-0,-0]},{"mesh":2,"translation":[-9,-0,-0]},{"mesh":0,"translation":[-10,-0,-0]},{"mesh":0,"translation":[-11,-0,-0]},{"mesh":3,"translation":[0,0,0]},{"mesh":4,"translation":[0,0,0]},{"mesh":5,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]}],"textures":[{"sampler":0,"source":0}]} ================================================ FILE: testdata/two_qubits_gates.gltf ================================================ {"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":2,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"control_x_line_cross","type":"VEC3"},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":3,"max":[0,0.400000005960464,0.346410155296326],"min":[0,-0.200000032782555,-0.346410185098648],"name":"circle_loop","type":"VEC3"},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"max":[0,0.5,0.5],"min":[0,-0.5,-0.5],"name":"cube","type":"VEC3"},{"bufferView":5,"byteOffset":0,"componentType":5126,"count":12,"max":[0.375,0.625],"min":[0.3125,0.5625],"name":"tex_coords_gate_ISWAP","type":"VEC2"},{"bufferView":6,"byteOffset":0,"componentType":5126,"count":12,"max":[0.4375,0.625],"min":[0.375,0.5625],"name":"tex_coords_gate_ISWAP_DAG","type":"VEC2"},{"bufferView":7,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.4375],"min":[0.6875,0.375],"name":"tex_coords_gate_SQRT_XX","type":"VEC2"},{"bufferView":8,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.4375],"min":[0.75,0.375],"name":"tex_coords_gate_SQRT_XX_DAG","type":"VEC2"},{"bufferView":9,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.5],"min":[0.6875,0.4375],"name":"tex_coords_gate_SQRT_YY","type":"VEC2"},{"bufferView":10,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.5],"min":[0.75,0.4375],"name":"tex_coords_gate_SQRT_YY_DAG","type":"VEC2"},{"bufferView":11,"byteOffset":0,"componentType":5126,"count":12,"max":[0.75,0.5625],"min":[0.6875,0.5],"name":"tex_coords_gate_SQRT_ZZ","type":"VEC2"},{"bufferView":12,"byteOffset":0,"componentType":5126,"count":12,"max":[0.8125,0.5625],"min":[0.75,0.5],"name":"tex_coords_gate_SQRT_ZZ_DAG","type":"VEC2"},{"bufferView":13,"byteOffset":0,"componentType":5126,"count":12,"max":[0.5,0.625],"min":[0.4375,0.5625],"name":"tex_coords_gate_SWAP","type":"VEC2"},{"bufferView":14,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":15,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.45254835486412,0.45254835486412],"min":[0,-0.45254835486412,-0.45254835486412],"name":"control_zswap_line_cross","type":"VEC3"},{"bufferView":16,"byteOffset":0,"componentType":5126,"count":17,"max":[0,0.400000005960464,0.400000005960464],"min":[0,-0.400000005960464,-0.400000005960464],"name":"circle_loop","type":"VEC3"},{"bufferView":17,"byteOffset":0,"componentType":5126,"count":4,"max":[0,0.45254835486412,0.45254835486412],"min":[0,-0.45254835486412,-0.45254835486412],"name":"control_xswap_line_cross","type":"VEC3"},{"bufferView":18,"byteOffset":0,"componentType":5126,"count":82,"max":[1,-0,-0],"min":[-12,-10,-0],"name":"buf_scattered_lines","type":"VEC3"},{"bufferView":19,"byteOffset":0,"componentType":5126,"count":6,"max":[0,2.5,-0],"min":[-3,1.5,-0],"name":"buf_red_scattered_lines","type":"VEC3"}],"asset":{"version":"2.0"},"bufferViews":[{"buffer":0,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":1,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":2,"byteLength":48,"byteOffset":0,"name":"control_x_line_cross","target":34962},{"buffer":3,"byteLength":36,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":4,"byteLength":144,"byteOffset":0,"name":"cube","target":34962},{"buffer":5,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ISWAP","target":34962},{"buffer":6,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_ISWAP_DAG","target":34962},{"buffer":7,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_XX","target":34962},{"buffer":8,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_XX_DAG","target":34962},{"buffer":9,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_YY","target":34962},{"buffer":10,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_YY_DAG","target":34962},{"buffer":11,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_ZZ","target":34962},{"buffer":12,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SQRT_ZZ_DAG","target":34962},{"buffer":13,"byteLength":96,"byteOffset":0,"name":"tex_coords_gate_SWAP","target":34962},{"buffer":14,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":15,"byteLength":48,"byteOffset":0,"name":"control_zswap_line_cross","target":34962},{"buffer":16,"byteLength":204,"byteOffset":0,"name":"circle_loop","target":34962},{"buffer":17,"byteLength":48,"byteOffset":0,"name":"control_xswap_line_cross","target":34962},{"buffer":18,"byteLength":984,"byteOffset":0,"name":"buf_scattered_lines","target":34962},{"buffer":19,"byteLength":72,"byteOffset":0,"name":"buf_red_scattered_lines","target":34962}],"buffers":[{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_x_line_cross","uri":"data:application/octet-stream;base64,AAAAAM3MzL4AAAAAAAAAAM3MzD4AAAAAAAAAAAAAAADNzMy+AAAAAAAAAADNzMw+"},{"byteLength":36,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAM/MTL6sXLE+AAAAAMvMTL6tXLG+"},{"byteLength":144,"name":"cube","uri":"data:application/octet-stream;base64,AAAAAAAAAD8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAC/AAAAAAAAAD8AAAC/AAAAAAAAAL8AAAA/AAAAAAAAAL8AAAA/AAAAAAAAAD8AAAC/AAAAAAAAAD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_ISWAP","uri":"data:application/octet-stream;base64,AADAPgAAED8AAKA+AAAQPwAAwD4AACA/AACgPgAAED8AAKA+AAAgPwAAwD4AACA/AADAPgAAID8AAMA+AAAQPwAAoD4AACA/AACgPgAAID8AAMA+AAAQPwAAoD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_ISWAP_DAG","uri":"data:application/octet-stream;base64,AADgPgAAED8AAMA+AAAQPwAA4D4AACA/AADAPgAAED8AAMA+AAAgPwAA4D4AACA/AADgPgAAID8AAOA+AAAQPwAAwD4AACA/AADAPgAAID8AAOA+AAAQPwAAwD4AABA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_XX","uri":"data:application/octet-stream;base64,AABAPwAAwD4AADA/AADAPgAAQD8AAOA+AAAwPwAAwD4AADA/AADgPgAAQD8AAOA+AABAPwAA4D4AAEA/AADAPgAAMD8AAOA+AAAwPwAA4D4AAEA/AADAPgAAMD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_XX_DAG","uri":"data:application/octet-stream;base64,AABQPwAAwD4AAEA/AADAPgAAUD8AAOA+AABAPwAAwD4AAEA/AADgPgAAUD8AAOA+AABQPwAA4D4AAFA/AADAPgAAQD8AAOA+AABAPwAA4D4AAFA/AADAPgAAQD8AAMA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_YY","uri":"data:application/octet-stream;base64,AABAPwAA4D4AADA/AADgPgAAQD8AAAA/AAAwPwAA4D4AADA/AAAAPwAAQD8AAAA/AABAPwAAAD8AAEA/AADgPgAAMD8AAAA/AAAwPwAAAD8AAEA/AADgPgAAMD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_YY_DAG","uri":"data:application/octet-stream;base64,AABQPwAA4D4AAEA/AADgPgAAUD8AAAA/AABAPwAA4D4AAEA/AAAAPwAAUD8AAAA/AABQPwAAAD8AAFA/AADgPgAAQD8AAAA/AABAPwAAAD8AAFA/AADgPgAAQD8AAOA+"},{"byteLength":96,"name":"tex_coords_gate_SQRT_ZZ","uri":"data:application/octet-stream;base64,AABAPwAAAD8AADA/AAAAPwAAQD8AABA/AAAwPwAAAD8AADA/AAAQPwAAQD8AABA/AABAPwAAED8AAEA/AAAAPwAAMD8AABA/AAAwPwAAED8AAEA/AAAAPwAAMD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_SQRT_ZZ_DAG","uri":"data:application/octet-stream;base64,AABQPwAAAD8AAEA/AAAAPwAAUD8AABA/AABAPwAAAD8AAEA/AAAQPwAAUD8AABA/AABQPwAAED8AAFA/AAAAPwAAQD8AABA/AABAPwAAED8AAFA/AAAAPwAAQD8AAAA/"},{"byteLength":96,"name":"tex_coords_gate_SWAP","uri":"data:application/octet-stream;base64,AAAAPwAAED8AAOA+AAAQPwAAAD8AACA/AADgPgAAED8AAOA+AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAAQPwAA4D4AACA/AADgPgAAID8AAAA/AAAQPwAA4D4AABA/"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_zswap_line_cross","uri":"data:application/octet-stream;base64,AAAAAGu0575rtOe+AAAAAGu05z5rtOc+AAAAAGu0575rtOc+AAAAAGu05z5rtOe+"},{"byteLength":204,"name":"circle_loop","uri":"data:application/octet-stream;base64,AAAAAM3MzD4AAAAAAAAAAOU1vT5Fvxw+AAAAAMPQkD7D0JA+AAAAAES/HD7lNb0+AAAAAPIwlrLNzMw+AAAAAEe/HL7lNb0+AAAAAMPQkL7D0JA+AAAAAOc1vb5Avxw+AAAAAM3MzL7yMBazAAAAAOU1vb5Evxy+AAAAAMHQkL7E0JC+AAAAADy/HL7nNb2+AAAAAPLkozHNzMy+AAAAAEm/HD7kNb2+AAAAAMbQkD6/0JC+AAAAAOY1vT5Evxy+AAAAAM3MzD4AAAAA"},{"byteLength":48,"name":"control_xswap_line_cross","uri":"data:application/octet-stream;base64,AAAAAGu0575rtOe+AAAAAGu05z5rtOc+AAAAAGu0575rtOc+AAAAAGu05z5rtOe+"},{"byteLength":984,"name":"buf_scattered_lines","uri":"data:application/octet-stream;base64,AAAAgAAAAIAAAACAAAAAgAAAAMAAAACAAAAAgAAAgMAAAACAAAAAgAAAwMAAAACAAAAAgAAAAMEAAACAAAAAgAAAIMEAAACAAACAvwAAIMEAAACAAACAvwAAAMEAAACAAACAvwAAAIAAAACAAACgvwAAAMAAAACAAACgvwAAAMAAAACAAACAvwAAgMAAAACAAACAvwAAAMAAAACAAACgvwAAgMAAAACAAACgvwAAgMAAAACAAACAvwAAwMAAAACAAAAAwAAAgMAAAACAAAAQwAAAwMAAAACAAAAQwAAAwMAAAACAAAAAwAAAAMEAAACAAAAAwAAAwMAAAACAAAAQwAAAAMEAAACAAAAQwAAAAMEAAACAAAAAwAAAIMEAAACAAABAwAAAAIAAAACAAABQwAAAoMAAAACAAABQwAAAoMAAAACAAABAwAAAIMEAAACAAABAwAAAwMAAAACAAABAwAAAAMEAAACAAACAwAAAAMEAAACAAACAwAAAwMAAAACAAACAwAAAAIAAAACAAACAwAAAAMAAAACAAACgwAAAgMAAAACAAACgwAAAwMAAAACAAACgwAAAAMEAAACAAACgwAAAIMEAAACAAACgwAAAAIAAAACAAACgwAAAAMAAAACAAADAwAAAgMAAAACAAADAwAAAwMAAAACAAADgwAAAwMAAAACAAADgwAAAAMEAAACAAADgwAAAAIAAAACAAADgwAAAAMAAAACAAAAAwQAAgMAAAACAAAAAwQAAwMAAAACAAAAAwQAAAMEAAACAAAAAwQAAIMEAAACAAAAAwQAAAIAAAACAAAAAwQAAAMAAAACAAAAQwQAAgMAAAACAAAAQwQAAwMAAAACAAAAQwQAAAMEAAACAAAAQwQAAIMEAAACAAAAgwQAAAIAAAACAAAAkwQAAoMAAAACAAAAkwQAAoMAAAACAAAAgwQAAIMEAAACAAAAgwQAAgMAAAACAAAAgwQAAwMAAAACAAAAgwQAAAMAAAACAAAAkwQAAoMAAAACAAAAkwQAAoMAAAACAAAAgwQAAAMEAAACAAAAwwQAAAIAAAACAAAAwwQAAAMAAAACAAAAwwQAAgMAAAACAAAAwwQAAwMAAAACAAACAPwAAAIAAAACAAABAwQAAAIAAAACAAACAPwAAAMAAAACAAABAwQAAAMAAAACAAACAPwAAgMAAAACAAABAwQAAgMAAAACAAACAPwAAwMAAAACAAABAwQAAwMAAAACAAACAPwAAAMEAAACAAABAwQAAAMEAAACAAACAPwAAIMEAAACAAABAwQAAIMEAAACA"},{"byteLength":72,"name":"buf_red_scattered_lines","uri":"data:application/octet-stream;base64,AAAAAAAAAEAAAACAAABAwAAAAEAAAACAAAAgwAAAwD8AAACAAABAwAAAAEAAAACAAAAgwAAAIEAAAACAAABAwAAAAEAAAACA"}],"images":[{"uri":"data:image/png;base64,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"}],"materials":[{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0.5,0.5,0.5,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":false,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,1,1,1],"metallicFactor":0.4,"roughnessFactor":0.5}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[0,0,0,1],"metallicFactor":1,"roughnessFactor":1}},{"doubleSided":true,"pbrMetallicRoughness":{"baseColorFactor":[1,0,0,1],"metallicFactor":1,"roughnessFactor":1}}],"meshes":[{"primitives":[{"attributes":{"POSITION":0},"material":0,"mode":6}]},{"primitives":[{"attributes":{"POSITION":1},"material":1,"mode":6},{"attributes":{"POSITION":1},"material":2,"mode":3},{"attributes":{"POSITION":2},"material":2,"mode":1}]},{"primitives":[{"attributes":{"POSITION":3},"material":3,"mode":2},{"attributes":{"POSITION":3},"material":4,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":5},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":6},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":7},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":8},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":9},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":10},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":11},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":12},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":4,"TEXCOORD_0":13},"material":5,"mode":4}]},{"primitives":[{"attributes":{"POSITION":14},"material":6,"mode":6},{"attributes":{"POSITION":15},"material":7,"mode":1}]},{"primitives":[{"attributes":{"POSITION":16},"material":8,"mode":6},{"attributes":{"POSITION":16},"material":9,"mode":3},{"attributes":{"POSITION":17},"material":9,"mode":1}]},{"primitives":[{"attributes":{"POSITION":18},"material":10,"mode":1}]},{"primitives":[{"attributes":{"POSITION":19},"material":11,"mode":1}]}],"nodes":[{"mesh":0,"translation":[-0,-0,-0]},{"mesh":1,"translation":[-0,-2,-0]},{"mesh":0,"translation":[-0,-4,-0]},{"mesh":1,"translation":[-0,-6,-0]},{"mesh":0,"translation":[-0,-8,-0]},{"mesh":2,"translation":[-0,-10,-0]},{"mesh":0,"translation":[-1,-10,-0]},{"mesh":2,"translation":[-1,-8,-0]},{"mesh":0,"translation":[-1,-0,-0]},{"mesh":0,"translation":[-1,-4,-0]},{"mesh":3,"translation":[-1,-2,-0]},{"mesh":3,"translation":[-1,-6,-0]},{"mesh":4,"translation":[-2,-4,-0]},{"mesh":4,"translation":[-2,-8,-0]},{"mesh":5,"translation":[-2,-6,-0]},{"mesh":5,"translation":[-2,-10,-0]},{"mesh":6,"translation":[-3,-0,-0]},{"mesh":6,"translation":[-3,-10,-0]},{"mesh":7,"translation":[-3,-6,-0]},{"mesh":7,"translation":[-3,-8,-0]},{"mesh":7,"translation":[-4,-8,-0]},{"mesh":7,"translation":[-4,-6,-0]},{"mesh":8,"translation":[-4,-0,-0]},{"mesh":8,"translation":[-4,-2,-0]},{"mesh":9,"translation":[-5,-4,-0]},{"mesh":9,"translation":[-5,-6,-0]},{"mesh":10,"translation":[-5,-8,-0]},{"mesh":10,"translation":[-5,-10,-0]},{"mesh":11,"translation":[-5,-0,-0]},{"mesh":11,"translation":[-5,-2,-0]},{"mesh":1,"translation":[-6,-4,-0]},{"mesh":1,"translation":[-6,-6,-0]},{"mesh":1,"translation":[-7,-6,-0]},{"mesh":2,"translation":[-7,-8,-0]},{"mesh":1,"translation":[-7,-0,-0]},{"mesh":0,"translation":[-7,-2,-0]},{"mesh":2,"translation":[-8,-4,-0]},{"mesh":1,"translation":[-8,-6,-0]},{"mesh":2,"translation":[-8,-8,-0]},{"mesh":2,"translation":[-8,-10,-0]},{"mesh":2,"translation":[-8,-0,-0]},{"mesh":0,"translation":[-8,-2,-0]},{"mesh":0,"translation":[-9,-4,-0]},{"mesh":1,"translation":[-9,-6,-0]},{"mesh":0,"translation":[-9,-8,-0]},{"mesh":2,"translation":[-9,-10,-0]},{"mesh":0,"translation":[-10,-0,-0]},{"mesh":0,"translation":[-10,-10,-0]},{"mesh":0,"translation":[-10,-4,-0]},{"mesh":0,"translation":[-10,-6,-0]},{"mesh":0,"translation":[-10,-2,-0]},{"mesh":0,"translation":[-10,-8,-0]},{"mesh":12,"translation":[-11,-0,-0]},{"mesh":13,"translation":[-11,-2,-0]},{"mesh":13,"translation":[-11,-4,-0]},{"mesh":12,"translation":[-11,-6,-0]},{"mesh":14,"translation":[0,0,0]},{"mesh":15,"translation":[0,0,0]}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"scene":0,"scenes":[{"nodes":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57]}],"textures":[{"sampler":0,"source":0}]}