diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 000000000..0ee843cc6 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +7.2.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 374590f41..3c6709c6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ jobs: {os: ubuntu-latest, dist: cp39-manylinux_x86_64}, {os: ubuntu-latest, dist: cp310-manylinux_x86_64}, {os: ubuntu-latest, dist: cp311-manylinux_x86_64}, + {os: ubuntu-latest, dist: cp312-manylinux_x86_64}, {os: ubuntu-latest, dist: cp36-manylinux_i686}, {os: ubuntu-latest, dist: cp37-manylinux_i686}, @@ -92,11 +93,13 @@ jobs: {os: macos-latest, dist: cp39-macosx_x86_64, macosarch: x86_64}, {os: macos-latest, dist: cp310-macosx_x86_64, macosarch: x86_64}, {os: macos-latest, dist: cp311-macosx_x86_64, macosarch: x86_64}, + {os: macos-latest, dist: cp312-macosx_x86_64, macosarch: x86_64}, {os: macos-latest, dist: cp38-macosx_arm64, macosarch: arm64}, {os: macos-latest, dist: cp39-macosx_arm64, macosarch: arm64}, {os: macos-latest, dist: cp310-macosx_arm64, macosarch: arm64}, {os: macos-latest, dist: cp311-macosx_arm64, macosarch: arm64}, + {os: macos-latest, dist: cp312-macosx_arm64, macosarch: arm64}, # pypy OSX builds disabled because numpy isn't prebuilt and fails to build. # @@ -119,6 +122,7 @@ jobs: {os: windows-2019, dist: cp39-win_amd64}, {os: windows-2019, dist: cp310-win_amd64}, {os: windows-2019, dist: cp311-win_amd64}, + {os: windows-2019, dist: cp312-win_amd64}, {os: windows-2019, dist: cp36-win32}, {os: windows-2019, dist: cp37-win32}, @@ -152,7 +156,7 @@ jobs: - run: mkdir -p output/stim - run: mkdir -p output/stimcirq - run: mkdir -p output/sinter - - run: python -m pip install pybind11==2.9.2 cibuildwheel==2.11.1 + - run: python -m pip install pybind11~=2.11.1 cibuildwheel~=2.16.2 setuptools wheel - run: python -m cibuildwheel --print-build-identifiers - run: python -m cibuildwheel --output-dir output/stim - run: python setup.py sdist @@ -173,7 +177,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - - run: python -m pip install pybind11==2.9.2 + - run: python -m pip install pybind11~=2.11.1 cibuildwheel~=2.16.2 setuptools wheel - run: python setup.py sdist - run: pip install dist/*.tar.gz upload_dev_release_to_pypi: @@ -211,7 +215,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - uses: bazelbuild/setup-bazelisk@v1 + - uses: bazel-contrib/setup-bazel@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: @@ -238,7 +247,7 @@ jobs: - 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++17 test.cc out/libstim.a -I src + - run: g++ -std=c++20 test.cc out/libstim.a -I src - run: ./a.out test build_lib_install: runs-on: ubuntu-latest @@ -249,7 +258,7 @@ jobs: - 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++17 test.cc install_dir/lib/libstim.a -I install_dir/include + - 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: @@ -258,8 +267,8 @@ jobs: - uses: actions/checkout@v1 - uses: microsoft/setup-msbuild@v1.0.2 - run: cmake . - - run: MSBuild.exe stim_benchmark.vcxproj /p:Configuration=Release /p:OutDir=msbuild_out /p:O=2 - - run: msbuild_out/stim_benchmark.exe + - 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-latest strategy: @@ -268,8 +277,8 @@ jobs: steps: - uses: actions/checkout@v1 - run: cmake . -DSIMD_WIDTH=${{ matrix.simd_width }} - - run: make stim_benchmark -j 2 - - run: out/stim_benchmark + - run: make stim_perf -j 2 + - run: out/stim_perf test: runs-on: ubuntu-latest strategy: @@ -306,7 +315,12 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - - uses: bazelbuild/setup-bazelisk@v1 + - uses: bazel-contrib/setup-bazel@0.8.5 + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }} + repository-cache: true + bazelisk-version: 1.x - uses: actions/setup-node@v1 with: node-version: 16.x @@ -318,9 +332,10 @@ jobs: - 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/regen_crumble_to_cpp_string_write_to_stdout.sh) src/stim/diagram/crumble_data.cc + - 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: @@ -328,8 +343,8 @@ jobs: steps: - uses: actions/checkout@v3 - run: dev/regen_file_lists.sh /tmp - - run: diff /tmp/benchmark_files file_lists/benchmark_files - - run: diff /tmp/python_api_files file_lists/python_api_files + - 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: @@ -337,7 +352,12 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - - uses: bazelbuild/setup-bazelisk@v1 + - uses: bazel-contrib/setup-bazel@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 @@ -348,7 +368,12 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - - uses: bazelbuild/setup-bazelisk@v1 + - uses: bazel-contrib/setup-bazel@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 @@ -360,7 +385,12 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - - uses: bazelbuild/setup-bazelisk@v1 + - uses: bazel-contrib/setup-bazel@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 @@ -373,7 +403,12 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - - uses: bazelbuild/setup-bazelisk@v1 + - uses: bazel-contrib/setup-bazel@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 @@ -384,7 +419,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: mymindstorm/setup-emsdk@v9 + - uses: mymindstorm/setup-emsdk@v14 with: version: 2.0.18 actions-cache-folder: 'emsdk-cache' diff --git a/BUILD b/BUILD index 6d0d74a76..e08e4f061 100644 --- a/BUILD +++ b/BUILD @@ -44,7 +44,7 @@ cc_library( name = "stim_lib", srcs = SOURCE_FILES_NO_MAIN, copts = [ - "-std=c++17", + "-std=c++20", ], includes = ["src/"], ) @@ -53,7 +53,7 @@ cc_binary( name = "stim", srcs = SOURCE_FILES_NO_MAIN + glob(["src/**/main.cc"]), copts = [ - "-std=c++17", + "-std=c++20", "-march=native", "-O3", ], @@ -64,7 +64,7 @@ cc_binary( name = "stim_benchmark", srcs = SOURCE_FILES_NO_MAIN + PERF_FILES, copts = [ - "-std=c++17", + "-std=c++20", "-march=native", "-O3", ], @@ -75,14 +75,14 @@ cc_test( name = "stim_test", srcs = SOURCE_FILES_NO_MAIN + TEST_FILES, copts = [ - "-std=c++17", + "-std=c++20", "-march=native", ], data = glob(["testdata/**"]), includes = ["src/"], deps = [ - "@gtest", - "@gtest//:gtest_main", + "@googletest//:gtest", + "@googletest//:gtest_main", ], ) @@ -91,7 +91,7 @@ cc_binary( srcs = SOURCE_FILES_NO_MAIN + PYBIND_FILES, copts = [ "-O3", - "-std=c++17", + "-std=c++20", "-fvisibility=hidden", "-march=native", "-DSTIM_PYBIND11_MODULE_NAME=stim", @@ -99,7 +99,14 @@ cc_binary( ], includes = ["src/"], linkshared = 1, - deps = ["@pybind11"], + deps = ["@pybind11//:pybind11"], +) + +genrule( + name = "stim_wheel_files", + srcs = ["doc/stim.pyi"], + outs = ["stim.pyi"], + cmd = "cp $(location doc/stim.pyi) $@", ) py_wheel( @@ -107,5 +114,8 @@ py_wheel( distribution = "stim", requires = ["numpy"], version = "0.0.dev0", - deps = [":stim.so"], + deps = [ + ":stim.so", + ":stim_wheel_files", + ], ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 05ebc200a..53118c80d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,32 +15,37 @@ cmake_minimum_required(VERSION 3.13) project(stim) include_directories(src) -set(CMAKE_CXX_STANDARD 17) +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(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") + +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/benchmark_files) -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS file_lists/python_api_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/benchmark_files BENCHMARK_FILES) -file(STRINGS file_lists/python_api_files PYTHON_API_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)) @@ -63,14 +68,13 @@ endif() install(TARGETS libstim LIBRARY DESTINATION) install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/" DESTINATION "include" FILES_MATCHING PATTERN "*.h" PATTERN "*.inl") -add_executable(stim_benchmark ${SOURCE_FILES_NO_MAIN} ${BENCHMARK_FILES}) +add_executable(stim_perf ${SOURCE_FILES_NO_MAIN} ${PERF_FILES}) if(NOT(MSVC)) - target_compile_options(stim_benchmark PRIVATE -Wall -Wpedantic -O3 -fno-strict-aliasing ${MACHINE_FLAG}) - target_link_options(stim_benchmark PRIVATE) + 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_benchmark PRIVATE ${MACHINE_FLAG}) + target_compile_options(stim_perf PRIVATE ${MACHINE_FLAG}) endif() -install(TARGETS stim_benchmark RUNTIME DESTINATION bin) find_package(GTest QUIET) if(${GTest_FOUND}) @@ -90,7 +94,7 @@ endif() find_package(Python COMPONENTS Interpreter Development) find_package(pybind11 CONFIG) if (${pybind11_FOUND} AND ${Python_FOUND}) - pybind11_add_module(stim_python_bindings ${PYTHON_API_FILES} ${SOURCE_FILES_NO_MAIN}) + 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)) diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 000000000..100de508f --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,2 @@ +bazel_dep(name = "googletest", version = "1.14.0") +bazel_dep(name = "pybind11_bazel", version = "2.11.1") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock new file mode 100644 index 000000000..abc083f01 --- /dev/null +++ b/MODULE.bazel.lock @@ -0,0 +1,116 @@ +{ + "lockFileVersion": 11, + "registryFileHashes": { + "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", + "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/source.json": "06cc0842d241da0c5edc755edb3c7d0d008d304330e57ecf2d6449fb0b633a82", + "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef", + "https://bcr.bazel.build/modules/apple_support/1.5.0/source.json": "eb98a7627c0bc486b57f598ad8da50f6625d974c8f723e9ea71bd39f709c9862", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/source.json": "c9320aa53cd1c441d24bd6b716da087ad7e4ff0d9742a9884587596edfe53015", + "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/source.json": "082ed5f9837901fada8c68c2f3ddc958bb22b6d654f71dd73f3df30d45d4b749", + "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", + "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", + "https://bcr.bazel.build/modules/googletest/1.14.0/source.json": "2478949479000fdd7de9a3d0107ba2c85bb5f961c3ecb1aa448f52549ce310b5", + "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://bcr.bazel.build/modules/platforms/0.0.9/source.json": "cd74d854bf16a9e002fb2ca7b1a421f4403cda29f824a765acd3a8c56f8d43e6", + "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/21.7/source.json": "bbe500720421e582ff2d18b0802464205138c06056f443184de39fbb8187b09b", + "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022", + "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", + "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/source.json": "1f1ba6fea244b616de4a554a0f4983c91a9301640c8fe0dd1d410254115c8430", + "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", + "https://bcr.bazel.build/modules/rules_java/7.6.1/source.json": "8f3f3076554e1558e8e468b2232991c510ecbcbed9e6f8c06ac31c93bcf38362", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/source.json": "a075731e1b46bc8425098512d038d416e966ab19684a10a34f4741295642fc35", + "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://bcr.bazel.build/modules/rules_license/0.0.7/source.json": "355cc5737a0f294e560d52b1b7a6492d4fff2caf0bef1a315df5a298fca2d34a", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c", + "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9", + "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", + "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", + "https://bcr.bazel.build/modules/rules_python/0.22.1/source.json": "57226905e783bae7c37c2dd662be078728e48fa28ee4324a7eabcafb5a43d014", + "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://bcr.bazel.build/modules/stardoc/0.5.1/source.json": "a96f95e02123320aa015b956f29c00cb818fa891ef823d55148e1a362caacf29", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/source.json": "f1ef7d3f9e0e26d4b23d1c39b5f5de71f584dd7d1b4ef83d9bbba6ec7a6a6459", + "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", + "https://bcr.bazel.build/modules/zlib/1.3/MODULE.bazel": "6a9c02f19a24dcedb05572b2381446e27c272cd383aed11d41d99da9e3167a72", + "https://bcr.bazel.build/modules/zlib/1.3/source.json": "b6b43d0737af846022636e6e255fd4a96fee0d34f08f3830e6e0bac51465c37c" + }, + "selectedYankedVersions": {}, + "moduleExtensions": { + "@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": { + "general": { + "bzlTransitiveDigest": "PjIds3feoYE8SGbbIq2SFTZy3zmxeO2tQevJZNDo7iY=", + "usagesDigest": "aLmqbvowmHkkBPve05yyDNGN7oh7QE9kBADr3QIZTZs=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_apple_cc": { + "bzlFile": "@@apple_support~//crosstool:setup.bzl", + "ruleClassName": "_apple_cc_autoconf", + "attributes": {} + }, + "local_config_apple_cc_toolchains": { + "bzlFile": "@@apple_support~//crosstool:setup.bzl", + "ruleClassName": "_apple_cc_autoconf_toolchains", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [ + [ + "apple_support~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@platforms//host:extension.bzl%host_platform": { + "general": { + "bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=", + "usagesDigest": "meSzxn3DUCcYEhq4HQwExWkWtU4EjriRBQLsZN+Q0SU=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "host_platform": { + "bzlFile": "@@platforms//host:extension.bzl", + "ruleClassName": "host_platform_repo", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [] + } + } + } +} diff --git a/WORKSPACE b/WORKSPACE index 9848ac9de..5d05cb0f9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,37 +1,12 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") - -http_archive( - name = "gtest", - sha256 = "353571c2440176ded91c2de6d6cd88ddd41401d14692ec1f99e35d013feda55a", - strip_prefix = "googletest-release-1.11.0", - urls = ["https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip"], -) +load("@pybind11_bazel//:python_configure.bzl", "python_configure") http_archive( name = "pybind11", build_file = "@pybind11_bazel//:pybind11.BUILD", - sha256 = "832e2f309c57da9c1e6d4542dedd34b24e4192ecb4d62f6f4866a737454c9970", - strip_prefix = "pybind11-2.10.4", - urls = ["https://github.com/pybind/pybind11/archive/v2.10.4.tar.gz"], -) - -http_archive( - name = "pybind11_bazel", - sha256 = "b72c5b44135b90d1ffaba51e08240be0b91707ac60bea08bb4d84b47316211bb", - strip_prefix = "pybind11_bazel-b162c7c88a253e3f6b673df0c621aca27596ce6b", - urls = ["https://github.com/pybind/pybind11_bazel/archive/b162c7c88a253e3f6b673df0c621aca27596ce6b.zip"], -) - -http_archive( - name = "rules_python", - sha256 = "84aec9e21cc56fbc7f1335035a71c850d1b9b5cc6ff497306f84cced9a769841", - strip_prefix = "rules_python-0.23.1", - url = "https://github.com/bazelbuild/rules_python/releases/download/0.23.1/rules_python-0.23.1.tar.gz", + sha256 = "bf8f242abd1abcd375d516a7067490fb71abd79519a282d22b6e4d19282185a7", + strip_prefix = "pybind11-2.12.0", + urls = ["https://github.com/pybind/pybind11/archive/refs/tags/v2.12.0.tar.gz"], ) -load("@rules_python//python:repositories.bzl", "py_repositories") -py_repositories() - -load("@pybind11_bazel//:python_configure.bzl", "python_configure") python_configure(name = "local_config_python") diff --git a/dev/canvas_with_texture_for_3d_diagrams.html b/dev/canvas_with_texture_for_3d_diagrams.html index 5104dbb0d..19173679d 100644 --- a/dev/canvas_with_texture_for_3d_diagrams.html +++ b/dev/canvas_with_texture_for_3d_diagrams.html @@ -104,6 +104,19 @@ ctx.fillText('ERASE', x * 32 + 16 - ctx.measureText('ERASE').width / 2, y * 32 + 18); } +function drawCpp(ctx, c1, c2, i) { + let {x, y} = pickRect(i); + ctx.fillStyle = '#' + (c1 === 'X' || c2 === 'X' ? 'f' : '4') + (c1 === 'Y' || c2 === 'Y' ? 'f' : '4') + (c1 === 'Z' || c2 === 'Z' ? 'f' : '4'); + ctx.fillRect(x * 32, y * 32, 32, 32); + ctx.fillStyle = 'black'; + ctx.font = '12pt serif'; + let t1 = 'CPP'; + ctx.fillText(t1, x * 32 + 16 - ctx.measureText(t1).width / 2, y * 32 + 5); + let t2 = c1 + ':' + c2; + ctx.font = '12pt serif'; + ctx.fillText(t2, x * 32 + 16 - ctx.measureText(t2).width / 2, y * 32 + 18); +} + function drawHeraldPauliError1(ctx, i) { let {x, y} = pickRect(i); ctx.fillStyle = 'black'; @@ -203,6 +216,30 @@ drawRect(ctx, "M_PAD", "#888", "#000", n++); drawHeraldErase(ctx, n++); drawHeraldPauliError1(ctx, n++); + + drawRect(ctx, "SPP_X", "#F44", "#000", n++); + drawRect(ctx, "SPP_Y", "#4F4", "#000", n++); + drawRect(ctx, "SPP_Z", "#44F", "#000", n++); + drawRect(ctx, "SPP_X†", "#F44", "#000", n++); + drawRect(ctx, "SPP_Y†", "#4F4", "#000", n++); + drawRect(ctx, "SPP_Z†", "#44F", "#000", n++); + + n = 128 + 48; + drawCpp(ctx, 'I', 'X', n++); + drawCpp(ctx, 'I', 'Y', n++); + drawCpp(ctx, 'I', 'Z', n++); + drawCpp(ctx, 'X', 'I', n++); + drawCpp(ctx, 'X', 'X', n++); + drawCpp(ctx, 'X', 'Y', n++); + drawCpp(ctx, 'X', 'Z', n++); + drawCpp(ctx, 'Y', 'I', n++); + drawCpp(ctx, 'Y', 'X', n++); + drawCpp(ctx, 'Y', 'Y', n++); + drawCpp(ctx, 'Y', 'Z', n++); + drawCpp(ctx, 'Z', 'I', n++); + drawCpp(ctx, 'Z', 'X', n++); + drawCpp(ctx, 'Z', 'Y', n++); + drawCpp(ctx, 'Z', 'Z', n++); } draw(document.getElementById('cv').getContext('2d')) diff --git a/dev/regen_crumble_to_cpp_string_write_to_stdout.sh b/dev/compile_crumble_into_cpp_string_file.sh similarity index 89% rename from dev/regen_crumble_to_cpp_string_write_to_stdout.sh rename to dev/compile_crumble_into_cpp_string_file.sh index 0aa66f843..dd3cca27e 100755 --- a/dev/regen_crumble_to_cpp_string_write_to_stdout.sh +++ b/dev/compile_crumble_into_cpp_string_file.sh @@ -9,7 +9,7 @@ echo '#include "stim/diagram/crumble_data.h"' echo ''; echo 'std::string stim_draw_internal::make_crumble_html() {' echo ' std::string result;' -dev/regen_crumble_html.sh | python -c ' +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): diff --git a/dev/regen_crumble_html.sh b/dev/compile_crumble_into_single_html_page.sh similarity index 86% rename from dev/regen_crumble_html.sh rename to dev/compile_crumble_into_single_html_page.sh index fa52b35c0..a4fe309a2 100755 --- a/dev/regen_crumble_html.sh +++ b/dev/compile_crumble_into_single_html_page.sh @@ -7,7 +7,7 @@ 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 > tmp_crumble.tmp +rollup glue/crumble/main.js --silent > tmp_crumble.tmp uglifyjs -c -m --mangle-props --toplevel < tmp_crumble.tmp; rm tmp_crumble.tmp echo ""; diff --git a/dev/doctest_proper.py b/dev/doctest_proper.py index 59311966f..881efac71 100755 --- a/dev/doctest_proper.py +++ b/dev/doctest_proper.py @@ -89,6 +89,15 @@ def main(): 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 ['__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 diff --git a/dev/gen_known_gates_for_js.sh b/dev/gen_known_gates_for_js.sh new file mode 100755 index 000000000..90b0b5503 --- /dev/null +++ b/dev/gen_known_gates_for_js.sh @@ -0,0 +1,12 @@ +#!/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};" diff --git a/dev/gen_stim_stub_file.py b/dev/gen_stim_stub_file.py index 45676c46b..333a27d0e 100644 --- a/dev/gen_stim_stub_file.py +++ b/dev/gen_stim_stub_file.py @@ -18,7 +18,7 @@ def main(): version = "v" + version print(f''' """Stim {version}: a fast quantum stabilizer circuit library.""" -# (This a stubs file describing the classes and methods in stim.) +# (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 if TYPE_CHECKING: diff --git a/dev/regen_crumble_to_cpp_string.sh b/dev/regen_crumble_cpp_resource.sh similarity index 61% rename from dev/regen_crumble_to_cpp_string.sh rename to dev/regen_crumble_cpp_resource.sh index 574378611..6e8e4a829 100755 --- a/dev/regen_crumble_to_cpp_string.sh +++ b/dev/regen_crumble_cpp_resource.sh @@ -5,4 +5,4 @@ set -e cd "$( dirname "${BASH_SOURCE[0]}" )" cd "$(git rev-parse --show-toplevel)" -dev/regen_crumble_to_cpp_string_write_to_stdout.sh > src/stim/diagram/crumble_data.cc +dev/compile_crumble_into_cpp_string_file.sh > src/stim/diagram/crumble_data.cc diff --git a/dev/regen_docs.sh b/dev/regen_docs.sh index 5cb916235..3f2266ead 100755 --- a/dev/regen_docs.sh +++ b/dev/regen_docs.sh @@ -16,3 +16,4 @@ 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 diff --git a/dev/regen_file_lists.sh b/dev/regen_file_lists.sh index 92c777813..29f27db23 100755 --- a/dev/regen_file_lists.sh +++ b/dev/regen_file_lists.sh @@ -18,8 +18,8 @@ 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}/benchmark_files" -find src | grep "\\.pybind\\.cc$" | LC_ALL=C sort > "${FOLDER}/python_api_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. { diff --git a/dev/util_gen_stub_file.py b/dev/util_gen_stub_file.py index ec0f723c2..26a34530c 100644 --- a/dev/util_gen_stub_file.py +++ b/dev/util_gen_stub_file.py @@ -94,6 +94,12 @@ def __init__(self): 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[') + assert sig.startswith('def') out = [] @@ -252,6 +258,8 @@ def print_doc(*, full_name: str, parent: object, obj: object, level: int) -> Opt 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): @@ -296,7 +304,6 @@ def print_doc(*, full_name: str, parent: object, obj: object, level: int) -> Opt 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): diff --git a/doc/developer_documentation.md b/doc/developer_documentation.md index 89e58acf2..59881ecd0 100644 --- a/doc/developer_documentation.md +++ b/doc/developer_documentation.md @@ -20,7 +20,7 @@ These notes generally assume you are on a Linux system. - [running performance benchmarks](#perf) - [with cmake](#perf.cmake) - [with bazel](#perf.bazel) - - [interpreting output from `stim_benchmark`](#perf.output) + - [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) @@ -166,7 +166,7 @@ bazel run stim find src \ | grep "\\.cc$" \ | grep -v "\\.\(test\|perf\|pybind\)\\.cc$" \ - | xargs g++ -I src -pthread -std=c++17 -O3 -march=native + | xargs g++ -I src -pthread -std=c++20 -O3 -march=native # output binary ends up at: # ./a.out @@ -348,19 +348,19 @@ bazel test stim_test ```bash cmake . -make stim_benchmark -./out/stim_benchmark +make stim_perf +./out/stim_perf ``` ## Running performance benchmarks with bazel ```bash -bazel run stim_benchmark +bazel run stim_perf ``` -## Interpreting output from `stim_benchmark` +## Interpreting output from `stim_perf` -When you run `stim_benchmark` you will see output like: +When you run `stim_perf` you will see output like: ``` [....................*....................] 460 ns (vs 450 ns) ( 21 GBits/s) simd_bits_randomize_10K @@ -377,7 +377,7 @@ Each tick away from the center `|` is 1 decibel slower or faster (i.e. each `<` Basically, if you see `[......*<<<<<<<<<<<<<|....................]` then something is *seriously* wrong, because the code is running 25x slower than expected. -The benchmark binary supports a `--only=BENCHMARK_NAME` filter flag. +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_*`. @@ -387,7 +387,7 @@ Ending a filter with a `*` turns it into a prefix filter `--only=sim_*`. find src \ | grep "\\.cc" \ | grep -v "\\.\(test\|perf\|pybind\)\\.cc" \ - | xargs g++ -I src -pthread -std=c++17 -O3 -march=native -g -fno-omit-frame-pointer + | 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 ``` diff --git a/doc/gates.md b/doc/gates.md index f8020a384..74718d299 100644 --- a/doc/gates.md +++ b/doc/gates.md @@ -26,6 +26,7 @@ - [CXSWAP](#CXSWAP) - [CY](#CY) - [CZ](#CZ) + - [CZSWAP](#CZSWAP) - [ISWAP](#ISWAP) - [ISWAP_DAG](#ISWAP_DAG) - [SQRT_XX](#SQRT_XX) @@ -36,6 +37,7 @@ - [SQRT_ZZ_DAG](#SQRT_ZZ_DAG) - [SWAP](#SWAP) - [SWAPCX](#SWAPCX) + - [SWAPCZ](#SWAPCZ) - [XCX](#XCX) - [XCY](#XCY) - [XCZ](#XCZ) @@ -60,7 +62,6 @@ - [Z_ERROR](#Z_ERROR) - Collapsing Gates - [M](#M) - - [MPP](#MPP) - [MR](#MR) - [MRX](#MRX) - [MRY](#MRY) @@ -76,6 +77,10 @@ - [MXX](#MXX) - [MYY](#MYY) - [MZZ](#MZZ) +- Generalized Pauli Product Gates + - [MPP](#MPP) + - [SPP](#SPP) + - [SPP_DAG](#SPP_DAG) - Control Flow - [REPEAT](#REPEAT) - Annotations @@ -113,11 +118,20 @@ Stabilizer Generators: X -> X Z -> Z -Bloch Rotation: +Bloch Rotation (axis angle): - Axis: - Angle: 0 degrees + 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 , ] @@ -154,11 +168,20 @@ Stabilizer Generators: X -> X Z -> -Z -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +X - Angle: 180 degrees + 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 ] @@ -197,11 +220,20 @@ Stabilizer Generators: X -> -X Z -> -Z -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +Y - Angle: 180 degrees + 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] @@ -243,11 +275,20 @@ Stabilizer Generators: X -> -X Z -> Z -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +Z - Angle: 180 degrees + 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 , ] @@ -286,11 +327,20 @@ Stabilizer Generators: X -> Y Z -> X -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +X+Y+Z - Angle: 120 degrees + 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] @@ -329,11 +379,20 @@ Stabilizer Generators: X -> Z Z -> Y -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +X+Y+Z - Angle: -120 degrees + 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] @@ -373,11 +432,20 @@ Stabilizer Generators: X -> Z Z -> X -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +X+Z - Angle: 180 degrees + 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 ] @@ -415,11 +483,20 @@ Stabilizer Generators: X -> Y Z -> -Z -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +X+Y - Angle: 180 degrees + 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] @@ -459,11 +536,20 @@ Stabilizer Generators: X -> -X Z -> Y -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +Y+Z - Angle: 180 degrees + 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] @@ -506,11 +592,20 @@ Stabilizer Generators: X -> Y Z -> Z -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +Z - Angle: 90 degrees + 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 , ] @@ -549,11 +644,20 @@ Stabilizer Generators: X -> X Z -> -Y -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +X - Angle: 90 degrees + 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] @@ -592,11 +696,20 @@ Stabilizer Generators: X -> X Z -> Y -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +X - Angle: -90 degrees + 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] @@ -635,11 +748,20 @@ Stabilizer Generators: X -> -Z Z -> X -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +Y - Angle: 90 degrees + 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] @@ -678,11 +800,20 @@ Stabilizer Generators: X -> Z Z -> -X -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +Y - Angle: -90 degrees + 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] @@ -723,11 +854,20 @@ Stabilizer Generators: X -> -Y Z -> Z -Bloch Rotation: +Bloch Rotation (axis angle): Axis: +Z - Angle: -90 degrees + 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 , ] @@ -968,6 +1108,51 @@ Decomposition (into H, S, CX, M, R): H 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 + + ### The 'ISWAP' Gate @@ -2253,62 +2438,6 @@ Decomposition (into H, S, CX, M, R): # (The decomposition is trivial because this gate is in the target gate set.) - -### The 'MPP' Instruction - -Measure Pauli products. - -Parens Arguments: - - Optional. - A single float specifying the probability of flipping each reported measurement result. - -Targets: - - A series of Pauli products to measure. - - Each Pauli product is a series of Pauli targets (`[XYZ]#`) separated by combiners (`*`). - Products can be negated by prefixing a Pauli target in the product with an inverter (`!`) - -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 - - ### The 'MR' Instruction @@ -2613,7 +2742,7 @@ Examples: # Reset qubit 5 into the |+> state. RX 5 - # Result multiple qubits into the |+> state. + # Reset multiple qubits into the |+> state. RX 2 3 5 Stabilizer Generators: @@ -2622,7 +2751,6 @@ Stabilizer Generators: Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `RX 0` - H 0 R 0 H 0 @@ -2646,7 +2774,7 @@ Examples: # Reset qubit 5 into the |i> state. RY 5 - # Result multiple qubits into the |i> state. + # Reset multiple qubits into the |i> state. RY 2 3 5 Stabilizer Generators: @@ -2655,10 +2783,6 @@ Stabilizer Generators: Decomposition (into H, S, CX, M, R): # The following circuit is equivalent (up to global phase) to `RY 0` - S 0 - S 0 - S 0 - H 0 R 0 H 0 S 0 @@ -2850,6 +2974,206 @@ Decomposition (into H, S, CX, M, R): CX 0 1 +## 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 + + + +### The 'SPP' Instruction + +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 + + + +### The 'SPP_DAG' Instruction + +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 + + ## Control Flow @@ -2966,6 +3290,12 @@ This can be useful for ensuring measurements are aligned to word boundaries, or 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. diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md index 17bae3a03..4ba62593f 100644 --- a/doc/python_api_reference_vDev.md +++ b/doc/python_api_reference_vDev.md @@ -27,15 +27,22 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`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.num_detectors`](#stim.Circuit.num_detectors) - [`stim.Circuit.num_measurements`](#stim.Circuit.num_measurements) - [`stim.Circuit.num_observables`](#stim.Circuit.num_observables) @@ -44,8 +51,14 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`stim.Circuit.num_ticks`](#stim.Circuit.num_ticks) - [`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.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.CircuitErrorLocation`](#stim.CircuitErrorLocation) @@ -68,6 +81,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`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.targets_copy`](#stim.CircuitInstruction.targets_copy) - [`stim.CircuitRepeatBlock`](#stim.CircuitRepeatBlock) - [`stim.CircuitRepeatBlock.__eq__`](#stim.CircuitRepeatBlock.__eq__) @@ -121,6 +135,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`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__) @@ -170,6 +185,8 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`stim.FlipSimulator`](#stim.FlipSimulator) - [`stim.FlipSimulator.__init__`](#stim.FlipSimulator.__init__) - [`stim.FlipSimulator.batch_size`](#stim.FlipSimulator.batch_size) + - [`stim.FlipSimulator.broadcast_pauli_errors`](#stim.FlipSimulator.broadcast_pauli_errors) + - [`stim.FlipSimulator.copy`](#stim.FlipSimulator.copy) - [`stim.FlipSimulator.do`](#stim.FlipSimulator.do) - [`stim.FlipSimulator.get_detector_flips`](#stim.FlipSimulator.get_detector_flips) - [`stim.FlipSimulator.get_measurement_flips`](#stim.FlipSimulator.get_measurement_flips) @@ -179,11 +196,21 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`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.reset`](#stim.FlipSimulator.reset) - [`stim.FlipSimulator.set_pauli_flip`](#stim.FlipSimulator.set_pauli_flip) - [`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.__ne__`](#stim.Flow.__ne__) + - [`stim.Flow.__repr__`](#stim.Flow.__repr__) + - [`stim.Flow.__str__`](#stim.Flow.__str__) + - [`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__) @@ -191,9 +218,14 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`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) @@ -247,12 +279,17 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`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__) @@ -288,6 +325,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`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) @@ -374,9 +412,11 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`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) @@ -1248,6 +1288,213 @@ def count_determined_measurements( """ ``` + +```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 @@ -1302,7 +1549,7 @@ def detector_error_model( 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 `Z_ERROR(0.2)`. This assumption is 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 @@ -1355,86 +1602,6 @@ def detector_error_model( # stim.Circuit.diagram # (in class stim.Circuit) -@overload -def diagram( - self, - type: 'Literal["timeline-text"]', -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["timeline-svg"]', - *, - tick: Union[None, int, range] = None, -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["timeline-3d", "timeline-3d-html"]', -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["matchgraph-svg"]', -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["matchgraph-3d"]', -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["matchgraph-3d-html"]', -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["detslice-text"]', - *, - tick: int, - filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["detslice-svg"]', - *, - tick: Union[int, range], - filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["detslice-with-ops-svg"]', - *, - tick: Union[int, range], - filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["timeslice-svg"]', - *, - tick: Union[int, range], - filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["interactive", "interactive-html"]', -) -> 'stim._DiagramHelper': - pass def diagram( self, type: str = 'timeline-text', @@ -1455,6 +1622,11 @@ def diagram( 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 @@ -1473,8 +1645,12 @@ def diagram( 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 @@ -1483,10 +1659,14 @@ def diagram( "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 @@ -1502,11 +1682,19 @@ def diagram( Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. - filter_coords: A set of acceptable coordinate prefixes, or - desired stim.DemTargets. For detector slice diagrams, only - detectors match one of the filters are included. If no filter - is specified, all detectors are included (but no observables). - To include an observable, add it as one of the filters. + 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 @@ -1652,6 +1840,64 @@ def flattened( """ ``` + +```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 @@ -1874,101 +2120,432 @@ def get_final_qubit_coordinates( """ ``` - + ```python -# stim.Circuit.inverse +# stim.Circuit.has_all_flows # (in class stim.Circuit) -def inverse( +def has_all_flows( self, -) -> stim.Circuit: - """Returns a circuit that applies the same operations but inverted and in reverse. + flows: Iterable[stim.Flow], + *, + unsigned: bool = False, +) -> bool: + """Determines if the circuit has all the given stabilizer flow or not. - 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. + 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. - Returns: - A `stim.Circuit` that applies inverted operations in the reverse order. + This method ignores any noise in the circuit. - 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. + 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(''' - ... S 0 1 - ... ISWAP 0 1 1 2 - ... ''').inverse() - stim.Circuit(''' - ISWAP_DAG 1 2 0 1 - S_DAG 1 0 - ''') + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ]) + False - >>> 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 - ''') + >>> 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.num_detectors +# stim.Circuit.has_flow # (in class stim.Circuit) -@property -def num_detectors( +def has_flow( self, -) -> int: - """Counts the number of bits produced when sampling the circuit's detectors. + flow: stim.Flow, + *, + unsigned: bool = False, +) -> bool: + """Determines if the circuit has the given stabilizer flow or not. - 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 - """ -``` + 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. - -```python -# stim.Circuit.num_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). -# (in class stim.Circuit) -@property -def num_measurements( - self, -) -> int: - """Counts the number of bits produced when sampling the circuit's measurements. + 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://maxhs.org/docs/wdimacs.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.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 @@ -2134,6 +2711,14 @@ def reference_sample( Returns: reference_sample: reference sample sampled from the given circuit. + + Examples: + >>> import stim + >>> stim.Circuit(''' + ... X 1 + ... M 0 1 + ... ''').reference_sample() + array([False, True]) """ ``` @@ -2237,6 +2822,80 @@ def search_for_undetectable_logical_errors( """ ``` + +```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://maxhs.org/docs/wdimacs.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 @@ -2314,6 +2973,246 @@ def shortest_graphlike_error( """ ``` + +```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, +) -> 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. + + 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' + """ +``` + ```python # stim.Circuit.to_file @@ -2355,6 +3254,165 @@ def to_file( """ ``` + +```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 operations (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 @@ -2457,6 +3515,26 @@ def without_noise( # (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 + } """ ``` @@ -2475,6 +3553,48 @@ def __init__( stack_frames: List[stim.CircuitErrorLocationStackFrame], ) -> 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, + ... ), + ... ), + ... ) + >>> print(err) + 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 + } """ ``` @@ -2488,7 +3608,21 @@ 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), []),), + ) """ ``` @@ -2502,7 +3636,19 @@ 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), [])] """ ``` @@ -2530,8 +3676,26 @@ def instruction_targets( def stack_frames( self, ) -> List[stim.CircuitErrorLocationStackFrame]: - """Where in the circuit's execution does the error mechanism occur, - accounting for things like nested loops that iterate multiple times. + """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, + )] """ ``` @@ -2544,8 +3708,23 @@ def stack_frames( def tick_offset( self, ) -> int: - """The number of TICKs that executed before the error mechanism being discussed, - including TICKs that occurred multiple times during loops. + """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 """ ``` @@ -2754,6 +3933,34 @@ def name( """ ``` + +```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]).num_measurements + 1 + """ +``` + ```python # stim.CircuitInstruction.targets_copy @@ -2889,7 +4096,7 @@ def body_copy( @property def name( self, -) -> object: +) -> str: """Returns the name "REPEAT". This is a duck-typing convenience method. It exists so that code that doesn't @@ -3396,25 +4603,6 @@ def __repr__( # stim.CompiledDetectorSampler.sample # (in class stim.CompiledDetectorSampler) -@overload -def sample( - self, - shots: int, - *, - prepend_observables: bool = False, - append_observables: bool = False, - bit_packed: bool = False, -) -> np.ndarray: - pass -@overload -def sample( - self, - shots: int, - *, - separate_observables: Literal[True], - bit_packed: bool = False, -) -> Tuple[np.ndarray, np.ndarray]: - pass def sample( self, shots: int, @@ -3423,6 +4611,8 @@ def sample( 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. @@ -3441,6 +4631,12 @@ def sample( 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. @@ -3500,12 +4696,12 @@ def sample_write( self, shots: int, *, - filepath: str, - format: str = '01', + 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, - obs_out_filepath: str = None, - obs_out_format: str = '01', ) -> None: """Samples detection events from the circuit and writes them to a file. @@ -4118,7 +5314,24 @@ def __str__( def args_copy( self, ) -> List[float]: - """Returns a copy of the list of numbers parameterizing the instruction (e.g. the probability of an error). + """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 """ ``` @@ -4132,8 +5345,21 @@ def targets_copy( ) -> List[Union[int, stim.DemTarget]]: """Returns a copy of the instruction's targets. - (Making a copy is enforced to make it clear that editing the result won't change - 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 """ ``` @@ -4248,6 +5474,18 @@ 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 """ ``` @@ -4317,6 +5555,33 @@ def __eq__( """ ``` + +```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__ @@ -4366,6 +5631,15 @@ def is_logical_observable_id( 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 """ ``` @@ -4381,6 +5655,15 @@ def is_relative_detector_id( 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 """ ``` @@ -4396,6 +5679,15 @@ def is_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 """ ``` @@ -4496,11 +5788,10 @@ def val( """Returns the target's integer value. Example: - >>> import stim - >>> stim.target_relative_detector_id(5).val + >>> stim.DemTarget("D5").val 5 - >>> stim.target_logical_observable_id(6).val + >>> stim.DemTarget("L6").val 6 """ ``` @@ -5215,24 +6506,6 @@ def copy( # stim.DetectorErrorModel.diagram # (in class stim.DetectorErrorModel) -@overload -def diagram( - self, - type: 'Literal["matchgraph-svg"]', -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["matchgraph-3d"]', -) -> 'stim._DiagramHelper': - pass -@overload -def diagram( - self, - type: 'Literal["matchgraph-3d-html"]', -) -> 'stim._DiagramHelper': - pass def diagram( self, type: str, @@ -5245,6 +6518,8 @@ def diagram( 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 @@ -5276,12 +6551,12 @@ def diagram( >>> dem = circuit.detector_error_model(decompose_errors=True) >>> with tempfile.TemporaryDirectory() as d: - ... diagram = circuit.diagram(type="match-graph-svg") + ... 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(type="match-graph-3d") + ... diagram = circuit.diagram("match-graph-3d") ... with open(f"{d}/dem_3d_model.gltf", "w") as f: ... print(diagram, file=f) """ @@ -5824,17 +7099,156 @@ def __init__( 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. + 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.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, +) -> 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. + + 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.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 is seeded from system entropy. + 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 @@ -5857,33 +7271,26 @@ def __init__( seed. Returns: - An initialized stim.FlipSimulator. + The copy of the simulator. Examples: >>> import stim - >>> sim = stim.FlipSimulator(batch_size=256) - """ -``` - - -```python -# stim.FlipSimulator.batch_size + >>> import numpy as np -# (in class stim.FlipSimulator) -@property -def batch_size( - self, -) -> int: - """Returns the number of instances being simulated by the simulator. + >>> 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 - Examples: - >>> import stim - >>> sim = stim.FlipSimulator(batch_size=256) - >>> sim.batch_size - 256 - >>> sim = stim.FlipSimulator(batch_size=42) - >>> sim.batch_size - 42 + >>> 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 """ ``` @@ -6323,6 +7730,38 @@ def peek_pauli_flips( """ ``` + +```python +# stim.FlipSimulator.reset + +# (in class stim.FlipSimulator) +def reset( + self, +) -> None: + """Resets the simulator's state, so it can be reused for another simulation. + + This empties the measurement flip history, empties 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 flips data. Otherwise + it randomizes all pauli flips to be I or Z with equal probability. + + 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.reset() + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (0, 256) + """ +``` + ```python # stim.FlipSimulator.set_pauli_flip @@ -6379,11 +7818,21 @@ class FlippedMeasurement: # (in class stim.FlippedMeasurement) def __init__( self, - *, - record_index: int, - observable: object, -) -> None: + 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=(), + ) """ ``` @@ -6417,6 +7866,219 @@ def record_index( """ ``` + +```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, +) -> 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]. + + Arguments: + 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: 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. + + 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") + """ +``` + + +```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.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 @@ -6534,6 +8196,210 @@ def aliases( """ ``` + +```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(unsigned=True) + stim.gate_data('H_YZ') + >>> 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 @@ -6663,6 +8529,62 @@ def is_single_qubit_gate( """ ``` + +```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 @@ -6679,6 +8601,10 @@ def is_two_qubit_gate( 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 @@ -7426,7 +9352,6 @@ class GateTargetWithCoords: # (in class stim.GateTargetWithCoords) def __init__( self, - *, gate_target: object, coords: List[float], ) -> None: @@ -7664,90 +9589,57 @@ def __imul__( # stim.PauliString.__init__ # (in class stim.PauliString) -@staticmethod def __init__( - *args, - **kwargs, -): - """Overloaded function. - - 1. __init__(self: stim.PauliString, num_qubits: int) -> None - - Creates an identity Pauli string over the given number of qubits. - - Examples: - >>> import stim - >>> p = stim.PauliString(5) - >>> print(p) - +_____ - - Args: - num_qubits: The number of qubits the Pauli string acts on. - - - 2. __init__(self: stim.PauliString, text: str) -> None + 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. - Creates a stim.PauliString from a text string. + 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'. - The string can optionally start with a sign ('+', '-', 'i', '+i', or '-i'). - The rest of the string should be characters from '_IXYZ' where - '_' and 'I' mean identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means - Pauli Z. + Arguments: + 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. Examples: >>> import stim - >>> print(stim.PauliString("YZ")) - +YZ - >>> print(stim.PauliString("+IXYZ")) - +_XYZ - >>> print(stim.PauliString("-___X_")) - -___X_ - >>> print(stim.PauliString("iX")) - +iX - - Args: - text: A text description of the Pauli string's contents, such as "+XXX" or - "-_YX" or "-iZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZY". - - Returns: - The created stim.PauliString. - - 3. __init__(self: stim.PauliString, copy: stim.PauliString) -> None + >>> stim.PauliString("-XYZ") + stim.PauliString("-XYZ") - Creates a copy of a stim.PauliString. + >>> stim.PauliString() + stim.PauliString("+") - Examples: - >>> import stim - >>> a = stim.PauliString("YZ") - >>> b = stim.PauliString(a) - >>> b is a - False - >>> b == a - True + >>> stim.PauliString(5) + stim.PauliString("+_____") - Args: - copy: The pauli string to make a copy of. - - - 4. __init__(self: stim.PauliString, pauli_indices: List[int]) -> None + >>> stim.PauliString(stim.PauliString("XX")) + stim.PauliString("+XX") - Creates a stim.PauliString from a list of integer pauli indices. + >>> stim.PauliString([0, 1, 3, 2]) + stim.PauliString("+_XZY") - The indexing scheme that is used is: - 0 -> I - 1 -> X - 2 -> Y - 3 -> Z + >>> stim.PauliString("X" for _ in range(4)) + stim.PauliString("+XXXX") - Examples: - >>> import stim - >>> stim.PauliString([0, 1, 2, 3, 0, 3]) - stim.PauliString("+_XYZ_Z") + >>> stim.PauliString("-X2*Y6") + stim.PauliString("-__X___Y") - Args: - pauli_indices: A sequence of integers from 0 to 3 (inclusive) indicating - paulis. + >>> stim.PauliString("X6*Y6") + stim.PauliString("+i______Z") """ ``` @@ -7790,6 +9682,13 @@ 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 """ ``` @@ -8117,19 +10016,19 @@ def after( # (in class stim.PauliString) @overload -def after( +def before( self, operation: Union[stim.Circuit, stim.CircuitInstruction], ) -> stim.PauliString: pass @overload -def after( +def before( self, operation: stim.Tableau, targets: Iterable[int], ) -> stim.PauliString: pass -def after( +def before( self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None, @@ -8293,7 +10192,7 @@ def from_numpy( # (in class stim.PauliString) @staticmethod def from_unitary_matrix( - matrix: Iterable[Iterable[float]], + matrix: Iterable[Iterable[Union[int, float, complex]]], *, endian: str = 'little', unsigned: bool = False, @@ -8346,6 +10245,112 @@ def from_unitary_matrix( """ ``` + +```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 @@ -8577,6 +10582,59 @@ def weight( """ ``` + +```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 @@ -8761,6 +10819,12 @@ 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 """ ``` @@ -9067,8 +11131,13 @@ def from_named_gate( def from_numpy( self, *, - bit_packed: bool = False, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + 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 @@ -9120,7 +11189,7 @@ def from_numpy( ... x2z=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... z2z=np.array([[1, 0], [1, 1]], dtype=np.bool_), ... ) - >>> print(repr(tableau)) + >>> tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), @@ -9134,7 +11203,7 @@ def from_numpy( >>> tableau == stim.Tableau.from_named_gate("CNOT") True - >>> tableau = stim.Tableau.from_numpy( + >>> 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), @@ -9142,7 +11211,6 @@ def from_numpy( ... x_signs=np.array([7], dtype=np.uint8), ... z_signs=np.array([9], dtype=np.uint8), ... ) - >>> print(repr(tableau)) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-Y_ZY"), @@ -9840,8 +11908,7 @@ def then( # (in class stim.Tableau) def to_circuit( self, - *, - method: str = 'elimination', + method: 'Literal["elimination", "graph_state"]' = 'elimination', ) -> stim.Circuit: """Synthesizes a circuit that implements the tableau's Clifford operation. @@ -9855,7 +11922,47 @@ def to_circuit( 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. @@ -9863,35 +11970,65 @@ def to_circuit( >>> import stim >>> tableau = stim.Tableau.from_conjugated_generators( ... xs=[ - ... stim.PauliString("-_YZ"), - ... stim.PauliString("-YY_"), - ... stim.PauliString("-XZX"), + ... stim.PauliString("+YZ__"), + ... stim.PauliString("-Y_XY"), + ... stim.PauliString("+___Y"), + ... stim.PauliString("+YZX_"), ... ], ... zs=[ - ... stim.PauliString("+Y_Y"), - ... stim.PauliString("-_XY"), - ... stim.PauliString("-Y__"), + ... stim.PauliString("+XZYY"), + ... stim.PauliString("-XYX_"), + ... stim.PauliString("-ZXXZ"), + ... stim.PauliString("+XXZ_"), ... ], ... ) - >>> tableau.to_circuit(method="elimination") + + >>> tableau.to_circuit() stim.Circuit(''' - CX 2 0 0 2 2 0 - S 0 - H 0 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 - CX 0 1 0 2 - H 1 2 - CX 1 0 2 0 2 1 1 2 2 1 - H 1 - S 1 2 - H 2 - CX 2 1 - S 2 - H 0 1 2 + 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 1 1 2 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 ''') """ ``` @@ -10102,6 +12239,62 @@ def to_pauli_string( """ ``` + +```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 @@ -12285,15 +14478,18 @@ def state_vector( >>> import numpy as np >>> s = stim.TableauSimulator() >>> s.x(2) - >>> list(s.state_vector(endian='little')) - [0j, 0j, 0j, 0j, (1+0j), 0j, 0j, 0j] + >>> 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) - >>> list(s.state_vector(endian='big')) - [0j, (1+0j), 0j, 0j, 0j, 0j, 0j, 0j] + >>> 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) - >>> list(s.state_vector()) - [(0.5+0j), 0j, -0.5j, 0j, 0.5j, 0j, (0.5+0j), 0j] + >>> 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) """ ``` @@ -12805,6 +15001,40 @@ def read_shot_data_file( """ ``` + +```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 @@ -12889,6 +15119,52 @@ def target_logical_observable_id( """ ``` + +```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 diff --git a/doc/stim.pyi b/doc/stim.pyi index 3b2def10e..cd232b6a7 100644 --- a/doc/stim.pyi +++ b/doc/stim.pyi @@ -1,5 +1,5 @@ """Stim (Development Version): a fast quantum stabilizer circuit library.""" -# (This a stubs file describing the classes and methods in stim.) +# (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 if TYPE_CHECKING: @@ -716,6 +716,199 @@ class Circuit: >>> 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, *, @@ -765,7 +958,7 @@ class Circuit: 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 `Z_ERROR(0.2)`. This assumption is 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 @@ -811,86 +1004,6 @@ class Circuit: error(0.25) D1 ''') """ - @overload - def diagram( - self, - type: 'Literal["timeline-text"]', - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["timeline-svg"]', - *, - tick: Union[None, int, range] = None, - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["timeline-3d", "timeline-3d-html"]', - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["matchgraph-svg"]', - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["matchgraph-3d"]', - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["matchgraph-3d-html"]', - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["detslice-text"]', - *, - tick: int, - filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["detslice-svg"]', - *, - tick: Union[int, range], - filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["detslice-with-ops-svg"]', - *, - tick: Union[int, range], - filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["timeslice-svg"]', - *, - tick: Union[int, range], - filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["interactive", "interactive-html"]', - ) -> 'stim._DiagramHelper': - pass def diagram( self, type: str = 'timeline-text', @@ -911,6 +1024,11 @@ class Circuit: 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 @@ -929,8 +1047,12 @@ class Circuit: 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 @@ -939,10 +1061,14 @@ class Circuit: "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 @@ -958,11 +1084,19 @@ class Circuit: Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. - filter_coords: A set of acceptable coordinate prefixes, or - desired stim.DemTargets. For detector slice diagrams, only - detectors match one of the filters are included. If no filter - is specified, all detectors are included (but no observables). - To include an observable, add it as one of the filters. + 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 @@ -1117,6 +1251,57 @@ class Circuit: ... ''').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], @@ -1311,6 +1496,230 @@ class Circuit: >>> 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: @@ -1362,6 +1771,85 @@ class Circuit: 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://maxhs.org/docs/wdimacs.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 + """ @property def num_detectors( self, @@ -1517,6 +2005,14 @@ class Circuit: Returns: reference_sample: reference sample sampled from the given circuit. + + Examples: + >>> import stim + >>> stim.Circuit(''' + ... X 1 + ... M 0 1 + ... ''').reference_sample() + array([False, True]) """ def search_for_undetectable_logical_errors( self, @@ -1611,6 +2107,73 @@ class Circuit: ... ))) 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://maxhs.org/docs/wdimacs.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, *, @@ -1623,63 +2186,289 @@ class Circuit: (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. + 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 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]")] - 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. + >>> 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 + [] - 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. + >>> 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")] - 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. + >>> 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, + ) -> str: + """Returns a URL that opens up crumble and loads this circuit into it. - 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. + 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. 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. + A URL that can be opened in a web browser. 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 + >>> stim.Circuit(''' + ... H 0 + ... CNOT 0 1 + ... S 1 + ... ''').to_crumble_url() + 'https://algassert.com/crumble#circuit=H_0;CX_0_1;S_1' """ def to_file( self, @@ -1715,6 +2504,144 @@ class Circuit: >>> 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 operations (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: @@ -1798,6 +2725,26 @@ class Circuit: """ 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, @@ -1809,20 +2756,88 @@ class CircuitErrorLocation: stack_frames: List[stim.CircuitErrorLocationStackFrame], ) -> 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, + ... ), + ... ), + ... ) + >>> print(err) + 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 + } """ @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( @@ -1836,15 +2851,48 @@ class CircuitErrorLocation: def stack_frames( self, ) -> List[stim.CircuitErrorLocationStackFrame]: - """Where in the circuit's execution does the error mechanism occur, - accounting for things like nested loops that iterate multiple times. + """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 mechanism being discussed, - including TICKs that occurred multiple times during loops. + """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 @@ -1960,6 +3008,27 @@ class CircuitInstruction: ) -> 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]).num_measurements + 1 + """ def targets_copy( self, ) -> List[stim.GateTarget]: @@ -2041,7 +3110,7 @@ class CircuitRepeatBlock: @property def name( self, - ) -> object: + ) -> str: """Returns the name "REPEAT". This is a duck-typing convenience method. It exists so that code that doesn't @@ -2443,25 +3512,6 @@ class CompiledDetectorSampler: ) -> str: """Returns valid python code evaluating to an equivalent `stim.CompiledDetectorSampler`. """ - @overload - def sample( - self, - shots: int, - *, - prepend_observables: bool = False, - append_observables: bool = False, - bit_packed: bool = False, - ) -> np.ndarray: - pass - @overload - def sample( - self, - shots: int, - *, - separate_observables: Literal[True], - bit_packed: bool = False, - ) -> Tuple[np.ndarray, np.ndarray]: - pass def sample( self, shots: int, @@ -2470,6 +3520,8 @@ class CompiledDetectorSampler: 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. @@ -2488,6 +3540,12 @@ class CompiledDetectorSampler: 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. @@ -2568,12 +3626,12 @@ class CompiledDetectorSampler: self, shots: int, *, - filepath: str, - format: str = '01', + 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, - obs_out_filepath: str = None, - obs_out_format: str = '01', ) -> None: """Samples detection events from the circuit and writes them to a file. @@ -3095,15 +4153,45 @@ class DemInstruction: def args_copy( self, ) -> List[float]: - """Returns a copy of the list of numbers parameterizing the instruction (e.g. the probability of an error). + """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 """ def targets_copy( self, ) -> List[Union[int, stim.DemTarget]]: """Returns a copy of the instruction's targets. - (Making a copy is enforced to make it clear that editing the result won't change - 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( @@ -3169,6 +4257,18 @@ class DemRepeatBlock: 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( @@ -3208,6 +4308,26 @@ class 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, @@ -3231,6 +4351,15 @@ class DemTarget: 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, @@ -3239,6 +4368,15 @@ class DemTarget: 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, @@ -3247,6 +4385,15 @@ class DemTarget: 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( @@ -3319,11 +4466,10 @@ class DemTarget: """Returns the target's integer value. Example: - >>> import stim - >>> stim.target_relative_detector_id(5).val + >>> stim.DemTarget("D5").val 5 - >>> stim.target_logical_observable_id(6).val + >>> stim.DemTarget("L6").val 6 """ class DemTargetWithCoords: @@ -3877,24 +5023,6 @@ class DetectorErrorModel: >>> c2 == c1 True """ - @overload - def diagram( - self, - type: 'Literal["matchgraph-svg"]', - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["matchgraph-3d"]', - ) -> 'stim._DiagramHelper': - pass - @overload - def diagram( - self, - type: 'Literal["matchgraph-3d-html"]', - ) -> 'stim._DiagramHelper': - pass def diagram( self, type: str, @@ -3907,6 +5035,8 @@ class DetectorErrorModel: 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 @@ -3938,12 +5068,12 @@ class DetectorErrorModel: >>> dem = circuit.detector_error_model(decompose_errors=True) >>> with tempfile.TemporaryDirectory() as d: - ... diagram = circuit.diagram(type="match-graph-svg") + ... 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(type="match-graph-3d") + ... diagram = circuit.diagram("match-graph-3d") ... with open(f"{d}/dem_3d_model.gltf", "w") as f: ... print(diagram, file=f) """ @@ -4435,6 +5565,124 @@ class FlipSimulator: >>> sim.batch_size 42 """ + def broadcast_pauli_errors( + self, + *, + pauli: Union[str, int], + mask: np.ndarray, + ) -> 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. + + 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 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], @@ -4808,6 +6056,31 @@ class FlipSimulator: >>> sorted(set(str(flips))) # Should have Zs from stabilizer randomization ['+', 'Z', '_'] """ + def reset( + self, + ) -> None: + """Resets the simulator's state, so it can be reused for another simulation. + + This empties the measurement flip history, empties 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 flips data. Otherwise + it randomizes all pauli flips to be I or Z with equal probability. + + 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.reset() + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (0, 256) + """ def set_pauli_flip( self, pauli: Union[str, int], @@ -4845,11 +6118,21 @@ class FlippedMeasurement: """ def __init__( self, - *, - record_index: int, - observable: object, - ) -> None: + 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( @@ -4867,6 +6150,156 @@ class FlippedMeasurement: For example, the fifth measurement in a circuit has a measurement record index of 4. """ +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, + ) -> 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]. + + Arguments: + 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: 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. + + 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") + """ + 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 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. @@ -4920,61 +6353,196 @@ class GateData: """Returns text describing the gate data. """ @property - def __unstable_flows( + 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, - ) -> object: - """[DEPRECATED] - This method is not actually deprecated it's just still in development. - Its API is not yet finalized and is subject to sudden change. - --- + ) -> stim.GateData: + """The closest-thing-to-an-inverse for the gate, if forced to pick something. - Returns the stabilizer flows of the gate, or else None. + The generalized inverse of a unitary gate U is its actual inverse U^-1. - Although some variable-qubit gates and and pauli-targeting gates - like MPP have stabilizer flows, this method returns None for them - because it does not have the necessary context. + 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]. - A stabilizer flow describes an input-output relationship that the - gate satisfies, where an input PauliString is transformed into an - output PauliString possibly mediated by measurement results. + 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 - >>> for e in stim.gate_data('H').__unstable_flows: - ... print(e) - +X -> +Z - +Z -> +X + >>> stim.gate_data('H').generalized_inverse + stim.gate_data('H') - >>> for e in stim.gate_data('ISWAP').__unstable_flows: - ... print(e) - +X_ -> +ZY - +Z_ -> +_Z - +_X -> +YZ - +_Z -> +Z_ + >>> stim.gate_data('CXSWAP').generalized_inverse + stim.gate_data('SWAPCX') - >>> for e in stim.gate_data('MXX').__unstable_flows: - ... print(e) - +X_ -> +X_ - +_X -> +_X - +ZZ -> +ZZ - +XX -> rec[-1] + >>> 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(unsigned=True) + stim.gate_data('H_YZ') + >>> stim.gate_data('DETECTOR').hadamard_conjugated(unsigned=True) + stim.gate_data('DETECTOR') """ @property - def aliases( + def inverse( self, - ) -> List[str]: - """Returns all aliases that can be used to name the gate. + ) -> Optional[stim.GateData]: + """The inverse of the gate, or None if it has no inverse. - Although gates can be referred to by lower case and mixed - case named, the result only includes upper cased aliases. + 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').aliases - ['H', 'H_XZ'] - >>> stim.gate_data('cnot').aliases - ['CNOT', 'CX', 'ZCX'] + + >>> 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( @@ -5085,6 +6653,55 @@ class GateData: 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: @@ -5095,6 +6712,10 @@ class GateData: 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 @@ -5660,7 +7281,6 @@ class GateTargetWithCoords: """ def __init__( self, - *, gate_target: object, coords: List[float], ) -> None: @@ -5835,90 +7455,57 @@ class PauliString: Returns: The mutated Pauli string. """ - @staticmethod def __init__( - *args, - **kwargs, - ): - """Overloaded function. - - 1. __init__(self: stim.PauliString, num_qubits: int) -> None - - Creates an identity Pauli string over the given number of qubits. - - Examples: - >>> import stim - >>> p = stim.PauliString(5) - >>> print(p) - +_____ - - Args: - num_qubits: The number of qubits the Pauli string acts on. - - - 2. __init__(self: stim.PauliString, text: str) -> None + 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. - Creates a stim.PauliString from a text string. + 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'. - The string can optionally start with a sign ('+', '-', 'i', '+i', or '-i'). - The rest of the string should be characters from '_IXYZ' where - '_' and 'I' mean identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means - Pauli Z. + Arguments: + 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. Examples: >>> import stim - >>> print(stim.PauliString("YZ")) - +YZ - >>> print(stim.PauliString("+IXYZ")) - +_XYZ - >>> print(stim.PauliString("-___X_")) - -___X_ - >>> print(stim.PauliString("iX")) - +iX - - Args: - text: A text description of the Pauli string's contents, such as "+XXX" or - "-_YX" or "-iZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZY". - Returns: - The created stim.PauliString. - - - 3. __init__(self: stim.PauliString, copy: stim.PauliString) -> None - - Creates a copy of a stim.PauliString. - - Examples: - >>> import stim - >>> a = stim.PauliString("YZ") - >>> b = stim.PauliString(a) - >>> b is a - False - >>> b == a - True + >>> stim.PauliString("-XYZ") + stim.PauliString("-XYZ") - Args: - copy: The pauli string to make a copy of. + >>> stim.PauliString() + stim.PauliString("+") + >>> stim.PauliString(5) + stim.PauliString("+_____") - 4. __init__(self: stim.PauliString, pauli_indices: List[int]) -> None + >>> stim.PauliString(stim.PauliString("XX")) + stim.PauliString("+XX") - Creates a stim.PauliString from a list of integer pauli indices. + >>> stim.PauliString([0, 1, 3, 2]) + stim.PauliString("+_XZY") - The indexing scheme that is used is: - 0 -> I - 1 -> X - 2 -> Y - 3 -> Z + >>> stim.PauliString("X" for _ in range(4)) + stim.PauliString("+XXXX") - Examples: - >>> import stim - >>> stim.PauliString([0, 1, 2, 3, 0, 3]) - stim.PauliString("+_XYZ_Z") + >>> stim.PauliString("-X2*Y6") + stim.PauliString("-__X___Y") - Args: - pauli_indices: A sequence of integers from 0 to 3 (inclusive) indicating - paulis. + >>> stim.PauliString("X6*Y6") + stim.PauliString("+i______Z") """ def __itruediv__( self, @@ -5947,6 +7534,13 @@ class PauliString: 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, @@ -6197,19 +7791,19 @@ class PauliString: string before the operation. """ @overload - def after( + def before( self, operation: Union[stim.Circuit, stim.CircuitInstruction], ) -> stim.PauliString: pass @overload - def after( + def before( self, operation: stim.Tableau, targets: Iterable[int], ) -> stim.PauliString: pass - def after( + def before( self, operation: Union[stim.Circuit, stim.Tableau, stim.CircuitInstruction], targets: Optional[Iterable[int]] = None, @@ -6351,7 +7945,7 @@ class PauliString: """ @staticmethod def from_unitary_matrix( - matrix: Iterable[Iterable[float]], + matrix: Iterable[Iterable[Union[int, float, complex]]], *, endian: str = 'little', unsigned: bool = False, @@ -6403,6 +7997,98 @@ class PauliString: 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, *, @@ -6591,6 +8277,38 @@ class PauliString: >>> 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. @@ -6728,6 +8446,12 @@ class Tableau: 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, @@ -6957,8 +8681,13 @@ class Tableau: def from_numpy( self, *, - bit_packed: bool = False, - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + 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 @@ -7010,7 +8739,7 @@ class Tableau: ... x2z=np.array([[0, 0], [0, 0]], dtype=np.bool_), ... z2z=np.array([[1, 0], [1, 1]], dtype=np.bool_), ... ) - >>> print(repr(tableau)) + >>> tableau stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("+XX"), @@ -7024,7 +8753,7 @@ class Tableau: >>> tableau == stim.Tableau.from_named_gate("CNOT") True - >>> tableau = stim.Tableau.from_numpy( + >>> 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), @@ -7032,7 +8761,6 @@ class Tableau: ... x_signs=np.array([7], dtype=np.uint8), ... z_signs=np.array([9], dtype=np.uint8), ... ) - >>> print(repr(tableau)) stim.Tableau.from_conjugated_generators( xs=[ stim.PauliString("-Y_ZY"), @@ -7625,8 +9353,7 @@ class Tableau: """ def to_circuit( self, - *, - method: str = 'elimination', + method: 'Literal["elimination", "graph_state"]' = 'elimination', ) -> stim.Circuit: """Synthesizes a circuit that implements the tableau's Clifford operation. @@ -7640,7 +9367,47 @@ class Tableau: 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. @@ -7648,35 +9415,65 @@ class Tableau: >>> import stim >>> tableau = stim.Tableau.from_conjugated_generators( ... xs=[ - ... stim.PauliString("-_YZ"), - ... stim.PauliString("-YY_"), - ... stim.PauliString("-XZX"), + ... stim.PauliString("+YZ__"), + ... stim.PauliString("-Y_XY"), + ... stim.PauliString("+___Y"), + ... stim.PauliString("+YZX_"), ... ], ... zs=[ - ... stim.PauliString("+Y_Y"), - ... stim.PauliString("-_XY"), - ... stim.PauliString("-Y__"), + ... stim.PauliString("+XZYY"), + ... stim.PauliString("-XYX_"), + ... stim.PauliString("-ZXXZ"), + ... stim.PauliString("+XXZ_"), ... ], ... ) - >>> tableau.to_circuit(method="elimination") + + >>> tableau.to_circuit() stim.Circuit(''' - CX 2 0 0 2 2 0 S 0 - H 0 - S 0 - H 1 - CX 0 1 0 2 - H 1 2 - CX 1 0 2 0 2 1 1 2 2 1 + 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 2 - H 2 - CX 2 1 - S 2 - H 0 1 2 + 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 1 1 2 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( @@ -7871,6 +9668,55 @@ class Tableau: >>> 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, *, @@ -9587,15 +11433,18 @@ class TableauSimulator: >>> import numpy as np >>> s = stim.TableauSimulator() >>> s.x(2) - >>> list(s.state_vector(endian='little')) - [0j, 0j, 0j, 0j, (1+0j), 0j, 0j, 0j] + >>> 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) - >>> list(s.state_vector(endian='big')) - [0j, (1+0j), 0j, 0j, 0j, 0j, 0j, 0j] + >>> 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) - >>> list(s.state_vector()) - [(0.5+0j), 0j, -0.5j, 0j, 0.5j, 0j, (0.5+0j), 0j] + >>> 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, @@ -9972,6 +11821,33 @@ def read_shot_data_file( 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. @@ -10035,6 +11911,45 @@ def target_logical_observable_id( 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: diff --git a/file_lists/benchmark_files b/file_lists/perf_files similarity index 67% rename from file_lists/benchmark_files rename to file_lists/perf_files index d91532e93..2d6ac07e1 100644 --- a/file_lists/benchmark_files +++ b/file_lists/perf_files @@ -1,20 +1,22 @@ -src/stim/benchmark_main.perf.cc -src/stim/benchmark_util.perf.cc src/stim/circuit/circuit.perf.cc -src/stim/circuit/gate_data.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/probability_util.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/conversions.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 diff --git a/file_lists/python_api_files b/file_lists/pybind_files similarity index 89% rename from file_lists/python_api_files rename to file_lists/pybind_files index c8e36a645..068bbe0fe 100644 --- a/file_lists/python_api_files +++ b/file_lists/pybind_files @@ -1,13 +1,13 @@ src/stim/circuit/circuit.pybind.cc src/stim/circuit/circuit_instruction.pybind.cc src/stim/circuit/circuit_repeat_block.pybind.cc -src/stim/circuit/gate_data.pybind.cc src/stim/circuit/gate_target.pybind.cc src/stim/cmd/command_diagram.pybind.cc src/stim/dem/detector_error_model.pybind.cc src/stim/dem/detector_error_model_instruction.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 @@ -20,6 +20,8 @@ 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/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 diff --git a/file_lists/source_files_no_main b/file_lists/source_files_no_main index 042c535fd..8555fcd89 100644 --- a/file_lists/source_files_no_main +++ b/file_lists/source_files_no_main @@ -1,21 +1,6 @@ src/stim.cc -src/stim/arg_parse.cc src/stim/circuit/circuit.cc src/stim/circuit/circuit_instruction.cc -src/stim/circuit/gate_data.cc -src/stim/circuit/gate_data_annotations.cc -src/stim/circuit/gate_data_blocks.cc -src/stim/circuit/gate_data_collapsing.cc -src/stim/circuit/gate_data_controlled.cc -src/stim/circuit/gate_data_hada.cc -src/stim/circuit/gate_data_heralded.cc -src/stim/circuit/gate_data_noisy.cc -src/stim/circuit/gate_data_pair_measure.cc -src/stim/circuit/gate_data_pauli.cc -src/stim/circuit/gate_data_period_3.cc -src/stim/circuit/gate_data_period_4.cc -src/stim/circuit/gate_data_pp.cc -src/stim/circuit/gate_data_swaps.cc src/stim/circuit/gate_decomposition.cc src/stim/circuit/gate_target.cc src/stim/cmd/command_analyze_errors.cc @@ -50,6 +35,21 @@ 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 @@ -65,7 +65,6 @@ 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/probability_util.cc src/stim/search/graphlike/algo.cc src/stim/search/graphlike/edge.cc src/stim/search/graphlike/graph.cc @@ -76,11 +75,25 @@ 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/transform_without_feedback.cc src/stim/simulators/vector_simulator.cc -src/stim/stabilizers/conversions.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/reference_sample_tree.cc +src/stim/util_top/simplified_circuit.cc +src/stim/util_top/transform_without_feedback.cc diff --git a/file_lists/test_files b/file_lists/test_files index 593a91d0f..9de5d724f 100644 --- a/file_lists/test_files +++ b/file_lists/test_files @@ -1,10 +1,7 @@ src/stim.test.cc -src/stim/arg_parse.test.cc src/stim/circuit/circuit.test.cc -src/stim/circuit/gate_data.test.cc src/stim/circuit/gate_decomposition.test.cc src/stim/circuit/gate_target.test.cc -src/stim/circuit/stabilizer_flow.test.cc src/stim/cmd/command_analyze_errors.test.cc src/stim/cmd/command_convert.test.cc src/stim/cmd/command_detect.test.cc @@ -14,6 +11,7 @@ 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 @@ -25,6 +23,7 @@ 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 @@ -45,7 +44,6 @@ 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/probability_util.test.cc src/stim/search/graphlike/algo.test.cc src/stim/search/graphlike/edge.test.cc src/stim/search/graphlike/graph.test.cc @@ -56,22 +54,45 @@ 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/simulators/count_determined_measurements.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/transform_without_feedback.test.cc src/stim/simulators/vector_simulator.test.cc -src/stim/stabilizers/conversions.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/str_util.test.cc -src/stim/test_util.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_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/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 diff --git a/glue/cirq/setup.py b/glue/cirq/setup.py index fe64f2548..c47fa1b59 100644 --- a/glue/cirq/setup.py +++ b/glue/cirq/setup.py @@ -17,7 +17,7 @@ with open('README.md', encoding='UTF-8') as f: long_description = f.read() -__version__ = '1.13.dev0' +__version__ = '1.14.dev0' setup( name='stimcirq', diff --git a/glue/cirq/stimcirq/__init__.py b/glue/cirq/stimcirq/__init__.py index 1e5543696..ce9f8cbae 100644 --- a/glue/cirq/stimcirq/__init__.py +++ b/glue/cirq/stimcirq/__init__.py @@ -1,6 +1,7 @@ -__version__ = '1.13.dev0' +__version__ = '1.14.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 @@ -20,5 +21,6 @@ "SweepPauli": SweepPauli, "TwoQubitAsymmetricDepolarizingChannel": TwoQubitAsymmetricDepolarizingChannel, "CXSwapGate": CXSwapGate, + "CZSwapGate": CZSwapGate, } JSON_RESOLVER = JSON_RESOLVERS_DICT.get diff --git a/glue/cirq/stimcirq/_cirq_to_stim.py b/glue/cirq/stimcirq/_cirq_to_stim.py index dec010861..248dcd53d 100644 --- a/glue/cirq/stimcirq/_cirq_to_stim.py +++ b/glue/cirq/stimcirq/_cirq_to_stim.py @@ -290,6 +290,32 @@ def _stim_append_pauli_measurement_gate( circuit.append_operation("MPP", new_targets) +def _stim_append_spp_gate( + circuit: stim.Circuit, gate: cirq.PauliStringPhasorGate, targets: List[int] +): + 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_operation("SPP" if d == 0.5 else "SPP_DAG", new_targets) + return True + + def _stim_append_dense_pauli_string_gate( c: stim.Circuit, g: cirq.BaseDensePauliString, t: List[int] ): @@ -383,7 +409,9 @@ def __init__(self): def process_circuit_operation_into_repeat_block(self, op: cirq.CircuitOperation) -> None: if self.flatten or op.repetitions == 1: - self.process_operations(cirq.decompose_once(op)) + 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() @@ -421,6 +449,9 @@ def process_operations(self, operations: Iterable[cirq.Operation]) -> None: continue # Special case measurement, because of its metadata. + if isinstance(gate, cirq.PauliStringPhasorGate): + if _stim_append_spp_gate(self.out, gate, targets): + continue if isinstance(gate, cirq.PauliMeasurementGate): self.key_out.append((gate.key, len(targets))) _stim_append_pauli_measurement_gate(self.out, gate, targets) diff --git a/glue/cirq/stimcirq/_cirq_to_stim_test.py b/glue/cirq/stimcirq/_cirq_to_stim_test.py index 21456dfae..65f3f1d2c 100644 --- a/glue/cirq/stimcirq/_cirq_to_stim_test.py +++ b/glue/cirq/stimcirq/_cirq_to_stim_test.py @@ -339,6 +339,31 @@ def test_on_loop(): result = stimcirq.StimSampler().run(c) assert result.measurements.keys() == {'0:a', '0:b', '1:a', '1:b', '2:a', '2:b'} + +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( diff --git a/glue/cirq/stimcirq/_cz_swap_gate.py b/glue/cirq/stimcirq/_cz_swap_gate.py new file mode 100644 index 000000000..6f9ded68c --- /dev/null +++ b/glue/cirq/stimcirq/_cz_swap_gate.py @@ -0,0 +1,46 @@ +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], **kwargs): + edit_circuit.append_operation('CZSWAP', targets) + + 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 {} diff --git a/glue/cirq/stimcirq/_cz_swap_test.py b/glue/cirq/stimcirq/_cz_swap_test.py new file mode 100644 index 000000000..34c2af445 --- /dev/null +++ b/glue/cirq/stimcirq/_cz_swap_test.py @@ -0,0 +1,72 @@ +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 diff --git a/glue/cirq/stimcirq/_stim_to_cirq.py b/glue/cirq/stimcirq/_stim_to_cirq.py index 43bf516d0..d97d1aa5f 100644 --- a/glue/cirq/stimcirq/_stim_to_cirq.py +++ b/glue/cirq/stimcirq/_stim_to_cirq.py @@ -16,6 +16,7 @@ import stim from ._cx_swap_gate import CXSwapGate +from ._cz_swap_gate import CZSwapGate from ._det_annotation import DetAnnotation from ._measure_and_or_reset_gate import MeasureAndOrResetGate from ._obs_annotation import CumulativeObservableAnnotation @@ -29,7 +30,7 @@ def _stim_targets_to_dense_pauli_string( obs = cirq.MutableDensePauliString("I" * len(targets)) for k, target in enumerate(targets): if target.is_inverted_result_target: - obs.coefficient *= -1 + obs *= -1 if target.is_x_target: obs.pauli_mask[k] = 1 elif target.is_y_target: @@ -231,6 +232,26 @@ def process_mpp(self, instruction: stim.CircuitInstruction) -> None: key = str(self.get_next_measure_id()) self.append_operation(cirq.PauliMeasurementGate(obs, key=key).on(*qubits)) + 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() + 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)) + def process_m_pair(self, instruction: stim.CircuitInstruction, basis: str) -> None: args = instruction.gate_args_copy() if args and args[0]: @@ -424,6 +445,7 @@ def handler( 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( @@ -497,6 +519,8 @@ def handler( "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, diff --git a/glue/cirq/stimcirq/_stim_to_cirq_test.py b/glue/cirq/stimcirq/_stim_to_cirq_test.py index 1c1865938..42fb3230b 100644 --- a/glue/cirq/stimcirq/_stim_to_cirq_test.py +++ b/glue/cirq/stimcirq/_stim_to_cirq_test.py @@ -644,3 +644,41 @@ def test_stim_circuit_to_cirq_circuit_mxx_myy_mzz(): 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 + """) diff --git a/glue/crumble/README.md b/glue/crumble/README.md index 9de5445bc..1b402a6f8 100644 --- a/glue/crumble/README.md +++ b/glue/crumble/README.md @@ -90,19 +90,29 @@ button (now labelled "Hide Import/Export") again. ## Keyboard Controls -**Markings** +**Pauli Propagation** -- `[XYZ]+[0-9]`: Place Pauli propagation markers at current selection. - The X, Y, or Z determines the basis of the marker. - The 1, 2, 3, or etc determines which of the tracked Pauli products is affected by the marker. - For example, `X+2` will place X type markers that multiply an X dependence into Pauli product #2. - The XYZ can be omitted, in which case the basis will be inferred based on the selected gates (for example, an RX gate implies X basis). - `spacebar`: Clear all Pauli propagation markers at current selection. -- `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. +- `#`: 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. @@ -112,11 +122,20 @@ button (now labelled "Hide Import/Export") again. - `ctrl+z`: Undo - `ctrl+y`: Redo - `ctrl+shift+z`: Redo -- `ctrl+c`: Copy selection to clipboard. -- `ctrl+v`: Past clipboard contents at current selection. -- `ctrl+x`: Cut selection to clipboard. +- `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** @@ -137,7 +156,11 @@ Note: use `shift` to get the inverse of a gate. - `m+r+x`: Overwrite selection with `MRX` gate - `m+r+y`: Overwrite selection with `MRY` gate - `m+r`: Overwrite selection with `MR` gate -- `f`: Overwrite selection with `C_XYZ` 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** @@ -161,9 +184,15 @@ Note: use `shift` to get the inverse of a gate. - `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 `MPP X*X` gate targeting mouse -- `c+m+y`: Overwrite selection with `MPP Y*Y` gate targeting mouse -- `c+m+z`: Overwrite selection with `MPP Z*Z` 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** @@ -187,7 +216,7 @@ Here are some examples: - `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 `MPP X1*X2`). +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) diff --git a/glue/crumble/base/revision.js b/glue/crumble/base/revision.js index 73929fb29..ad4cc595b 100644 --- a/glue/crumble/base/revision.js +++ b/glue/crumble/base/revision.js @@ -109,8 +109,8 @@ class Revision { * the previous state. * @returns {void} */ - startedWorkingOnCommit() { - this.isWorkingOnCommit = true; + startedWorkingOnCommit(newCheckpoint) { + this.isWorkingOnCommit = newCheckpoint !== this.history[this.index]; this._changes.send(undefined); } diff --git a/glue/crumble/base/seq.js b/glue/crumble/base/seq.js new file mode 100644 index 000000000..c4622ee69 --- /dev/null +++ b/glue/crumble/base/seq.js @@ -0,0 +1,22 @@ +/** + * @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}; diff --git a/glue/crumble/base/seq.test.js b/glue/crumble/base/seq.test.js new file mode 100644 index 000000000..5aec2f80a --- /dev/null +++ b/glue/crumble/base/seq.test.js @@ -0,0 +1,12 @@ +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]], + ])) +}); diff --git a/glue/crumble/circuit/circuit.js b/glue/crumble/circuit/circuit.js index 61d1f0f87..dc36e7c2e 100644 --- a/glue/crumble/circuit/circuit.js +++ b/glue/crumble/circuit/circuit.js @@ -1,28 +1,8 @@ import {Operation} from "./operation.js" -import {GATE_MAP} from "../gates/gateset.js" +import {GATE_ALIAS_MAP, GATE_MAP} from "../gates/gateset.js" import {Layer} from "./layer.js" -import {make_mpp_gate} from '../gates/gateset_mpp.js'; - -/** - * @param {!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; -} +import {make_mpp_gate, make_spp_gate} from '../gates/gateset_mpp.js'; +import {describe} from "../base/describe.js"; /** * @param {!string} targetText @@ -86,12 +66,16 @@ function splitUncombinedTargets(targets) { /** * @param {!Float32Array} args * @param {!Array.} combinedTargets + * @param {!boolean} convertIntoOtherGates * @returns {!Operation} */ -function simplifiedMPP(args, combinedTargets) { +function simplifiedMPP(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)); @@ -104,13 +88,51 @@ function simplifiedMPP(args, combinedTargets) { } } - let gate = GATE_MAP.get('M' + bases); + 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, args, new Uint32Array(qubits)); } +/** + * @param {!Float32Array} args + * @param {!boolean} dag + * @param {!Array.} combinedTargets + * @returns {!Operation} + */ +function simplifiedSPP(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, args, new Uint32Array(qubits)); +} + class Circuit { /** @@ -137,19 +159,24 @@ class Circuit { */ static fromStimCircuit(stimCircuit) { let lines = stimCircuit.replaceAll(';', '\n'). + 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('H XY', 'H_XY'). + replaceAll('H XZ', 'H_XZ'). replaceAll('H YZ', 'H_YZ'). replaceAll(' INCLUDE', '_INCLUDE'). replaceAll('SQRT ', 'SQRT_'). replaceAll(' DAG ', '_DAG '). replaceAll('C ZYX', 'C_ZYX').split('\n'); let layers = [new Layer()]; + let num_detectors = 0; let i2q = new Map(); let used_positions = new Set(); @@ -193,6 +220,7 @@ class Circuit { } }; + let measurement_locs = []; let processLine = line => { let args = []; let targets = []; @@ -216,32 +244,20 @@ class Circuit { let reverse_pairs = false; if (name === '') { return; - } else if (name === 'XCZ') { - reverse_pairs = true; - name = 'CX'; - } else if (name === 'SWAPCX') { - reverse_pairs = true; - name = 'CXSWAP'; - } else if (name === 'CNOT') { - name = 'CX'; - } else if (name === 'RZ') { - name = 'R'; - } else if (name === 'MZ') { - name = 'M'; - } else if (name === 'MRZ') { - name = 'MR'; - } else if (name === 'ZCX') { - name = 'CX'; - } else if (name === 'ZCY') { - name = 'CY'; - } else if (name === 'ZCZ') { - name = 'CZ'; - } else if (name === 'YCX') { - reverse_pairs = true; - name = 'XCY'; - } else if (name === 'YCZ') { - reverse_pairs = true; - name = 'CY'; + } + 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; @@ -249,25 +265,52 @@ class Circuit { let combinedTargets = splitUncombinedTargets(targets); let layer = layers[layers.length - 1] for (let combo of combinedTargets) { + let op = simplifiedMPP(new Float32Array(args), combo); try { - layer.put(simplifiedMPP(new Float32Array(args), combo), false); + layer.put(op, false); } catch (_) { layers.push(new Layer()); layer = layers[layers.length - 1]; - layer.put(simplifiedMPP(new Float32Array(args), combo), false); + 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), + new Float32Array([argIndex]), + new Uint32Array([loc.targets[0]]), + )); } + num_detectors += isDet; return; - } else if (name === "X_ERROR" || - name === "Y_ERROR" || - name === "Z_ERROR" || - name === "DETECTOR" || - name === "OBSERVABLE_INCLUDE" || - name === "DEPOLARIZE1" || - name === "DEPOLARIZE2" || - name === "SHIFT_COORDS" || - name === "REPEAT" || - name === "}") { + } 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(new Float32Array(args), dag, combo), false); + } catch (_) { + layers.push(new Layer()); + layer = layers[layers.length - 1]; + layer.put(simplifiedSPP(new Float32Array(args), dag, combo), false); + } + } return; } else if (name.startsWith('QUBIT_COORDS')) { let x = args.length < 1 ? 0 : args[0]; @@ -286,27 +329,35 @@ class Circuit { return; } - let ignored = false; + let has_feedback = false; for (let targ of targets) { if (targ.startsWith("rec[")) { if (name === "CX" || name === "CY" || name === "CZ" || name === "ZCX" || name === "ZCY") { - ignored = true; - break; + has_feedback = true; } - } - let t = parseInt(targ); - if (typeof parseInt(targ) !== 'number') { + } else if (typeof parseInt(targ) !== 'number') { throw new Error(line); } } - if (ignored) { - console.warn("IGNORED", name); - return; + if (has_feedback) { + let clean_targets = []; + for (let k = 0; k < targets.length; k += 2) { + if (targets[k].startsWith("rec[") || targets[k + 1].startsWith("rec[")) { + 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("Unrecognized gate name in " + line); + console.warn("Ignoring unrecognized instruction: " + line); return; } let a = new Float32Array(args); @@ -324,12 +375,16 @@ class Circuit { sub_targets.reverse(); } let qs = new Uint32Array(sub_targets); + let op = new Operation(gate, a, qs); try { - layer.put(new Operation(gate, a, qs), false); + layer.put(op, false); } catch (_) { layers.push(new Layer()); layer = layers[layers.length - 1]; - layer.put(new Operation(gate, a, qs), false); + layer.put(op, false); + } + if (op.countMeasurements() > 0) { + measurement_locs.push({layer: layers.length - 1, targets: op.id_targets}); } } } @@ -479,6 +534,88 @@ class Circuit { 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} */ @@ -492,6 +629,11 @@ class Circuit { } } + 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]; @@ -514,40 +656,28 @@ class Circuit { 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 = groupBy(layer.iter_gates_and_markers(), op => { - let key = op.gate.name; - if (key.startsWith('M') && !GATE_MAP.has(key)) { - key = 'MPP'; - } - 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; - }); - - for (let nameWithArgs of namesWithArgs) { - let group = opsByName.get(nameWithArgs); + let opsByName = layer.opsGroupedByNameWithArgs(); + + for (let [nameWithArgs, group] of opsByName.entries()) { let targetGroups = []; let gateName = nameWithArgs.split('(')[0]; + if (gateName === 'DETECTOR' || gateName === 'OBSERVABLE_INCLUDE') { + continue; + } let gate = GATE_MAP.get(gateName); - if (gate === undefined && gateName === 'MPP') { - let line = ['MPP ']; + 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(op.gate.name[k + 1] + old2new.get(op.id_targets[k])); + line.push(bases[k] + old2new.get(op.id_targets[k])); line.push('*'); } line.pop(); @@ -558,11 +688,13 @@ class Circuit { 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]) } } @@ -576,6 +708,72 @@ class Circuit { } } } + + // 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') { @@ -585,6 +783,17 @@ class Circuit { return out.join('\n'); } + /** + * @returns {!int} + */ + countMeasurements() { + let total = 0; + for (let layer of this.layers) { + total += layer.countMeasurements(); + } + return total; + } + /** * @param {!Iterable} coords */ diff --git a/glue/crumble/circuit/circuit.test.js b/glue/crumble/circuit/circuit.test.js index 744758313..38e7880e9 100644 --- a/glue/crumble/circuit/circuit.test.js +++ b/glue/crumble/circuit/circuit.test.js @@ -47,6 +47,53 @@ S 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 @@ -531,3 +578,147 @@ 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 + `)); +}); diff --git a/glue/crumble/circuit/layer.js b/glue/crumble/circuit/layer.js index 958a449e5..fc5b30c61 100644 --- a/glue/crumble/circuit/layer.js +++ b/glue/crumble/circuit/layer.js @@ -1,4 +1,6 @@ import {Operation} from "./operation.js" +import {GATE_MAP} from "../gates/gateset.js"; +import {groupBy} from "../base/seq.js"; class Layer { constructor() { @@ -6,6 +8,57 @@ class Layer { 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.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} */ @@ -16,6 +69,111 @@ class Layer { 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} */ diff --git a/glue/crumble/circuit/operation.js b/glue/crumble/circuit/operation.js index fa4a99db1..1cd4907fe 100644 --- a/glue/crumble/circuit/operation.js +++ b/glue/crumble/circuit/operation.js @@ -40,6 +40,29 @@ class Operation { this.id_targets = targets; } + /** + * @returns {!string} + */ + toString() { + return `${this.gate.name}(${[...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} @@ -48,10 +71,16 @@ class Operation { 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(this.gate.name[k + 1]); + let b = 'XYZ'.indexOf(bases[k]); if (a >= 0 && b >= 0 && a !== b) { differences++; } @@ -59,8 +88,30 @@ class Operation { 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); } - return before; } if (before.length !== this.gate.num_qubits) { throw new Error(`before.length !== this.gate.num_qubits`); diff --git a/glue/crumble/circuit/pauli_frame.js b/glue/crumble/circuit/pauli_frame.js index 4c3bda6ff..6ac63a406 100644 --- a/glue/crumble/circuit/pauli_frame.js +++ b/glue/crumble/circuit/pauli_frame.js @@ -1,3 +1,5 @@ +import {describe} from "../base/describe.js"; + class PauliFrame { /** * @param {!int} num_frames @@ -234,6 +236,9 @@ class PauliFrame { * @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]; @@ -254,6 +259,52 @@ class PauliFrame { } } + /** + * @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 @@ -307,8 +358,8 @@ class PauliFrame { */ do_swap(targets) { for (let k = 0; k < targets.length; k += 2) { - let a = k; - let b = k + 1; + let a = targets[k]; + let b = targets[k + 1]; let xa = this.xs[a]; let za = this.zs[a]; let xb = this.xs[b]; @@ -325,8 +376,8 @@ class PauliFrame { */ do_iswap(targets) { for (let k = 0; k < targets.length; k += 2) { - let a = k; - let b = k + 1; + let a = targets[k]; + let b = targets[k + 1]; let xa = this.xs[a]; let za = this.zs[a]; @@ -345,8 +396,8 @@ class PauliFrame { */ do_sqrt_xx(targets) { for (let k = 0; k < targets.length; k += 2) { - let a = k; - let b = k + 1; + 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; @@ -358,8 +409,8 @@ class PauliFrame { */ do_sqrt_yy(targets) { for (let k = 0; k < targets.length; k += 2) { - let a = k; - let b = k + 1; + let a = targets[k]; + let b = targets[k + 1]; let xa = this.xs[a]; let za = this.zs[a]; @@ -385,8 +436,8 @@ class PauliFrame { */ do_sqrt_zz(targets) { for (let k = 0; k < targets.length; k += 2) { - let a = k; - let b = k + 1; + 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; @@ -398,8 +449,8 @@ class PauliFrame { */ do_xcx(targets) { for (let k = 0; k < targets.length; k += 2) { - let control = k; - let target = k + 1; + let control = targets[k]; + let target = targets[k + 1]; this.xs[target] ^= this.zs[control]; this.xs[control] ^= this.zs[target]; } @@ -410,8 +461,8 @@ class PauliFrame { */ do_xcy(targets) { for (let k = 0; k < targets.length; k += 2) { - let control = k; - let target = k + 1; + 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]; @@ -424,8 +475,8 @@ class PauliFrame { */ do_ycy(targets) { for (let k = 0; k < targets.length; k += 2) { - let control = k; - let target = k + 1; + 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; @@ -440,8 +491,8 @@ class PauliFrame { */ do_cx(targets) { for (let k = 0; k < targets.length; k += 2) { - let control = k; - let target = k + 1; + let control = targets[k]; + let target = targets[k + 1]; this.xs[target] ^= this.xs[control]; this.zs[control] ^= this.zs[target]; } @@ -452,8 +503,8 @@ class PauliFrame { */ do_cx_swap(targets) { for (let k = 0; k < targets.length; k += 2) { - let c = k; - let t = k + 1; + let c = targets[k]; + let t = targets[k + 1]; let xc = this.xs[c]; let zc = this.zs[c]; let xt = this.xs[t]; @@ -465,13 +516,49 @@ class PauliFrame { } } + /** + * @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 = k; - let target = k + 1; + 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]; @@ -484,8 +571,8 @@ class PauliFrame { */ do_cz(targets) { for (let k = 0; k < targets.length; k += 2) { - let control = k; - let target = k + 1; + let control = targets[k]; + let target = targets[k + 1]; this.zs[target] ^= this.xs[control]; this.zs[control] ^= this.xs[target]; } @@ -499,6 +586,14 @@ class PauliFrame { gate.frameDo(this, targets); } + /** + * @param {!Gate} gate + * @param {!Array} targets + */ + undo_gate(gate, targets) { + gate.frameUndo(this, targets); + } + /** * @param {*} other * @returns {!boolean} diff --git a/glue/crumble/circuit/pauli_frame.test.js b/glue/crumble/circuit/pauli_frame.test.js index 32c9984b1..530c1ae2a 100644 --- a/glue/crumble/circuit/pauli_frame.test.js +++ b/glue/crumble/circuit/pauli_frame.test.js @@ -1,7 +1,7 @@ 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} from "../gates/gateset_mpp.js" +import {make_mpp_gate, make_spp_gate} from "../gates/gateset_mpp.js" import {Operation} from "./operation.js"; test("pauli_frame.to_from", () => { @@ -31,23 +31,37 @@ test("pauli_frame.to_from_dicts", () => { }); test("pauli_frame.do_gate_vs_old_frame_updates", () => { - let gates = [...GATE_MAP.values(), make_mpp_gate("XYY")]; + let gates = [...GATE_MAP.values(), make_mpp_gate("XYY"), make_spp_gate("XYY")]; for (let g of gates) { - let before, after; + 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, [0, 1]); + 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(); diff --git a/glue/crumble/circuit/propagated_pauli_frames.js b/glue/crumble/circuit/propagated_pauli_frames.js index 737f47e6f..2a65df42f 100644 --- a/glue/crumble/circuit/propagated_pauli_frames.js +++ b/glue/crumble/circuit/propagated_pauli_frames.js @@ -1,3 +1,7 @@ +import {PauliFrame} from './pauli_frame.js'; +import {equate} from '../base/equate.js'; +import {Layer} from './layer.js'; + class PropagatedPauliFrameLayer { /** * @param {!Map} bases @@ -9,6 +13,66 @@ class PropagatedPauliFrameLayer { 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 { @@ -19,8 +83,31 @@ class PropagatedPauliFrames { 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); @@ -83,12 +170,142 @@ class PropagatedPauliFrames { } } - if (bases.size > 0 || errors.size > 0 || crossings.size > 0) { - result.id_layers.set(k, new PropagatedPauliFrameLayer(bases, errors, crossings)); + 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}; +export {PropagatedPauliFrames, PropagatedPauliFrameLayer}; diff --git a/glue/crumble/circuit/propagated_pauli_frames.test.js b/glue/crumble/circuit/propagated_pauli_frames.test.js new file mode 100644 index 000000000..769c37e85 --- /dev/null +++ b/glue/crumble/circuit/propagated_pauli_frames.test.js @@ -0,0 +1,127 @@ +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(), + [], + )], + ]))); +}); diff --git a/glue/crumble/crumble.html b/glue/crumble/crumble.html index 43ed9f8f0..dea58f16f 100644 --- a/glue/crumble/crumble.html +++ b/glue/crumble/crumble.html @@ -5,22 +5,196 @@ Crumble +
- Crumble is a prototype stabilizer circuit editor.
- Read the manual
- Load example: surface code (d=3,r=2)
- Load example: bacon shor code (d=7,r=2)
- Load example: three coupler surface code (d=7,r=4)
+
+
+ Crumble is a prototype stabilizer circuit editor.
+
+ Read the manual +
+
+
+
+
+
+ + +
+ +
- +
+
-
- +
+
@@ -41,12 +215,14 @@
+
+
+
+ +
+
-
- - -